Skip to content

Commit 157e18e

Browse files
committed
refac: consolidated the template ordering logic used across template dropdown and library
1 parent c144f9e commit 157e18e

10 files changed

Lines changed: 307 additions & 72 deletions

File tree

packages/imagekit-editor-dev/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "imagekit-editor-dev",
3-
"version": "2.2.0-stage-30-04-2026.1",
3+
"version": "2.2.0-stage-30-04-2026.2",
44
"description": "AI Image Editor powered by ImageKit",
55
"scripts": {
66
"prepack": "yarn build",

packages/imagekit-editor-dev/src/components/header/SettingsModal.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ import { useTemplateStorage } from "../../context/TemplateStorageContext"
1010
import { applyTemplateStorageAccessFailure } from "../../storage/templateAccessError"
1111
import type { TemplateRecord } from "../../storage/types"
1212
import { useEditorStore } from "../../store"
13-
import { formatTemplateNameForUI } from "../../utils"
14-
import { chakraAny } from "../../utils/chakraAny"
13+
import { chakraAny, formatTemplateNameForUI } from "../../utils"
1514

1615
// ---------------------------------------------------------------------------
1716
// Type casts — Chakra's strict generic signatures conflict with our JSX usage

packages/imagekit-editor-dev/src/components/header/TemplateNameInput.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import { Input } from "@chakra-ui/react"
22
import type React from "react"
33
import { useEffect, useRef, useState } from "react"
44
import { useEditorStore } from "../../store"
5-
import { formatTemplateNameForUI } from "../../utils"
6-
import { chakraAny } from "../../utils/chakraAny"
5+
import { chakraAny, formatTemplateNameForUI } from "../../utils"
76

87
const UNTITLED = "Untitled Template"
98
const InputAny = chakraAny(Input)

packages/imagekit-editor-dev/src/components/header/TemplateStatus.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { useEffect, useRef, useState } from "react"
1818
import { useTemplateStorage } from "../../context/TemplateStorageContext"
1919
import { useSaveTemplate } from "../../hooks/useSaveTemplate"
2020
import { useEditorStore } from "../../store"
21-
import { chakraAny } from "../../utils/chakraAny"
21+
import { chakraAny } from "../../utils"
2222

2323
const NOTIFICATION_DURATION_MS = 3000
2424

packages/imagekit-editor-dev/src/components/header/TemplatesDropdown.tsx

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,12 @@ import { useTemplateSync } from "../../hooks/useTemplateSync"
3232
import type { TemplateRecord } from "../../storage"
3333
import { applyTemplateStorageAccessFailure } from "../../storage/templateAccessError"
3434
import { useEditorStore } from "../../store"
35-
import { formatTemplateNameForUI, truncateTemplateName } from "../../utils"
36-
import { chakraAny } from "../../utils/chakraAny"
35+
import {
36+
chakraAny,
37+
formatTemplateNameForUI,
38+
getDisplayTemplates,
39+
truncateTemplateName,
40+
} from "../../utils"
3741

3842
const PopoverContentAny = chakraAny(PopoverContent)
3943
const PopoverBodyAny = chakraAny(PopoverBody)
@@ -128,35 +132,15 @@ export function TemplatesDropdown({
128132
: transformations.length
129133

130134
const filtered = useMemo(() => {
131-
const base = templates
132-
.filter((t) => t.id !== templateId)
133-
.filter((t) => {
134-
if (
135-
shouldShowCurrent &&
136-
templateId === null &&
137-
t.name === templateName
138-
) {
139-
return false
140-
}
141-
return true
142-
})
143-
.filter((t) => t.name.toLowerCase().includes(search.toLowerCase()))
144-
145-
// Sort by: pinned first, then by most recently used/updated
146-
return [...base]
147-
.sort((a, b) => {
148-
const aPinned = a.isPinned ? 1 : 0
149-
const bPinned = b.isPinned ? 1 : 0
150-
if (aPinned !== bPinned) {
151-
return bPinned - aPinned
152-
}
153-
154-
const aTime = a.lastUsedAt ?? a.updatedAt
155-
const bTime = b.lastUsedAt ?? b.updatedAt
156-
return bTime - aTime
157-
})
158-
.slice(0, MAX_VISIBLE)
159-
}, [templates, templateId, search, shouldShowCurrent, templateName])
135+
return getDisplayTemplates({
136+
templates,
137+
templateId,
138+
templateName,
139+
shouldShowCurrent,
140+
search,
141+
searchMode: "name",
142+
}).slice(0, MAX_VISIBLE)
143+
}, [templates, templateId, templateName, shouldShowCurrent, search])
160144

161145
useEffect(() => {
162146
if (!isOpen) return

packages/imagekit-editor-dev/src/components/templates/TemplatesLibraryView.tsx

Lines changed: 16 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,12 @@ import { useTemplateStorage } from "../../context/TemplateStorageContext"
3535
import { useDebounce } from "../../hooks/useDebounce"
3636
import type { TemplateRecord } from "../../storage"
3737
import { useEditorStore } from "../../store"
38-
import { formatTemplateNameForUI, truncateTemplateName } from "../../utils"
39-
import { chakraAny } from "../../utils/chakraAny"
38+
import {
39+
chakraAny,
40+
formatTemplateNameForUI,
41+
getDisplayTemplates,
42+
truncateTemplateName,
43+
} from "../../utils"
4044
import FilterChipsField from "../common/FilterChipsField"
4145
import MultiSelectListField from "../common/MultiSelectListField"
4246
import { SettingsModal } from "../header/SettingsModal"
@@ -140,25 +144,14 @@ export function TemplatesLibraryView({ onClose }: Props) {
140144
}, [templates])
141145

142146
const filtered = useMemo(() => {
143-
const base = templates
144-
.filter((t) => t.id !== templateId)
145-
.filter((t) => {
146-
if (
147-
shouldShowCurrent &&
148-
templateId === null &&
149-
t.name === templateName
150-
) {
151-
return false
152-
}
153-
return true
154-
})
155-
.filter((t) =>
156-
search
157-
? t.name.toLowerCase().includes(search.toLowerCase()) ||
158-
t.createdBy.name.toLowerCase().includes(search.toLowerCase()) ||
159-
t.createdBy.email.toLowerCase().includes(search.toLowerCase())
160-
: true,
161-
)
147+
const base = getDisplayTemplates({
148+
templates,
149+
templateId,
150+
templateName,
151+
shouldShowCurrent,
152+
search,
153+
searchMode: "nameOrCreator",
154+
})
162155
.filter((t) => {
163156
if (visibilityFilter.length === 0) return true
164157
const allowPrivate = visibilityFilter.includes("private")
@@ -175,19 +168,8 @@ export function TemplatesLibraryView({ onClose }: Props) {
175168
: true,
176169
)
177170

178-
// Sort so that pinned templates (for the local user) come first,
179-
// then all others by most recently used / updated.
180-
return [...base].sort((a, b) => {
181-
const aPinned = a.isPinned ? 1 : 0
182-
const bPinned = b.isPinned ? 1 : 0
183-
if (aPinned !== bPinned) {
184-
return bPinned - aPinned
185-
}
186-
187-
const aTime = a.lastUsedAt ?? a.updatedAt
188-
const bTime = b.lastUsedAt ?? b.updatedAt
189-
return bTime - aTime
190-
})
171+
// getDisplayTemplates already returns a pinned+recent sorted list.
172+
return base
191173
}, [
192174
templates,
193175
templateId,

packages/imagekit-editor-dev/src/utils/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,10 @@ export const truncateTemplateName = (name: string) => {
150150
}
151151
return `${normalized.slice(0, TEMPLATE_NAME_UI_MAX_LENGTH)}...`
152152
}
153+
154+
export { chakraAny } from "./chakraAny"
155+
export {
156+
getDisplayTemplates,
157+
shouldHideTemplateBecauseMatchesUnsavedCurrent,
158+
sortTemplatesPinnedThenRecent,
159+
} from "./templateList"
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { describe, expect, it } from "vitest"
2+
import type { TemplateRecord } from "../storage"
3+
import {
4+
getDisplayTemplates,
5+
shouldHideTemplateBecauseMatchesUnsavedCurrent,
6+
sortTemplatesPinnedThenRecent,
7+
} from "./templateList"
8+
9+
function makeTemplate(partial: Partial<TemplateRecord>): TemplateRecord {
10+
const now = 1_000_000
11+
return {
12+
id: "t-1",
13+
clientNumber: "c1",
14+
isPrivate: true,
15+
name: "Template 1",
16+
transformations: [],
17+
isPinned: false,
18+
createdBy: { userId: "u1", name: "Creator", email: "c@example.com" },
19+
updatedBy: { userId: "u1", name: "Creator", email: "c@example.com" },
20+
createdAt: now,
21+
updatedAt: now,
22+
...partial,
23+
}
24+
}
25+
26+
describe("templateList utilities", () => {
27+
describe("shouldHideTemplateBecauseMatchesUnsavedCurrent", () => {
28+
it("hides a record with same name when current is unsaved (templateId null) and Current row is shown", () => {
29+
const record = makeTemplate({ id: "saved", name: "In progress" })
30+
expect(
31+
shouldHideTemplateBecauseMatchesUnsavedCurrent({
32+
record,
33+
templateId: null,
34+
shouldShowCurrent: true,
35+
templateName: "In progress",
36+
}),
37+
).toBe(true)
38+
})
39+
40+
it("does not hide when Current row is not shown", () => {
41+
const record = makeTemplate({ id: "saved", name: "In progress" })
42+
expect(
43+
shouldHideTemplateBecauseMatchesUnsavedCurrent({
44+
record,
45+
templateId: null,
46+
shouldShowCurrent: false,
47+
templateName: "In progress",
48+
}),
49+
).toBe(false)
50+
})
51+
52+
it("does not hide when a saved template is active (templateId not null)", () => {
53+
const record = makeTemplate({ id: "saved", name: "Same" })
54+
expect(
55+
shouldHideTemplateBecauseMatchesUnsavedCurrent({
56+
record,
57+
templateId: "active-id",
58+
shouldShowCurrent: true,
59+
templateName: "Same",
60+
}),
61+
).toBe(false)
62+
})
63+
})
64+
65+
describe("sortTemplatesPinnedThenRecent", () => {
66+
it("sorts pinned before unpinned", () => {
67+
const a = makeTemplate({ id: "a", isPinned: false, updatedAt: 5 })
68+
const b = makeTemplate({ id: "b", isPinned: true, updatedAt: 1 })
69+
const sorted = [a, b].sort(sortTemplatesPinnedThenRecent)
70+
expect(sorted.map((t) => t.id)).toEqual(["b", "a"])
71+
})
72+
73+
it("sorts by lastUsedAt when present, else updatedAt", () => {
74+
const olderButUsed = makeTemplate({
75+
id: "used",
76+
isPinned: false,
77+
updatedAt: 10,
78+
lastUsedAt: 200,
79+
})
80+
const newerButNotUsed = makeTemplate({
81+
id: "updated",
82+
isPinned: false,
83+
updatedAt: 300,
84+
lastUsedAt: undefined,
85+
})
86+
const sorted = [olderButUsed, newerButNotUsed].sort(
87+
sortTemplatesPinnedThenRecent,
88+
)
89+
expect(sorted.map((t) => t.id)).toEqual(["updated", "used"])
90+
})
91+
})
92+
93+
describe("getDisplayTemplates", () => {
94+
it("excludes the active template by id", () => {
95+
const t1 = makeTemplate({ id: "t1", name: "One", updatedAt: 1 })
96+
const t2 = makeTemplate({ id: "t2", name: "Two", updatedAt: 2 })
97+
const list = getDisplayTemplates({
98+
templates: [t1, t2],
99+
templateId: "t2",
100+
templateName: "Two",
101+
shouldShowCurrent: true,
102+
search: "",
103+
})
104+
expect(list.map((t) => t.id)).toEqual(["t1"])
105+
})
106+
107+
it("hides the saved record that matches unsaved current name when templateId is null", () => {
108+
const savedSameName = makeTemplate({
109+
id: "saved",
110+
name: "In progress",
111+
updatedAt: 2,
112+
})
113+
const other = makeTemplate({ id: "other", name: "Other", updatedAt: 1 })
114+
const list = getDisplayTemplates({
115+
templates: [savedSameName, other],
116+
templateId: null,
117+
templateName: "In progress",
118+
shouldShowCurrent: true,
119+
search: "",
120+
})
121+
expect(list.map((t) => t.id)).toEqual(["other"])
122+
})
123+
124+
it("filters by name search (case-insensitive) by default", () => {
125+
const alpha = makeTemplate({ id: "a", name: "Alpha", updatedAt: 1 })
126+
const beta = makeTemplate({ id: "b", name: "Beta", updatedAt: 2 })
127+
const list = getDisplayTemplates({
128+
templates: [alpha, beta],
129+
templateId: null,
130+
templateName: "New",
131+
shouldShowCurrent: false,
132+
search: "alP",
133+
})
134+
expect(list.map((t) => t.id)).toEqual(["a"])
135+
})
136+
137+
it('supports searchMode "nameOrCreator"', () => {
138+
const byCreator = makeTemplate({
139+
id: "c",
140+
name: "Unrelated",
141+
createdBy: { userId: "u2", name: "Ada Lovelace", email: "ada@ex.com" },
142+
updatedAt: 1,
143+
})
144+
const other = makeTemplate({
145+
id: "o",
146+
name: "Other",
147+
createdBy: { userId: "u3", name: "Grace", email: "g@ex.com" },
148+
updatedAt: 2,
149+
})
150+
const list = getDisplayTemplates({
151+
templates: [byCreator, other],
152+
templateId: null,
153+
templateName: "New",
154+
shouldShowCurrent: false,
155+
search: "ada",
156+
searchMode: "nameOrCreator",
157+
})
158+
expect(list.map((t) => t.id)).toEqual(["c"])
159+
})
160+
161+
it("returns pinned+recent sorted results", () => {
162+
const pinnedOld = makeTemplate({
163+
id: "p",
164+
name: "Pinned",
165+
isPinned: true,
166+
updatedAt: 1,
167+
})
168+
const unpinnedNew = makeTemplate({
169+
id: "u",
170+
name: "Unpinned",
171+
isPinned: false,
172+
updatedAt: 999,
173+
})
174+
const list = getDisplayTemplates({
175+
templates: [unpinnedNew, pinnedOld],
176+
templateId: null,
177+
templateName: "New",
178+
shouldShowCurrent: false,
179+
search: "",
180+
})
181+
expect(list.map((t) => t.id)).toEqual(["p", "u"])
182+
})
183+
})
184+
})

0 commit comments

Comments
 (0)