···15import { CardSortField, SortOrder } from '../../domain/ICardQueryRepository';
16import { createTestSchema } from '../test-utils/createTestSchema';
17import { CardTypeEnum } from '../../domain/value-objects/CardType';
01819describe('DrizzleCardQueryRepository - getLibrariesForUrl', () => {
20 let container: StartedPostgreSqlContainer;
···281 // Should return empty since card is not in any library
282 expect(result.items).toHaveLength(0);
283 expect(result.totalCount).toBe(0);
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000284 });
285 });
286
···15import { CardSortField, SortOrder } from '../../domain/ICardQueryRepository';
16import { createTestSchema } from '../test-utils/createTestSchema';
17import { CardTypeEnum } from '../../domain/value-objects/CardType';
18+import { PublishedRecordId } from '../../domain/value-objects/PublishedRecordId';
1920describe('DrizzleCardQueryRepository - getLibrariesForUrl', () => {
21 let container: StartedPostgreSqlContainer;
···282 // Should return empty since card is not in any library
283 expect(result.items).toHaveLength(0);
284 expect(result.totalCount).toBe(0);
285+ });
286+ });
287+288+ describe('sorting', () => {
289+ it('should sort by createdAt in descending order by default', async () => {
290+ const testUrl = 'https://example.com/sort-test';
291+ const url = URL.create(testUrl).unwrap();
292+293+ // Create cards with different creation times
294+ const card1 = new CardBuilder()
295+ .withCuratorId(curator1.value)
296+ .withType(CardTypeEnum.URL)
297+ .withUrl(url)
298+ .buildOrThrow();
299+300+ await new Promise((resolve) => setTimeout(resolve, 1000));
301+ const card2 = new CardBuilder()
302+ .withCuratorId(curator2.value)
303+ .withType(CardTypeEnum.URL)
304+ .withUrl(url)
305+ .buildOrThrow();
306+307+ await new Promise((resolve) => setTimeout(resolve, 1000));
308+ const card3 = new CardBuilder()
309+ .withCuratorId(curator3.value)
310+ .withType(CardTypeEnum.URL)
311+ .withUrl(url)
312+ .buildOrThrow();
313+314+ card1.addToLibrary(curator1);
315+ card2.addToLibrary(curator2);
316+ card3.addToLibrary(curator3);
317+318+ // Save cards with slight delays to ensure different timestamps
319+ await cardRepository.save(card1);
320+ await new Promise((resolve) => setTimeout(resolve, 10));
321+ await cardRepository.save(card2);
322+ await new Promise((resolve) => setTimeout(resolve, 10));
323+ await cardRepository.save(card3);
324+325+ const result = await queryRepository.getLibrariesForUrl(testUrl, {
326+ page: 1,
327+ limit: 10,
328+ sortBy: CardSortField.CREATED_AT,
329+ sortOrder: SortOrder.DESC,
330+ });
331+332+ expect(result.items).toHaveLength(3);
333+334+ // Should be sorted by creation time, newest first
335+ const cardIds = result.items.map((lib) => lib.card.id);
336+ expect(cardIds[0]).toBe(card3.cardId.getStringValue()); // Most recent
337+ expect(cardIds[1]).toBe(card2.cardId.getStringValue()); // Middle
338+ expect(cardIds[2]).toBe(card1.cardId.getStringValue()); // Oldest
339+ });
340+341+ it('should sort by createdAt in ascending order when specified', async () => {
342+ const testUrl = 'https://example.com/sort-asc-test';
343+ const url = URL.create(testUrl).unwrap();
344+345+ // Create cards with different creation times
346+ const card1 = new CardBuilder()
347+ .withCuratorId(curator1.value)
348+ .withType(CardTypeEnum.URL)
349+ .withUrl(url)
350+ .buildOrThrow();
351+352+ const card2 = new CardBuilder()
353+ .withCuratorId(curator2.value)
354+ .withType(CardTypeEnum.URL)
355+ .withUrl(url)
356+ .buildOrThrow();
357+358+ card1.addToLibrary(curator1);
359+ card2.addToLibrary(curator2);
360+361+ // Save cards with slight delay to ensure different timestamps
362+ await cardRepository.save(card1);
363+ await new Promise((resolve) => setTimeout(resolve, 10));
364+ await cardRepository.save(card2);
365+366+ const result = await queryRepository.getLibrariesForUrl(testUrl, {
367+ page: 1,
368+ limit: 10,
369+ sortBy: CardSortField.CREATED_AT,
370+ sortOrder: SortOrder.ASC,
371+ });
372+373+ expect(result.items).toHaveLength(2);
374+375+ // Should be sorted by creation time, oldest first
376+ const cardIds = result.items.map((lib) => lib.card.id);
377+ expect(cardIds[0]).toBe(card1.cardId.getStringValue()); // Oldest
378+ expect(cardIds[1]).toBe(card2.cardId.getStringValue()); // Newest
379+ });
380+381+ it('should sort by updatedAt in descending order', async () => {
382+ const testUrl = 'https://example.com/sort-updated-test';
383+ const url = URL.create(testUrl).unwrap();
384+385+ // Create cards
386+ const card1 = new CardBuilder()
387+ .withCuratorId(curator1.value)
388+ .withType(CardTypeEnum.URL)
389+ .withUrl(url)
390+ .buildOrThrow();
391+392+ const card2 = new CardBuilder()
393+ .withCuratorId(curator2.value)
394+ .withType(CardTypeEnum.URL)
395+ .withUrl(url)
396+ .buildOrThrow();
397+398+ card1.addToLibrary(curator1);
399+ card2.addToLibrary(curator2);
400+401+ // Save cards
402+ await cardRepository.save(card1);
403+ await cardRepository.save(card2);
404+405+ // Update card1 to have a more recent updatedAt
406+ await new Promise((resolve) => setTimeout(resolve, 1000));
407+ card1.markAsPublished(
408+ PublishedRecordId.create({
409+ uri: 'at://did:plc:publishedrecord1',
410+ cid: 'bafyreicpublishedrecord1',
411+ }),
412+ );
413+ await cardRepository.save(card1); // This should update the updatedAt timestamp
414+415+ const result = await queryRepository.getLibrariesForUrl(testUrl, {
416+ page: 1,
417+ limit: 10,
418+ sortBy: CardSortField.UPDATED_AT,
419+ sortOrder: SortOrder.DESC,
420+ });
421+422+ expect(result.items).toHaveLength(2);
423+424+ // card1 should be first since it was updated more recently
425+ const cardIds = result.items.map((lib) => lib.card.id);
426+ expect(cardIds[0]).toBe(card1.cardId.getStringValue()); // Most recently updated
427+ expect(cardIds[1]).toBe(card2.cardId.getStringValue()); // Less recently updated
428+ });
429+430+ it('should sort by libraryCount in descending order', async () => {
431+ const testUrl = 'https://example.com/sort-library-count-test';
432+ const url = URL.create(testUrl).unwrap();
433+434+ // Create cards
435+ const card1 = new CardBuilder()
436+ .withCuratorId(curator1.value)
437+ .withType(CardTypeEnum.URL)
438+ .withUrl(url)
439+ .buildOrThrow();
440+441+ const card2 = new CardBuilder()
442+ .withCuratorId(curator2.value)
443+ .withType(CardTypeEnum.URL)
444+ .withUrl(url)
445+ .buildOrThrow();
446+447+ const card3 = new CardBuilder()
448+ .withCuratorId(curator3.value)
449+ .withType(CardTypeEnum.URL)
450+ .withUrl(url)
451+ .buildOrThrow();
452+453+ // Add cards to libraries with different counts
454+ card1.addToLibrary(curator1);
455+456+ card2.addToLibrary(curator2);
457+ card2.addToLibrary(curator1); // card2 has 2 library memberships
458+459+ card3.addToLibrary(curator3);
460+ card3.addToLibrary(curator1); // card3 has 3 library memberships
461+ card3.addToLibrary(curator2);
462+463+ await cardRepository.save(card1);
464+ await cardRepository.save(card2);
465+ await cardRepository.save(card3);
466+467+ const result = await queryRepository.getLibrariesForUrl(testUrl, {
468+ page: 1,
469+ limit: 10,
470+ sortBy: CardSortField.LIBRARY_COUNT,
471+ sortOrder: SortOrder.DESC,
472+ });
473+474+ // Should return all library memberships, but sorted by the card's library count
475+ expect(result.items.length).toBeGreaterThan(0);
476+477+ // Group by card ID to check sorting
478+ const cardGroups = new Map<string, any[]>();
479+ result.items.forEach((item) => {
480+ const cardId = item.card.id;
481+ if (!cardGroups.has(cardId)) {
482+ cardGroups.set(cardId, []);
483+ }
484+ cardGroups.get(cardId)!.push(item);
485+ });
486+487+ // Get the first occurrence of each card to check library count ordering
488+ const uniqueCards = Array.from(cardGroups.entries()).map(
489+ ([cardId, items]) => ({
490+ cardId,
491+ libraryCount: items[0]!.card.libraryCount,
492+ }),
493+ );
494+495+ // Should be sorted by library count descending
496+ for (let i = 0; i < uniqueCards.length - 1; i++) {
497+ expect(uniqueCards[i]!.libraryCount).toBeGreaterThanOrEqual(
498+ uniqueCards[i + 1]!.libraryCount,
499+ );
500+ }
501+ });
502+503+ it('should sort by libraryCount in ascending order when specified', async () => {
504+ const testUrl = 'https://example.com/sort-library-count-asc-test';
505+ const url = URL.create(testUrl).unwrap();
506+507+ // Create cards with different library counts
508+ const card1 = new CardBuilder()
509+ .withCuratorId(curator1.value)
510+ .withType(CardTypeEnum.URL)
511+ .withUrl(url)
512+ .buildOrThrow();
513+514+ const card2 = new CardBuilder()
515+ .withCuratorId(curator2.value)
516+ .withType(CardTypeEnum.URL)
517+ .withUrl(url)
518+ .buildOrThrow();
519+520+ // card1 has 1 library membership, card2 has 2
521+ card1.addToLibrary(curator1);
522+ card2.addToLibrary(curator2);
523+ card2.addToLibrary(curator1);
524+525+ await cardRepository.save(card1);
526+ await cardRepository.save(card2);
527+528+ const result = await queryRepository.getLibrariesForUrl(testUrl, {
529+ page: 1,
530+ limit: 10,
531+ sortBy: CardSortField.LIBRARY_COUNT,
532+ sortOrder: SortOrder.ASC,
533+ });
534+535+ expect(result.items.length).toBeGreaterThan(0);
536+537+ // Group by card ID and check ascending order
538+ const cardGroups = new Map<string, any[]>();
539+ result.items.forEach((item) => {
540+ const cardId = item.card.id;
541+ if (!cardGroups.has(cardId)) {
542+ cardGroups.set(cardId, []);
543+ }
544+ cardGroups.get(cardId)!.push(item);
545+ });
546+547+ const uniqueCards = Array.from(cardGroups.entries()).map(
548+ ([cardId, items]) => ({
549+ cardId,
550+ libraryCount: items[0]!.card.libraryCount,
551+ }),
552+ );
553+554+ // Should be sorted by library count ascending
555+ for (let i = 0; i < uniqueCards.length - 1; i++) {
556+ expect(uniqueCards[i]!.libraryCount).toBeLessThanOrEqual(
557+ uniqueCards[i + 1]!.libraryCount,
558+ );
559+ }
560 });
561 });
562
-2
src/webapp/app/bookmarklet/page.tsx
···15 Anchor,
16 CopyButton,
17} from '@mantine/core';
18-import { useState } from 'react';
19-import { BiInfoCircle } from 'react-icons/bi';
20import SembleLogo from '@/assets/semble-logo.svg';
21import Link from 'next/link';
22
···15 Anchor,
16 CopyButton,
17} from '@mantine/core';
0018import SembleLogo from '@/assets/semble-logo.svg';
19import Link from 'next/link';
20