diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index 01a4c29d42..e837d16659 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -1,6 +1,12 @@ # @labkey/components Components, models, actions, and utility functions for LabKey applications and pages +### version 7.X.0 +*Released*: ? February 2026 +- Remove GridAliquotViewSelector +- Remove SampleAliquotViewSelector +- Remove isSampleAliquotSelectorEnabled + ### version 7.16.0 *Released*: 5 February 2026 - File import warnings for cross type sample import case diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index b0064baa26..9a644e7356 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -340,8 +340,6 @@ import { } from './internal/components/entities/models'; import { EntityMoveModal } from './internal/components/entities/EntityMoveModal'; import { EntityMoveConfirmationModal } from './internal/components/entities/EntityMoveConfirmationModal'; -import { SampleAliquotViewSelector } from './internal/components/entities/SampleAliquotViewSelector'; -import { GridAliquotViewSelector } from './internal/components/entities/GridAliquotViewSelector'; import { FindDerivativesButton, FindDerivativesMenuItem, @@ -733,7 +731,6 @@ import { isProjectContainer, isProtectedDataEnabled, isRegistryEnabled, - isSampleAliquotSelectorEnabled, isSampleStatusEnabled, isSharedContainer, isSourceTypeEnabled, @@ -939,7 +936,6 @@ const App = { isPlatesEnabled, isBiologicsEnabled, isPremiumApplication, - isSampleAliquotSelectorEnabled, isProjectContainer, isProtectedDataEnabled, isDataChangeCommentRequirementFeatureEnabled, @@ -1429,7 +1425,6 @@ export { GlobalStateContextProvider, Grid, GRID_CHECKBOX_OPTIONS, - GridAliquotViewSelector, getTextAlignClassName, GridColumn, GridPanel, @@ -1645,7 +1640,6 @@ export { SAMPLE_TYPE_AUDIT_QUERY, SAMPLE_TYPE_CONCEPT_URI, SAMPLE_TYPE_DESIGNER_ROLE, - SampleAliquotViewSelector, SampleAmountEditModal, sampleDeleteDependencyText, SampleOperation, diff --git a/packages/components/src/internal/app/constants.ts b/packages/components/src/internal/app/constants.ts index 07131d83fd..3260c60813 100644 --- a/packages/components/src/internal/app/constants.ts +++ b/packages/components/src/internal/app/constants.ts @@ -111,7 +111,6 @@ export const SERVER_NOTIFICATION_MAX_ROWS = 8; export const EXPERIMENTAL_PRODUCT_ALL_FOLDER_LOOKUPS = 'queryProductAllFolderLookups'; export const EXPERIMENTAL_PRODUCT_FOLDER_DATA_LISTING_SCOPED = 'queryProductProjectDataListingScoped'; export const EXPERIMENTAL_REQUESTS_MENU = 'experimental-biologics-requests-menu'; -export const EXPERIMENTAL_SAMPLE_ALIQUOT_SELECTOR = 'experimental-sample-aliquot-selector'; export const FOLDER_DATA_TYPE_EXCLUSIONS = 'dataTypeExclusions'; export const ARCHIVED_FOLDERS = 'archivedContainers'; diff --git a/packages/components/src/internal/app/utils.ts b/packages/components/src/internal/app/utils.ts index daefea1f7f..ab948a1f6f 100644 --- a/packages/components/src/internal/app/utils.ts +++ b/packages/components/src/internal/app/utils.ts @@ -27,7 +27,6 @@ import { EXPERIMENTAL_PRODUCT_ALL_FOLDER_LOOKUPS, EXPERIMENTAL_PRODUCT_FOLDER_DATA_LISTING_SCOPED, EXPERIMENTAL_REQUESTS_MENU, - EXPERIMENTAL_SAMPLE_ALIQUOT_SELECTOR, FOLDER_DATA_TYPE_EXCLUSIONS, FREEZER_MANAGER_APP_PROPERTIES, FREEZERS_KEY, @@ -419,10 +418,6 @@ export function isFeatureEnabled(flag: ProductFeature, moduleContext?: ModuleCon return resolveModuleContext(moduleContext)?.core?.productFeatures?.indexOf(flag) >= 0; } -export function isSampleAliquotSelectorEnabled(moduleContext?: ModuleContext): boolean { - return resolveModuleContext(moduleContext)?.samplemanagement?.[EXPERIMENTAL_SAMPLE_ALIQUOT_SELECTOR] === true; -} - export function hasModule(moduleName: string, moduleContext?: ModuleContext): boolean { return resolveModuleContext(moduleContext).api?.moduleNames?.indexOf(moduleName.toLowerCase()) >= 0; } diff --git a/packages/components/src/internal/components/entities/GridAliquotViewSelector.test.tsx b/packages/components/src/internal/components/entities/GridAliquotViewSelector.test.tsx deleted file mode 100644 index 5fe9523703..0000000000 --- a/packages/components/src/internal/components/entities/GridAliquotViewSelector.test.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; -import { Filter } from '@labkey/api'; - -import { makeTestQueryModel } from '../../../public/QueryModel/testUtils'; -import { SchemaQuery } from '../../../public/SchemaQuery'; -import { IS_ALIQUOT_COL } from '../samples/constants'; - -import { GridAliquotViewSelector } from './GridAliquotViewSelector'; - -describe('', () => { - beforeEach(() => { - LABKEY.moduleContext = { samplemanagement: { 'experimental-sample-aliquot-selector': true } }; - }); - - test('no queryModel', () => { - render(); - expect(document.querySelector('.aliquot-view-selector')).not.toBeInTheDocument(); - }); - - test('experimental flag disabled', () => { - LABKEY.moduleContext = { samplemanagement: { 'experimental-sample-aliquot-selector': false } }; - const model = makeTestQueryModel(new SchemaQuery('a', 'b')); - render(); - expect(document.querySelector('.aliquot-view-selector')).not.toBeInTheDocument(); - }); - - // Note: this method is identical to the one for SampleAliquotViewSelector, so it feels like we're mostly just - // testing that component again. Is there something more specific to GridAliquotViewSelector that we should be - // testing? - function verifyOptions(all?: boolean, samples?: boolean, aliquots?: boolean) { - const items = document.querySelectorAll('.lk-menu-item'); - const buttonText = document.querySelector('.dropdown-toggle').textContent; - expect(items).toHaveLength(3); - expect(document.querySelector('.dropdown-header')).toHaveTextContent('Show Samples'); - expect(items[0]).toHaveTextContent('Samples and Aliquots'); - expect(items[1]).toHaveTextContent('Samples Only'); - expect(items[2]).toHaveTextContent('Aliquots Only'); - - if (all) { - expect(items[0].getAttribute('class')).toContain('active'); - expect(buttonText).toEqual('All Samples'); - } else { - expect(items[0].getAttribute('class')).not.toContain('active'); - } - - if (samples) { - expect(items[1].getAttribute('class')).toContain('active'); - expect(buttonText).toEqual('Samples Only'); - } else { - expect(items[1].getAttribute('class')).not.toContain('active'); - } - - if (aliquots) { - expect(items[2].getAttribute('class')).toContain('active'); - expect(buttonText).toEqual('Aliquots Only'); - } else { - expect(items[2].getAttribute('class')).not.toContain('active'); - } - } - - test('with queryModel, without filter', () => { - const model = makeTestQueryModel(new SchemaQuery('a', 'b')); - render(); - verifyOptions(true); - }); - - test('with queryModel, filtered to aliquots only', () => { - let model = makeTestQueryModel(new SchemaQuery('a', 'b')); - model = model.mutate({ - filterArray: [Filter.create(IS_ALIQUOT_COL, true)], - }); - render(); - - verifyOptions(false, false, true); - }); - - test('with queryModel, filtered to samples only', () => { - let model = makeTestQueryModel(new SchemaQuery('a', 'b')); - model = model.mutate({ - filterArray: [Filter.create(IS_ALIQUOT_COL, false)], - }); - render(); - - verifyOptions(false, true); - }); -}); diff --git a/packages/components/src/internal/components/entities/GridAliquotViewSelector.tsx b/packages/components/src/internal/components/entities/GridAliquotViewSelector.tsx deleted file mode 100644 index 84f866cbce..0000000000 --- a/packages/components/src/internal/components/entities/GridAliquotViewSelector.tsx +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Copyright (c) 2019 LabKey Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import React, { Component, ReactNode } from 'react'; - -import { Filter } from '@labkey/api'; - -import { isSampleAliquotSelectorEnabled } from '../../app/utils'; -import { ALIQUOT_FILTER_MODE, IS_ALIQUOT_COL } from '../samples/constants'; -import { Actions } from '../../../public/QueryModel/withQueryModels'; -import { QueryModel } from '../../../public/QueryModel/QueryModel'; - -import { SampleAliquotViewSelector } from './SampleAliquotViewSelector'; - -interface Props { - actions?: Actions; - initAliquotMode?: ALIQUOT_FILTER_MODE; // allow to set aliquot filter from a init value - queryModel?: QueryModel; - updateFilter?: (filter: Filter.IFilter, newModel: ALIQUOT_FILTER_MODE) => void; -} - -interface State { - initAliquotModeSynced: boolean; -} - -export class GridAliquotViewSelector extends Component { - state: Readonly = { - initAliquotModeSynced: false, - }; - - componentDidMount(): void { - this.syncInitMode(); - } - - componentDidUpdate = (): void => { - this.syncInitMode(); - }; - - syncInitMode = () => { - const { initAliquotMode, queryModel } = this.props; - const { initAliquotModeSynced } = this.state; - // if bindURL = true, rely on url param to set aliquot filter and ignore the initAliquotMode prop - if (!initAliquotModeSynced && initAliquotMode && !queryModel.bindURL && !queryModel?.isLoading) { - const currentMode = this.getAliquotFilterMode(); - if (initAliquotMode != currentMode) this.updateAliquotFilter(initAliquotMode); - this.setState(() => ({ initAliquotModeSynced: true })); - } - }; - - getAliquotColName = (): string => { - // account for the case where the aliquot column is in the queryModel via a lookup from the sample ID - const { queryModel } = this.props; - let aliquotColName; - if (!queryModel.getColumnByFieldKey(IS_ALIQUOT_COL)) { - aliquotColName = queryModel.allColumns?.find( - c => c.fieldKey.toLowerCase().indexOf('/' + IS_ALIQUOT_COL.toLowerCase()) > -1 - )?.fieldKey; - } - - return aliquotColName ?? IS_ALIQUOT_COL; - }; - - updateAliquotFilter = (newMode: ALIQUOT_FILTER_MODE): void => { - const { queryModel, actions, updateFilter } = this.props; - const aliquotColName = this.getAliquotColName(); - - let newFilter: Filter.IFilter; - if (newMode === ALIQUOT_FILTER_MODE.all || newMode === ALIQUOT_FILTER_MODE.none) { - newFilter = null; - } else { - newFilter = Filter.create(aliquotColName, newMode === ALIQUOT_FILTER_MODE.aliquots); - } - - if (updateFilter) { - updateFilter(newFilter, newMode); - return; - } - - if (queryModel.queryInfo && actions) { - // keep any existing filters that do not match the aliquot column name - const updatedFilters = queryModel.filterArray.filter(filter => { - return aliquotColName.toLowerCase() !== filter.getColumnName().toLowerCase(); - }); - - if (newFilter) updatedFilters.push(newFilter); - - actions.setFilters(queryModel.id, updatedFilters, true); - } - }; - - getAliquotFilterMode = (): ALIQUOT_FILTER_MODE => { - const { queryModel } = this.props; - let mode = ALIQUOT_FILTER_MODE.all; - const filterArray = queryModel?.filterArray; - if (filterArray) { - const aliquotColName = this.getAliquotColName(); - - filterArray.forEach(filter => { - if (filter.getColumnName().toLowerCase() === aliquotColName?.toLowerCase()) { - const filterType = filter.getFilterType(); - const value = filter.getValue(); - - if (filterType == Filter.Types.ISBLANK || filterType == Filter.Types.MISSING) { - mode = ALIQUOT_FILTER_MODE.none; - } else if (filterType == Filter.Types.EQUAL || filterType.getURLSuffix() == 'eq') { - if (value === '') return; - mode = - value === 'true' || value === true - ? ALIQUOT_FILTER_MODE.aliquots - : ALIQUOT_FILTER_MODE.samples; - } else if ( - filterType == Filter.Types.NOT_EQUAL || - filterType == Filter.Types.NEQ || - filterType == Filter.Types.NEQ_OR_NULL - ) { - if (value === '') return; - mode = - value === 'true' || value === true - ? ALIQUOT_FILTER_MODE.samples - : ALIQUOT_FILTER_MODE.aliquots; - } - } - }); - } - - return mode; - }; - - render(): ReactNode { - const { queryModel } = this.props; - - if (!queryModel) return null; - - if (!isSampleAliquotSelectorEnabled()) return null; - - return ( - - ); - } -} diff --git a/packages/components/src/internal/components/entities/SampleAliquotViewSelector.test.tsx b/packages/components/src/internal/components/entities/SampleAliquotViewSelector.test.tsx deleted file mode 100644 index c2d9afa019..0000000000 --- a/packages/components/src/internal/components/entities/SampleAliquotViewSelector.test.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React from 'react'; -import { render } from '@testing-library/react'; - -import { ALIQUOT_FILTER_MODE } from '../samples/constants'; - -import { SampleAliquotViewSelector } from './SampleAliquotViewSelector'; - -describe('', () => { - function verifyOptions(all?: boolean, samples?: boolean, aliquots?: boolean) { - const buttonText = document.querySelector('.dropdown-toggle').textContent; - const items = document.querySelectorAll('.lk-menu-item'); - expect(items).toHaveLength(3); - expect(document.querySelector('.dropdown-header')).toHaveTextContent('Show Samples'); - expect(items[0]).toHaveTextContent('Samples and Aliquots'); - expect(items[1]).toHaveTextContent('Samples Only'); - expect(items[2]).toHaveTextContent('Aliquots Only'); - - if (all) { - expect(items[0].getAttribute('class')).toContain('active'); - expect(buttonText).toEqual('All Samples'); - } else { - expect(items[0].getAttribute('class')).not.toContain('active'); - } - - if (samples) { - expect(items[1].getAttribute('class')).toContain('active'); - expect(buttonText).toEqual('Samples Only'); - } else { - expect(items[1].getAttribute('class')).not.toContain('active'); - } - - if (aliquots) { - expect(items[2].getAttribute('class')).toContain('active'); - expect(buttonText).toEqual('Aliquots Only'); - } else { - expect(items[2].getAttribute('class')).not.toContain('active'); - } - } - - test('aliquotFilterMode undefined', () => { - render( - - ); - - verifyOptions(true); - }); - - test('aliquotFilterMode: all', () => { - render( - - ); - - verifyOptions(true); - }); - - test('aliquotFilterMode: samples', () => { - render( - - ); - - verifyOptions(false, true); - }); - - test('aliquotFilterMode: aliquots', () => { - render( - - ); - verifyOptions(false, false, true); - }); - - test('customized labels', () => { - render( - - ); - - expect(document.querySelector('.dropdown-header')).toHaveTextContent('Show Jobs with Samples'); - - const items = document.querySelectorAll('.lk-menu-item'); - expect(items).toHaveLength(3); - expect(items[0]).toHaveTextContent('Parent Sample and Aliquots'); - expect(items[1]).toHaveTextContent('Parent Sample Only'); - expect(items[2]).toHaveTextContent('Aliquots Only'); - }); -}); diff --git a/packages/components/src/internal/components/entities/SampleAliquotViewSelector.tsx b/packages/components/src/internal/components/entities/SampleAliquotViewSelector.tsx deleted file mode 100644 index d5e0a4c49a..0000000000 --- a/packages/components/src/internal/components/entities/SampleAliquotViewSelector.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { FC, useCallback } from 'react'; - -import { ALIQUOT_FILTER_MODE } from '../samples/constants'; -import { DropdownButton, MenuHeader, MenuItem } from '../../dropdowns'; - -interface ViewMenuItemProps { - currentFilterMode: ALIQUOT_FILTER_MODE; - filterMode: ALIQUOT_FILTER_MODE; - label: string; - onClick: (filterMode: ALIQUOT_FILTER_MODE) => void; -} - -const ViewMenuItem: FC = ({ currentFilterMode, filterMode, label, onClick }) => { - const onClick_ = useCallback(() => onClick(filterMode), [filterMode, onClick]); - return ( - - {label} - - ); -}; -ViewMenuItem.displayName = 'ViewMenuItem'; - -interface Props { - aliquotFilterMode: ALIQUOT_FILTER_MODE; - aliquotsLabel?: string; - allLabel?: string; - headerLabel?: string; - samplesLabel?: string; - updateAliquotFilter: (newMode: ALIQUOT_FILTER_MODE) => void; -} - -export const SampleAliquotViewSelector: FC = props => { - const { - aliquotsLabel = 'Aliquots Only', - aliquotFilterMode = ALIQUOT_FILTER_MODE.all, - allLabel = 'Samples and Aliquots', - headerLabel = 'Show Samples', - samplesLabel = 'Samples Only', - updateAliquotFilter, - } = props; - let title = 'All Samples'; - - if (aliquotFilterMode === ALIQUOT_FILTER_MODE.samples) { - title = samplesLabel; - } else if (aliquotFilterMode === ALIQUOT_FILTER_MODE.aliquots) { - title = aliquotsLabel; - } else if (aliquotFilterMode === ALIQUOT_FILTER_MODE.none) { - title = 'None'; - } - - return ( - - - - - - - ); -}; -SampleAliquotViewSelector.displayName = 'SampleAliquotViewSelector';