diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 860a8391ed..e30b2f282d 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.23.1", + "version": "7.23.2-deriveActions.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.23.1", + "version": "7.23.2-deriveActions.9", "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 bd62ccf582..f31c290ddf 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.23.1", + "version": "7.23.2-deriveActions.9", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index 4321f482a8..da41829db8 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -1,6 +1,13 @@ # @labkey/components Components, models, actions, and utility functions for LabKey applications and pages +### version TBD +*Released*: TBD +- Update `isAllSamplesSchema` to account for move of `JobInputSamples` to `workflow` schema +- Add placement prop for `DisableableButton` +- add `fitlerArrayToString` method in QueryModel utils +- add `pronoun` utility method for the it/they or it/them text choices + ### version 7.23.1 *Released*: 11 March 2026 - Merge from release26.3-SNAPSHOT to develop diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 30edb6a55d..085da4c6f3 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -69,6 +69,7 @@ import { makeCommaSeparatedString, parseCsvString, parseScientificInt, + pronoun, quoteValueWithDelimiters, setIsTestEnv, uncapitalizeFirstChar, @@ -541,6 +542,7 @@ import { createOrderedSnapshotSelectionKey, createSnapshotSelectionKey, createSnapshotSelectionKeyStr, + filterArrayToString, runDetailsColumnsForQueryModel, } from './public/QueryModel/utils'; import { CONFIRM_MESSAGE, useRouteLeave } from './internal/util/RouteLeave'; @@ -1299,6 +1301,7 @@ export { FileInput, FileTree, FilterAction, + filterArrayToString, FilterCriteriaRenderer, FilterStatus, FIND_BY_IDS_QUERY_PARAM, @@ -1592,6 +1595,7 @@ export { ProductMenuModel, ProductNavigationMenu, Progress, + pronoun, pushParameters, QUERY_UPDATE_AUDIT_QUERY, QueryColumn, diff --git a/packages/components/src/internal/app/constants.ts b/packages/components/src/internal/app/constants.ts index 3260c60813..e43e2125dc 100644 --- a/packages/components/src/internal/app/constants.ts +++ b/packages/components/src/internal/app/constants.ts @@ -198,6 +198,6 @@ export const FREEZER_MANAGER_APP_PROPERTIES: AppProperties = { export const APPLICATION_PROPERTIES = { [FREEZER_MANAGER_PRODUCT_ID]: FREEZER_MANAGER_APP_PROPERTIES, [SAMPLE_MANAGER_PRODUCT_ID]: SAMPLE_MANAGER_APP_PROPERTIES, - [LIMS_PRODUCT_ID] : LIMS_APP_PROPERTIES, + [LIMS_PRODUCT_ID]: LIMS_APP_PROPERTIES, [BIOLOGICS_PRODUCT_ID]: BIOLOGICS_APP_PROPERTIES -} +}; diff --git a/packages/components/src/internal/components/buttons/DisableableButton.tsx b/packages/components/src/internal/components/buttons/DisableableButton.tsx index 83c5b54ecd..8e96b0794e 100644 --- a/packages/components/src/internal/components/buttons/DisableableButton.tsx +++ b/packages/components/src/internal/components/buttons/DisableableButton.tsx @@ -1,4 +1,4 @@ -import React, { memo, FC, useMemo, PropsWithChildren } from 'react'; +import React, { FC, memo, PropsWithChildren, useMemo } from 'react'; import { createPortal } from 'react-dom'; @@ -10,11 +10,12 @@ interface Props extends PropsWithChildren { className?: string; disabledMsg?: string; onClick?: () => void; + placement?: 'bottom' | 'left' | 'right' | 'top'; title?: string; } export const DisableableButton: FC = memo(props => { - const { bsStyle = 'default', children, className = '', disabledMsg, onClick, title } = props; + const { bsStyle = 'default', children, className = '', disabledMsg, onClick, placement = 'bottom', title } = props; const { onMouseEnter, onMouseLeave, portalEl, show, targetRef } = useOverlayTriggerState( 'disabled-button-overlay', disabledMsg !== undefined, @@ -22,11 +23,11 @@ export const DisableableButton: FC = memo(props => { ); const popover = useMemo( () => ( - + {disabledMsg} ), - [disabledMsg, targetRef, title] + [disabledMsg, placement, targetRef, title] ); // Note: we use onPointerEnter/Leave so events propagate when the button is disabled @@ -37,8 +38,8 @@ export const DisableableButton: FC = memo(props => { onClick={onClick} onPointerEnter={onMouseEnter} onPointerLeave={onMouseLeave} - type="button" ref={targetRef} + type="button" > {children} {show && createPortal(popover, portalEl)} diff --git a/packages/components/src/internal/components/entities/EntityMoveModal.tsx b/packages/components/src/internal/components/entities/EntityMoveModal.tsx index a7261a24f9..772ad7f4fe 100644 --- a/packages/components/src/internal/components/entities/EntityMoveModal.tsx +++ b/packages/components/src/internal/components/entities/EntityMoveModal.tsx @@ -6,7 +6,7 @@ import { LoadingSpinner } from '../base/LoadingSpinner'; import { Alert } from '../base/Alert'; import { Container } from '../base/models/Container'; import { useNotificationsContext } from '../notifications/NotificationsContext'; -import { capitalizeFirstChar, makeCommaSeparatedString } from '../../util/utils'; +import { capitalizeFirstChar, makeCommaSeparatedString, pronoun } from '../../util/utils'; import { HelpLink, MOVE_SAMPLES_TOPIC } from '../../util/helpLinks'; import { isLoading, LoadingState } from '../../../public/LoadingState'; import { AppURL } from '../../url/AppURL'; @@ -241,12 +241,12 @@ export const getMoveConfirmationProperties = ( text = `${text} ${noun} will be moved.`; } else { const cannotMoveNoun = numCannotMove === 1 ? nounSingular : nounPlural; - const pronoun = numCannotMove === 1 ? 'it' : 'they'; + const _pronoun = pronoun(numCannotMove, 'they'); const verb = numCannotMove === 1 ? 'has' : 'have'; const parts = []; if (numNotPermitted > 0) parts.push('you lack the proper permissions'); - if (numNotAllowed > 0) parts.push(`${pronoun} ${verb} a status or related data that prevents moving`); - if (numMissing > 0) parts.push(`${pronoun} may have been deleted`); + if (numNotAllowed > 0) parts.push(`${_pronoun} ${verb} a status or related data that prevents moving`); + if (numMissing > 0) parts.push(`${_pronoun} may have been deleted`); const error = makeCommaSeparatedString(parts, ', or ', '.'); if (numCanMove === 0) { diff --git a/packages/components/src/internal/components/entities/actions.ts b/packages/components/src/internal/components/entities/actions.ts index 1c521d9b51..9baf3c6257 100644 --- a/packages/components/src/internal/components/entities/actions.ts +++ b/packages/components/src/internal/components/entities/actions.ts @@ -620,7 +620,6 @@ export async function getFolderConfigurableEntityTypeOptions( * @param targetQueryName the name of the listing schema query that represents the initial target for creation. * @param allowParents are parents of this entity type allowed or not * @param isItemSamples use the selectionKey from inventory.items table to query sample parents - * @param combineParentTypes */ export function getEntityTypeData( model: EntityIdCreationModel, diff --git a/packages/components/src/internal/components/forms/BulkUpdateForm.tsx b/packages/components/src/internal/components/forms/BulkUpdateForm.tsx index bf9598466b..9b63c052a6 100644 --- a/packages/components/src/internal/components/forms/BulkUpdateForm.tsx +++ b/packages/components/src/internal/components/forms/BulkUpdateForm.tsx @@ -15,6 +15,7 @@ import { getCommonDataValues, getUpdatedData, makeCommaSeparatedString, + pronoun, } from '../../util/utils'; import { ComponentsAPIWrapper } from '../../APIWrapper'; @@ -66,8 +67,7 @@ export const SelectionWarning: FC = props => { } if (missingCount > 0) { - const pronoun = missingCount > 1 ? 'they' : 'it'; - messages.push(`Cannot edit ${missingCount} of the selected ${nounPlural}, ${pronoun} may have been deleted.`); + messages.push(`Cannot edit ${missingCount} of the selected ${nounPlural}, ${pronoun(missingCount, 'they')} may have been deleted.`); } if (messages.length === 0) return null; @@ -102,12 +102,11 @@ export function errorMessage( if (missingCount + notPermittedCount !== selectedCount) return undefined; const noun = selectedCount > 1 ? nounPlural : nounSingular; - const pronoun = selectedCount > 1 ? 'they' : 'it'; const parts = []; if (notPermittedCount > 0) parts.push(`you do not have the required permissions`); - if (missingCount > 0) parts.push(`${pronoun} may have been deleted`); + if (missingCount > 0) parts.push(`${pronoun(selectedCount, 'they')} may have been deleted`); return `Cannot edit selected ${noun}, ${makeCommaSeparatedString(parts, ', or ', '.')}`; } diff --git a/packages/components/src/internal/components/samples/utils.tsx b/packages/components/src/internal/components/samples/utils.tsx index 0a9557aa5a..a3cb94de43 100644 --- a/packages/components/src/internal/components/samples/utils.tsx +++ b/packages/components/src/internal/components/samples/utils.tsx @@ -276,11 +276,13 @@ export function isAllSamplesSchema(schemaQuery: SchemaQuery): boolean { return true; if (lcSchemaName === SCHEMAS.SAMPLE_MANAGEMENT.SCHEMA) { - return ( - lcQueryName === SCHEMAS.SAMPLE_MANAGEMENT.SOURCE_SAMPLES.queryName.toLowerCase() || - lcQueryName === SCHEMAS.WORKFLOW.JOB_INPUT_SAMPLES.queryName.toLowerCase() - ); + return lcQueryName === SCHEMAS.SAMPLE_MANAGEMENT.SOURCE_SAMPLES.queryName.toLowerCase(); } + if ( + lcSchemaName === SCHEMAS.WORKFLOW.SCHEMA && + lcQueryName === SCHEMAS.WORKFLOW.JOB_INPUT_SAMPLES.queryName.toLowerCase() + ) + return true; return false; } diff --git a/packages/components/src/internal/util/utils.test.ts b/packages/components/src/internal/util/utils.test.ts index ec4d3963e8..eee7937227 100644 --- a/packages/components/src/internal/util/utils.test.ts +++ b/packages/components/src/internal/util/utils.test.ts @@ -50,6 +50,7 @@ import { makeCommaSeparatedString, parseCsvString, parseScientificInt, + pronoun, quoteValueWithDelimiters, styleStringToObj, toLowerSafe, @@ -58,24 +59,22 @@ import { withTransformedKeys, } from './utils'; -const emptyList = List(); - describe('toLowerSafe', () => { test('strings', () => { - expect(toLowerSafe(List(['TEST ', ' Test', 'TeSt', 'test']))).toEqual( - List(['test ', ' test', 'test', 'test']) + expect(toLowerSafe(['TEST ', ' Test', 'TeSt', 'test'])).toEqual( + ['test ', ' test', 'test', 'test'] ); }); test('numbers', () => { - expect(toLowerSafe(List([1, 2, 3]))).toEqual(emptyList); - expect(toLowerSafe(List([1.0]))).toEqual(emptyList); - expect(toLowerSafe(List([1.0, 2]))).toEqual(emptyList); + expect(toLowerSafe([1, 2, 3])).toEqual([]); + expect(toLowerSafe([1.0])).toEqual([]); + expect(toLowerSafe([1.0, 2])).toEqual([]); }); test('strings and numbers', () => { - expect(toLowerSafe(List([1, 2, 'TEST ', ' Test', 3.0, 4.4, 'TeSt', 'test']))).toEqual( - List(['test ', ' test', 'test', 'test']) + expect(toLowerSafe([1, 2, 'TEST ', ' Test', 3.0, 4.4, 'TeSt', 'test'])).toEqual( + ['test ', ' test', 'test', 'test'] ); }); }); @@ -96,6 +95,18 @@ describe('camelCaseToTitleCase', () => { }); }); +describe('pronoun', () => { + test('singular', () => { + expect(pronoun(1)).toBe('it'); + expect(pronoun(1, 'some')).toBe('it'); + }); + test('plural', () => { + expect(pronoun(2)).toBe('them'); + expect(pronoun(2, 'some')).toBe('some'); + expect(pronoun(undefined)).toBe('them'); + }); +}); + describe('capitalizeFirstChar', () => { test('capitalizeFirstChar', () => { const testStrings = { diff --git a/packages/components/src/internal/util/utils.ts b/packages/components/src/internal/util/utils.ts index f1efe6d3fd..3b29cef301 100644 --- a/packages/components/src/internal/util/utils.ts +++ b/packages/components/src/internal/util/utils.ts @@ -90,7 +90,7 @@ export function withTransformedKeys(obj: Record, keyTransformFn: (v } /** - * Returns a copy of List and ensures that in copy all values are lower case strings. + * Returns a copy of string[] and ensures that in copy all values are lower case strings. * @param a */ export function toLowerSafe(a: string[]): string[] { @@ -107,6 +107,10 @@ export function camelCaseToTitleCase(text: string): string { return saferText.charAt(0).toUpperCase() + saferText.slice(1); } +export function pronoun(count, plural = 'them'): string { + return count === 1 ? 'it' : plural; +} + export function not(predicate: (...args: any[]) => boolean): (...args: any[]) => boolean { return function () { return !predicate.apply(this, arguments); diff --git a/packages/components/src/public/QueryModel/utils.ts b/packages/components/src/public/QueryModel/utils.ts index 8c7465d46b..3d41a235d9 100644 --- a/packages/components/src/public/QueryModel/utils.ts +++ b/packages/components/src/public/QueryModel/utils.ts @@ -40,6 +40,13 @@ export function filterArraysEqual(a: Filter.IFilter[], b: Filter.IFilter[]): boo return aStr === bStr; } +export function filterArrayToString(filterArray: Filter.IFilter[]): string { + if (!filterArray) { + return ''; + } + return filterArray.map(filterToString).sort().join(';'); +} + export function sortsEqual(a: QuerySort, b: QuerySort): boolean { return a.toRequestString() === b.toRequestString(); }