diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 3bf56ca420..12375b58dd 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.58.5", + "version": "7.58.6-fb-issue1470.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.58.5", + "version": "7.58.6-fb-issue1470.1", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", diff --git a/packages/components/package.json b/packages/components/package.json index edf2eb6e44..852adf1b7f 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.58.5", + "version": "7.58.6-fb-issue1470.1", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 7e65104f96..9ff3178493 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -858,6 +858,12 @@ import { EditInlineField } from './internal/components/EditInlineField'; import { FileAttachmentArea } from './internal/components/files/FileAttachmentArea'; import { Discussions } from './internal/announcements/Discussions'; import { useModalState, useNotAuthorized, useNotFound, usePortalRef, useTimeout } from './internal/hooks'; +import { + useCanonicalQueryName, + UseCanonicalQueryName, + useSampleTypeParam, + useSourceTypeParam, +} from './internal/components/entities/useEntityTypeParam'; import { TEST_BIO_LIMS_ENTERPRISE_MODULE_CONTEXT, TEST_BIO_LIMS_STARTER_MODULE_CONTEXT, @@ -1105,6 +1111,7 @@ const App = { const Hooks = { useAppContext, + useCanonicalQueryName, useContainerPath, useContainerUser, useEnterEscape, @@ -1781,6 +1788,7 @@ export { useAdministrationSubNav, useAppContext, useAppNavigate, + useCanonicalQueryName, useContainerPath, useContainerUser, useDataChangeCommentsRequired, @@ -1809,8 +1817,10 @@ export { UserProfile, UserSelectInput, UsersGridPanel, + useSampleTypeParam, useServerContext, useServerContextDispatch, + useSourceTypeParam, useSubNavTabsContext, useTimeout, useUserProperties, @@ -1935,6 +1945,7 @@ export type { IDataViewInfo } from './internal/DataViewInfo'; export type { BSStyle } from './internal/dropdowns'; export type { MenuSectionItem } from './internal/DropdownSection'; export type { UseTimeout } from './internal/hooks'; +export type { UseCanonicalQueryName } from './internal/components/entities/useEntityTypeParam'; export type { ModalProps } from './internal/Modal'; export type { AddEntitiesComplete, ModalRendererProps } from './internal/ModalRenderFactory'; export type { TriggerType } from './internal/OverlayTrigger'; diff --git a/packages/components/src/internal/actions.ts b/packages/components/src/internal/actions.ts index 0a1d3d664b..5d1d78d82a 100644 --- a/packages/components/src/internal/actions.ts +++ b/packages/components/src/internal/actions.ts @@ -607,7 +607,7 @@ export function saveGridView( containerPath, views: [{ ...ViewInfo.serialize(viewInfo), replace, session, inherit, shared, hidden }], success: () => { - invalidateQueryDetailsCache(schemaQuery, containerPath); + invalidateQueryDetailsCache(schemaQuery); resolve(); }, failure: response => { @@ -640,7 +640,7 @@ export function saveSessionView( hidden: false, replace, success: () => { - invalidateQueryDetailsCache(schemaQuery, containerPath); + invalidateQueryDetailsCache(schemaQuery); resolve(); }, failure: response => { @@ -720,12 +720,12 @@ export function deleteView( containerPath, revert, success: () => { - invalidateQueryDetailsCache(schemaQuery, containerPath); + invalidateQueryDetailsCache(schemaQuery); resolve(); }, failure: response => { if (response.exceptionClass === VIEW_NOT_FOUND_EXCEPTION_CLASS) { - invalidateQueryDetailsCache(schemaQuery, containerPath); + invalidateQueryDetailsCache(schemaQuery); resolve(); // view has already been deleted } else { console.error(response); @@ -760,7 +760,7 @@ export function renameGridView( newName, }, success: Utils.getCallbackWrapper(response => { - invalidateQueryDetailsCache(schemaQuery, containerPath); + invalidateQueryDetailsCache(schemaQuery); resolve(); }), failure: Utils.getCallbackWrapper(error => { diff --git a/packages/components/src/internal/components/entities/useEntityTypeParam.ts b/packages/components/src/internal/components/entities/useEntityTypeParam.ts new file mode 100644 index 0000000000..9d80ab515c --- /dev/null +++ b/packages/components/src/internal/components/entities/useEntityTypeParam.ts @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2026 LabKey Corporation. All rights reserved. No portion of this work may be reproduced in + * any form or by any electronic or mechanical means without written permission from LabKey Corporation. + */ +import { useEffect, useMemo, useState } from 'react'; +import { useParams } from 'react-router'; + +import { isLoading, LoadingState } from '../../../public/LoadingState'; +import { QueryInfo } from '../../../public/QueryInfo'; +import { SchemaQuery } from '../../../public/SchemaQuery'; +import { useAppContext } from '../../AppContext'; +import { resolveErrorMessage } from '../../util/messaging'; +import { SCHEMAS } from '../../schemas'; + +export interface UseCanonicalQueryName { + error?: string; + isLoaded: boolean; + notFound: boolean; + queryInfo?: QueryInfo; + queryName?: string; + schemaName?: string; + schemaQuery?: SchemaQuery; +} + +/** + * Resolves a possibly wrong-case schema/query name (e.g. a route param) to the server's canonical case via the cached getQueryDetails. + * `notFound` is set when the type does not exist, to drive a NotFound page. + */ +export function useCanonicalQueryName( + schemaName: string, + rawQueryName: string, + containerPath?: string +): UseCanonicalQueryName { + const { api } = useAppContext(); + const [queryInfo, setQueryInfo] = useState(); + const [error, setError] = useState(); + const [notFound, setNotFound] = useState(false); + const [loadingState, setLoadingState] = useState(LoadingState.INITIALIZED); + + useEffect(() => { + setQueryInfo(undefined); + setError(undefined); + setNotFound(false); + + if (!schemaName || !rawQueryName) return; + + (async () => { + setLoadingState(LoadingState.LOADING); + try { + const queryInfo_ = await api.query.getQueryDetails({ + schemaName, + queryName: rawQueryName, + containerPath, + }); + setQueryInfo(queryInfo_); + } catch (e) { + setNotFound(true); + setError(resolveErrorMessage(e)); + } finally { + setLoadingState(LoadingState.LOADED); + } + })(); + }, [api, schemaName, rawQueryName, containerPath]); + + return useMemo( + () => ({ + error, + isLoaded: !isLoading(loadingState), + notFound, + queryInfo, + queryName: queryInfo?.name, + schemaName: queryInfo?.schemaQuery?.schemaName, + schemaQuery: queryInfo?.schemaQuery, + }), + [error, loadingState, notFound, queryInfo] + ); +} + +export function useSampleTypeParam(containerPath?: string): UseCanonicalQueryName { + const { sampleType } = useParams(); + return useCanonicalQueryName(SCHEMAS.SAMPLE_SETS.SCHEMA, sampleType, containerPath); +} + +export function useSourceTypeParam(entityTypeKey = 'sourceType', containerPath?: string): UseCanonicalQueryName { + const params = useParams(); + return useCanonicalQueryName(SCHEMAS.DATA_CLASSES.SCHEMA, params[entityTypeKey], containerPath); +} diff --git a/packages/components/src/internal/query/api.ts b/packages/components/src/internal/query/api.ts index 8ffe6c7bde..df5f5a3a5f 100644 --- a/packages/components/src/internal/query/api.ts +++ b/packages/components/src/internal/query/api.ts @@ -64,22 +64,13 @@ function getQueryDetailsCacheKey( export function invalidateQueryDetailsCache( schemaQuery: SchemaQuery, - containerPath?: string, - fk?: string, - fields?: string | string[], - exactKeyMatch = false ): void { - if (exactKeyMatch) { - const key = getQueryDetailsCacheKey(schemaQuery, containerPath, fk, fields); - delete queryDetailsCache[key]; - } else { - const prefix = getQueryDetailsCacheKey(schemaQuery); - Object.keys(queryDetailsCache).forEach(cacheKey => { - if (cacheKey.startsWith(prefix)) { - delete queryDetailsCache[cacheKey]; - } - }); - } + const prefix = getQueryDetailsCacheKey(schemaQuery); + Object.keys(queryDetailsCache).forEach(cacheKey => { + if (cacheKey.toLowerCase().startsWith(prefix.toLowerCase())) { + delete queryDetailsCache[cacheKey]; + } + }); } interface GetQueryDetailsBasic extends Omit< @@ -127,7 +118,7 @@ export function getQueryDetails(options: GetQueryDetailsOptions): Promise { console.error(error); - invalidateQueryDetailsCache(schemaQuery, containerPath, fk, fields); + invalidateQueryDetailsCache(schemaQuery); reject({ message: error.exception, exceptionClass: error.exceptionClass, diff --git a/packages/components/src/public/QueryModel/QueryModel.ts b/packages/components/src/public/QueryModel/QueryModel.ts index dc774134b3..f331ed474e 100644 --- a/packages/components/src/public/QueryModel/QueryModel.ts +++ b/packages/components/src/public/QueryModel/QueryModel.ts @@ -540,11 +540,11 @@ export class QueryModel { } get schemaName(): string { - return this.schemaQuery.schemaName; + return (this.queryInfo?.schemaQuery ?? this.schemaQuery).schemaName; } get queryName(): string { - return this.schemaQuery.queryName; + return (this.queryInfo?.schemaQuery ?? this.schemaQuery).queryName; } get viewName(): string {