Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(website): show authors at the top on sequence details page #1729

Merged
merged 1 commit into from
May 5, 2024
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
3 changes: 3 additions & 0 deletions backend/src/main/kotlin/org/loculus/backend/config/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ enum class MetadataType {
@JsonProperty("boolean")
BOOLEAN,

@JsonProperty("authors")
AUTHORS,

;

override fun toString(): String = lowerCase(name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ class ProcessedSequenceEntryValidator(
}

val isOfCorrectPrimitiveType = when (metadata.type) {
MetadataType.STRING -> fieldValue.isTextual
MetadataType.STRING, MetadataType.AUTHORS -> fieldValue.isTextual
MetadataType.INTEGER -> fieldValue.isInt
MetadataType.FLOAT -> fieldValue.isFloatingPointNumber
MetadataType.NUMBER -> fieldValue.isNumber
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ data:
metadata:
{{- range (concat $commonMetadata .metadata) }}
- name: {{ .name }}
type: {{ (.type | eq "timestamp") | ternary "int" .type }}
type: {{ (.type | eq "timestamp") | ternary "int" ((.type | eq "authors") | ternary "string" .type) }}
{{- if .generateIndex }}
generateIndex: {{ .generateIndex }}
{{- end }}
Expand Down
2 changes: 1 addition & 1 deletion kubernetes/loculus/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ defaultOrganisms:
header: Authors
- name: authors
displayName: Authors
type: string
type: authors
header: Authors
- name: submitter_country
displayName: Submitter country
Expand Down
63 changes: 63 additions & 0 deletions website/src/components/SequenceDetailsPage/AuthorList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { type FC, useMemo, useState } from 'react';

type AuthorsListProps = {
authors: string[];
};

const DEFAULT_AT_LEAST_VISIBLE = 25;
const NUMBER_VISIBLE_LAST_AUTHORS = 1;

export const AuthorList: FC<AuthorsListProps> = ({ authors }) => {
const [showMore, setShowMore] = useState(false);
const data = useMemo(() => {
if (authors.length <= DEFAULT_AT_LEAST_VISIBLE + 3) {
return {
showMoreNeeded: false,
} as const;
}
return {
showMoreNeeded: true,
beforeEllipsis: authors.slice(0, DEFAULT_AT_LEAST_VISIBLE - NUMBER_VISIBLE_LAST_AUTHORS),
afterEllipsis: authors.slice(authors.length - NUMBER_VISIBLE_LAST_AUTHORS, authors.length),
} as const;
}, [authors]);

let authorsElements;
if (!data.showMoreNeeded || showMore) {
authorsElements = authors.map((author, index) => (
<span key={index}>
{author}
{index !== authors.length - 1 ? ', ' : ''}
</span>
));
} else {
authorsElements = (
<>
{data.beforeEllipsis.map((author, index) => (
<span key={index}>
{author}
{', '}
</span>
))}
<span>..., </span>
{data.afterEllipsis.map((author, index) => (
<span key={index}>
{author}
{index !== data.afterEllipsis.length - 1 ? ', ' : ''}
</span>
))}
</>
);
}

return (
<div>
{authorsElements}
{data.showMoreNeeded && (
<button onClick={() => setShowMore(!showMore)} className='ml-2 underline'>
{showMore ? 'Show less' : 'Show more'}
</button>
)}
</div>
);
};
19 changes: 14 additions & 5 deletions website/src/components/SequenceDetailsPage/DataTable.astro
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
---
import { AuthorList } from './AuthorList';
import { DataUseTermsHistoryModal } from './DataUseTermsHistoryModal';
import { SubstitutionsContainer } from './MutationBadge';
import { toHeaderMap, type TableDataEntry } from './getTableData';
import { getDataTableData } from './getDataTableData';
import { type TableDataEntry } from './getTableData';
import { type DataUseTermsHistoryEntry } from '../../types/backend';

interface Props {
Expand All @@ -10,18 +12,25 @@ interface Props {
}

const { tableData, dataUseTermsHistory } = Astro.props;
const headerMap = toHeaderMap(tableData);
const data = getDataTableData(tableData);
---

<div class='mt-2'>
<div>
{
data.topmatter.authors !== undefined && data.topmatter.authors.length > 0 && (
<div class='px-6 mb-4'>
<AuthorList authors={data.topmatter.authors} client:load />
</div>
)
}
<div>
{
Object.entries(headerMap).map(([header, names]) => (
data.table.map(({ header, rows }) => (
<div class='pb-8'>
{header !== '' && <h1 class='py-2 font-medium text-primary-600'>{header}</h1>}
<table class='table-auto'>
<tbody class='bg-white'>
{names.map(({ label, value, customDisplay }) => (
{rows.map(({ label, value, customDisplay }) => (
<tr>
<td class='py-1 w-44 text-sm font-medium text-gray-900 text-right'>{label}</td>
<td class='px-4 py-1 text-sm text-gray-600'>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { describe, expect, test } from 'vitest';

import { getDataTableData } from './getDataTableData.ts';
import { type TableDataEntry } from './getTableData.ts';

describe('getDataTableData', () => {
test('should group entries according to header', () => {
const data = getDataTableData(testTableDataEntries);
expect(data.table.length).toStrictEqual(2);
expect(data.table[0].header).toStrictEqual('Header 1');
expect(data.table[0].rows.map((r) => r.name)).toStrictEqual(['metadata_field_1', 'metadata_field_2']);
expect(data.table[1].header).toStrictEqual('Header 2');
expect(data.table[1].rows.map((r) => r.name)).toStrictEqual(['metadata_field_3']);
});

test('should move authors to topmatter', () => {
const data = getDataTableData(testTableDataEntries);
expect(data.topmatter.authors).toStrictEqual(['author 1', 'author 2', 'author 3']);
expect(
data.table
.flatMap((x) => x.rows)
.find((x) => x.type.kind === 'metadata' && x.type.metadataType === 'authors'),
).toBeUndefined();
});
});

const testTableDataEntries: TableDataEntry[] = [
{
label: 'Metadata Field 1',
name: 'metadata_field_1',
value: 'value1',
header: 'Header 1',
type: { kind: 'metadata', metadataType: 'string' },
},
{
label: 'Metadata Field 2',
name: 'metadata_field_2',
value: 'value2',
header: 'Header 1',
type: { kind: 'metadata', metadataType: 'timestamp' },
},
{
label: 'Metadata Field 3',
name: 'metadata_field_3',
value: 'value3',
header: 'Header 2',
type: { kind: 'metadata', metadataType: 'int' },
},
{
label: 'Authors',
name: 'authors',
value: 'author 1, author 2, author 3',
header: 'Header 2',
type: { kind: 'metadata', metadataType: 'authors' },
},
];
46 changes: 46 additions & 0 deletions website/src/components/SequenceDetailsPage/getDataTableData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { TableDataEntry } from './getTableData.ts';

type DataTableData = {
topmatter: {
authors: string[] | undefined;
};
table: {
header: string;
rows: TableDataEntry[];
}[];
};

export function getDataTableData(listTableDataEntries: TableDataEntry[]): DataTableData {
const result: DataTableData = {
topmatter: {
authors: undefined,
},
table: [],
};

const tableHeaderMap = new Map<string, TableDataEntry[]>();
for (const entry of listTableDataEntries) {
// Move the first entry with type authors to the topmatter
if (
result.topmatter.authors === undefined &&
entry.type.kind === 'metadata' &&
entry.type.metadataType === 'authors'
) {
result.topmatter.authors = entry.value
.toString()
.split(',')
.map((x) => x.trim());
break;
}

if (!tableHeaderMap.has(entry.header)) {
tableHeaderMap.set(entry.header, []);
}
tableHeaderMap.get(entry.header)!.push(entry);
}
for (const [header, rows] of tableHeaderMap.entries()) {
result.table.push({ header, rows });
}

return result;
}
Loading
Loading