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,
+ * },
+ * },
+ */
],
},
{