Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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": "6.64.1",
"version": "6.64.2",
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
"sideEffects": false,
"files": [
Expand Down
5 changes: 5 additions & 0 deletions packages/components/releaseNotes/components.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# @labkey/components
Components, models, actions, and utility functions for LabKey applications and pages

### version 6.64.2
*Released*: 10 October 2025
- Issue 52878: Attachment thumbnails from ancestor columns not rendered in Sample Type grid
- Modify `AncestorRenderer` and `MultiValueRenderer` to handle file/attachment columns

### version 6.64.1
*Released*: 9 October 2025
- Issue 53997: Establish a maximum size for query selections
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ export function getSelectionLineageData(

export interface GroupedSampleDisplayColumns {
aliquotHeaderDisplayColumns: QueryColumn[];
aliquotOnlyColumns: string[]; // should hide from parent panel
displayColumns: QueryColumn[];
editColumns: QueryColumn[];
}
Expand All @@ -239,6 +240,7 @@ export function getGroupedSampleDisplayColumns(
const editColumns = [];
const displayColumns = [];
const aliquotHeaderDisplayColumns = [];
const aliquotOnlyColumns = [];

allDisplayColumns.forEach(col => {
const lcFieldKey = col.fieldKey.toLowerCase();
Expand All @@ -259,6 +261,8 @@ export function getGroupedSampleDisplayColumns(
sampleTypeDomainFields.independentFields.indexOf(lcFieldKey) > -1
) {
aliquotHeaderDisplayColumns.push(col);
if (sampleTypeDomainFields.aliquotFields.indexOf(lcFieldKey) > -1)
aliquotOnlyColumns.push(col.fieldKey);
}
} else {
if (sampleTypeDomainFields.aliquotFields.indexOf(lcFieldKey) === -1) {
Expand Down Expand Up @@ -294,6 +298,7 @@ export function getGroupedSampleDisplayColumns(

return {
aliquotHeaderDisplayColumns,
aliquotOnlyColumns,
displayColumns,
editColumns,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Map } from 'immutable';
import { render } from '@testing-library/react';

import { AncestorRenderer } from './AncestorRenderer';
import { QueryColumn } from '../../public/QueryColumn';

describe('AncestorRenderer', () => {
test('No data', () => {
Expand All @@ -28,6 +29,18 @@ describe('AncestorRenderer', () => {
expect(links).toHaveLength(1);
expect(links[0].getAttribute('href')).toBe(data.url);
expect(links[0].textContent).toBe(data.displayValue);
expect(document.querySelectorAll('.attachment-card')).toHaveLength(0);
});

test('positive value, and file type column', () => {
const data = {
value: 'a.txt',
displayValue: 'a.txt',
url: 'http://samples.org/Sample-123',
};
render(<AncestorRenderer col={new QueryColumn({ inputType: 'file' })} data={Map(data)} />);
expect(document.querySelectorAll('span.text-muted')).toHaveLength(0);
expect(document.querySelectorAll('.attachment-card')).toHaveLength(1);
});

test('negative value', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ import { DefaultRenderer } from './DefaultRenderer';
export const ANCESTOR_LOOKUP_CONCEPT_URI = 'http://www.labkey.org/types#ancestorLookup';

interface Props {
col?: any;
data: Map<any, any>;
}

export const AncestorRenderer: FC<Props> = memo(({ data }) => {
export const AncestorRenderer: FC<Props> = memo(({ data, col }) => {
if (Map.isMap(data) && data.size > 0) {
const { displayValue, value } = data.toJS();
if (value < 0 && displayValue) {
Expand All @@ -35,7 +36,7 @@ export const AncestorRenderer: FC<Props> = memo(({ data }) => {
);
}

return <DefaultRenderer data={data} />;
return <DefaultRenderer col={col} data={data} />;
}

return null;
Expand Down
10 changes: 7 additions & 3 deletions packages/components/src/internal/renderers/DefaultRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { isConditionalFormattingEnabled } from '../app/utils';
import { AppLink } from '../url/AppLink';

import { MultiValueRenderer } from './MultiValueRenderer';
import { FileColumnRenderer } from './FileColumnRenderer';

interface Props {
col?: QueryColumn;
Expand Down Expand Up @@ -54,8 +55,11 @@ export const DefaultRenderer: FC<Props> = memo(({ col, data, noLink }) => {
display = data ? 'true' : 'false';
} else if (List.isList(data)) {
// defensively return a MultiValueRenderer, this column likely wasn't declared properly as "multiValue"
return <MultiValueRenderer data={data} />;
} else {
return <MultiValueRenderer data={data} col={col} />;
} else if (col?.isFileInput) {
return <FileColumnRenderer data={data} />;
}
else {
if (isConditionalFormattingEnabled()) {
style = getDataStyling(data);
if (style?.backgroundColor) {
Expand All @@ -74,7 +78,7 @@ export const DefaultRenderer: FC<Props> = memo(({ col, data, noLink }) => {
if (url && !noLink) {
const targetBlank = data.get('urlTarget') === TARGET_BLANK;
return (
<AppLink className={className} to={url} targetBlank={targetBlank} style={style}>
<AppLink className={className} style={style} targetBlank={targetBlank} to={url}>
{display}
</AppLink>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ export const getAttachmentCardProp = (
let url, value, display;
if (Iterable.isIterable(data)) {
url = data.get('url');
value = data.get('value');
value = data.get('value')?.toString();
display = getFileDisplayValue(data.get('displayValue') ?? value);
} else {
url = caseInsensitive(data, 'url');
value = caseInsensitive(data, 'value');
value = caseInsensitive(data, 'value')?.toString();
display = getFileDisplayValue(caseInsensitive(data, 'displayValue') ?? value);
}
const titleStyle = isConditionalFormattingEnabled() ? getDataStyling(data) : undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { fromJS, Map } from 'immutable';
import { render } from '@testing-library/react';

import { MultiValueRenderer } from './MultiValueRenderer';
import { QueryColumn } from '../../public/QueryColumn';

describe('MultiValueRenderer', () => {
test('empty data', () => {
Expand All @@ -23,6 +24,20 @@ describe('MultiValueRenderer', () => {
expect(document.body.textContent).toBe('24');
});

test('data shapes, value, file column', () => {
const data = fromJS({ 24: { value: 'a.txt', url: 'a.txt' } });
render(<MultiValueRenderer col={new QueryColumn({ inputType: 'file' })} data={data} />);
expect(document.body.textContent).toBe('a.txt');
expect(document.querySelectorAll('.attachment-card')).toHaveLength(0);
});

test('data list, value, file column', () => {
const data = fromJS([{ value: 'a.txt', url: 'a.txt' }]);
render(<MultiValueRenderer col={new QueryColumn({ inputType: 'file' })} data={data} />);
expect(document.body.textContent).toBe('a.txtDownload');
expect(document.querySelectorAll('.attachment-card')).toHaveLength(1);
});

test('data shapes, displayValue', () => {
const data = fromJS({ 24: { displayValue: 'Griffey', value: 24 } });
render(<MultiValueRenderer data={data} />);
Expand Down Expand Up @@ -59,6 +74,26 @@ describe('MultiValueRenderer', () => {
expect(link[0].getAttribute('href')).toEqual('https://www.mariners.com/ichiro');
});

test('multiple values, file column', () => {
const data = fromJS({
11: { value: 'a.txt', url: 'a.txt' },
24: { value: 'b.txt', url: 'b.txt' },
51: { value: 'c.txt', url: 'c.txt' },
});
render(<MultiValueRenderer col={new QueryColumn({ inputType: 'file' })} data={data} />);
const spans = document.querySelectorAll('span');
expect(spans.length).toBe(3);
expect(spans[0].textContent).toEqual('a.txt');
expect(spans[1].textContent).toEqual(', b.txt');
expect(spans[2].textContent).toEqual(', c.txt');

const link = spans[2].querySelectorAll('a');
expect(link).toHaveLength(1);
expect(link[0].getAttribute('href')).toEqual('c.txt');

expect(document.querySelectorAll('.attachment-card')).toHaveLength(0);
});

test('non-Map values', () => {
const data = Map({
11: [],
Expand Down
17 changes: 14 additions & 3 deletions packages/components/src/internal/renderers/MultiValueRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,28 @@
* limitations under the License.
*/
import React, { FC, Fragment, memo, ReactNode } from 'react';
import { Map } from 'immutable';
import { List, Map } from 'immutable';
import { QueryColumn } from '../../public/QueryColumn';
import { FileColumnRenderer } from './FileColumnRenderer';

export interface MultiValueRendererProps {
col?: QueryColumn;
data: Map<any, any>;
}

export const MultiValueRenderer: FC<MultiValueRendererProps> = memo(({ data }) => {
export const MultiValueRenderer: FC<MultiValueRendererProps> = memo(({ data, col }) => {
if (!data || data.size === 0) {
return null;
}

if (
List.isList(data) &&
data.size === 1 &&
(col?.isFileInput)
) {
return <FileColumnRenderer data={data.get(0)} />;
}

let i = -1;
return (
<div>
Expand Down Expand Up @@ -58,7 +69,7 @@ export const MultiValueRenderer: FC<MultiValueRendererProps> = memo(({ data }) =
return (
// IntelliJ mistakenly presumes that key is an index.
// In fact, it is the key of the map which is unique.
// eslint-disable-next-line react/no-array-index-key

<span key={key}>
{++i > 0 ? ', ' : ''}
{url ? <a href={url}>{text}</a> : text}
Expand Down