From 758aca2b493aa8bfdbf2f43929fb85081a7a1d82 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Tue, 25 Nov 2025 10:53:45 -0600 Subject: [PATCH 1/4] feat: move stats collection out of initialContext Previously, stats would block the application until it was resolved. This led to a serious issue in which the stats server was not responding and the request timed out: the application would be completely inacessible while this was occuring. Because it's not essential, stats are now sent in the background. (cherry picked from commit f1904fec7b6b6d048fffcec5afe993dcab1b3adf) --- .../lib/components/Core/ContextLoader.tsx | 27 +++++-- .../lib/components/InitialContext/index.ts | 28 +++++++ .../lib/components/InitialContext/stats.ts | 81 +++++++++++++++++++ .../components/InitialContext/systemInfo.ts | 80 ------------------ 4 files changed, 130 insertions(+), 86 deletions(-) create mode 100644 specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts diff --git a/specifyweb/frontend/js_src/lib/components/Core/ContextLoader.tsx b/specifyweb/frontend/js_src/lib/components/Core/ContextLoader.tsx index 1742c55324e..620c55f2295 100644 --- a/specifyweb/frontend/js_src/lib/components/Core/ContextLoader.tsx +++ b/specifyweb/frontend/js_src/lib/components/Core/ContextLoader.tsx @@ -5,25 +5,40 @@ import { useDelay } from '../../hooks/useDelay'; import { commonText } from '../../localization/common'; import { f } from '../../utils/functools'; import { SECOND } from '../Atoms/timeUnits'; -import { crash } from '../Errors/Crash'; +import { crash, softFail } from '../Errors/Crash'; import { useMenuItems } from '../Header/menuItemProcessing'; -import { initialContext } from '../InitialContext'; +import { initialContext, secondaryContext } from '../InitialContext'; import { Main } from './Main'; import { SplashScreen } from './SplashScreen'; // Show loading splash screen if didn't finish load within 2 seconds const LOADING_TIMEOUT = 2 * SECOND; -const fetchContext = async (): Promise => - initialContext.then(f.true).catch(crash); +const fetchContext = + ( + context: Promise, + errorMode: 'crash' | 'console' + ): (() => Promise) => + async () => + context.then(f.true).catch(errorMode === 'crash' ? crash : softFail); /** - * - Load initial context + * - Load initial and secondary context * - Display loading screen while loading * - Display the main component afterward */ export function ContextLoader(): JSX.Element | null { - const [isContextLoaded = false] = useAsyncState(fetchContext, false); + const [isContextLoaded = false] = useAsyncState( + React.useCallback(fetchContext(initialContext, 'crash'), []), + false + ); + const [_secondaryContextLoaded = false] = useAsyncState( + React.useCallback( + isContextLoaded ? fetchContext(secondaryContext, 'console') : f.undefined, + [isContextLoaded] + ), + false + ); const menuItems = useMenuItems(); const isLoaded = isContextLoaded && typeof menuItems === 'object'; diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/index.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/index.ts index 5b2f5a36efb..d3f94c9283f 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/index.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/index.ts @@ -65,6 +65,34 @@ export const load = async (path: string, mimeType: MimeType): Promise => return data; }); +/** + * These endpoints should still be called as a part of loading the initial + * context, but are not strictly required for normal operation of the + * application. + * Because of this, these endpoints are called after the initialContext and do + * not block or prevent access to Specify + */ +export const secondaryContext = Promise.all([ + /** REFACTOR: Move non-essential endpoints here from initialContext to speed + * up initial loading times. + * Icon Definitions, Legacy UI Localization, Uniqueness Rules, and possibly + * even Field Formatters and Remote Prefs can all theoretically be moved here. + * + * Some more work would need to be done to handle the case where a component + * attempts to access the resources as they're being fetched. + */ + // Send basic stats + import('./stats'), +]).then(async (modules) => + Promise.all(modules.map(async ({ fetchContext }) => fetchContext)) +); + +/** + * These endpoints are essential for nearly all operations in Specify and have + * to be fetched before the application can even be accessed. + * That is, the application will necessarily be blocked until + * all of these requests are resolved. + */ export const initialContext = Promise.all([ // Fetch general context information (NOT CACHED) import('../DataModel/schema'), diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts new file mode 100644 index 00000000000..47a182932a1 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts @@ -0,0 +1,81 @@ +import { fetchContext as fetchSystemInfo } from './systemInfo'; +import { ping } from '../../utils/ajax/ping'; +import { softFail } from '../Errors/Crash'; +import { formatUrl } from '../Router/queryString'; +import { load } from './index'; + +type StatsCounts = { + readonly Collectionobject: number; + readonly Collection: number; + readonly Specifyuser: number; +}; + +function buildStatsLambdaUrl(base: string | null | undefined): string | null { + if (!base) return null; + let u = base.trim(); + + if (!/^https?:\/\//i.test(u)) u = `https://${u}`; + + const hasRoute = /\/(prod|default)\/[^\s/]+/.test(u); + if (!hasRoute) { + const stage = 'prod'; + const route = 'AggrgatedSp7Stats'; + u = `${u.replace(/\/$/, '')}/${stage}/${route}`; + } + return u; +} + +export const fetchContext = fetchSystemInfo.then(async (systemInfo) => { + if (systemInfo === undefined) { + return; + } + if (systemInfo.stats_url === null && systemInfo.stats_2_url === null) { + return; + } + let counts: StatsCounts | null = null; + try { + counts = await load( + '/context/stats_counts.json', + 'application/json' + ); + } catch { + // If counts fetch fails, proceed without them. + counts = null; + } + + const parameters = { + version: systemInfo.version, + dbVersion: systemInfo.database_version, + institution: systemInfo.institution, + institutionGUID: systemInfo.institution_guid, + discipline: systemInfo.discipline, + collection: systemInfo.collection, + collectionGUID: systemInfo.collection_guid, + isaNumber: systemInfo.isa_number, + disciplineType: systemInfo.discipline_type, + collectionObjectCount: counts?.Collectionobject ?? 0, + collectionCount: counts?.Collection ?? 0, + userCount: counts?.Specifyuser ?? 0, + }; + if (systemInfo.stats_url) + await ping( + formatUrl( + systemInfo.stats_url, + parameters, + /* + * I don't know if the receiving server handles GET parameters in a + * case-sensitive way. Thus, don't convert keys to lower case, but leave + * them as they were sent in previous versions of Specify 7 + */ + false + ), + { errorMode: 'silent' } + ).catch(softFail); + + const lambdaUrl = buildStatsLambdaUrl(systemInfo.stats_2_url); + if (lambdaUrl) { + await ping(formatUrl(lambdaUrl, parameters, false), { + errorMode: 'silent', + }).catch(softFail); + } +}); diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts index 21518f41335..c2893510082 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts @@ -4,9 +4,6 @@ import type { LocalizedString } from 'typesafe-i18n'; -import { ping } from '../../utils/ajax/ping'; -import { softFail } from '../Errors/Crash'; -import { formatUrl } from '../Router/queryString'; import { load } from './index'; type SystemInfo = { @@ -26,91 +23,14 @@ type SystemInfo = { readonly discipline_type: string; }; -type StatsCounts = { - readonly Collectionobject: number; - readonly Collection: number; - readonly Specifyuser: number; -}; - let systemInfo: SystemInfo; -function buildStatsLambdaUrl(base: string | null | undefined): string | null { - if (!base) return null; - let u = base.trim(); - - if (!/^https?:\/\//i.test(u)) u = `https://${u}`; - - const hasRoute = /\/(prod|default)\/[^\s/]+/.test(u); - if (!hasRoute) { - const stage = 'prod'; - const route = 'AggrgatedSp7Stats'; - u = `${u.replace(/\/$/, '')}/${stage}/${route}`; - } - return u; -} - export const fetchContext = load( '/context/system_info.json', 'application/json' ).then(async (data) => { systemInfo = data; - if (systemInfo.stats_url !== null) { - let counts: StatsCounts | null = null; - try { - counts = await load( - '/context/stats_counts.json', - 'application/json' - ); - } catch { - // If counts fetch fails, proceed without them. - counts = null; - } - - const parameters = { - version: systemInfo.version, - dbVersion: systemInfo.database_version, - institution: systemInfo.institution, - institutionGUID: systemInfo.institution_guid, - discipline: systemInfo.discipline, - collection: systemInfo.collection, - collectionGUID: systemInfo.collection_guid, - isaNumber: systemInfo.isa_number, - disciplineType: systemInfo.discipline_type, - collectionObjectCount: counts?.Collectionobject ?? 0, - collectionCount: counts?.Collection ?? 0, - userCount: counts?.Specifyuser ?? 0, - }; - - await ping( - formatUrl( - systemInfo.stats_url, - parameters, - /* - * I don't know if the receiving server handles GET parameters in a - * case-sensitive way. Thus, don't convert keys to lower case, but leave - * them as they were sent in previous versions of Specify 7 - */ - false - ), - { errorMode: 'silent' } - ).catch(softFail); - - /* - * Await ping( - * formatUrl(systemInfo.stats_2_url, parameters, false), - * { errorMode: 'silent' } - * ).catch(softFail); - */ - - const lambdaUrl = buildStatsLambdaUrl(systemInfo.stats_2_url); - if (lambdaUrl) { - await ping(formatUrl(lambdaUrl, parameters, false), { - errorMode: 'silent', - }).catch(softFail); - } - } - return systemInfo; }); From 28450dc126748c4e2ca795c97d0fc34fc1df5275 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Tue, 25 Nov 2025 17:02:04 +0000 Subject: [PATCH 2/4] Lint code with ESLint and Prettier Triggered by f1904fec7b6b6d048fffcec5afe993dcab1b3adf on branch refs/heads/secondary-context (cherry picked from commit 3148768714ed5f05149b768e68ee6c252c90e6d6) --- .../frontend/js_src/lib/components/Core/ContextLoader.tsx | 2 +- .../frontend/js_src/lib/components/InitialContext/index.ts | 3 ++- .../frontend/js_src/lib/components/InitialContext/stats.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Core/ContextLoader.tsx b/specifyweb/frontend/js_src/lib/components/Core/ContextLoader.tsx index 620c55f2295..f4538602eb6 100644 --- a/specifyweb/frontend/js_src/lib/components/Core/ContextLoader.tsx +++ b/specifyweb/frontend/js_src/lib/components/Core/ContextLoader.tsx @@ -17,7 +17,7 @@ const LOADING_TIMEOUT = 2 * SECOND; const fetchContext = ( context: Promise, - errorMode: 'crash' | 'console' + errorMode: 'console' | 'crash' ): (() => Promise) => async () => context.then(f.true).catch(errorMode === 'crash' ? crash : softFail); diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/index.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/index.ts index d3f94c9283f..a459b73f363 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/index.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/index.ts @@ -73,7 +73,8 @@ export const load = async (path: string, mimeType: MimeType): Promise => * not block or prevent access to Specify */ export const secondaryContext = Promise.all([ - /** REFACTOR: Move non-essential endpoints here from initialContext to speed + /** + * REFACTOR: Move non-essential endpoints here from initialContext to speed * up initial loading times. * Icon Definitions, Legacy UI Localization, Uniqueness Rules, and possibly * even Field Formatters and Remote Prefs can all theoretically be moved here. diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts index 47a182932a1..4bf4d164617 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/stats.ts @@ -1,8 +1,8 @@ -import { fetchContext as fetchSystemInfo } from './systemInfo'; import { ping } from '../../utils/ajax/ping'; import { softFail } from '../Errors/Crash'; import { formatUrl } from '../Router/queryString'; import { load } from './index'; +import { fetchContext as fetchSystemInfo } from './systemInfo'; type StatsCounts = { readonly Collectionobject: number; From 9dcfaeec39e11fba183e60cbbafa374e9acf0e54 Mon Sep 17 00:00:00 2001 From: melton-jason Date: Mon, 1 Dec 2025 09:36:55 -0600 Subject: [PATCH 3/4] refactor: Remove unnecessary async in systemInfo endpoint (cherry picked from commit 1cb0fac5d8b13cc89e5c8ebd2b175d665705712b) --- .../frontend/js_src/lib/components/InitialContext/systemInfo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts b/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts index c2893510082..e7729565eda 100644 --- a/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts +++ b/specifyweb/frontend/js_src/lib/components/InitialContext/systemInfo.ts @@ -28,7 +28,7 @@ let systemInfo: SystemInfo; export const fetchContext = load( '/context/system_info.json', 'application/json' -).then(async (data) => { +).then((data) => { systemInfo = data; return systemInfo; From f352eef2329e63496a93b667a751ad60c1a76e13 Mon Sep 17 00:00:00 2001 From: Grant Fitzsimmons <37256050+grantfitzsimmons@users.noreply.github.com> Date: Mon, 5 Jan 2026 21:48:22 +0000 Subject: [PATCH 4/4] Lint code with ESLint and Prettier Triggered by 568c60671b5a96aef1c8405d222d7fe7760e88ef on branch refs/heads/secondary-context --- .../lib/components/AppResources/Filters.tsx | 2 +- .../lib/components/Attachments/Plugin.tsx | 2 +- .../AttachmentsBulkImport/Upload.tsx | 2 +- .../lib/components/DataModel/businessRules.ts | 6 +- .../FormPlugins/__tests__/dateUtils.test.ts | 36 ++-- .../lib/components/Preferences/Aside.tsx | 3 +- .../Preferences/CollectionDefinitions.tsx | 9 +- .../lib/components/Preferences/index.tsx | 74 ++++--- .../lib/components/TreeView/Actions.tsx | 2 +- .../components/WbImportAttachments/index.tsx | 2 +- .../WbPlanView/__tests__/automapper.test.ts | 49 ++--- .../WbPlanView/__tests__/linesGetter.test.ts | 187 +++++++++--------- 12 files changed, 192 insertions(+), 182 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/AppResources/Filters.tsx b/specifyweb/frontend/js_src/lib/components/AppResources/Filters.tsx index 02811602936..44e458072e4 100644 --- a/specifyweb/frontend/js_src/lib/components/AppResources/Filters.tsx +++ b/specifyweb/frontend/js_src/lib/components/AppResources/Filters.tsx @@ -15,6 +15,7 @@ import { Input, Label } from '../Atoms/Form'; import { icons } from '../Atoms/Icons'; import { Link } from '../Atoms/Link'; import { Dialog } from '../Molecules/Dialog'; +import { hasPermission } from '../Permissions/helpers'; import { allAppResources, countAppResources, @@ -23,7 +24,6 @@ import { } from './filtersHelpers'; import type { AppResources } from './hooks'; import { appResourceSubTypes, appResourceTypes } from './types'; -import { hasPermission } from '../Permissions/helpers'; export function AppResourcesFilters({ initialResources, diff --git a/specifyweb/frontend/js_src/lib/components/Attachments/Plugin.tsx b/specifyweb/frontend/js_src/lib/components/Attachments/Plugin.tsx index 57084c9fd9c..3924016f65f 100644 --- a/specifyweb/frontend/js_src/lib/components/Attachments/Plugin.tsx +++ b/specifyweb/frontend/js_src/lib/components/Attachments/Plugin.tsx @@ -23,11 +23,11 @@ import { loadingBar } from '../Molecules'; import { Dialog } from '../Molecules/Dialog'; import { FilePicker } from '../Molecules/FilePicker'; import { ProtectedTable } from '../Permissions/PermissionDenied'; +import { collectionPreferences } from '../Preferences/collectionPreferences'; import { userPreferences } from '../Preferences/userPreferences'; import { AttachmentPluginSkeleton } from '../SkeletonLoaders/AttachmentPlugin'; import { attachmentSettingsPromise, uploadFile } from './attachments'; import { AttachmentViewer } from './Viewer'; -import { collectionPreferences } from '../Preferences/collectionPreferences'; export function AttachmentsPlugin( props: Parameters[0] diff --git a/specifyweb/frontend/js_src/lib/components/AttachmentsBulkImport/Upload.tsx b/specifyweb/frontend/js_src/lib/components/AttachmentsBulkImport/Upload.tsx index fa85db57fd3..e9b9bd05253 100644 --- a/specifyweb/frontend/js_src/lib/components/AttachmentsBulkImport/Upload.tsx +++ b/specifyweb/frontend/js_src/lib/components/AttachmentsBulkImport/Upload.tsx @@ -23,6 +23,7 @@ import { strictGetTable } from '../DataModel/tables'; import type { Attachment, Tables } from '../DataModel/types'; import { Dialog } from '../Molecules/Dialog'; import { hasPermission } from '../Permissions/helpers'; +import { collectionPreferences } from '../Preferences/collectionPreferences'; import { ActionState } from './ActionState'; import type { AttachmentUploadSpec, EagerDataSet } from './Import'; import { PerformAttachmentTask } from './PerformAttachmentTask'; @@ -39,7 +40,6 @@ import { saveForAttachmentUpload, validateAttachmentFiles, } from './utils'; -import { collectionPreferences } from '../Preferences/collectionPreferences'; async function prepareForUpload( dataSet: EagerDataSet, diff --git a/specifyweb/frontend/js_src/lib/components/DataModel/businessRules.ts b/specifyweb/frontend/js_src/lib/components/DataModel/businessRules.ts index 87a88209da6..fbbcd168f57 100644 --- a/specifyweb/frontend/js_src/lib/components/DataModel/businessRules.ts +++ b/specifyweb/frontend/js_src/lib/components/DataModel/businessRules.ts @@ -71,8 +71,10 @@ export class BusinessRuleManager { fieldName: string & (keyof SCHEMA['fields'] | keyof SCHEMA['toOneIndependent']) ): Promise>> { - // REFACTOR: When checkField is called directly, the promises are not - // added to the public pendingPromise + /* + * REFACTOR: When checkField is called directly, the promises are not + * added to the public pendingPromise + */ const field = this.resource.specifyTable.getField(fieldName); if (field === undefined) return []; diff --git a/specifyweb/frontend/js_src/lib/components/FormPlugins/__tests__/dateUtils.test.ts b/specifyweb/frontend/js_src/lib/components/FormPlugins/__tests__/dateUtils.test.ts index b2b20c07d2f..f38a72f6a8e 100644 --- a/specifyweb/frontend/js_src/lib/components/FormPlugins/__tests__/dateUtils.test.ts +++ b/specifyweb/frontend/js_src/lib/components/FormPlugins/__tests__/dateUtils.test.ts @@ -17,24 +17,24 @@ describe('getDateParser', () => { new Date() ) ).toMatchInlineSnapshot(` - { - "formatters": [ - [Function], - [Function], - ], - "max": "9999-12-31", - "minLength": 10, - "parser": [Function], - "required": false, - "title": "Required Format: MM/DD/YYYY.", - "type": "date", - "validators": [ - [Function], - ], - "value": "2022-08-31", - "whiteSpaceSensitive": false, - } -`)); + { + "formatters": [ + [Function], + [Function], + ], + "max": "9999-12-31", + "minLength": 10, + "parser": [Function], + "required": false, + "title": "Required Format: MM/DD/YYYY.", + "type": "date", + "validators": [ + [Function], + ], + "value": "2022-08-31", + "whiteSpaceSensitive": false, + } + `)); test('month-year', () => expect(getDateParser(undefined, 'month-year', undefined)) diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/Aside.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/Aside.tsx index 6aaa1115190..2e4e6cd813e 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/Aside.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/Aside.tsx @@ -7,7 +7,8 @@ import type { GetSet, WritableArray } from '../../utils/types'; import { Link } from '../Atoms/Link'; import { pathIsOverlay } from '../Router/UnloadProtect'; import { scrollIntoView } from '../TreeView/helpers'; -import { PreferenceType, usePrefDefinitions } from './index'; +import type { PreferenceType } from './index'; +import { usePrefDefinitions } from './index'; export function PreferencesAside({ activeCategory, diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx index 7af14f3f74c..e487b6876cc 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/CollectionDefinitions.tsx @@ -1,4 +1,5 @@ -import { LocalizedString } from 'typesafe-i18n'; +import type { LocalizedString } from 'typesafe-i18n'; + import { attachmentsText } from '../../localization/attachments'; import { preferencesText } from '../../localization/preferences'; import { queryText } from '../../localization/query'; @@ -8,13 +9,13 @@ import { treeText } from '../../localization/tree'; import { f } from '../../utils/functools'; import type { RA } from '../../utils/types'; import { ensure } from '../../utils/types'; +import { camelToHuman } from '../../utils/utils'; import { genericTables } from '../DataModel/tables'; -import { Tables } from '../DataModel/types'; +import type { Tables } from '../DataModel/types'; +import type { QueryView } from '../QueryBuilder/Header'; import type { StatLayout } from '../Statistics/types'; import type { GenericPreferences } from './types'; import { definePref } from './types'; -import { camelToHuman } from '../../utils/utils'; -import { QueryView } from '../QueryBuilder/Header'; const tableLabel = (tableName: keyof Tables): LocalizedString => genericTables[tableName]?.label ?? camelToHuman(tableName); diff --git a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx index 246bc9ffb34..2c8b96df308 100644 --- a/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/Preferences/index.tsx @@ -9,9 +9,11 @@ import type { LocalizedString } from 'typesafe-i18n'; import { usePromise } from '../../hooks/useAsyncState'; import { useBooleanState } from '../../hooks/useBooleanState'; import { commonText } from '../../localization/common'; +import { headerText } from '../../localization/header'; import { preferencesText } from '../../localization/preferences'; import { StringToJsx } from '../../localization/utils'; import { f } from '../../utils/functools'; +import type { IR } from '../../utils/types'; import { Container, H2, Key } from '../Atoms'; import { Button } from '../Atoms/Button'; import { className } from '../Atoms/className'; @@ -21,7 +23,13 @@ import { Submit } from '../Atoms/Submit'; import { LoadingContext, ReadOnlyContext } from '../Core/Contexts'; import { ErrorBoundary } from '../Errors/ErrorBoundary'; import { hasPermission } from '../Permissions/helpers'; +import { + ProtectedAction, + ProtectedTool, +} from '../Permissions/PermissionDenied'; import { PreferencesAside } from './Aside'; +import type { BasePreferences } from './BasePreferences'; +import { collectionPreferenceDefinitions } from './CollectionDefinitions'; import { collectionPreferences } from './collectionPreferences'; import { useDarkMode } from './Hooks'; import { DefaultPreferenceItemRender } from './Renderers'; @@ -29,14 +37,6 @@ import type { GenericPreferences, PreferenceItem } from './types'; import { userPreferenceDefinitions } from './UserDefinitions'; import { userPreferences } from './userPreferences'; import { useTopChild } from './useTopChild'; -import { IR } from '../../utils/types'; -import { headerText } from '../../localization/header'; -import { BasePreferences } from './BasePreferences'; -import { - ProtectedAction, - ProtectedTool, -} from '../Permissions/PermissionDenied'; -import { collectionPreferenceDefinitions } from './CollectionDefinitions'; export type PreferenceType = keyof typeof preferenceInstances; @@ -142,9 +142,9 @@ function Preferences({ > @@ -323,9 +323,7 @@ export function PreferencesContent({ />

{item.description !== undefined && ( -

+

{item.description !== undefined && ( { - return ( - - -

- {typeof title === 'function' ? title() : title} -

- {description !== undefined && ( -

- {typeof description === 'function' - ? description() - : description} -

- )} - {subCategories.map(([subcategory, data]) => - renderSubCategory(category, subcategory, data) - )} - - - ); - } + ) => ( + + +

+ {typeof title === 'function' ? title() : title} +

+ {description !== undefined && ( +

+ {typeof description === 'function' + ? description() + : description} +

+ )} + {subCategories.map(([subcategory, data]) => + renderSubCategory(category, subcategory, data) + )} +
+
+ ) )} ); @@ -484,7 +480,7 @@ function UserPrefItem(props: PreferenceItemProps) { props.subcategory as any, props.name as any ); - return ; + return ; } function CollectionPrefItem(props: PreferenceItemProps) { @@ -493,7 +489,7 @@ function CollectionPrefItem(props: PreferenceItemProps) { props.subcategory as any, props.name as any ); - return ; + return ; } function CollectionPreferences(): JSX.Element { diff --git a/specifyweb/frontend/js_src/lib/components/TreeView/Actions.tsx b/specifyweb/frontend/js_src/lib/components/TreeView/Actions.tsx index 7155fc0a9d7..28c3c8c7faf 100644 --- a/specifyweb/frontend/js_src/lib/components/TreeView/Actions.tsx +++ b/specifyweb/frontend/js_src/lib/components/TreeView/Actions.tsx @@ -20,9 +20,9 @@ import { DeleteButton } from '../Forms/DeleteButton'; import { Dialog } from '../Molecules/Dialog'; import { ResourceLink } from '../Molecules/ResourceLink'; import { hasPermission, hasTablePermission } from '../Permissions/helpers'; +import { collectionPreferences } from '../Preferences/collectionPreferences'; import type { Row } from './helpers'; import { checkMoveViolatesEnforced } from './helpers'; -import { collectionPreferences } from '../Preferences/collectionPreferences'; const treeActions = [ 'add', diff --git a/specifyweb/frontend/js_src/lib/components/WbImportAttachments/index.tsx b/specifyweb/frontend/js_src/lib/components/WbImportAttachments/index.tsx index 0f46b44f669..54abf29e444 100644 --- a/specifyweb/frontend/js_src/lib/components/WbImportAttachments/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbImportAttachments/index.tsx @@ -36,6 +36,7 @@ import { loadingBar } from '../Molecules'; import { Dialog } from '../Molecules/Dialog'; import { FilePicker } from '../Molecules/FilePicker'; import { Preview } from '../Molecules/FilePicker'; +import { collectionPreferences } from '../Preferences/collectionPreferences'; import { uniquifyDataSetName } from '../WbImport/helpers'; import { ChooseName } from '../WbImport/index'; import { @@ -43,7 +44,6 @@ import { attachmentsToCell, BASE_TABLE_NAME, } from '../WorkBench/attachmentHelpers'; -import { collectionPreferences } from '../Preferences/collectionPreferences'; export function WbImportAttachmentsView(): JSX.Element { useMenuItem('workBench'); diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/automapper.test.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/automapper.test.ts index f1838e3e7cf..801eca21018 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/automapper.test.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/automapper.test.ts @@ -27,16 +27,19 @@ theories( * TODO: The tests are mapping these Taxon headers to Component * over Determination. The issue is not happening within the * application - * */ - // 'Class', - // 'Superfamily', - // 'Family', - // 'Genus', - // 'Subgenus', - // 'Species', - // 'Subspecies', - // 'Species Author', - // 'Subspecies Author', + * + */ + /* + * 'Class', + * 'Superfamily', + * 'Family', + * 'Genus', + * 'Subgenus', + * 'Species', + * 'Subspecies', + * 'Species Author', + * 'Subspecies Author', + */ 'Who ID First Name', 'Determiner 1 Title', 'Determiner 1 First Name', @@ -184,18 +187,20 @@ theories( Latitude2: [['collectingEvent', 'locality', 'latitude2']], Longitude1: [['collectingEvent', 'locality', 'longitude1']], Longitude2: [['collectingEvent', 'locality', 'longitude2']], - // Class: [['determinations', '#1', 'taxon', '$Class', 'name']], - // Family: [['determinations', '#1', 'taxon', '$Family', 'name']], - // Genus: [['determinations', '#1', 'taxon', '$Genus', 'name']], - // Subgenus: [['determinations', '#1', 'taxon', '$Subgenus', 'name']], - // 'Species Author': [ - // ['determinations', '#1', 'taxon', '$Species', 'author'], - // ], - // Species: [['determinations', '#1', 'taxon', '$Species', 'name']], - // 'Subspecies Author': [ - // ['determinations', '#1', 'taxon', '$Subspecies', 'author'], - // ], - // Subspecies: [['determinations', '#1', 'taxon', '$Subspecies', 'name']], + /* + * Class: [['determinations', '#1', 'taxon', '$Class', 'name']], + * Family: [['determinations', '#1', 'taxon', '$Family', 'name']], + * Genus: [['determinations', '#1', 'taxon', '$Genus', 'name']], + * Subgenus: [['determinations', '#1', 'taxon', '$Subgenus', 'name']], + * 'Species Author': [ + * ['determinations', '#1', 'taxon', '$Species', 'author'], + * ], + * Species: [['determinations', '#1', 'taxon', '$Species', 'name']], + * 'Subspecies Author': [ + * ['determinations', '#1', 'taxon', '$Subspecies', 'author'], + * ], + * Subspecies: [['determinations', '#1', 'taxon', '$Subspecies', 'name']], + */ 'Prep Type 1': [['preparations', '#1', 'prepType', 'name']], Country: [ ['collectingEvent', 'locality', 'geography', '$Country', 'name'], diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/linesGetter.test.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/linesGetter.test.ts index d3703ff070f..8d804e02644 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/linesGetter.test.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/__tests__/linesGetter.test.ts @@ -21,16 +21,19 @@ theories(getLinesFromHeaders, [ * TODO: The tests are mapping these Taxon headers to Component * over Determination. The issue is not happening within the * application - * */ - // 'Class', - // 'Superfamily', - // 'Family', - // 'Genus', - // 'Subgenus', - // 'Species', - // 'Subspecies', - // 'Species Author', - // 'Subspecies Author', + * + */ + /* + * 'Class', + * 'Superfamily', + * 'Family', + * 'Genus', + * 'Subgenus', + * 'Species', + * 'Subspecies', + * 'Species Author', + * 'Subspecies Author', + */ ], runAutoMapper: true, baseTableName: 'CollectionObject', @@ -46,87 +49,89 @@ theories(getLinesFromHeaders, [ default: null, }, }, - // { - // mappingPath: ['determinations', '#1', 'taxon', '$Class', 'name'], - // headerName: 'Class', - // columnOptions: { - // matchBehavior: 'ignoreNever', - // nullAllowed: true, - // default: null, - // }, - // }, - // { - // mappingPath: [emptyMapping], - // headerName: 'Superfamily', - // columnOptions: { - // matchBehavior: 'ignoreNever', - // nullAllowed: true, - // default: null, - // }, - // }, - // { - // mappingPath: ['determinations', '#1', 'taxon', '$Family', 'name'], - // headerName: 'Family', - // columnOptions: { - // matchBehavior: 'ignoreNever', - // nullAllowed: true, - // default: null, - // }, - // }, - // { - // mappingPath: ['determinations', '#1', 'taxon', '$Genus', 'name'], - // headerName: 'Genus', - // columnOptions: { - // matchBehavior: 'ignoreNever', - // nullAllowed: true, - // default: null, - // }, - // }, - // { - // mappingPath: ['determinations', '#1', 'taxon', '$Subgenus', 'name'], - // headerName: 'Subgenus', - // columnOptions: { - // matchBehavior: 'ignoreNever', - // nullAllowed: true, - // default: null, - // }, - // }, - // { - // mappingPath: ['determinations', '#1', 'taxon', '$Species', 'name'], - // headerName: 'Species', - // columnOptions: { - // matchBehavior: 'ignoreNever', - // nullAllowed: true, - // default: null, - // }, - // }, - // { - // mappingPath: ['determinations', '#1', 'taxon', '$Subspecies', 'name'], - // headerName: 'Subspecies', - // columnOptions: { - // matchBehavior: 'ignoreNever', - // nullAllowed: true, - // default: null, - // }, - // }, - // { - // mappingPath: ['determinations', '#1', 'taxon', '$Species', 'author'], - // headerName: 'Species Author', - // columnOptions: { - // matchBehavior: 'ignoreNever', - // nullAllowed: true, - // default: null, - // }, - // }, - // { - // mappingPath: ['determinations', '#1', 'taxon', '$Subspecies', 'author'], - // headerName: 'Subspecies Author', - // columnOptions: { - // matchBehavior: 'ignoreNever', - // nullAllowed: true, - // default: null, - // }, - // }, + /* + * { + * mappingPath: ['determinations', '#1', 'taxon', '$Class', 'name'], + * headerName: 'Class', + * columnOptions: { + * matchBehavior: 'ignoreNever', + * nullAllowed: true, + * default: null, + * }, + * }, + * { + * mappingPath: [emptyMapping], + * headerName: 'Superfamily', + * columnOptions: { + * matchBehavior: 'ignoreNever', + * nullAllowed: true, + * default: null, + * }, + * }, + * { + * mappingPath: ['determinations', '#1', 'taxon', '$Family', 'name'], + * headerName: 'Family', + * columnOptions: { + * matchBehavior: 'ignoreNever', + * nullAllowed: true, + * default: null, + * }, + * }, + * { + * mappingPath: ['determinations', '#1', 'taxon', '$Genus', 'name'], + * headerName: 'Genus', + * columnOptions: { + * matchBehavior: 'ignoreNever', + * nullAllowed: true, + * default: null, + * }, + * }, + * { + * mappingPath: ['determinations', '#1', 'taxon', '$Subgenus', 'name'], + * headerName: 'Subgenus', + * columnOptions: { + * matchBehavior: 'ignoreNever', + * nullAllowed: true, + * default: null, + * }, + * }, + * { + * mappingPath: ['determinations', '#1', 'taxon', '$Species', 'name'], + * headerName: 'Species', + * columnOptions: { + * matchBehavior: 'ignoreNever', + * nullAllowed: true, + * default: null, + * }, + * }, + * { + * mappingPath: ['determinations', '#1', 'taxon', '$Subspecies', 'name'], + * headerName: 'Subspecies', + * columnOptions: { + * matchBehavior: 'ignoreNever', + * nullAllowed: true, + * default: null, + * }, + * }, + * { + * mappingPath: ['determinations', '#1', 'taxon', '$Species', 'author'], + * headerName: 'Species Author', + * columnOptions: { + * matchBehavior: 'ignoreNever', + * nullAllowed: true, + * default: null, + * }, + * }, + * { + * mappingPath: ['determinations', '#1', 'taxon', '$Subspecies', 'author'], + * headerName: 'Subspecies Author', + * columnOptions: { + * matchBehavior: 'ignoreNever', + * nullAllowed: true, + * default: null, + * }, + * }, + */ ], }, {