From def26cd62d2247f85e4b9b1fc676b09dd344cf29 Mon Sep 17 00:00:00 2001 From: alec_dev Date: Tue, 21 Oct 2025 12:07:50 -0500 Subject: [PATCH 1/5] Special case for disambiguation: Host taxon under CollectionObject --- .../js_src/lib/components/WbPlanView/mappingPreview.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts index fec15a14a18..4ef7d1eb24f 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts @@ -171,7 +171,13 @@ export function generateMappingPathPreview( ? [parentTableOrTreeName, tableNameNonEmpty] : [tableNameNonEmpty]; + // Special case for disambiguation: Host taxon under CollectionObject + const isHostTaxonCase = + baseTableName === 'CollectionObject' && + parentTableOrTreeName === 'Host taxon'; + return filterArray([ + ...(isHostTaxonCase ? ['Host'] : []), ...(valueIsTreeRank(databaseTableOrRankName) ? [isAnyRank ? parentTableOrTreeName : tableOrRankName] : tableNameFormatted), From 605765db1f863381db0ecd76644d178473aa01b5 Mon Sep 17 00:00:00 2001 From: alec_dev Date: Tue, 21 Oct 2025 15:27:00 -0500 Subject: [PATCH 2/5] configure tree query join cache to use sqlalchemy alias --- .../backend/stored_queries/query_construct.py | 43 ++++++++++++------- .../backend/stored_queries/queryfieldspec.py | 19 +++++--- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/specifyweb/backend/stored_queries/query_construct.py b/specifyweb/backend/stored_queries/query_construct.py index 848795bf7e2..ad10c5b9512 100644 --- a/specifyweb/backend/stored_queries/query_construct.py +++ b/specifyweb/backend/stored_queries/query_construct.py @@ -2,6 +2,8 @@ from collections import namedtuple, deque from sqlalchemy import orm, sql, or_ +from sqlalchemy import inspect +from sqlalchemy.orm.util import AliasedClass import specifyweb.specify.models as spmodels from specifyweb.backend.trees.utils import get_treedefs @@ -38,38 +40,46 @@ def handle_tree_field(self, node, table, tree_rank: TreeRankQuery, next_join_pat treedefitem_column = table.name + 'TreeDefItemID' treedef_column = table.name + 'TreeDefID' - if (table, 'TreeRanks') in query.join_cache: - logger.debug("using join cache for %r tree ranks.", table) - ancestors, treedefs = query.join_cache[(table, 'TreeRanks')] + # Ensure 'node' is an ORM alias (start anchor). If it’s a mapped class, alias it. + start_alias = node if isinstance(node, AliasedClass) else orm.aliased(node) + + # Use the specific start alias in the join cache key so different branches don't collide + cache_key = (start_alias, "TreeRanks") + + if cache_key in query.join_cache: + logger.debug("using join cache for %r tree ranks.", start_alias) + ancestors, treedefs = query.join_cache[cache_key] else: - treedefs = get_treedefs(query.collection, table.name) # We need to take the max here. Otherwise, it is possible that the same rank # name may not occur at the same level across tree defs. max_depth = max(depth for _, depth in treedefs) - - ancestors = [node] - for _ in range(max_depth-1): - ancestor = orm.aliased(node) + + # Start ancestry from the provided alias (e.g., HostTaxon alias) + ancestors = [start_alias] + + # Create new aliases of the *mapped class* behind the start alias + mapped_cls = inspect(start_alias).mapper.class_ + + for _ in range(max_depth - 1): + ancestor = orm.aliased(mapped_cls) query = query.outerjoin(ancestor, ancestors[-1].ParentID == ancestor._id) ancestors.append(ancestor) - - logger.debug("adding to join cache for %r tree ranks.", table) + logger.debug("adding to join cache for %r tree ranks.", start_alias) query = query._replace(join_cache=query.join_cache.copy()) - query.join_cache[(table, 'TreeRanks')] = (ancestors, treedefs) + query.join_cache[cache_key] = (ancestors, treedefs) item_model = getattr(spmodels, table.django_name + "treedefitem") # TODO: optimize out the ranks that appear? cache them - treedefs_with_ranks: list[tuple[int, int]] = [tup for tup in [ - (treedef_id, _safe_filter(item_model.objects.filter(treedef_id=treedef_id, name=tree_rank.name).values_list('id', flat=True))) + treedefs_with_ranks: list[tuple[int, int]] = [ + (treedef_id, _safe_filter(item_model.objects.filter(treedef_id=treedef_id, name=tree_rank.name).values_list("id", flat=True))) for treedef_id, _ in treedefs # For constructing tree queries for batch edit if (tree_rank.treedef_id is None or tree_rank.treedef_id == treedef_id) - ] if tup[1] is not None] - + ] assert len(treedefs_with_ranks) >= 1, "Didn't find the tree rank across any tree" treedefitem_params = [treedefitem_id for (_, treedefitem_id) in treedefs_with_ranks] @@ -96,7 +106,8 @@ def make_tree_field_spec(tree_node): # We don't want to include treedef if the rank is not present. new_filters = [ *query.internal_filters, - or_(getattr(node, treedef_column).in_(defs_to_filter_on), getattr(node, treedef_column) == None)] + or_(getattr(start_alias, treedef_column).in_(defs_to_filter_on), getattr(start_alias, treedef_column) == None), + ] query = query._replace(internal_filters=new_filters) return query, column, field, table diff --git a/specifyweb/backend/stored_queries/queryfieldspec.py b/specifyweb/backend/stored_queries/queryfieldspec.py index e09728719f6..da214d4994e 100644 --- a/specifyweb/backend/stored_queries/queryfieldspec.py +++ b/specifyweb/backend/stored_queries/queryfieldspec.py @@ -459,17 +459,26 @@ def add_spec_to_query( cycle_detector, ) else: - query, orm_model, table, field = self.build_join(query, self.join_path) - if isinstance(field, TreeRankQuery): - tree_rank_idx = self.join_path.index(field) + tree_rank_idxs = [i for i, n in enumerate(self.join_path) if isinstance(n, TreeRankQuery)] + if tree_rank_idxs: + tree_rank_idx = tree_rank_idxs[0] + prefix = self.join_path[:tree_rank_idx] # up to (but not including) the tree-rank node + tree_rank_node = self.join_path[tree_rank_idx] + suffix = self.join_path[tree_rank_idx + 1 :] # field after the rank, e.g., "Name" + + # Join only the prefix to obtain the correct starting alias (e.g., HostTaxon) + query, orm_model, table, _ = self.build_join(query, prefix) + + # Build the CASE/joins for the tree rank starting at that alias query, orm_field, field, table = query.handle_tree_field( orm_model, table, - field, - self.join_path[tree_rank_idx + 1 :], + tree_rank_node, + suffix, self, ) else: + query, orm_model, table, field = self.build_join(query, self.join_path) try: field_name = self.get_field().name orm_field = getattr(orm_model, field_name) From 7d02ec26b7ad46096e45884398bccd6e0978e239 Mon Sep 17 00:00:00 2001 From: alec_dev Date: Thu, 23 Oct 2025 09:49:23 -0500 Subject: [PATCH 3/5] fix anchor alias for join cache --- .../backend/stored_queries/query_construct.py | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/specifyweb/backend/stored_queries/query_construct.py b/specifyweb/backend/stored_queries/query_construct.py index ad10c5b9512..230e7b788a4 100644 --- a/specifyweb/backend/stored_queries/query_construct.py +++ b/specifyweb/backend/stored_queries/query_construct.py @@ -4,6 +4,7 @@ from sqlalchemy import orm, sql, or_ from sqlalchemy import inspect from sqlalchemy.orm.util import AliasedClass +from sqlalchemy.inspection import inspect as sa_inspect import specifyweb.specify.models as spmodels from specifyweb.backend.trees.utils import get_treedefs @@ -31,19 +32,24 @@ def __new__(cls, *args, **kwargs): def handle_tree_field(self, node, table, tree_rank: TreeRankQuery, next_join_path, current_field_spec: QueryFieldSpec): query = self - if query.collection is None: raise AssertionError( # Not sure it makes sense to query across collections - f"No Collection found in Query for {table}", - {"table" : table, - "localizationKey" : "noCollectionInQuery"}) - logger.info('handling treefield %s rank: %s field: %s', table, tree_rank.name, next_join_path) + if query.collection is None: + raise AssertionError( + f"No Collection found in Query for {table}", + {"table": table, "localizationKey": "noCollectionInQuery"}, + ) + logger.info("handling treefield %s rank: %s field: %s", table, tree_rank.name, next_join_path) treedefitem_column = table.name + 'TreeDefItemID' treedef_column = table.name + 'TreeDefID' - # Ensure 'node' is an ORM alias (start anchor). If it’s a mapped class, alias it. - start_alias = node if isinstance(node, AliasedClass) else orm.aliased(node) + # Determine starting anchor correctly: + # If node is already an alias (from a relationship path), use that alias. + # If node is the mapped class (base table), don't alias it, start from the base table. + is_alias = isinstance(node, AliasedClass) + start_alias = node # keep as-is + mapped_cls = sa_inspect(node).mapper.class_ if is_alias else node - # Use the specific start alias in the join cache key so different branches don't collide + # Use the specific start anchor in the cache key, so each branch has its own chain cache_key = (start_alias, "TreeRanks") if cache_key in query.join_cache: @@ -51,17 +57,12 @@ def handle_tree_field(self, node, table, tree_rank: TreeRankQuery, next_join_pat ancestors, treedefs = query.join_cache[cache_key] else: treedefs = get_treedefs(query.collection, table.name) - - # We need to take the max here. Otherwise, it is possible that the same rank - # name may not occur at the same level across tree defs. max_depth = max(depth for _, depth in treedefs) # Start ancestry from the provided alias (e.g., HostTaxon alias) ancestors = [start_alias] - # Create new aliases of the *mapped class* behind the start alias - mapped_cls = inspect(start_alias).mapper.class_ - + # Build parent chain using aliases of the mapped class for _ in range(max_depth - 1): ancestor = orm.aliased(mapped_cls) query = query.outerjoin(ancestor, ancestors[-1].ParentID == ancestor._id) From 05e9641ed0338d1c938cf1411fef4d73f8f88eb4 Mon Sep 17 00:00:00 2001 From: alec_dev Date: Tue, 23 Dec 2025 18:54:02 +0000 Subject: [PATCH 4/5] Lint code with ESLint and Prettier Triggered by df0d024fc040684cf9233651877845a26daadfd3 on branch refs/heads/issue-7436 --- .../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, + * }, + * }, + */ ], }, { From 24bcedeeec5a188937c6b7144ad5617ed84dc965 Mon Sep 17 00:00:00 2001 From: alec_dev Date: Fri, 2 Jan 2026 10:18:39 -0600 Subject: [PATCH 5/5] host taxon disambiguation case sensitivity --- .../frontend/js_src/lib/components/WbPlanView/mappingPreview.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts index 4ef7d1eb24f..7f3b6b1aa2e 100644 --- a/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts +++ b/specifyweb/frontend/js_src/lib/components/WbPlanView/mappingPreview.ts @@ -174,7 +174,7 @@ export function generateMappingPathPreview( // Special case for disambiguation: Host taxon under CollectionObject const isHostTaxonCase = baseTableName === 'CollectionObject' && - parentTableOrTreeName === 'Host taxon'; + (parentTableOrTreeName ?? '').trim().toLowerCase() === 'host taxon'; return filterArray([ ...(isHostTaxonCase ? ['Host'] : []),