Skip to content

Commit 8e514e8

Browse files
committed
feat: improving contributions page, linking to get-query
1 parent 9ae974f commit 8e514e8

6 files changed

Lines changed: 1401 additions & 33 deletions

File tree

web/src/__tests__/contributions-view.test.js

Lines changed: 139 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@ import { describe, it, expect } from 'vitest';
99
import {
1010
CREDIT_CATEGORIES,
1111
CONTRIBUTION_LEVELS,
12+
CREDIT_ROLE_ENUM,
13+
CREDIT_ROLE_ENUM_REVERSE,
1214
parseAssetNames,
1315
extractAuthors,
1416
initMatrix,
1517
formatAuthorForLatex,
1618
generateLatex,
19+
toEndpointPayload,
20+
fromEndpointPayload,
21+
rowsToWidgetAuthors,
1722
} from '../contributions/view.js';
1823

1924
// ---------------------------------------------------------------------------
@@ -248,18 +253,18 @@ describe('generateLatex', () => {
248253
const rows = initMatrix(['Alice Smith']);
249254
// All None by default → all zeros
250255
const tex = generateLatex(rows);
251-
expect(tex).toContain('{0,0,0,0,0,0,0,0,0}');
256+
expect(tex).toContain('{0,0,0,0,0,0,0,0,0,0,0,0,0,0}');
252257
});
253258

254-
it('uses \\lo for Low contributions', () => {
259+
it('uses \\lo for Supporting contributions', () => {
255260
const rows = initMatrix(['Alice Smith']);
256-
rows[0]['Conceptualization'] = 'Low';
261+
rows[0]['Conceptualization'] = 'Supporting';
257262
expect(generateLatex(rows)).toContain('\\lo');
258263
});
259264

260-
it('uses \\hi for High contributions', () => {
265+
it('uses \\hi for Lead contributions', () => {
261266
const rows = initMatrix(['Alice Smith']);
262-
rows[0]['Conceptualization'] = 'High';
267+
rows[0]['Conceptualization'] = 'Lead';
263268
expect(generateLatex(rows)).toContain('\\hi');
264269
});
265270

@@ -273,18 +278,141 @@ describe('generateLatex', () => {
273278
// ---------------------------------------------------------------------------
274279

275280
describe('CREDIT_CATEGORIES', () => {
276-
it('has 9 entries', () => {
277-
expect(CREDIT_CATEGORIES).toHaveLength(9);
281+
it('has 14 entries', () => {
282+
expect(CREDIT_CATEGORIES).toHaveLength(14);
278283
});
279284

280-
it('includes Conceptualization and Funding acquisition', () => {
285+
it('includes Conceptualization and Funding Acquisition', () => {
281286
expect(CREDIT_CATEGORIES).toContain('Conceptualization');
282-
expect(CREDIT_CATEGORIES).toContain('Funding acquisition');
287+
expect(CREDIT_CATEGORIES).toContain('Funding Acquisition');
283288
});
284289
});
285290

286291
describe('CONTRIBUTION_LEVELS', () => {
287-
it('contains None, Low, High in that order', () => {
288-
expect(CONTRIBUTION_LEVELS).toEqual(['None', 'Low', 'High']);
292+
it('contains None, Supporting, Equal, Lead in that order', () => {
293+
expect(CONTRIBUTION_LEVELS).toEqual(['None', 'Supporting', 'Equal', 'Lead']);
294+
});
295+
});
296+
297+
// ---------------------------------------------------------------------------
298+
// CREDIT_ROLE_ENUM / CREDIT_ROLE_ENUM_REVERSE
299+
// ---------------------------------------------------------------------------
300+
301+
describe('CREDIT_ROLE_ENUM', () => {
302+
it('maps every CREDIT_CATEGORIES entry to a kebab-case string', () => {
303+
for (const cat of CREDIT_CATEGORIES) {
304+
expect(typeof CREDIT_ROLE_ENUM[cat]).toBe('string');
305+
expect(CREDIT_ROLE_ENUM[cat]).toMatch(/^[a-z-]+$/);
306+
}
307+
});
308+
309+
it('maps Conceptualization to conceptualization', () => {
310+
expect(CREDIT_ROLE_ENUM['Conceptualization']).toBe('conceptualization');
311+
});
312+
});
313+
314+
describe('CREDIT_ROLE_ENUM_REVERSE', () => {
315+
it('is a proper inverse of CREDIT_ROLE_ENUM', () => {
316+
for (const [display, enumVal] of Object.entries(CREDIT_ROLE_ENUM)) {
317+
expect(CREDIT_ROLE_ENUM_REVERSE[enumVal]).toBe(display);
318+
}
319+
});
320+
});
321+
322+
// ---------------------------------------------------------------------------
323+
// toEndpointPayload
324+
// ---------------------------------------------------------------------------
325+
326+
describe('toEndpointPayload', () => {
327+
it('sets project_name correctly', () => {
328+
const rows = initMatrix(['Alice Smith']);
329+
const payload = toEndpointPayload(rows, 'my-project');
330+
expect(payload.project_name).toBe('my-project');
331+
});
332+
333+
it('omits None contributions from credit_levels', () => {
334+
const rows = initMatrix(['Alice Smith']);
335+
// All None by default
336+
const payload = toEndpointPayload(rows, 'proj');
337+
expect(payload.contributors[0].credit_levels).toHaveLength(0);
338+
});
339+
340+
it('includes non-None contributions with kebab-case role and lowercase level', () => {
341+
const rows = initMatrix(['Alice Smith']);
342+
rows[0]['Conceptualization'] = 'Lead';
343+
rows[0]['Software'] = 'Supporting';
344+
const payload = toEndpointPayload(rows, 'proj');
345+
const levels = payload.contributors[0].credit_levels;
346+
expect(levels).toContainEqual({ role: 'conceptualization', level: 'lead' });
347+
expect(levels).toContainEqual({ role: 'software', level: 'supporting' });
348+
});
349+
350+
it('includes person.name for each contributor', () => {
351+
const rows = initMatrix(['Bob Jones']);
352+
const payload = toEndpointPayload(rows, 'proj');
353+
expect(payload.contributors[0].person.name).toBe('Bob Jones');
354+
});
355+
});
356+
357+
// ---------------------------------------------------------------------------
358+
// fromEndpointPayload
359+
// ---------------------------------------------------------------------------
360+
361+
describe('fromEndpointPayload', () => {
362+
it('converts endpoint payload back into matrix rows', () => {
363+
const data = {
364+
project_name: 'proj',
365+
contributors: [
366+
{
367+
person: { name: 'Alice Smith' },
368+
credit_levels: [
369+
{ role: 'conceptualization', level: 'lead' },
370+
{ role: 'software', level: 'supporting' },
371+
],
372+
},
373+
],
374+
};
375+
const rows = fromEndpointPayload(data);
376+
expect(rows).toHaveLength(1);
377+
expect(rows[0].name).toBe('Alice Smith');
378+
expect(rows[0]['Conceptualization']).toBe('Lead');
379+
expect(rows[0]['Software']).toBe('Supporting');
380+
expect(rows[0]['Methodology']).toBe('None');
381+
});
382+
383+
it('returns empty array for empty contributors', () => {
384+
expect(fromEndpointPayload({ project_name: 'p', contributors: [] })).toEqual([]);
385+
});
386+
387+
it('round-trips through toEndpointPayload → fromEndpointPayload', () => {
388+
const original = initMatrix(['Alice Smith', 'Bob Jones']);
389+
original[0]['Conceptualization'] = 'Lead';
390+
original[1]['Software'] = 'Equal';
391+
const payload = toEndpointPayload(original, 'proj');
392+
const restored = fromEndpointPayload(payload);
393+
expect(restored[0]['Conceptualization']).toBe('Lead');
394+
expect(restored[1]['Software']).toBe('Equal');
395+
expect(restored[0]['Software']).toBe('None');
396+
});
397+
});
398+
399+
// ---------------------------------------------------------------------------
400+
// rowsToWidgetAuthors
401+
// ---------------------------------------------------------------------------
402+
403+
describe('rowsToWidgetAuthors', () => {
404+
it('converts rows to widget author format with display role names', () => {
405+
const rows = initMatrix(['Alice Smith']);
406+
rows[0]['Conceptualization'] = 'Lead';
407+
const authors = rowsToWidgetAuthors(rows);
408+
expect(authors[0].name).toBe('Alice Smith');
409+
expect(authors[0].credit_levels).toContainEqual({ role: 'Conceptualization', level: 'lead' });
410+
});
411+
412+
it('omits None levels from credit_levels', () => {
413+
const rows = initMatrix(['Bob Jones']);
414+
// All None
415+
const authors = rowsToWidgetAuthors(rows);
416+
expect(authors[0].credit_levels).toHaveLength(0);
289417
});
290418
});

web/src/constants.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ export const URL_PARAM_EXTRA_FILTERS = 'extraFilters';
5656
/** Fallback project name when none is set via URL or user selection. */
5757
export const DEFAULT_PROJECT = null;
5858

59+
// ---------------------------------------------------------------------------
60+
// Contributions / authorship API
61+
// ---------------------------------------------------------------------------
62+
63+
/**
64+
* Base URL for the aind-metadata-viz contributions REST API.
65+
* Proxied via Vite in dev (→ localhost:8000) and nginx in prod.
66+
*/
67+
export const CONTRIBUTIONS_API_BASE = '/metadata-viz';
68+
5969
// ---------------------------------------------------------------------------
6070
// DuckDB server connector
6171
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)