Skip to content

Commit 80cc4d5

Browse files
abelpzcursoragent
andcommitted
refactor: use top-level fields as source of truth for resource metadata
- Simplified normalizeResourceInfo to promote metadata fields to top-level - Updated WordsLinksViewer to use resource.title directly - Updated TranslationNotesViewer to use resource.title directly - Updated TranslationQuestionsViewer to use resource.title directly - Removed fallback titles (metadata now always promoted via normalization) This fixes the issue where localized resource titles weren't showing in the viewer headers. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent cfd1353 commit 80cc4d5

File tree

9 files changed

+129
-38
lines changed

9 files changed

+129
-38
lines changed

apps/tc-study/src/components/read/SimplifiedReadView.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,16 +1000,8 @@ export function SimplifiedReadView({ initialLanguage }: SimplifiedReadViewProps
10001000
const viewerProps: any = {
10011001
resourceId: resource.id,
10021002
resourceKey: resourceKey,
1003-
}
1004-
1005-
// Add metadata - use stored metadata or resource object as fallback
1006-
if (resource.metadata != null) {
1007-
viewerProps.metadata = resource.metadata
1008-
} else if (resource.ingredients != null && resource.type === 'words') {
1009-
viewerProps.metadata = { ingredients: resource.ingredients, contentMetadata: { ingredients: resource.ingredients } }
1010-
} else {
1011-
// Use resource object itself as metadata fallback for other resource types
1012-
viewerProps.metadata = resource
1003+
// Metadata is now guaranteed to be populated via normalization
1004+
metadata: resource.metadata,
10131005
}
10141006

10151007
// Add onEntryLinkClick for entry-organized resources

apps/tc-study/src/components/resources/TranslationNotesViewer/index.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
*/
77

88
import { useSignal, useSignalHandler } from '@bt-synergy/resource-panels'
9-
import { BookOpen, ExternalLink, Loader } from 'lucide-react'
9+
import { BookOpen, ExternalLink, Loader, FileText } from 'lucide-react'
1010
import { useCallback, useEffect, useMemo, useState } from 'react'
1111
import { useCatalogManager, useCurrentReference, useResourceTypeRegistry } from '../../../contexts'
1212
import type { EntryLinkClickSignal, TokenClickSignal } from '../../../signals/studioSignals'
1313
import { checkDependenciesReady } from '../../../utils/resourceDependencies'
14+
import { getBookTitleStatic } from '../../../utils/bookNames'
15+
import { ResourceViewerHeader } from '../common/ResourceViewerHeader'
1416
import { TranslationNoteCard } from './components/TranslationNoteCard'
1517
import { useTranslationNotesContent } from './hooks/useTranslationNotesContent'
1618
import { useTATitles } from './hooks/useTATitles'
@@ -19,17 +21,19 @@ import { useAlignedTokens, useQuoteTokens, useScriptureTokens } from '../WordsLi
1921
import { generateSemanticIdsForQuoteTokens } from '../WordsLinksViewer/utils'
2022
import { TokenFilterBanner } from '../WordsLinksViewer/components/TokenFilterBanner'
2123

24+
import type { ResourceInfo } from '../../../contexts/types'
25+
2226
interface TranslationNotesViewerProps {
2327
resourceKey: string
2428
resourceId: string
25-
metadata?: any
29+
resource: ResourceInfo
2630
onEntryLinkClick?: (resourceKey: string, entryId: string) => void
2731
}
2832

2933
export function TranslationNotesViewer({
3034
resourceKey,
3135
resourceId,
32-
metadata,
36+
resource,
3337
onEntryLinkClick,
3438
}: TranslationNotesViewerProps) {
3539
const currentRef = useCurrentReference()
@@ -404,6 +408,12 @@ export function TranslationNotesViewer({
404408

405409
return (
406410
<div className="h-full flex flex-col">
411+
<ResourceViewerHeader
412+
title={resource.title}
413+
icon={FileText}
414+
subtitle={resource.languageTitle}
415+
/>
416+
407417
{tokenFilter && (
408418
<TokenFilterBanner
409419
tokenFilter={tokenFilter}
@@ -442,7 +452,7 @@ export function TranslationNotesViewer({
442452
<div className="flex items-center gap-2 pb-2 border-b border-gray-200">
443453
<BookOpen className="w-4 h-4 text-amber-600" />
444454
<h3 className="text-sm font-semibold text-gray-700">
445-
{currentRef.book.toUpperCase()} {verse}
455+
{getBookTitleStatic(currentRef.book)} {verse}
446456
</h3>
447457
<span className="ml-auto px-2 py-0.5 bg-amber-100 text-amber-700 rounded-full text-xs font-medium">
448458
{verseNotes.length}

apps/tc-study/src/components/resources/TranslationQuestionsViewer/index.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@
77

88
import type { ProcessedQuestions } from '@bt-synergy/resource-parsers'
99
import type { ResourceViewerProps } from '@bt-synergy/resource-types'
10-
import { AlertCircle, CheckCircle, ChevronDown, ChevronUp, HelpCircle } from 'lucide-react'
10+
import { AlertCircle, CheckCircle, ChevronDown, ChevronUp, HelpCircle, MessageCircleQuestion } from 'lucide-react'
1111
import { useEffect, useMemo, useState } from 'react'
1212
import { useCurrentReference } from '../../../contexts'
1313
import { useLoaderRegistry } from '../../../contexts/CatalogContext'
14+
import { ResourceViewerHeader } from '../common/ResourceViewerHeader'
1415

15-
export function TranslationQuestionsViewer({ resourceKey, metadata }: ResourceViewerProps) {
16+
export function TranslationQuestionsViewer({ resourceKey, resource }: ResourceViewerProps & { resource: any }) {
1617
const [questions, setQuestions] = useState<ProcessedQuestions | null>(null)
1718
const [loading, setLoading] = useState(false)
1819
const [error, setError] = useState<string | null>(null)
@@ -161,8 +162,15 @@ export function TranslationQuestionsViewer({ resourceKey, metadata }: ResourceVi
161162
}
162163

163164
return (
164-
<div className="h-full overflow-y-auto bg-gray-50 p-4 space-y-3">
165-
{relevantQuestions.map((question, index) => {
165+
<div className="h-full flex flex-col">
166+
<ResourceViewerHeader
167+
title={resource.title}
168+
icon={MessageCircleQuestion}
169+
subtitle={resource.languageTitle}
170+
/>
171+
172+
<div className="flex-1 overflow-y-auto bg-gray-50 p-4 space-y-3">
173+
{relevantQuestions.map((question, index) => {
166174
const isExpanded = expandedQuestions.has(question.id)
167175

168176
return (
@@ -212,6 +220,7 @@ export function TranslationQuestionsViewer({ resourceKey, metadata }: ResourceVi
212220
</div>
213221
)
214222
})}
223+
</div>
215224
</div>
216225
)
217226
}

apps/tc-study/src/components/resources/WordsLinksViewer/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { generateSemanticIdsForQuoteTokens, parseTWLink } from './utils'
3232
export function WordsLinksViewer({
3333
resourceId,
3434
resourceKey,
35-
metadata,
35+
resource,
3636
wordsLinksContent,
3737
onEntryLinkClick,
3838
}: WordsLinksViewerProps) {
@@ -413,9 +413,9 @@ export function WordsLinksViewer({
413413
return (
414414
<div className="h-full flex flex-col">
415415
<ResourceViewerHeader
416-
title={metadata?.title || 'Translation Words Links'}
416+
title={resource.title}
417417
icon={Link}
418-
subtitle={metadata?.language_title}
418+
subtitle={resource.languageTitle}
419419
/>
420420

421421
{tokenFilter && (

apps/tc-study/src/components/resources/WordsLinksViewer/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import type { ProcessedWordsLinks, TranslationWordsLink } from '@bt-synergy/reso
88

99
export type { ProcessedWordsLinks, TranslationWordsLink }
1010

11+
import type { ResourceInfo } from '../../../contexts/types'
12+
1113
export interface WordsLinksViewerProps {
1214
resourceId: string
1315
resourceKey: string
14-
metadata?: any
16+
resource: ResourceInfo
1517
wordsLinksContent?: ProcessedWordsLinks
1618
onEntryLinkClick?: (resourceKey: string, entryId: string) => void
1719
}

apps/tc-study/src/components/studio/LinkedPanelsStudio.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -760,23 +760,21 @@ export function LinkedPanelsStudio() {
760760
viewerProps.isAnchor = isAnchor
761761
} else if (resource.type === 'words' || resource.category === 'words') {
762762
// TranslationWordsViewer needs metadata and onEntryLinkClick
763-
viewerProps.metadata = resource.metadata || resource // Use stored metadata or resource as fallback
763+
viewerProps.metadata = resource.metadata
764764
viewerProps.onEntryLinkClick = handleOpenEntry
765765
} else if (resource.type === 'words-links' || resource.category === 'words-links' || resource.type === 'twl') {
766766
// WordsLinksViewer needs metadata and onEntryLinkClick
767-
viewerProps.metadata = resource.metadata || resource // Use stored metadata or resource as fallback
767+
viewerProps.metadata = resource.metadata
768768
viewerProps.onEntryLinkClick = handleOpenEntry
769769
} else if (resource.type === 'notes' || resource.type === 'tn' || resource.type === 'questions' || resource.type === 'tq') {
770770
// Translation Notes/Questions viewers need metadata and onEntryLinkClick
771-
viewerProps.metadata = resource.metadata || resource // Use stored metadata or resource as fallback
771+
viewerProps.metadata = resource.metadata
772772
if (resource.type === 'notes' || resource.type === 'tn') {
773773
viewerProps.onEntryLinkClick = handleOpenEntry
774774
}
775775
} else {
776-
// For any other viewer that might need metadata, provide it if available
777-
if (resource.metadata) {
778-
viewerProps.metadata = resource.metadata
779-
}
776+
// For any other viewer that might need metadata, provide it
777+
viewerProps.metadata = resource.metadata
780778
}
781779

782780
return <ViewerComponent {...viewerProps} />

apps/tc-study/src/contexts/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,13 @@ export interface ResourceInfo {
4141
category: string
4242
toc?: ResourceTOC
4343

44+
// Full ResourceMetadata object - ALWAYS populated via normalization
45+
// This is the single source of truth for resource metadata
46+
metadata: any // ResourceMetadata from @bt-synergy/resource-catalog (any for now to avoid circular deps)
47+
4448
// Additional metadata for resource management
49+
// These fields are kept for backward compatibility and convenience,
50+
// but metadata object should be the primary source
4551
language?: string
4652
languageCode?: string
4753
languageName?: string // Human-readable language name (e.g., "English", "español, Latinoamérica")
@@ -54,7 +60,6 @@ export interface ResourceInfo {
5460
ingredients?: any[] // Full ingredient objects from Door43
5561
version?: string
5662
contentStructure?: 'book' | 'entry' // How content is organized
57-
metadata?: any // Full ResourceMetadata object for viewers that need it (e.g., TranslationWordsViewer)
5863
release?: any // Release object from Door43 API (contains tag_name, published_at, etc.)
5964

6065
// Extended metadata for resource information display

apps/tc-study/src/lib/stores/workspaceStore.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import { create } from 'zustand'
1212
import { immer } from 'zustand/middleware/immer'
1313
import type { ResourceInfo } from '../../contexts/types'
14+
import { normalizeResourceInfo } from '../../utils/normalizeResourceInfo'
1415

1516
export interface PanelConfig {
1617
id: string // Unique panel ID (e.g., 'panel-1', 'panel-2', 'panel-3')
@@ -370,18 +371,21 @@ export const useWorkspaceStore = create<WorkspaceStore>()(
370371
version: collection.version,
371372
description: collection.description,
372373
resources: new Map(
373-
(collection.resources || []).map((res: any) => [
374-
`${res.owner}/${res.language}/${res.resourceId}`,
375-
{
376-
key: `${res.owner}/${res.language}/${res.resourceId}`,
374+
(collection.resources || []).map((res: any) => {
375+
const resourceKey = `${res.owner}/${res.language}/${res.resourceId}`
376+
const resourceInfo: ResourceInfo = {
377+
key: resourceKey,
377378
id: res.resourceId,
378379
title: res.displayName || res.resourceId,
379380
type: 'unknown',
381+
category: 'unknown',
380382
owner: res.owner,
381383
language: res.language,
382384
server: res.server,
383-
} as ResourceInfo
384-
])
385+
}
386+
// Normalize to ensure metadata is populated
387+
return [resourceKey, normalizeResourceInfo(resourceInfo)]
388+
})
385389
),
386390
panels: collection.panelLayout?.panels?.map((panel: any, idx: number) => ({
387391
id: panel.id,
@@ -422,9 +426,13 @@ export const useWorkspaceStore = create<WorkspaceStore>()(
422426
if (!saved) return false
423427

424428
const data = JSON.parse(saved)
429+
430+
// Import normalization utility
431+
const { normalizeResourceInfoMap } = require('../../utils/normalizeResourceInfo')
432+
425433
const workspace: WorkspacePackage = {
426434
...data,
427-
resources: new Map(data.resources || []),
435+
resources: normalizeResourceInfoMap(new Map(data.resources || [])),
428436
}
429437

430438
get().loadPackage(workspace)
@@ -531,6 +539,7 @@ export const useWorkspaceStore = create<WorkspaceStore>()(
531539
description: metadata.description || door43Data?.description,
532540
readme: metadata.longDescription || door43Data?.readme,
533541
license: typeof metadata.license === 'string' ? metadata.license : metadata.license?.id || door43Data?.license,
542+
metadata: metadata, // ⭐ Include full metadata
534543
})
535544

536545
added++
@@ -550,9 +559,12 @@ export const useWorkspaceStore = create<WorkspaceStore>()(
550559

551560
// Resource management
552561
addResourceToPackage: (resource) => {
562+
// Normalize resource to ensure metadata is always populated
563+
const normalizedResource = normalizeResourceInfo(resource)
564+
553565
set((state) => {
554566
if (state.currentPackage) {
555-
state.currentPackage.resources.set(resource.key, resource)
567+
state.currentPackage.resources.set(normalizedResource.key, normalizedResource)
556568
state.isPackageModified = true
557569
}
558570
})
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* Resource Metadata Normalization Utility
3+
*
4+
* Ensures all ResourceInfo objects have a complete metadata field,
5+
* regardless of where they come from (UI, collections, preloaded, etc.)
6+
*/
7+
8+
import type { ResourceInfo } from '../contexts/types'
9+
import type { ResourceMetadata } from '@bt-synergy/resource-catalog'
10+
11+
/**
12+
* Normalize a ResourceInfo object - promotes metadata to top-level fields
13+
*
14+
* Top-level fields are the source of truth. If rich metadata exists (from Door43),
15+
* we promote those fields to top-level so all viewers can access them consistently.
16+
*
17+
* @param resource - ResourceInfo object (possibly with nested metadata)
18+
* @returns ResourceInfo with promoted top-level fields
19+
*/
20+
export function normalizeResourceInfo(resource: ResourceInfo): ResourceInfo {
21+
// If we have rich metadata from Door43 API, promote it to top-level
22+
if (resource.metadata && typeof resource.metadata === 'object') {
23+
return {
24+
...resource,
25+
// Promote metadata fields to top-level (these become source of truth)
26+
title: resource.metadata.title || resource.title,
27+
language: resource.metadata.language || resource.language,
28+
languageCode: resource.metadata.language || resource.languageCode,
29+
languageTitle: resource.metadata.languageTitle || resource.languageTitle,
30+
owner: resource.metadata.owner || resource.owner,
31+
resourceId: resource.metadata.resourceId || resource.resourceId,
32+
type: resource.metadata.type || resource.type,
33+
subject: resource.metadata.subject || resource.subject,
34+
description: resource.metadata.description || resource.description,
35+
version: resource.metadata.version || resource.version,
36+
// Keep metadata object for modal displays and detailed info
37+
metadata: resource.metadata,
38+
}
39+
}
40+
41+
// No metadata - top-level fields are already set, nothing to promote
42+
return resource
43+
}
44+
45+
/**
46+
* Normalize an array of ResourceInfo objects
47+
*/
48+
export function normalizeResourceInfoArray(resources: ResourceInfo[]): ResourceInfo[] {
49+
return resources.map(normalizeResourceInfo)
50+
}
51+
52+
/**
53+
* Normalize a Map of ResourceInfo objects
54+
*/
55+
export function normalizeResourceInfoMap(resourcesMap: Map<string, ResourceInfo>): Map<string, ResourceInfo> {
56+
const normalized = new Map<string, ResourceInfo>()
57+
58+
for (const [key, resource] of resourcesMap) {
59+
normalized.set(key, normalizeResourceInfo(resource))
60+
}
61+
62+
return normalized
63+
}

0 commit comments

Comments
 (0)