Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
ddacc29
Sample listing grid updates to include ExpMaterialColor and use SAMPL…
cnathe Jul 31, 2026
a3fe2ab
update success message to use bold instead of quotes
cnathe Jul 31, 2026
38afba3
SampleColorRenderer minor styling update
cnathe Jul 31, 2026
c949765
7.50.1-fb-sampleColor2.0
cnathe Jul 31, 2026
ea55487
QueryInfo.showSampleColorCol
cnathe Jul 31, 2026
e113739
7.50.1-fb-sampleColor2.1
cnathe Jul 31, 2026
6eb84ee
Include ExpMaterialColor for aliquot editable field
cnathe Jul 31, 2026
49823a5
App helpers for getting hasActiveProjectColors from moduleContext
cnathe Jul 31, 2026
eade72e
getOmittedSampleTypeColumns to include SAMPLE_COLOR_REQUIRED_COLUMNS …
cnathe Jul 31, 2026
b4adfc2
7.50.1-fb-sampleColor2.2
cnathe Jul 31, 2026
129ef71
remove experimental feature flag
cnathe Jul 31, 2026
efe8308
jset snapshot update
cnathe Jul 31, 2026
abb17de
SampleColorInput option renderer to include ColorIcon
cnathe Aug 3, 2026
37df271
SampleColorRenderer updates for cls and showLabel props
cnathe Aug 3, 2026
f968e0f
7.50.1-fb-sampleColor2.4
cnathe Aug 3, 2026
4fc9294
SampleColorRenderer useSmall prop
cnathe Aug 3, 2026
4ffb8a7
7.50.1-fb-sampleColor2.5
cnathe Aug 3, 2026
251a4c3
Merge remote-tracking branch 'origin/develop' into fb_sampleColor2
cnathe Aug 4, 2026
35d46b9
7.53.1-fb-sampleColor2.0
cnathe Aug 4, 2026
add42fa
Sample color display in lineage details
cnathe Aug 5, 2026
266caa0
7.53.1-fb-sampleColor2.1
cnathe Aug 5, 2026
d2e3693
export SAMPLE_COLOR_COLOR_COLUMN_NAME
cnathe Aug 5, 2026
023ba6c
7.53.1-fb-sampleColor2.2
cnathe Aug 5, 2026
1d95958
Merge remote-tracking branch 'origin/develop' into fb_sampleColor2
cnathe Aug 6, 2026
7f3eadf
7.54.2-fb-sampleColor2.0
cnathe Aug 6, 2026
0794842
Merge remote-tracking branch 'origin/develop' into fb_sampleColor2
cnathe Aug 10, 2026
0d4a08d
7.54.3-fb-sampleColor2.0
cnathe Aug 10, 2026
709d31f
sample.scss small update
cnathe Aug 10, 2026
23c2415
7.54.3-fb-sampleColor2.1
cnathe Aug 10, 2026
ee9e780
HorizontalBarSection.tsx and ItemsLegend.tsx update to handle "sectio…
cnathe Aug 11, 2026
d3ab476
7.54.3-fb-sampleColor2.2
cnathe Aug 11, 2026
8dcb710
include ExpMaterialColor in sample type system fields based on hasAct…
cnathe Aug 12, 2026
9441e4a
7.54.3-fb-sampleColor2.3
cnathe Aug 12, 2026
3501fa5
jest update for SystemFields.test.tsx
cnathe Aug 12, 2026
06e9aa0
Merge remote-tracking branch 'origin/develop' into fb_sampleColor2
cnathe Aug 12, 2026
152a514
QueryModel update to flattenValuesFromRow for displayValue
cnathe Aug 12, 2026
671284c
7.54.4-fb-sampleColor2.0
cnathe Aug 12, 2026
ff6c753
Merge remote-tracking branch 'origin/develop' into fb_sampleColor2
cnathe Aug 13, 2026
3215a19
7.55.2-fb-sampleColor2.0
cnathe Aug 13, 2026
6213653
SampleColorsSetting.tsx de/select all button
cnathe Aug 13, 2026
614c585
7.55.2-fb-sampleColor2.1
cnathe Aug 13, 2026
889ef13
lint
cnathe Aug 13, 2026
d56d1ea
getOmittedSampleTypeColumns to include check for hasProductFolders
cnathe Aug 13, 2026
21c1303
7.55.2-fb-sampleColor2.2
cnathe Aug 13, 2026
fb8f302
jest fix
cnathe Aug 13, 2026
3185fbe
Merge remote-tracking branch 'origin/develop' into fb_sampleColor2
cnathe Aug 14, 2026
3ded6f9
7.55.3-fb-sampleColor2.0
cnathe Aug 14, 2026
e52b0ed
jest test additions
cnathe Aug 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/components/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@labkey/components",
"version": "7.55.2",
"version": "7.55.3-fb-sampleColor2.0",
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
"sideEffects": false,
"files": [
Expand Down
6 changes: 4 additions & 2 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,7 @@ import {
DEFAULT_SAMPLE_FIELD_CONFIG,
FIND_BY_IDS_QUERY_PARAM,
IS_ALIQUOT_COL,
SAMPLE_COLOR_COLOR_COLUMN_NAME,
SAMPLE_COLOR_COLUMN_NAME,
SAMPLE_COLOR_REQUIRED_COLUMNS,
SAMPLE_DATA_EXPORT_CONFIG,
Expand Down Expand Up @@ -714,6 +715,7 @@ import {
getFolderSampleTypeExclusion,
getPrimaryAppProperties,
getProjectPath,
hasActiveProjectColors,
hasModule,
hasPremiumModule,
hasProductFolders,
Expand Down Expand Up @@ -741,7 +743,6 @@ import {
isProjectContainer,
isProtectedDataEnabled,
isRegistryEnabled,
isSampleColorsEnabled,
isSampleStatusEnabled,
isSharedContainer,
isSourceTypeEnabled,
Expand Down Expand Up @@ -958,7 +959,6 @@ const App = {
isSharedContainer,
freezerManagerIsCurrentApp,
isSampleManagerEnabled,
isSampleColorsEnabled,
isSampleStatusEnabled,
isProductFoldersEnabled,
isAllProductFoldersFilteringEnabled,
Expand All @@ -976,6 +976,7 @@ const App = {
getLabelsTestAPIWrapper,
getSecurityTestAPIWrapper,
getQueryTestAPIWrapper,
hasActiveProjectColors,
hasPremiumModule,
hasProductFolders,
hasModule,
Expand Down Expand Up @@ -1663,6 +1664,7 @@ export {
ResponsiveMenuButton,
ResponsiveMenuButtonGroup,
runDetailsColumnsForQueryModel,
SAMPLE_COLOR_COLOR_COLUMN_NAME,
SAMPLE_COLOR_COLUMN_NAME,
SAMPLE_COLOR_REQUIRED_COLUMNS,
SAMPLE_DATA_EXPORT_CONFIG,
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/internal/app/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export const EXPERIMENTAL_REQUESTS_MENU = 'experimental-biologics-requests-menu'

export const FOLDER_DATA_TYPE_EXCLUSIONS = 'dataTypeExclusions';
export const ARCHIVED_FOLDERS = 'archivedContainers';
export const HAS_ACTIVE_PROJECT_COLORS = 'hasActiveProjectColors';

export const BASE_APP_HELP_LINK = 'https://www.labkey.org/SampleManagerHelp/wiki-page.view?name=';

Expand Down
13 changes: 13 additions & 0 deletions packages/components/src/internal/app/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
getProjectPath,
getSamplesSectionConfig,
getStorageSectionConfig,
hasActiveProjectColors,
hasPremiumModule,
isAppHomeFolder,
isAssayEnabled,
Expand Down Expand Up @@ -812,6 +813,18 @@ describe('utils', () => {
expect(LABKEY.moduleContext.query).toEqual({ hasProductFolders: false, ken: 'griffey' });
});

test('hasActiveProjectColors', () => {
expect(hasActiveProjectColors({})).toBe(false);
expect(hasActiveProjectColors({ samplemanagement: {} })).toBe(false);
expect(hasActiveProjectColors({ samplemanagement: { hasActiveProjectColors: false } })).toBe(false);
expect(hasActiveProjectColors({ samplemanagement: { hasActiveProjectColors: true } })).toBe(true);

const moduleContext = LABKEY.moduleContext;
LABKEY.moduleContext = { samplemanagement: { hasActiveProjectColors: true } };
expect(hasActiveProjectColors()).toBe(true); // falls back to the global moduleContext
LABKEY.moduleContext = moduleContext;
});

test('hasPremiumModule', () => {
expect(hasPremiumModule({})).toBeFalsy();
expect(hasPremiumModule({ api: { moduleNames: ['samplemanagement'] } })).toBeFalsy();
Expand Down
17 changes: 15 additions & 2 deletions packages/components/src/internal/app/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
FOLDER_DATA_TYPE_EXCLUSIONS,
FREEZER_MANAGER_APP_PROPERTIES,
FREEZERS_KEY,
HAS_ACTIVE_PROJECT_COLORS,
HOME_KEY,
LABKEY_SERVER_PRODUCT_NAME,
LIMS_APP_PROPERTIES,
Expand Down Expand Up @@ -204,8 +205,20 @@ export function isSampleStatusEnabled(moduleContext?: ModuleContext): boolean {
return hasSampleManagementModule(moduleContext);
}

export function isSampleColorsEnabled(moduleContext?: ModuleContext): boolean {
return resolveModuleContext(moduleContext)?.experiment?.SampleColors === true;
/** True if the current container's project has at least one active (non-archived) sample color. */
export function hasActiveProjectColors(moduleContext?: ModuleContext): boolean {
return resolveModuleContext(moduleContext)?.samplemanagement?.[HAS_ACTIVE_PROJECT_COLORS] === true;
}

export function setActiveProjectColors(moduleContext: ModuleContext, hasActiveProjectColors: boolean): ModuleContext {
// side-effect set global moduleContext
if (LABKEY?.moduleContext?.samplemanagement) {
LABKEY.moduleContext.samplemanagement.hasActiveProjectColors = hasActiveProjectColors;
}

return Object.assign(moduleContext ?? {}, {
samplemanagement: Object.assign(moduleContext?.samplemanagement ?? {}, { hasActiveProjectColors }),
});
}

export function isQueryMetadataEditor(): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,36 @@ describe('HorizontalBarSection', () => {
expect(parts[2].getAttribute('class')).toContain('horizontal-bar--open');
expect(parts[2].parentElement.getAttribute('style')).toBe('width: 50%;');
});

// section headers offset the legend rows from the bars, so the hovered bar must be matched by barIndex
test('summary tooltip highlights the hovered bar across section headers', async () => {
const sectioned: HorizontalBarData[] = [
{ title: 'red', name: 'Red', sectionLabel: 'Blood', count: 12, totalCount: 40, percent: 30, filled: true },
{ title: 'blue', name: 'Blue', sectionLabel: 'Blood', count: 8, totalCount: 40, percent: 20, filled: true },
{
title: 'plasma',
name: 'No Color',
sectionLabel: 'Plasma',
count: 20,
totalCount: 40,
percent: 50,
filled: true,
unlabeled: true,
},
];
render(<HorizontalBarSection data={sectioned} showSummaryTooltip />);

// legend is: [Blood header, Red, Blue, Plasma] -- hovering the 2nd bar must bold the 3rd legend row
await userEvent.hover(document.querySelectorAll('.horizontal-bar-part')[1]);
await waitFor(() => {
expect(document.querySelector('.popover-content')).toBeInTheDocument();
});

const labels = document.querySelectorAll('.popover-content .cell-legend-label');
expect(labels).toHaveLength(3);
expect(document.querySelectorAll('.popover-content .cell-legend-section')).toHaveLength(1);
expect(labels[1]).toHaveTextContent('Blue');
expect(labels[1].getAttribute('class')).toContain('bold-text');
expect(document.querySelectorAll('.popover-content .bold-text')).toHaveLength(1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ export const HorizontalBarSection: FC<Props> = memo(props => {
}

horizontalBars = data
.filter(row => row.percent > 0)
.map((row, index) => {
.map((row, barIndex) => ({ row, barIndex }))
.filter(({ row }) => row.percent > 0)
.map(({ row, barIndex }) => {
const style: CSSProperties = { width: row.percent + '%', background: row.backgroundColor };
const section = (
<div
Expand All @@ -68,15 +69,19 @@ export const HorizontalBarSection: FC<Props> = memo(props => {
const overlay = (
<Popover id="grid-cell-popover" placement="top" isFlexPlacement>
{showSummaryTooltip && summaryLegendData?.length > 0 ? (
<ItemsLegend legendData={summaryLegendData} activeIndex={index} />
// section headers shift legend rows out of step with the bars, so match on barIndex
<ItemsLegend
legendData={summaryLegendData}
activeIndex={summaryLegendData.findIndex(legend => legend.barIndex === barIndex)}
/>
) : (
row.title
)}
</Popover>
);

return (
<OverlayTrigger key={index} id={index.toString()} overlay={overlay} style={style}>
<OverlayTrigger id={barIndex.toString()} key={barIndex} overlay={overlay} style={style}>
{section}
</OverlayTrigger>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,4 +192,50 @@ describe('ItemsLegend', () => {
expect(legends[1].querySelectorAll('.bold-text')).toHaveLength(1);
expect(legends[2].querySelectorAll('a')).toHaveLength(0);
});

test('section headers', () => {
render(
<ItemsLegend
activeIndex={2}
legendData={[
{
circleColor: 'none',
backgroundColor: 'none',
legendLabel: 'Blood',
isSectionHeader: true,
data: Map.of('value', 20),
},
{ circleColor: '#ff0000', backgroundColor: 'none', legendLabel: 'Red', data: Map.of('value', 12) },
{ circleColor: '#0000ff', backgroundColor: 'none', legendLabel: 'Blue', data: Map.of('value', 8) },
{
circleColor: 'green',
backgroundColor: 'none',
legendLabel: 'Plasma',
separatorAbove: true,
data: Map.of('value', 22),
},
]}
/>
);

const rows = document.querySelectorAll('tr');
expect(rows).toHaveLength(4);

// the header carries the section total but no color swatch, and is not bolded
expect(document.getElementsByClassName('cell-legend-section')).toHaveLength(1);
expect(rows[0]).toHaveTextContent('Blood');
expect(rows[0]).toHaveTextContent('20');
expect(rows[0].querySelectorAll('.cell-legend-icon')).toHaveLength(0);
expect(rows[0].querySelectorAll('.cell-legend-section-label')).toHaveLength(2);
expect(rows[0].getAttribute('class')).not.toContain('cell-legend-row--separator');

expect(rows[1].querySelectorAll('.cell-legend-circle')).toHaveLength(1);
expect(rows[1].querySelectorAll('.bold-text')).toHaveLength(0);
expect(rows[2].querySelectorAll('.bold-text')).toHaveLength(1); // activeIndex
expect(rows[3]).toHaveTextContent('Plasma');

// the rule closing off the Blood section sits on the row that follows it
expect(document.getElementsByClassName('cell-legend-row--separator')).toHaveLength(1);
expect(rows[3].getAttribute('class')).toContain('cell-legend-row--separator');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,27 @@ export const ItemsLegend: FC<Props> = memo(props => {
<table>
<tbody>
{legendData.map((legend, index) => {
const rowClassName = classNames('cell-legend-row', {
'cell-legend-row--separator': legend.separatorAbove,
});

if (legend.isSectionHeader) {
return (
<tr key={`cell-legend-${index}`} className={classNames(rowClassName, 'cell-legend-section')}>
<td colSpan={2}>
<span className="cell-legend-section-label">{legend.legendLabel}</span>
</td>
{legend.data && (
<td className="text-align-right">
<span className="cell-legend-data cell-legend-section-label">
<DefaultRenderer data={legend.data} />
</span>
</td>
)}
</tr>
);
}

let icon;
if (legend.circleColor && legend.circleColor !== 'none') {
icon = (
Expand Down Expand Up @@ -55,7 +76,7 @@ export const ItemsLegend: FC<Props> = memo(props => {
});

return (
<tr key={`cell-legend-${index}`} className="cell-legend-row">
<tr className={rowClassName} key={`cell-legend-${index}`}>
<td>
<span className={iconClassName} style={style}>
{icon}
Expand Down
Loading