From ed17aa17c8874e60b73ac6efdfdea348fe26d5ae Mon Sep 17 00:00:00 2001 From: lcomplete Date: Wed, 21 Jan 2026 03:49:04 +0800 Subject: [PATCH 1/2] refactor: simplify UNSORTED_SAVED_AT sort to use createdAt directly Replace complex multi-field fallback (collectedAt -> savedAt -> archivedAt) with simpler createdAt sorting for unsorted pages. Also ensure collectedAt is always set when pages are added to library to prevent null values. Co-Authored-By: Claude Opus 4.5 --- app/client/src/components/MagazineItem.tsx | 2 +- app/client/src/pages/CollectionList.tsx | 2 +- .../external/query/PageListSort.java | 29 +++++-------------- .../server/domain/mapper/PageItemMapper.java | 13 ++------- .../server/repository/PageRepository.java | 11 ++++--- .../server/service/BatchOrganizeService.java | 12 ++++++-- .../server/service/CapturePageService.java | 9 +++--- .../server/service/PageListService.java | 19 ++---------- 8 files changed, 33 insertions(+), 64 deletions(-) diff --git a/app/client/src/components/MagazineItem.tsx b/app/client/src/components/MagazineItem.tsx index 44b79deb..f56fb4cf 100644 --- a/app/client/src/components/MagazineItem.tsx +++ b/app/client/src/components/MagazineItem.tsx @@ -34,7 +34,7 @@ const sortLabelMap: Record = { 'CONNECTED_AT': 'Published', 'VOTE_SCORE': 'Created', 'COLLECTED_AT': 'Collected', - 'UNSORTED_SAVED_AT': 'Collected', + 'UNSORTED_SAVED_AT': 'Created', }; type MagazineItemProps = { diff --git a/app/client/src/pages/CollectionList.tsx b/app/client/src/pages/CollectionList.tsx index 7c038e34..22bed105 100644 --- a/app/client/src/pages/CollectionList.tsx +++ b/app/client/src/pages/CollectionList.tsx @@ -40,7 +40,7 @@ const CollectionList = () => { const [collection, setCollection] = useState(null); const isUnsorted = id === 'unsorted'; - // Use UNSORTED_SAVED_AT for unsorted (fallback: collectedAt -> savedAt -> archivedAt), COLLECTED_AT for regular collections + // Use UNSORTED_SAVED_AT for unsorted (uses createdAt), COLLECTED_AT for regular collections const [pageFilterOptions, setPageFilterOptions] = useState(() => ({ defaultSortValue: isUnsorted ? 'UNSORTED_SAVED_AT' : 'COLLECTED_AT', sortFields: [{ diff --git a/app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/query/PageListSort.java b/app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/query/PageListSort.java index 282d4c8a..f2d36567 100644 --- a/app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/query/PageListSort.java +++ b/app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/query/PageListSort.java @@ -23,36 +23,21 @@ public enum PageListSort { COLLECTED_AT("collectedAt"), /** - * For unsorted pages only: sorts by collectedAt, savedAt, archivedAt in order. - * Uses multiple sort fields for proper ordering. + * For unsorted pages only: sorts by createdAt which always exists. */ - UNSORTED_SAVED_AT("collectedAt", "savedAt", "archivedAt"); + UNSORTED_SAVED_AT("createdAt"); - private final String[] sortFields; + private final String sortField; - PageListSort(String... sortFields) { - this.sortFields = sortFields; + PageListSort(String sortField) { + this.sortField = sortField; } /** - * Returns the primary sort field. + * Returns the sort field. */ public String getSortField() { - return sortFields[0]; - } - - /** - * Returns all sort fields for multi-field sorting. - */ - public String[] getSortFields() { - return sortFields; - } - - /** - * Returns true if this sort uses multiple fields. - */ - public boolean isMultiFieldSort() { - return sortFields.length > 1; + return sortField; } static PageListSort valueOfSort(String sort) { diff --git a/app/server/huntly-server/src/main/java/com/huntly/server/domain/mapper/PageItemMapper.java b/app/server/huntly-server/src/main/java/com/huntly/server/domain/mapper/PageItemMapper.java index 06aa655f..07aebe9f 100644 --- a/app/server/huntly-server/src/main/java/com/huntly/server/domain/mapper/PageItemMapper.java +++ b/app/server/huntly-server/src/main/java/com/huntly/server/domain/mapper/PageItemMapper.java @@ -40,8 +40,8 @@ default PageItem updateRecordAt(@MappingTarget PageItem item, Page page, PageLis item.setRecordAt(page.getCollectedAt()); break; case UNSORTED_SAVED_AT: - // Fallback: collectedAt -> savedAt -> archivedAt - item.setRecordAt(coalesce(page.getCollectedAt(), page.getSavedAt(), page.getArchivedAt())); + // Use createdAt which always exists + item.setRecordAt(page.getCreatedAt()); break; case LAST_READ_AT: default: @@ -51,15 +51,6 @@ default PageItem updateRecordAt(@MappingTarget PageItem item, Page page, PageLis return item; } - default java.time.Instant coalesce(java.time.Instant... values) { - for (java.time.Instant value : values) { - if (value != null) { - return value; - } - } - return null; - } - default PageItem updateFromSource(@MappingTarget PageItem pageItem, Source source) { pageItem.setSiteName(source.getSiteName()); pageItem.setDomain(source.getDomain()); diff --git a/app/server/huntly-server/src/main/java/com/huntly/server/repository/PageRepository.java b/app/server/huntly-server/src/main/java/com/huntly/server/repository/PageRepository.java index f219d888..06e57b28 100644 --- a/app/server/huntly-server/src/main/java/com/huntly/server/repository/PageRepository.java +++ b/app/server/huntly-server/src/main/java/com/huntly/server/repository/PageRepository.java @@ -116,23 +116,26 @@ List findByCollectionIdAndLibrarySaveStatusGreaterThanOrderBySavedAtDesc(L List findUnsortedLibraryPages(Pageable pageable); /** - * Batch update collection only, keeping original collectedAt. + * Batch update collection and ensure collectedAt is set. + * Keeps original collectedAt if exists, otherwise uses createdAt as fallback. * Used for pages already in library. */ @Transactional @Modifying(clearAutomatically = true) - @Query("UPDATE Page p SET p.collectionId = :collectionId WHERE p.id IN :ids") + @Query("UPDATE Page p SET p.collectionId = :collectionId, " + + "p.collectedAt = COALESCE(p.collectedAt, p.createdAt) " + + "WHERE p.id IN :ids") int batchUpdateCollection(@Param("ids") List ids, @Param("collectionId") Long collectionId); /** * Batch update collection and set collectedAt to connectedAt (publish time). - * Fallback to original collectedAt if connectedAt is null. + * Fallback to original collectedAt, then createdAt if connectedAt is null. * Used for pages already in library. */ @Transactional @Modifying(clearAutomatically = true) @Query("UPDATE Page p SET p.collectionId = :collectionId, " + - "p.collectedAt = COALESCE(p.connectedAt, p.collectedAt) " + + "p.collectedAt = COALESCE(p.connectedAt, p.collectedAt, p.createdAt) " + "WHERE p.id IN :ids") int batchUpdateCollectionWithPublishTime(@Param("ids") List ids, @Param("collectionId") Long collectionId); } \ No newline at end of file diff --git a/app/server/huntly-server/src/main/java/com/huntly/server/service/BatchOrganizeService.java b/app/server/huntly-server/src/main/java/com/huntly/server/service/BatchOrganizeService.java index d8eb724e..f95e486c 100644 --- a/app/server/huntly-server/src/main/java/com/huntly/server/service/BatchOrganizeService.java +++ b/app/server/huntly-server/src/main/java/com/huntly/server/service/BatchOrganizeService.java @@ -142,11 +142,17 @@ private BatchMoveResult batchMoveByFilter(BatchMoveRequest request) { // Handle collectedAt based on mode if ("USE_PUBLISH_TIME".equals(mode)) { - // Set collectedAt to publish time (connectedAt), fallback to original collectedAt if null + // Set collectedAt to publish time (connectedAt), fallback to original collectedAt, then createdAt update.set(root.get("collectedAt"), - cb.coalesce(root.get("connectedAt"), root.get("collectedAt")).as(Instant.class)); + cb.coalesce( + cb.coalesce(root.get("connectedAt"), root.get("collectedAt")), + root.get("createdAt") + ).as(Instant.class)); + } else { + // KEEP mode: keep original collectedAt, fallback to createdAt if null + update.set(root.get("collectedAt"), + cb.coalesce(root.get("collectedAt"), root.get("createdAt")).as(Instant.class)); } - // KEEP mode: don't modify collectedAt int updated = entityManager.createQuery(update).executeUpdate(); return BatchMoveResult.of(updated, updated); diff --git a/app/server/huntly-server/src/main/java/com/huntly/server/service/CapturePageService.java b/app/server/huntly-server/src/main/java/com/huntly/server/service/CapturePageService.java index 337793fe..7dcd45fe 100644 --- a/app/server/huntly-server/src/main/java/com/huntly/server/service/CapturePageService.java +++ b/app/server/huntly-server/src/main/java/com/huntly/server/service/CapturePageService.java @@ -208,14 +208,13 @@ else if (isLike && hasSetting(setting.getLikeToLibraryType(), setting.getLikeToC default: break; } + if(page.getCollectedAt() == null) { + page.setCollectedAt(Instant.now()); + } } - // Set collection if configured - if (collectionId != null && page.getCollectionId() == null) { + if (collectionId != null) { page.setCollectionId(collectionId); - if (page.getCollectedAt() == null) { - page.setCollectedAt(Instant.now()); - } } } return save(page); diff --git a/app/server/huntly-server/src/main/java/com/huntly/server/service/PageListService.java b/app/server/huntly-server/src/main/java/com/huntly/server/service/PageListService.java index 8c7e4d09..839624f5 100644 --- a/app/server/huntly-server/src/main/java/com/huntly/server/service/PageListService.java +++ b/app/server/huntly-server/src/main/java/com/huntly/server/service/PageListService.java @@ -78,8 +78,7 @@ public List getPageItems(PageListQuery listQuery) { : PageListSort.CREATED_AT.getSortField(); } var specs = Specifications.and() - // Skip this filter for multi-field sort (UNSORTED_SAVED_AT) since filterUnsorted handles it - .ne(StringUtils.isNotBlank(sortField) && !listSort.isMultiFieldSort(), sortField, (Object) null) + .ne(StringUtils.isNotBlank(sortField), sortField, (Object) null) .gt(listQuery.getLastRecordAt() != null && listQuery.isAsc(), sortField, listQuery.getLastRecordAt()) .lt(listQuery.getLastRecordAt() != null && !listQuery.isAsc(), sortField, listQuery.getLastRecordAt()) .lt(listQuery.getFirstRecordAt() != null && listQuery.isAsc(), sortField, listQuery.getFirstRecordAt()) @@ -131,21 +130,7 @@ public List getPageItems(PageListQuery listQuery) { .eq("collectionId", (Object) null) .build()) .build(); - // Build sort - handle multi-field sorting for UNSORTED_SAVED_AT - org.springframework.data.domain.Sort sort; - if (listSort.isMultiFieldSort()) { - var sortBuilder = Sorts.builder(); - for (String field : listSort.getSortFields()) { - if (listQuery.isAsc()) { - sortBuilder.asc(field); - } else { - sortBuilder.desc(field); - } - } - sort = sortBuilder.build(); - } else { - sort = (listQuery.isAsc() ? Sorts.builder().asc(sortField) : Sorts.builder().desc(sortField)).build(); - } + org.springframework.data.domain.Sort sort = (listQuery.isAsc() ? Sorts.builder().asc(sortField) : Sorts.builder().desc(sortField)).build(); var size = PageSizeUtils.getPageSize(listQuery.getCount()); List pages = pageRepository.findAll(specs, size, sort); // todo enhance query From 1f1609f4ccf6742cd409408e7b8cedcfc5240ca9 Mon Sep 17 00:00:00 2001 From: lcomplete Date: Wed, 21 Jan 2026 03:52:22 +0800 Subject: [PATCH 2/2] feat: ensure collectedAt is set when collectionId is assigned in CapturePageService --- .../java/com/huntly/server/service/CapturePageService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/server/huntly-server/src/main/java/com/huntly/server/service/CapturePageService.java b/app/server/huntly-server/src/main/java/com/huntly/server/service/CapturePageService.java index 7dcd45fe..65f976e6 100644 --- a/app/server/huntly-server/src/main/java/com/huntly/server/service/CapturePageService.java +++ b/app/server/huntly-server/src/main/java/com/huntly/server/service/CapturePageService.java @@ -215,6 +215,10 @@ else if (isLike && hasSetting(setting.getLikeToLibraryType(), setting.getLikeToC if (collectionId != null) { page.setCollectionId(collectionId); + // Ensure collectedAt is set when collectionId is set + if (page.getCollectedAt() == null) { + page.setCollectedAt(page.getCreatedAt() != null ? page.getCreatedAt() : Instant.now()); + } } } return save(page);