@@ -32,6 +32,142 @@ const { formatMessage } = useVIntl()
3232const route = useRoute ()
3333const router = useRouter ()
3434
35+ const CACHE_TTL = 24 * 60 * 60 * 1000
36+
37+ type CachedSource = {
38+ source: string
39+ timestamp: number
40+ }
41+
42+ function getCachedSource(detailId : string ): string | null {
43+ try {
44+ const cached = localStorage .getItem (` tech_review_source_${detailId } ` )
45+ if (! cached ) return null
46+
47+ const data: CachedSource = JSON .parse (cached )
48+ const now = Date .now ()
49+
50+ if (now - data .timestamp > CACHE_TTL ) {
51+ localStorage .removeItem (` tech_review_source_${detailId } ` )
52+ return null
53+ }
54+
55+ return data .source
56+ } catch {
57+ return null
58+ }
59+ }
60+
61+ function setCachedSource(detailId : string , source : string ): void {
62+ try {
63+ const data: CachedSource = {
64+ source ,
65+ timestamp: Date .now (),
66+ }
67+ localStorage .setItem (` tech_review_source_${detailId } ` , JSON .stringify (data ))
68+ } catch (error ) {
69+ console .error (' Failed to cache source:' , error )
70+ }
71+ }
72+
73+ function clearExpiredCache(): void {
74+ try {
75+ const now = Date .now ()
76+ const keys = Object .keys (localStorage )
77+
78+ for (const key of keys ) {
79+ if (key .startsWith (' tech_review_source_' )) {
80+ const cached = localStorage .getItem (key )
81+ if (cached ) {
82+ const data: CachedSource = JSON .parse (cached )
83+ if (now - data .timestamp > CACHE_TTL ) {
84+ localStorage .removeItem (key )
85+ }
86+ }
87+ }
88+ }
89+ } catch (error ) {
90+ console .error (' Failed to clear expired cache:' , error )
91+ }
92+ }
93+
94+ clearExpiredCache ()
95+
96+ const loadingIssues = ref <Set <string >>(new Set ())
97+
98+ async function loadIssueSource(issueId : string , projectId : string ): Promise <void > {
99+ if (loadingIssues .value .has (issueId )) return
100+
101+ loadingIssues .value .add (issueId )
102+
103+ try {
104+ const issueData = await client .labrinth .tech_review_internal .getIssue (issueId )
105+
106+ for (const detail of issueData .details ) {
107+ if (detail .decompiled_source ) {
108+ setCachedSource (detail .id , detail .decompiled_source )
109+
110+ const review = reviewItems .value .find ((r ) => r .project .id === projectId )
111+ if (review ) {
112+ for (const report of review .reports ) {
113+ for (const issue of report .issues ) {
114+ if (issue .id === issueId ) {
115+ const existingDetail = issue .details .find ((d ) => d .id === detail .id )
116+ if (existingDetail ) {
117+ existingDetail .decompiled_source = detail .decompiled_source
118+ }
119+ }
120+ }
121+ }
122+ }
123+ }
124+ }
125+ } catch (error ) {
126+ console .error (' Failed to load issue source:' , error )
127+ } finally {
128+ loadingIssues .value .delete (issueId )
129+ }
130+ }
131+
132+ function tryLoadCachedSources(issueId : string , projectId : string ): void {
133+ const review = reviewItems .value .find ((r ) => r .project .id === projectId )
134+ if (! review ) return
135+
136+ for (const report of review .reports ) {
137+ for (const issue of report .issues ) {
138+ if (issue .id === issueId ) {
139+ for (const detail of issue .details ) {
140+ if (! detail .decompiled_source ) {
141+ const cached = getCachedSource (detail .id )
142+ if (cached ) {
143+ detail .decompiled_source = cached
144+ }
145+ }
146+ }
147+ }
148+ }
149+ }
150+ }
151+
152+ function handleLoadSource(issueId : string , projectId : string ): void {
153+ tryLoadCachedSources (issueId , projectId )
154+
155+ const review = reviewItems .value .find ((r ) => r .project .id === projectId )
156+ if (! review ) return
157+
158+ for (const report of review .reports ) {
159+ for (const issue of report .issues ) {
160+ if (issue .id === issueId ) {
161+ const hasUncached = issue .details .some ((d ) => ! d .decompiled_source )
162+ if (hasUncached ) {
163+ loadIssueSource (issueId , projectId )
164+ }
165+ return
166+ }
167+ }
168+ }
169+ }
170+
35171const messages = defineMessages ({
36172 searchPlaceholder: {
37173 id: ' moderation.search.placeholder' ,
@@ -200,6 +336,18 @@ const filteredItems = computed(() => {
200336 return filtered
201337})
202338
339+ const filteredIssuesCount = computed (() => {
340+ return filteredItems .value .reduce ((total , review ) => {
341+ if (currentFilterType .value === ' All issues' ) {
342+ return total + review .reports .reduce ((sum , report ) => sum + report .issues .length , 0 )
343+ } else {
344+ return total + review .reports .reduce ((sum , report ) => {
345+ return sum + report .issues .filter ((issue ) => issue .issue_type === currentFilterType .value ).length
346+ }, 0 )
347+ }
348+ }, 0 )
349+ })
350+
203351const totalPages = computed (() => Math .ceil ((filteredItems .value ?.length || 0 ) / itemsPerPage ))
204352const paginatedItems = computed (() => {
205353 if (! filteredItems .value ) return []
@@ -241,13 +389,7 @@ const {
241389 page: 0 ,
242390 sort_by: toApiSort (currentSortType .value ),
243391 })
244- },
245- initialData: {
246- reports: [],
247- projects: {},
248- threads: {},
249- ownership: {},
250- } as Labrinth .TechReview .Internal .SearchResponse ,
392+ }
251393})
252394
253395// TEMPORARY: Mock data for development (58 items to match batch scan progress)
@@ -259,8 +401,6 @@ const {
259401// searchResponse.value = generateMockSearchResponse(58)
260402// }
261403
262- // Adapter: Transform flat SearchResponse into project-grouped structure
263- // for easier consumption by existing UI components
264404const reviewItems = computed (() => {
265405 if (! searchResponse .value || searchResponse .value .reports .length === 0 ) {
266406 return []
@@ -283,13 +423,14 @@ const reviewItems = computed(() => {
283423 const projectId = report .project_id
284424
285425 if (! projectMap .has (projectId )) {
286- // Find the thread associated with this project
287- const thread = Object .values (response .threads ).find ((t ) => t .project_id === projectId )
426+ // Get project and thread using direct lookups
427+ const project = response .projects [projectId ]
428+ const thread = project ?.thread_id ? response .threads [project .thread_id ] : undefined
288429
289430 if (! thread ) continue // Skip if no thread found
290431
291432 projectMap .set (projectId , {
292- project: response . projects [ projectId ] ,
433+ project ,
293434 project_owner: response .ownership [projectId ],
294435 thread ,
295436 reports: [],
@@ -302,6 +443,8 @@ const reviewItems = computed(() => {
302443 return Array .from (projectMap .values ())
303444})
304445
446+ console .log (reviewItems .value )
447+
305448watch (currentSortType , () => {
306449 goToPage (1 )
307450 refetch ()
@@ -355,7 +498,7 @@ watch(currentSortType, () => {
355498 <template #selected >
356499 <span class =" flex flex-row gap-2 align-middle font-semibold text-primary" >
357500 <FilterIcon class =" size-4 flex-shrink-0" />
358- <span class =" truncate" >{{ currentFilterType }} ({{ filteredItems.length }})</span >
501+ <span class =" truncate" >{{ currentFilterType }} ({{ filteredIssuesCount }})</span >
359502 </span >
360503 </template >
361504 </Combobox >
@@ -392,7 +535,12 @@ watch(currentSortType, () => {
392535 class =" universal-card h-24 animate-pulse"
393536 ></div >
394537 <div v-for =" (item, idx) in paginatedItems" v-else :key =" item.project.id ?? idx" class =" " >
395- <ModerationTechRevCard :item =" item" @refetch =" refetch" />
538+ <ModerationTechRevCard
539+ :item =" item"
540+ :loading-issues =" loadingIssues"
541+ @refetch =" refetch"
542+ @load-source =" (issueId: string) => handleLoadSource(issueId, item.project.id)"
543+ />
396544 </div >
397545 </div >
398546
0 commit comments