Skip to content

Commit 7d06de8

Browse files
authored
Feat - collection sorting (#1212)
* refactor collection item sorting into a separate function * refactor: update sorting function to use a new type for collection items * test: add unit tests for sortCollectionItems function * refactor: update sortCollectionItems function to accept an object parameter * enhance unit tests for sortCollectionItems with additional sorting scenarios * refactor: move sorting logic to a dedicated sortCollectionItems function and add comprehensive unit tests * fix: correct import path for SortableCardProps in sortCollectionItems tests * fix: update test assertion * test: add "arrange", "act", "assert" comment for better structuring * refactor: enhance sortCollectionItems function to support sorting by title and date with improved logic * add defaultSortBy and defaultSortDirection options to CollectionPagePageSchema for enhanced sorting capabilities * update props type source + pass in props accordingly * refactor: make sortBy and sortDirection optional in GetCollectionItemsProps * refactor: simplify SortCollectionItemsProps by extending from GetCollectionItemsProps * refactor: update defaultSortDirection to extend props type in page router * refactor: change defaultSortDirection from "asc" to "desc" in page router for improved sorting behavior * refactor: enhance CollectionPagePageSchema by adding metadata to defaultSortBy and defaultSortDirection options * refactor: simplify assertions in sortCollectionItems tests * update test name * refactor: enhance sorting logic in sortCollectionItems to handle numeric title comparisons * fix: correct sorting logic and default direction in sortCollectionItems for accurate item ordering * refactor: improve sorting logic in sortCollectionItems to handle date and title comparisons more effectively * fix type * feat: add category sorting to sortCollectionItems with title as tiebreaker * fix: handle equal dates in sortCollectionItems to ensure stable sorting
1 parent 9ee109f commit 7d06de8

File tree

6 files changed

+493
-33
lines changed

6 files changed

+493
-33
lines changed

apps/studio/src/server/modules/page/page.router.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import type { IsomerSchema } from "@opengovsg/isomer-components"
1+
import type {
2+
CollectionPagePageProps,
3+
IsomerSchema,
4+
} from "@opengovsg/isomer-components"
25
import {
36
getLayoutMetadataSchema,
47
ISOMER_USABLE_PAGE_LAYOUTS,
@@ -809,8 +812,8 @@ export const pageRouter = router({
809812
title: parent.title,
810813
subtitle: `Read more on ${parent.title.toLowerCase()} here.`,
811814
defaultSortBy: "date",
812-
defaultSortDirection: "asc",
813-
},
815+
defaultSortDirection: "desc",
816+
} as CollectionPagePageProps,
814817
content: [],
815818
version: "0.1.0",
816819
}

packages/components/src/templates/next/layouts/Collection/Collection.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,14 @@ const CollectionLayout = ({
6161
LinkComponent,
6262
ScriptComponent,
6363
}: CollectionPageSchemaType) => {
64-
const { permalink } = page
64+
const { permalink, defaultSortBy, defaultSortDirection } = page
6565

66-
const items = getCollectionItems(site, permalink)
66+
const items = getCollectionItems({
67+
site,
68+
permalink,
69+
sortBy: defaultSortBy,
70+
sortDirection: defaultSortDirection,
71+
})
6772
const processedItems = processedCollectionItems(items)
6873
const breadcrumb = getBreadcrumbFromSiteMap(
6974
site.siteMap,
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
import { beforeAll, describe, expect, it } from "vitest"
2+
3+
import type { SortableCardProps } from "../sortCollectionItems"
4+
import { sortCollectionItems } from "../sortCollectionItems"
5+
6+
describe("sortCollectionItems", () => {
7+
let itemCounter = 0
8+
9+
const createItem = ({
10+
title = "Test Item",
11+
date,
12+
variant = "article",
13+
url = "",
14+
category = "Others",
15+
}: {
16+
title?: string
17+
date?: Date
18+
variant?: "file" | "link" | "article"
19+
url?: string
20+
category?: string
21+
}): SortableCardProps => {
22+
itemCounter++
23+
return {
24+
id: `test-${itemCounter}`,
25+
title,
26+
rawDate: date,
27+
variant,
28+
url,
29+
description: "",
30+
category,
31+
site: {
32+
siteMap: {
33+
id: "root",
34+
title: "Test Site",
35+
summary: "",
36+
lastModified: "2024-01-01",
37+
permalink: "/",
38+
layout: "homepage",
39+
},
40+
siteName: "Test Site",
41+
theme: "isomer-next",
42+
logoUrl: "",
43+
search: {
44+
type: "localSearch",
45+
searchUrl: "/search",
46+
},
47+
navBarItems: [],
48+
footerItems: {
49+
siteNavItems: [],
50+
privacyStatementLink: "/privacy",
51+
termsOfUseLink: "/terms",
52+
},
53+
lastUpdated: "2024-01-01",
54+
},
55+
} as SortableCardProps
56+
}
57+
58+
it("should sort items by date (newest first)", () => {
59+
// Arrange
60+
const items = [
61+
createItem({ title: "Oldest", date: new Date("2023-01-01") }),
62+
createItem({ title: "Newest", date: new Date("2023-12-31") }),
63+
createItem({ title: "Middle", date: new Date("2023-06-15") }),
64+
]
65+
66+
// Act
67+
const sorted = sortCollectionItems({ items })
68+
69+
// Assert
70+
const expectedTitles = ["Newest", "Middle", "Oldest"]
71+
expect(sorted.map((item) => item.title)).toEqual(expectedTitles)
72+
})
73+
74+
it("should sort by title when dates are equal", () => {
75+
// Arrange
76+
const sameDate = new Date("2023-01-01")
77+
const items = [
78+
createItem({ title: "Charlie", date: sameDate }),
79+
createItem({ title: "Alice", date: sameDate }),
80+
createItem({ title: "Bob", date: sameDate }),
81+
]
82+
83+
// Act
84+
const sorted = sortCollectionItems({ items })
85+
86+
// Assert
87+
const expectedTitles = ["Alice", "Bob", "Charlie"]
88+
expect(sorted.map((item) => item.title)).toEqual(expectedTitles)
89+
})
90+
91+
it("should sort by title when dates are equal and take into account numbers in the title", () => {
92+
// Arrange
93+
const sameDate = new Date("2023-01-01")
94+
const items = [
95+
createItem({ title: "2 ogpeople", date: sameDate }),
96+
createItem({ title: "1 ogpeople", date: sameDate }),
97+
createItem({ title: "10 ogpeople", date: sameDate }),
98+
]
99+
100+
// Act
101+
const sorted = sortCollectionItems({
102+
items,
103+
sortBy: "title",
104+
sortDirection: "desc",
105+
})
106+
107+
// Assert
108+
const expectedTitles = ["10 ogpeople", "2 ogpeople", "1 ogpeople"]
109+
expect(sorted.map((item) => item.title)).toEqual(expectedTitles)
110+
})
111+
112+
describe("should place items without dates at the end, sorted alphabetically by title:", () => {
113+
let items: SortableCardProps[]
114+
115+
beforeAll(() => {
116+
items = [
117+
createItem({ title: "No Date" }),
118+
createItem({ title: "Newest", date: new Date("2023-12-31") }),
119+
createItem({ title: "Also No Date" }),
120+
createItem({ title: "Oldest", date: new Date("2023-01-01") }),
121+
]
122+
})
123+
124+
it("sortBy is title", () => {
125+
// Act
126+
const sorted = sortCollectionItems({
127+
items,
128+
sortBy: "title",
129+
sortDirection: "asc",
130+
})
131+
132+
// Assert
133+
const expectedTitles = ["Newest", "Oldest", "Also No Date", "No Date"]
134+
expect(sorted.map((item) => item.title)).toEqual(expectedTitles)
135+
})
136+
137+
it("sortBy is category", () => {
138+
// Act
139+
const sorted = sortCollectionItems({
140+
items,
141+
sortBy: "category",
142+
sortDirection: "asc",
143+
})
144+
145+
// Assert
146+
const expectedTitles = ["Newest", "Oldest", "Also No Date", "No Date"]
147+
expect(sorted.map((item) => item.title)).toEqual(expectedTitles)
148+
})
149+
150+
it("sortBy is date", () => {
151+
// Act
152+
const sorted = sortCollectionItems({
153+
items,
154+
sortBy: "date",
155+
sortDirection: "desc",
156+
})
157+
158+
// Assert
159+
const expectedTitles = ["Newest", "Oldest", "Also No Date", "No Date"]
160+
expect(sorted.map((item) => item.title)).toEqual(expectedTitles)
161+
})
162+
})
163+
164+
describe("should sort by title with date as tiebreaker (newest first) when sortBy is title and", () => {
165+
let items: SortableCardProps[]
166+
167+
beforeAll(() => {
168+
// Arrange
169+
const sameDate = new Date("2023-01-01")
170+
items = [
171+
createItem({
172+
title: "Charlie",
173+
date: new Date("2023-12-30"),
174+
url: "/charlie-2023-12-30",
175+
}),
176+
createItem({
177+
title: "Charlie",
178+
date: new Date("2023-12-31"),
179+
url: "/charlie-2023-12-31",
180+
}),
181+
createItem({ title: "Alice", date: new Date("2022-01-01") }),
182+
createItem({ title: "Bob", date: sameDate }),
183+
createItem({ title: "David", date: sameDate }),
184+
]
185+
})
186+
187+
it("sortDirection is desc", () => {
188+
// Act
189+
const sorted = sortCollectionItems({
190+
items,
191+
sortBy: "title",
192+
sortDirection: "desc",
193+
})
194+
195+
// Assert
196+
const expectedTitles = ["David", "Charlie", "Charlie", "Bob", "Alice"]
197+
expect(sorted.map((item) => item.title)).toEqual(expectedTitles)
198+
expect(sorted[1]?.url).toEqual("/charlie-2023-12-31")
199+
expect(sorted[2]?.url).toEqual("/charlie-2023-12-30")
200+
})
201+
202+
it("sortDirection is asc", () => {
203+
// Act
204+
const sorted = sortCollectionItems({
205+
items,
206+
sortBy: "title",
207+
sortDirection: "asc",
208+
})
209+
210+
// Assert
211+
const expectedTitles = ["Alice", "Bob", "Charlie", "Charlie", "David"]
212+
expect(sorted.map((item) => item.title)).toEqual(expectedTitles)
213+
expect(sorted[2]?.url).toEqual("/charlie-2023-12-31")
214+
expect(sorted[3]?.url).toEqual("/charlie-2023-12-30")
215+
})
216+
})
217+
218+
describe("should sort by category with title as tiebreaker (alphabetically) when sortBy is category and", () => {
219+
let items: SortableCardProps[]
220+
221+
beforeAll(() => {
222+
items = [
223+
createItem({ category: "2000" }),
224+
createItem({ category: "2001", title: "Alice" }),
225+
createItem({ category: "2001", title: "Bob" }),
226+
createItem({ category: "2002" }),
227+
]
228+
})
229+
230+
it("sortDirection is desc", () => {
231+
// Act
232+
const sorted = sortCollectionItems({
233+
items,
234+
sortBy: "category",
235+
sortDirection: "desc",
236+
})
237+
238+
// Assert
239+
const expectedCategories = ["2002", "2001", "2001", "2000"]
240+
expect(sorted.map((item) => item.category)).toEqual(expectedCategories)
241+
expect(sorted[1]?.title).toEqual("Alice")
242+
expect(sorted[2]?.title).toEqual("Bob")
243+
})
244+
245+
it("sortDirection is asc", () => {
246+
// Act
247+
const sorted = sortCollectionItems({
248+
items,
249+
sortBy: "category",
250+
sortDirection: "asc",
251+
})
252+
253+
// Assert
254+
const expectedCategories = ["2000", "2001", "2001", "2002"]
255+
expect(sorted.map((item) => item.category)).toEqual(expectedCategories)
256+
expect(sorted[1]?.title).toEqual("Alice")
257+
expect(sorted[2]?.title).toEqual("Bob")
258+
})
259+
})
260+
261+
describe("should sort by date with title as tiebreaker (alphabetically) when sortBy is date and", () => {
262+
let items: SortableCardProps[]
263+
264+
beforeAll(() => {
265+
// Arrange
266+
const sameDate = new Date("2023-01-01")
267+
items = [
268+
createItem({ title: "Newest", date: new Date("2023-12-31") }),
269+
createItem({ title: "Charlie", date: sameDate }),
270+
createItem({ title: "Alice", date: sameDate }),
271+
createItem({ title: "Bob", date: sameDate }),
272+
createItem({ title: "Oldest", date: new Date("2022-01-01") }),
273+
]
274+
})
275+
276+
it("sortDirection is desc", () => {
277+
// Act
278+
const sorted = sortCollectionItems({
279+
items,
280+
sortBy: "date",
281+
sortDirection: "desc",
282+
})
283+
284+
// Assert
285+
const expectedTitles = ["Newest", "Alice", "Bob", "Charlie", "Oldest"]
286+
expect(sorted.map((item) => item.title)).toEqual(expectedTitles)
287+
})
288+
289+
it("sortDirection is asc", () => {
290+
// Act
291+
const sorted = sortCollectionItems({
292+
items,
293+
sortBy: "date",
294+
sortDirection: "asc",
295+
})
296+
297+
// Assert
298+
const expectedTitles = ["Oldest", "Alice", "Bob", "Charlie", "Newest"]
299+
expect(sorted.map((item) => item.title)).toEqual(expectedTitles)
300+
})
301+
})
302+
})

0 commit comments

Comments
 (0)