From d3eb4b5cdad7748bad9140034cc9e2368d1c27fa Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 13 Aug 2025 13:57:22 -0400 Subject: [PATCH 01/18] Reload when opening existing WebViews --- .../src/utils/project-manager.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/platform.bible-extension/src/utils/project-manager.ts b/platform.bible-extension/src/utils/project-manager.ts index b3d1a8570b..c92adaf58d 100644 --- a/platform.bible-extension/src/utils/project-manager.ts +++ b/platform.bible-extension/src/utils/project-manager.ts @@ -1,4 +1,4 @@ -import papi, { logger } from '@papi/backend'; +import { logger, projectDataProviders, webViews } from '@papi/backend'; import type { MandatoryProjectDataTypes } from '@papi/core'; import type { OpenWebViewOptionsWithProjectId, @@ -79,13 +79,15 @@ export class ProjectManager { layout?: Layout, options?: OpenWebViewOptionsWithProjectId, ): Promise { - const existingId = this.webViewIds[webViewType]; - const newOptions = { ...options, existingId, projectId: this.projectId }; + const webViewId = this.webViewIds[webViewType]; + const newOptions = { ...options, projectId: this.projectId }; logger.info(`Opening ${webViewType} WebView for project ${this.projectId}`); - logger.info(`WebView options: ${JSON.stringify(options)}`); - const newId = await papi.webViews.openWebView(webViewType, layout, newOptions); - if (newId) { - this.webViewIds[webViewType] = newId; + logger.info(`WebView options: ${JSON.stringify(newOptions)}`); + if (webViewId && (await webViews.reloadWebView(webViewType, webViewId, newOptions))) { + return true; + } + this.webViewIds[webViewType] = await webViews.openWebView(webViewType, layout, newOptions); + if (this.webViewIds[webViewType]) { return true; } logger.warn(`Failed to open ${webViewType} WebView for project ${this.projectId}`); @@ -95,7 +97,7 @@ export class ProjectManager { private async getDataProvider(): Promise< IBaseProjectDataProvider | undefined > { - this.dataProvider ||= await papi.projectDataProviders.get('platform.base', this.projectId); + this.dataProvider ||= await projectDataProviders.get('platform.base', this.projectId); return this.dataProvider; } From 61066d396a7a7efcbead84946b511a3b00756f7a Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Mon, 8 Sep 2025 16:37:34 -0400 Subject: [PATCH 02/18] Start matching styles of dictionary extension --- .../contributions/localizedStrings.json | 7 +- .../src/components/entry-list.tsx | 22 ++++++ .../src/types/localized-string-keys.ts | 10 +++ .../src/utils/use-is-wide-screen.tsx | 20 ++++++ .../src/web-views/find-word.web-view.tsx | 69 ++++++++++++++----- 5 files changed, 109 insertions(+), 19 deletions(-) create mode 100644 platform.bible-extension/src/components/entry-list.tsx create mode 100644 platform.bible-extension/src/types/localized-string-keys.ts create mode 100644 platform.bible-extension/src/utils/use-is-wide-screen.tsx diff --git a/platform.bible-extension/contributions/localizedStrings.json b/platform.bible-extension/contributions/localizedStrings.json index fda3ce5f44..ee60d675ba 100644 --- a/platform.bible-extension/contributions/localizedStrings.json +++ b/platform.bible-extension/contributions/localizedStrings.json @@ -5,9 +5,14 @@ "%fwLiteExtension_addWord_title%": "Add to FieldWorks", "%fwLiteExtension_browseDictionary_title%": "FieldWorks Lite", "%fwLiteExtension_selectDictionary_title%": "Select FieldWorks dictionary", + "%fwLiteExtension_error_failedToAddEntry%": "Failed to add entry!", + "%fwLiteExtension_error_gettingNetworkObject%": "Error getting network object:", + "%fwLiteExtension_error_missingParam%": "Missing required parameter: ", "%fwLiteExtension_findRelatedWords_title%": "Find related words in FieldWorks", "%fwLiteExtension_findWord_title%": "Search in FieldWorks", - "%fwLiteExtension_find_in_dictionary%": "Find in FieldWorks Lite", + "%fwLiteExtension_findWord_textField%": "Find in dictionary...", + "%fwLiteExtension_findWord_loading%": "Loading...", + "%fwLiteExtension_findWord_noMatchingEntries%": "No matching entries", "%fwLiteExtension_list_projects_label%": "List all local FieldWorks projects", "%fwLiteExtension_menu_addEntry%": "Add to FieldWorks...", "%fwLiteExtension_menu_browseDictionary%": "Browse FieldWorks dictionary", diff --git a/platform.bible-extension/src/components/entry-list.tsx b/platform.bible-extension/src/components/entry-list.tsx new file mode 100644 index 0000000000..fb3580b6ae --- /dev/null +++ b/platform.bible-extension/src/components/entry-list.tsx @@ -0,0 +1,22 @@ +import type { IEntry } from 'fw-lite-extension'; +import type { ReactElement } from 'react'; +import EntryCard from './entry-card'; + +interface EntryListProps { + entries: IEntry[]; +} + +// Match className from paranext-core/extensions/src/components/dictionary/dictionary-list.component.tsx +export default function EntryList({ entries }: EntryListProps): ReactElement { + return ( +
+
+
    + {entries.map((entry) => ( + + ))} +
+
+
+ ); +} diff --git a/platform.bible-extension/src/types/localized-string-keys.ts b/platform.bible-extension/src/types/localized-string-keys.ts new file mode 100644 index 0000000000..a85f9e8e92 --- /dev/null +++ b/platform.bible-extension/src/types/localized-string-keys.ts @@ -0,0 +1,10 @@ +import { LocalizeKey } from 'platform-bible-utils'; + +export const LOCALIZED_STRING_KEYS: LocalizeKey[] = [ + '%fwLiteExtension_error_failedToAddEntry%', + '%fwLiteExtension_error_gettingNetworkObject%', + '%fwLiteExtension_error_missingParam%', + '%fwLiteExtension_findWord_loading%', + '%fwLiteExtension_findWord_noMatchingEntries%', + '%fwLiteExtension_findWord_textField%', +]; diff --git a/platform.bible-extension/src/utils/use-is-wide-screen.tsx b/platform.bible-extension/src/utils/use-is-wide-screen.tsx new file mode 100644 index 0000000000..946efe34d2 --- /dev/null +++ b/platform.bible-extension/src/utils/use-is-wide-screen.tsx @@ -0,0 +1,20 @@ +import { useEffect, useState } from 'react'; + +export function useIsWideScreen() { + const [isWide, setIsWide] = useState(() => window.innerWidth >= 1024); + + useEffect(() => { + // Matches Tailwind css lg breakpoint + const mediaQuery = window.matchMedia('(min-width: 1024px)'); + + const handler = (e: MediaQueryListEvent) => setIsWide(e.matches); + mediaQuery.addEventListener('change', handler); + + // Set initial state + setIsWide(mediaQuery.matches); + + return () => mediaQuery.removeEventListener('change', handler); + }, []); + + return isWide; +} diff --git a/platform.bible-extension/src/web-views/find-word.web-view.tsx b/platform.bible-extension/src/web-views/find-word.web-view.tsx index 926b1bed3f..1c816e4a64 100644 --- a/platform.bible-extension/src/web-views/find-word.web-view.tsx +++ b/platform.bible-extension/src/web-views/find-word.web-view.tsx @@ -1,11 +1,13 @@ import type { NetworkObject } from '@papi/core'; import papi, { logger } from '@papi/frontend'; +import { useLocalizedStrings } from '@papi/frontend/react'; import type { IEntry, IEntryService, PartialEntry, WordWebViewOptions } from 'fw-lite-extension'; -import { SearchBar } from 'platform-bible-react'; +import { Label, SearchBar } from 'platform-bible-react'; import { debounce } from 'platform-bible-utils'; import { useCallback, useEffect, useMemo, useState } from 'react'; import AddNewEntry from '../components/add-new-entry'; -import EntryCard from '../components/entry-card'; +import EntryList from '../components/entry-list'; +import { LOCALIZED_STRING_KEYS } from '../types/localized-string-keys'; /* eslint-disable react-hooks/rules-of-hooks */ @@ -20,6 +22,7 @@ globalThis.webViewComponent = function fwLiteFindWord({ NetworkObject | undefined >(); const [isFetching, setIsFetching] = useState(false); + const [localizedStrings] = useLocalizedStrings(LOCALIZED_STRING_KEYS); const [searchTerm, setSearchTerm] = useState(word ?? ''); useEffect(() => { @@ -30,14 +33,23 @@ globalThis.webViewComponent = function fwLiteFindWord({ logger.info('Got network object:', networkObject); setFwLiteNetworkObject(networkObject); }) - .catch((e) => logger.error('Error getting network object:', JSON.stringify(e))); - }, []); + .catch((e) => + logger.error( + `${localizedStrings['%fwLiteExtension_error_gettingNetworkObject%']}:`, + JSON.stringify(e), + ), + ); + }, [localizedStrings]); const fetchEntries = useCallback( async (untrimmedSurfaceForm: string) => { if (!projectId || !fwLiteNetworkObject) { - if (!projectId) logger.warn('Missing required parameter: projectId'); - if (!fwLiteNetworkObject) logger.warn('Missing required parameter: fwLiteNetworkObject'); + if (!projectId) + logger.warn(`${localizedStrings['%fwLiteExtension_error_missingParam%']}projectId`); + if (!fwLiteNetworkObject) + logger.warn( + `${localizedStrings['%fwLiteExtension_error_missingParam%']}fwLiteNetworkObject`, + ); return; } @@ -53,7 +65,7 @@ globalThis.webViewComponent = function fwLiteFindWord({ setIsFetching(false); setMatchingEntries(entries ?? []); }, - [fwLiteNetworkObject, projectId], + [fwLiteNetworkObject, localizedStrings, projectId], ); const debouncedFetchEntries = useMemo(() => debounce(fetchEntries, 500), [fetchEntries]); @@ -69,8 +81,12 @@ globalThis.webViewComponent = function fwLiteFindWord({ const addEntry = useCallback( async (entry: PartialEntry) => { if (!projectId || !fwLiteNetworkObject) { - if (!projectId) logger.warn('Missing required parameter: projectId'); - if (!fwLiteNetworkObject) logger.warn('Missing required parameter: fwLiteNetworkObject'); + if (!projectId) + logger.warn(`${localizedStrings['%fwLiteExtension_error_missingParam%']}projectId`); + if (!fwLiteNetworkObject) + logger.warn( + `${localizedStrings['%fwLiteExtension_error_missingParam%']}fwLiteNetworkObject`, + ); return; } @@ -80,21 +96,38 @@ globalThis.webViewComponent = function fwLiteFindWord({ onSearch(Object.values(addedEntry.lexemeForm).pop() ?? ''); await papi.commands.sendCommand('fwLiteExtension.displayEntry', projectId, addedEntry.id); } else { - logger.error('Failed to add entry!'); + logger.error(`${localizedStrings['%fwLiteExtension_error_failedToAddEntry%']}`); } }, - [fwLiteNetworkObject, onSearch, projectId], + [fwLiteNetworkObject, localizedStrings, onSearch, projectId], ); + // Match className from paranext-core/extensions/src/platform-lexical-tools/src/web-views/dictionary.web-view.tsx return ( -
- +
+
+
+
+ +
+
+
- {isFetching &&

Loading...

} - {!matchingEntries?.length && !isFetching &&

No matching entries

} - {matchingEntries?.map((entry) => ( - - ))} + {isFetching && ( +
+ +
+ )} + {!matchingEntries?.length && !isFetching && ( +
+ +
+ )} + {matchingEntries && } Date: Tue, 9 Sep 2025 15:53:02 -0400 Subject: [PATCH 03/18] Continue matching styles of dictionary extension --- .../contributions/localizedStrings.json | 2 + .../src/components/back-to-list-button.tsx | 52 +++++++ .../components/dictionary-entry-display.tsx | 124 +++++++++++++++ .../src/components/dictionary-list-item.tsx | 64 ++++++++ .../src/components/dictionary-list.tsx | 143 ++++++++++++++++++ .../src/components/domains-display.tsx | 37 +++++ .../src/components/entry-card.tsx | 17 +-- .../src/components/entry-list.tsx | 22 --- .../src/types/localized-string-keys.ts | 2 + .../src/utils/entry-display-text.ts | 34 +++++ .../src/web-views/find-word.web-view.tsx | 22 +-- 11 files changed, 473 insertions(+), 46 deletions(-) create mode 100644 platform.bible-extension/src/components/back-to-list-button.tsx create mode 100644 platform.bible-extension/src/components/dictionary-entry-display.tsx create mode 100644 platform.bible-extension/src/components/dictionary-list-item.tsx create mode 100644 platform.bible-extension/src/components/dictionary-list.tsx create mode 100644 platform.bible-extension/src/components/domains-display.tsx delete mode 100644 platform.bible-extension/src/components/entry-list.tsx create mode 100644 platform.bible-extension/src/utils/entry-display-text.ts diff --git a/platform.bible-extension/contributions/localizedStrings.json b/platform.bible-extension/contributions/localizedStrings.json index ee60d675ba..b0a9e0e529 100644 --- a/platform.bible-extension/contributions/localizedStrings.json +++ b/platform.bible-extension/contributions/localizedStrings.json @@ -5,6 +5,8 @@ "%fwLiteExtension_addWord_title%": "Add to FieldWorks", "%fwLiteExtension_browseDictionary_title%": "FieldWorks Lite", "%fwLiteExtension_selectDictionary_title%": "Select FieldWorks dictionary", + "%fwLiteExtension_entryDisplay_partOfSpeech%": "Part of speech: ", + "%fwLiteExtension_entryDisplay_senses%": "Senses", "%fwLiteExtension_error_failedToAddEntry%": "Failed to add entry!", "%fwLiteExtension_error_gettingNetworkObject%": "Error getting network object:", "%fwLiteExtension_error_missingParam%": "Missing required parameter: ", diff --git a/platform.bible-extension/src/components/back-to-list-button.tsx b/platform.bible-extension/src/components/back-to-list-button.tsx new file mode 100644 index 0000000000..ced08875c3 --- /dev/null +++ b/platform.bible-extension/src/components/back-to-list-button.tsx @@ -0,0 +1,52 @@ +// Modified from paranext-core/extensions/src/components/dictionary/back-to-list-button.component.tsx + +import type { IEntry } from 'fw-lite-extension'; +import { ArrowLeft } from 'lucide-react'; +import { ListboxOption, Button, DrawerClose } from 'platform-bible-react'; +import { LanguageStrings } from 'platform-bible-utils'; + +/** Props for the BackToListButton component */ +type BackToListButtonProps = { + /** Callback function to handle back button click, returning to the list view */ + handleBackToListButton?: (option: ListboxOption) => void; + /** Dictionary entry to display in the button */ + dictionaryEntry: IEntry; + /** Whether the display is in a drawer or just next to the list */ + isDrawer: boolean; + /** Localized strings for the button */ + localizedStrings: LanguageStrings; +}; + +/** + * A button that appears above the detailed view of a dictionary entry. + * + * If the user is viewing the detailed view in a drawer, this button is a drawer close button. + * Otherwise, it is a regular button. + * + * Clicking the button will return the user to the list view of all dictionary entries. + */ +export function BackToListButton({ + handleBackToListButton, + dictionaryEntry, + isDrawer, + localizedStrings, +}: BackToListButtonProps) { + if (!handleBackToListButton) return undefined; + + const button = ( + + ); + + return ( +
+ {isDrawer ? {button} : button} +
+ ); +} diff --git a/platform.bible-extension/src/components/dictionary-entry-display.tsx b/platform.bible-extension/src/components/dictionary-entry-display.tsx new file mode 100644 index 0000000000..7e16d92175 --- /dev/null +++ b/platform.bible-extension/src/components/dictionary-entry-display.tsx @@ -0,0 +1,124 @@ +// Modified from paranext-core/extensions/src/components/dictionary/dictionary-entry-display.component.tsx + +import { useLocalizedStrings } from '@papi/frontend/react'; +import type { IEntry } from 'fw-lite-extension'; +import { ChevronUpIcon } from 'lucide-react'; +import { + Button, + DrawerDescription, + DrawerTitle, + Separator, + ListboxOption, +} from 'platform-bible-react'; +import { BackToListButton } from './back-to-list-button'; +import { DomainsDisplay } from './domains-display'; +import { LOCALIZED_STRING_KEYS } from '../types/localized-string-keys'; +import { + entryGlossText, + entryHeadwordText, + partOfSpeechText, + senseDefinitionText, + senseGlossText, +} from '../utils/entry-display-text'; + +/** Props for the DictionaryEntryDisplay component */ +export type DictionaryEntryDisplayProps = { + /** Dictionary entry object to display */ + dictionaryEntry: IEntry; + /** Whether the display is in a drawer or just next to the list */ + isDrawer: boolean; + /** Callback function to handle back button click, returning to the list view */ + handleBackToListButton?: (option: ListboxOption) => void; + /** Callback function to handle scroll to top */ + onClickScrollToTop: () => void; +}; + +/** + * Renders a detailed view of a dictionary entry, displaying its key properties such as Hebrew text, + * transliteration, Strong's number, part of speech, definition, and usage occurrences. Includes a + * back button to navigate back to the list view. + */ +export function DictionaryEntryDisplay({ + dictionaryEntry, + isDrawer, + handleBackToListButton, + onClickScrollToTop, +}: DictionaryEntryDisplayProps) { + const [localizedStrings] = useLocalizedStrings(LOCALIZED_STRING_KEYS); + + // Cannot use Drawer components when there is no Drawer, if the screen is considered wide it will render Button and span here. + const TitleComponent = isDrawer ? DrawerTitle : 'span'; + const DescriptionComponent = isDrawer ? DrawerDescription : 'span'; + + return ( + <> + +
+
+ + + {entryHeadwordText(dictionaryEntry)} + + + {entryGlossText(dictionaryEntry)} + + +
+
+ + + +
+

+ {localizedStrings['%fwLiteExtension_entryDisplay_senses%']} +

+
+ {Object.values(dictionaryEntry.senses) + .flat() + .filter((sense) => sense !== undefined) + .map((sense, senseIndex) => ( +
+
+ {senseIndex + 1} + {senseGlossText(sense)} +
+ + {Object.values(sense.definition).some(Boolean) && ( +
+ {senseDefinitionText(sense)} +
+ )} + + {sense.partOfSpeech?.id && ( +
+ {`${localizedStrings['%fwLiteExtension_entryDisplay_partOfSpeech%']}${partOfSpeechText(sense.partOfSpeech)}`} +
+ )} + + +
+ ))} +
+
+ +
+ +
+ + ); +} diff --git a/platform.bible-extension/src/components/dictionary-list-item.tsx b/platform.bible-extension/src/components/dictionary-list-item.tsx new file mode 100644 index 0000000000..dc7b0fac2f --- /dev/null +++ b/platform.bible-extension/src/components/dictionary-list-item.tsx @@ -0,0 +1,64 @@ +// Modified from paranext-core/extensions/src/components/dictionary/dictionary-list-item.component.tsx + +import type { IEntry } from 'fw-lite-extension'; +import { cn, Separator } from 'platform-bible-react'; +import { entryHeadwordText } from '../utils/entry-display-text'; + +/** Props for the DictionaryListItem component */ +type DictionaryListItemProps = { + /** The dictionary entry to display */ + entry: IEntry; + /** Whether the dictionary entry is selected */ + isSelected: boolean; + /** Callback function to handle click on the entry */ + onClick: () => void; +}; + +/** + * A list item for a dictionary entry. + * + * This component is used to display a dictionary entry in a list of dictionary entries. + * + * The component renders a list item with the lemma of the dictionary entry, the number of + * occurrences in the chapter, and the Strong's codes for the dictionary entry. The component also + * renders a tooltip that displays the number of occurrences in the chapter. + * + * The component uses the `useListbox` hook from the `listbox-keyboard-navigation.util` module to + * handle keyboard navigation of the list. + */ +export function DictionaryListItem({ entry, isSelected, onClick }: DictionaryListItemProps) { + return ( + <> + {/* This component does have keyboard navigation, it is being handled through the useListbox hook */} + {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */} +
  • +
    + {entryHeadwordText(entry)} +
    + +
    +

    + {entry.senses + .map((s) => Object.values(s.gloss).filter(Boolean).join('; ')) + .filter(Boolean) + .join(' | ')} +

    +
    +
  • + + + ); +} diff --git a/platform.bible-extension/src/components/dictionary-list.tsx b/platform.bible-extension/src/components/dictionary-list.tsx new file mode 100644 index 0000000000..fc84ca741f --- /dev/null +++ b/platform.bible-extension/src/components/dictionary-list.tsx @@ -0,0 +1,143 @@ +// Modified from paranext-core/extensions/src/components/dictionary/dictionary-list.component.tsx + +import type { IEntry } from 'fw-lite-extension'; +import { + cn, + Drawer, + DrawerContent, + DrawerTrigger, + useListbox, + type ListboxOption, +} from 'platform-bible-react'; +import { useState, useEffect, RefObject, useMemo, useRef } from 'react'; +import { DictionaryEntryDisplay } from './dictionary-entry-display'; +import { DictionaryListItem } from './dictionary-list-item'; +import { useIsWideScreen } from '../utils/use-is-wide-screen'; + +/** Props for the DictionaryList component */ +type DictionaryListProps = { + /** Array of dictionary entries */ + dictionaryData: IEntry[]; + /** Callback function to handle character press */ + onCharacterPress?: (char: string) => void; +}; + +/** + * A list of dictionary entries. + * + * This component renders a listbox of dictionary entries. Each list item contains the lemma of the + * dictionary entry, the number of occurrences in the chapter, and a list of Strong's codes. The + * component also renders a drawer to the right of the list item that contains a detailed view of + * the dictionary entry. + * + * The component uses the `useListbox` hook from the `listbox-keyboard-navigation.util` module to + * handle keyboard navigation of the list. + */ +export default function DictionaryList({ dictionaryData, onCharacterPress }: DictionaryListProps) { + const isWideScreen = useIsWideScreen(); + const [selectedEntryId, setSelectedEntryId] = useState(undefined); + const [drawerOpen, setDrawerOpen] = useState(false); + + const options: ListboxOption[] = dictionaryData.map((entry) => ({ + id: entry.id, + })); + + const selectedEntry = useMemo(() => { + return dictionaryData.find((entry) => entry.id === selectedEntryId); + }, [dictionaryData, selectedEntryId]); + + const handleOptionSelect = (option: ListboxOption) => { + setSelectedEntryId((prevId) => (prevId === option.id ? undefined : option.id)); + }; + + const { listboxRef, activeId, handleKeyDown } = useListbox({ + options, + onOptionSelect: handleOptionSelect, + onCharacterPress, + }); + + // ref.current expects null and not undefined when we pass it to the div + // eslint-disable-next-line no-null/no-null + const dictionaryEntryRef = useRef(null); + + const scrollToTop = () => { + dictionaryEntryRef.current?.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + useEffect(() => { + if (selectedEntryId && !isWideScreen) { + setDrawerOpen(true); + } else { + setDrawerOpen(false); + } + }, [selectedEntryId, isWideScreen]); + + return ( +
    +
    +
      } + aria-activedescendant={activeId ?? undefined} + className="tw-outline-none focus:tw-ring-2 focus:tw-ring-ring focus:tw-ring-offset-1 focus:tw-ring-offset-background" + onKeyDown={handleKeyDown} + > + {dictionaryData.map((entry) => { + const isSelected = selectedEntryId === entry.id; + return ( +
      + setSelectedEntryId(entry.id)} + /> +
      + ); + })} +
    +
    + {selectedEntryId && + selectedEntry && + (isWideScreen ? ( +
    + +
    + ) : ( + setSelectedEntryId(undefined)} + > + +
    + + +
    + setSelectedEntryId(undefined)} + onClickScrollToTop={scrollToTop} + /> +
    +
    + + ))} +
    + ); +} diff --git a/platform.bible-extension/src/components/domains-display.tsx b/platform.bible-extension/src/components/domains-display.tsx new file mode 100644 index 0000000000..bb5a9b9e79 --- /dev/null +++ b/platform.bible-extension/src/components/domains-display.tsx @@ -0,0 +1,37 @@ +// Modified from paranext-core/extensions/src/platform-lexical-tools/src/components/dictionary/domains-display.component.tsx + +import { ISemanticDomain } from 'fw-lite-extension'; +import { Network } from 'lucide-react'; +import { domainText } from '../utils/entry-display-text'; + +/** Props for the DomainsDisplay component */ +type DomainsDisplayProps = { + /** Domains to display */ + domains: ISemanticDomain[]; + /** Function to trigger when a domain is clicked */ + onClickDomain?: (domain: ISemanticDomain) => void; +}; + +/** + * Renders a list of domains for a dictionary entry or sense. + * + * The component displays each domain as a rounded, colored pill with a small icon. The text of the + * pill is the code of the domain, followed by the label. + */ +export function DomainsDisplay({ domains, onClickDomain }: DomainsDisplayProps) { + return ( +
    + {domains.map((domain) => ( + + ))} +
    + ); +} diff --git a/platform.bible-extension/src/components/entry-card.tsx b/platform.bible-extension/src/components/entry-card.tsx index b27903f1de..a50b94f9de 100644 --- a/platform.bible-extension/src/components/entry-card.tsx +++ b/platform.bible-extension/src/components/entry-card.tsx @@ -1,14 +1,7 @@ -import type { IEntry, IPartOfSpeech, ISemanticDomain } from 'fw-lite-extension'; +import type { IEntry, ISemanticDomain } from 'fw-lite-extension'; import { Button, Card, CardContent, CardHeader } from 'platform-bible-react'; import type { ReactElement } from 'react'; - -function domainText(domain: ISemanticDomain, lang = 'en'): string { - return `${domain.code}: ${domain.name[lang] || domain.name.en}`; -} - -function partOfSpeechText(partOfSpeech: IPartOfSpeech, lang = 'en'): string { - return partOfSpeech.name[lang] || partOfSpeech.name.en; -} +import { domainText, entryHeadwordText, partOfSpeechText } from '../utils/entry-display-text'; interface EntryCardProps { entry: IEntry; @@ -18,11 +11,7 @@ interface EntryCardProps { export default function EntryCard({ entry, onClickSemanticDomain }: EntryCardProps): ReactElement { return ( - - {Object.keys(entry.citationForm).length - ? JSON.stringify(entry.citationForm) - : JSON.stringify(entry.lexemeForm)} - + {entryHeadwordText(entry)}

    Senses:

    {entry.senses.map((sense) => ( diff --git a/platform.bible-extension/src/components/entry-list.tsx b/platform.bible-extension/src/components/entry-list.tsx deleted file mode 100644 index fb3580b6ae..0000000000 --- a/platform.bible-extension/src/components/entry-list.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import type { IEntry } from 'fw-lite-extension'; -import type { ReactElement } from 'react'; -import EntryCard from './entry-card'; - -interface EntryListProps { - entries: IEntry[]; -} - -// Match className from paranext-core/extensions/src/components/dictionary/dictionary-list.component.tsx -export default function EntryList({ entries }: EntryListProps): ReactElement { - return ( -
    -
    -
      - {entries.map((entry) => ( - - ))} -
    -
    -
    - ); -} diff --git a/platform.bible-extension/src/types/localized-string-keys.ts b/platform.bible-extension/src/types/localized-string-keys.ts index a85f9e8e92..c5c6c23cd2 100644 --- a/platform.bible-extension/src/types/localized-string-keys.ts +++ b/platform.bible-extension/src/types/localized-string-keys.ts @@ -1,6 +1,8 @@ import { LocalizeKey } from 'platform-bible-utils'; export const LOCALIZED_STRING_KEYS: LocalizeKey[] = [ + '%fwLiteExtension_entryDisplay_partOfSpeech%', + '%fwLiteExtension_entryDisplay_senses%', '%fwLiteExtension_error_failedToAddEntry%', '%fwLiteExtension_error_gettingNetworkObject%', '%fwLiteExtension_error_missingParam%', diff --git a/platform.bible-extension/src/utils/entry-display-text.ts b/platform.bible-extension/src/utils/entry-display-text.ts new file mode 100644 index 0000000000..730b28fb55 --- /dev/null +++ b/platform.bible-extension/src/utils/entry-display-text.ts @@ -0,0 +1,34 @@ +import { IEntry, IPartOfSpeech, ISemanticDomain, ISense } from 'fw-lite-extension'; + +export function domainText(domain: ISemanticDomain, lang = 'en'): string { + return `${domain.code}: ${domain.name[lang] || domain.name.en}`; +} + +export function entryGlossText(entry: IEntry): string { + return entry.senses + .map((s) => Object.values(s.gloss).filter(Boolean).join('; ')) + .filter(Boolean) + .join(' | '); +} + +export function entryHeadwordText(entry: IEntry, lang = 'en'): string { + return ( + entry.citationForm[lang] || + entry.lexemeForm[lang] || + Object.values(entry.citationForm).filter(Boolean)[0] || + Object.values(entry.lexemeForm).filter(Boolean)[0] || + '' + ); +} + +export function partOfSpeechText(partOfSpeech: IPartOfSpeech, lang = 'en'): string { + return partOfSpeech.name[lang] || partOfSpeech.name.en; +} + +export function senseDefinitionText(sense: ISense): string { + return JSON.stringify(sense.definition); +} + +export function senseGlossText(sense: ISense): string { + return JSON.stringify(sense.gloss); +} diff --git a/platform.bible-extension/src/web-views/find-word.web-view.tsx b/platform.bible-extension/src/web-views/find-word.web-view.tsx index 1c816e4a64..b9a8ef76c2 100644 --- a/platform.bible-extension/src/web-views/find-word.web-view.tsx +++ b/platform.bible-extension/src/web-views/find-word.web-view.tsx @@ -6,7 +6,7 @@ import { Label, SearchBar } from 'platform-bible-react'; import { debounce } from 'platform-bible-utils'; import { useCallback, useEffect, useMemo, useState } from 'react'; import AddNewEntry from '../components/add-new-entry'; -import EntryList from '../components/entry-list'; +import DictionaryList from '../components/dictionary-list'; import { LOCALIZED_STRING_KEYS } from '../types/localized-string-keys'; /* eslint-disable react-hooks/rules-of-hooks */ @@ -106,7 +106,7 @@ globalThis.webViewComponent = function fwLiteFindWord({ return (
    -
    +
    + +
    + +
    @@ -127,14 +136,7 @@ globalThis.webViewComponent = function fwLiteFindWord({
    )} - {matchingEntries && } - - + {matchingEntries && }
    ); }; From 33e5048a869114257e1d9a960b2f4cf080131828 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 10 Sep 2025 11:30:07 -0400 Subject: [PATCH 04/18] Continue matching styles of dictionary extension --- .../contributions/localizedStrings.json | 5 ++- .../src/components/back-to-list-button.tsx | 4 +- .../components/dictionary-entry-display.tsx | 6 +-- .../src/components/dictionary-list-item.tsx | 6 ++- .../components/dictionary-list-wrapper.tsx | 43 +++++++++++++++++++ .../src/components/dictionary-list.tsx | 6 +-- .../src/components/domains-display.tsx | 2 +- platform.bible-extension/src/styles.css | 18 -------- platform.bible-extension/src/tailwind.css | 24 +++++++++++ .../src/types/localized-string-keys.ts | 5 ++- .../src/utils/use-is-wide-screen.tsx | 2 +- .../src/web-views/find-word.web-view.tsx | 29 +++++-------- .../src/web-views/index.tsx | 2 +- 13 files changed, 100 insertions(+), 52 deletions(-) create mode 100644 platform.bible-extension/src/components/dictionary-list-wrapper.tsx delete mode 100644 platform.bible-extension/src/styles.css diff --git a/platform.bible-extension/contributions/localizedStrings.json b/platform.bible-extension/contributions/localizedStrings.json index b0a9e0e529..7507ae0cd9 100644 --- a/platform.bible-extension/contributions/localizedStrings.json +++ b/platform.bible-extension/contributions/localizedStrings.json @@ -5,6 +5,9 @@ "%fwLiteExtension_addWord_title%": "Add to FieldWorks", "%fwLiteExtension_browseDictionary_title%": "FieldWorks Lite", "%fwLiteExtension_selectDictionary_title%": "Select FieldWorks dictionary", + "%fwLiteExtension_dictionary_backToList%": "Back to list", + "%fwLiteExtension_dictionary_loading%": "Loading...", + "%fwLiteExtension_dictionary_noResults%": "No results", "%fwLiteExtension_entryDisplay_partOfSpeech%": "Part of speech: ", "%fwLiteExtension_entryDisplay_senses%": "Senses", "%fwLiteExtension_error_failedToAddEntry%": "Failed to add entry!", @@ -13,8 +16,6 @@ "%fwLiteExtension_findRelatedWords_title%": "Find related words in FieldWorks", "%fwLiteExtension_findWord_title%": "Search in FieldWorks", "%fwLiteExtension_findWord_textField%": "Find in dictionary...", - "%fwLiteExtension_findWord_loading%": "Loading...", - "%fwLiteExtension_findWord_noMatchingEntries%": "No matching entries", "%fwLiteExtension_list_projects_label%": "List all local FieldWorks projects", "%fwLiteExtension_menu_addEntry%": "Add to FieldWorks...", "%fwLiteExtension_menu_browseDictionary%": "Browse FieldWorks dictionary", diff --git a/platform.bible-extension/src/components/back-to-list-button.tsx b/platform.bible-extension/src/components/back-to-list-button.tsx index ced08875c3..de3ebb552e 100644 --- a/platform.bible-extension/src/components/back-to-list-button.tsx +++ b/platform.bible-extension/src/components/back-to-list-button.tsx @@ -25,7 +25,7 @@ type BackToListButtonProps = { * * Clicking the button will return the user to the list view of all dictionary entries. */ -export function BackToListButton({ +export default function BackToListButton({ handleBackToListButton, dictionaryEntry, isDrawer, @@ -40,7 +40,7 @@ export function BackToListButton({ variant="link" > - {localizedStrings['%platformLexicalTools_dictionary_backToList%']} + {localizedStrings['%fwLiteExtension_dictionary_backToList%']} ); diff --git a/platform.bible-extension/src/components/dictionary-entry-display.tsx b/platform.bible-extension/src/components/dictionary-entry-display.tsx index 7e16d92175..bc2d0f8d8f 100644 --- a/platform.bible-extension/src/components/dictionary-entry-display.tsx +++ b/platform.bible-extension/src/components/dictionary-entry-display.tsx @@ -10,8 +10,8 @@ import { Separator, ListboxOption, } from 'platform-bible-react'; -import { BackToListButton } from './back-to-list-button'; -import { DomainsDisplay } from './domains-display'; +import BackToListButton from './back-to-list-button'; +import DomainsDisplay from './domains-display'; import { LOCALIZED_STRING_KEYS } from '../types/localized-string-keys'; import { entryGlossText, @@ -38,7 +38,7 @@ export type DictionaryEntryDisplayProps = { * transliteration, Strong's number, part of speech, definition, and usage occurrences. Includes a * back button to navigate back to the list view. */ -export function DictionaryEntryDisplay({ +export default function DictionaryEntryDisplay({ dictionaryEntry, isDrawer, handleBackToListButton, diff --git a/platform.bible-extension/src/components/dictionary-list-item.tsx b/platform.bible-extension/src/components/dictionary-list-item.tsx index dc7b0fac2f..3fcf6d65da 100644 --- a/platform.bible-extension/src/components/dictionary-list-item.tsx +++ b/platform.bible-extension/src/components/dictionary-list-item.tsx @@ -26,7 +26,11 @@ type DictionaryListItemProps = { * The component uses the `useListbox` hook from the `listbox-keyboard-navigation.util` module to * handle keyboard navigation of the list. */ -export function DictionaryListItem({ entry, isSelected, onClick }: DictionaryListItemProps) { +export default function DictionaryListItem({ + entry, + isSelected, + onClick, +}: DictionaryListItemProps) { return ( <> {/* This component does have keyboard navigation, it is being handled through the useListbox hook */} diff --git a/platform.bible-extension/src/components/dictionary-list-wrapper.tsx b/platform.bible-extension/src/components/dictionary-list-wrapper.tsx new file mode 100644 index 0000000000..f02d03a186 --- /dev/null +++ b/platform.bible-extension/src/components/dictionary-list-wrapper.tsx @@ -0,0 +1,43 @@ +import { useLocalizedStrings } from '@papi/frontend/react'; +import { Label } from 'platform-bible-react'; +import { ReactNode } from 'react'; +import { LOCALIZED_STRING_KEYS } from '../types/localized-string-keys'; + +/** Props for the DictionaryListWrapper component */ +type DictionaryListWrapperProps = { + elementHeader: ReactNode; + elementList: ReactNode; + isLoading: boolean; + hasItems: boolean; +}; + +/** A sticky header for above a dictionary list. */ +export default function DictionaryListWrapper({ + elementHeader, + elementList, + hasItems, + isLoading, +}: DictionaryListWrapperProps) { + const [localizedStrings] = useLocalizedStrings(LOCALIZED_STRING_KEYS); + + // Match className from paranext-core/extensions/src/platform-lexical-tools/src/web-views/dictionary.web-view.tsx + return ( +
    +
    + {elementHeader} +
    + + {isLoading && ( +
    + +
    + )} + {!hasItems && !isLoading && ( +
    + +
    + )} + {hasItems && elementList} +
    + ); +} diff --git a/platform.bible-extension/src/components/dictionary-list.tsx b/platform.bible-extension/src/components/dictionary-list.tsx index fc84ca741f..9e14189969 100644 --- a/platform.bible-extension/src/components/dictionary-list.tsx +++ b/platform.bible-extension/src/components/dictionary-list.tsx @@ -10,9 +10,9 @@ import { type ListboxOption, } from 'platform-bible-react'; import { useState, useEffect, RefObject, useMemo, useRef } from 'react'; -import { DictionaryEntryDisplay } from './dictionary-entry-display'; -import { DictionaryListItem } from './dictionary-list-item'; -import { useIsWideScreen } from '../utils/use-is-wide-screen'; +import DictionaryEntryDisplay from './dictionary-entry-display'; +import DictionaryListItem from './dictionary-list-item'; +import useIsWideScreen from '../utils/use-is-wide-screen'; /** Props for the DictionaryList component */ type DictionaryListProps = { diff --git a/platform.bible-extension/src/components/domains-display.tsx b/platform.bible-extension/src/components/domains-display.tsx index bb5a9b9e79..1e559b17ee 100644 --- a/platform.bible-extension/src/components/domains-display.tsx +++ b/platform.bible-extension/src/components/domains-display.tsx @@ -18,7 +18,7 @@ type DomainsDisplayProps = { * The component displays each domain as a rounded, colored pill with a small icon. The text of the * pill is the code of the domain, followed by the label. */ -export function DomainsDisplay({ domains, onClickDomain }: DomainsDisplayProps) { +export default function DomainsDisplay({ domains, onClickDomain }: DomainsDisplayProps) { return (
    {domains.map((domain) => ( diff --git a/platform.bible-extension/src/styles.css b/platform.bible-extension/src/styles.css deleted file mode 100644 index eefcdbf213..0000000000 --- a/platform.bible-extension/src/styles.css +++ /dev/null @@ -1,18 +0,0 @@ -body, -html, -div { - overflow: hidden; -} - -body, -html, -div, -iframe { - height: 100%; - width: 100%; - margin: 0; -} - -iframe { - border: 0; -} diff --git a/platform.bible-extension/src/tailwind.css b/platform.bible-extension/src/tailwind.css index 77e4962a83..f0f6d59298 100644 --- a/platform.bible-extension/src/tailwind.css +++ b/platform.bible-extension/src/tailwind.css @@ -138,3 +138,27 @@ } /* #endregion */ + +@layer base { + /* Set 100% height/width and remove margin for html, body, div, iframe */ + html, + body, + div, + iframe { + height: 100%; + width: 100%; + margin: 0; + } + + /* Prevent scrollbars by default */ + html, + body, + div { + overflow: hidden; + } + + /* Remove border from iframes */ + iframe { + border: 0; + } +} diff --git a/platform.bible-extension/src/types/localized-string-keys.ts b/platform.bible-extension/src/types/localized-string-keys.ts index c5c6c23cd2..c570d6fec6 100644 --- a/platform.bible-extension/src/types/localized-string-keys.ts +++ b/platform.bible-extension/src/types/localized-string-keys.ts @@ -1,12 +1,13 @@ import { LocalizeKey } from 'platform-bible-utils'; export const LOCALIZED_STRING_KEYS: LocalizeKey[] = [ + '%fwLiteExtension_dictionary_backToList%', + '%fwLiteExtension_dictionary_loading%', + '%fwLiteExtension_dictionary_noResults%', '%fwLiteExtension_entryDisplay_partOfSpeech%', '%fwLiteExtension_entryDisplay_senses%', '%fwLiteExtension_error_failedToAddEntry%', '%fwLiteExtension_error_gettingNetworkObject%', '%fwLiteExtension_error_missingParam%', - '%fwLiteExtension_findWord_loading%', - '%fwLiteExtension_findWord_noMatchingEntries%', '%fwLiteExtension_findWord_textField%', ]; diff --git a/platform.bible-extension/src/utils/use-is-wide-screen.tsx b/platform.bible-extension/src/utils/use-is-wide-screen.tsx index 946efe34d2..b9fe57abf5 100644 --- a/platform.bible-extension/src/utils/use-is-wide-screen.tsx +++ b/platform.bible-extension/src/utils/use-is-wide-screen.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; -export function useIsWideScreen() { +export default function useIsWideScreen() { const [isWide, setIsWide] = useState(() => window.innerWidth >= 1024); useEffect(() => { diff --git a/platform.bible-extension/src/web-views/find-word.web-view.tsx b/platform.bible-extension/src/web-views/find-word.web-view.tsx index b9a8ef76c2..fa7c525488 100644 --- a/platform.bible-extension/src/web-views/find-word.web-view.tsx +++ b/platform.bible-extension/src/web-views/find-word.web-view.tsx @@ -2,11 +2,12 @@ import type { NetworkObject } from '@papi/core'; import papi, { logger } from '@papi/frontend'; import { useLocalizedStrings } from '@papi/frontend/react'; import type { IEntry, IEntryService, PartialEntry, WordWebViewOptions } from 'fw-lite-extension'; -import { Label, SearchBar } from 'platform-bible-react'; +import { SearchBar } from 'platform-bible-react'; import { debounce } from 'platform-bible-utils'; import { useCallback, useEffect, useMemo, useState } from 'react'; import AddNewEntry from '../components/add-new-entry'; import DictionaryList from '../components/dictionary-list'; +import DictionaryListWrapper from '../components/dictionary-list-wrapper'; import { LOCALIZED_STRING_KEYS } from '../types/localized-string-keys'; /* eslint-disable react-hooks/rules-of-hooks */ @@ -102,10 +103,9 @@ globalThis.webViewComponent = function fwLiteFindWord({ [fwLiteNetworkObject, localizedStrings, onSearch, projectId], ); - // Match className from paranext-core/extensions/src/platform-lexical-tools/src/web-views/dictionary.web-view.tsx return ( -
    -
    +
    -
    - - {isFetching && ( -
    - -
    - )} - {!matchingEntries?.length && !isFetching && ( -
    - -
    - )} - {matchingEntries && } -
    + } + elementList={ + matchingEntries ? : undefined + } + isLoading={isFetching} + hasItems={!!matchingEntries?.length} + /> ); }; diff --git a/platform.bible-extension/src/web-views/index.tsx b/platform.bible-extension/src/web-views/index.tsx index 615b588a5c..8cde4c5064 100644 --- a/platform.bible-extension/src/web-views/index.tsx +++ b/platform.bible-extension/src/web-views/index.tsx @@ -4,7 +4,7 @@ import type { OpenWebViewOptionsWithProjectId, WordWebViewOptions, } from 'fw-lite-extension'; -import mainStyles from '../styles.css?inline'; +import mainStyles from '../tailwind.css?inline'; import { WebViewType } from '../types/enums'; import fwAddWordWindow from './add-word.web-view?inline'; import fwDictionarySelectWindow from './dictionary-select.web-view?inline'; From 90a7f5299caf84cb07a86524f00dc644978a4df1 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Wed, 10 Sep 2025 16:09:47 -0400 Subject: [PATCH 05/18] Continue matching styles of dictionary extension --- .../components/dictionary-entry-display.tsx | 12 +- .../src/components/dictionary-list-item.tsx | 24 ++-- .../src/components/dictionary-list.tsx | 14 ++- .../src/components/domains-display.tsx | 14 ++- platform.bible-extension/src/styles.css | 18 +++ platform.bible-extension/src/tailwind.css | 24 ---- .../web-views/find-related-words.web-view.tsx | 106 +++++++++--------- .../src/web-views/index.tsx | 9 +- 8 files changed, 124 insertions(+), 97 deletions(-) create mode 100644 platform.bible-extension/src/styles.css diff --git a/platform.bible-extension/src/components/dictionary-entry-display.tsx b/platform.bible-extension/src/components/dictionary-entry-display.tsx index bc2d0f8d8f..66b3b7b936 100644 --- a/platform.bible-extension/src/components/dictionary-entry-display.tsx +++ b/platform.bible-extension/src/components/dictionary-entry-display.tsx @@ -1,7 +1,7 @@ // Modified from paranext-core/extensions/src/components/dictionary/dictionary-entry-display.component.tsx import { useLocalizedStrings } from '@papi/frontend/react'; -import type { IEntry } from 'fw-lite-extension'; +import type { IEntry, ISemanticDomain } from 'fw-lite-extension'; import { ChevronUpIcon } from 'lucide-react'; import { Button, @@ -31,6 +31,8 @@ export type DictionaryEntryDisplayProps = { handleBackToListButton?: (option: ListboxOption) => void; /** Callback function to handle scroll to top */ onClickScrollToTop: () => void; + /** Callback function to handle click on a semantic domain */ + onClickSemanticDomain?: (domain: ISemanticDomain) => void; }; /** @@ -43,6 +45,7 @@ export default function DictionaryEntryDisplay({ isDrawer, handleBackToListButton, onClickScrollToTop, + onClickSemanticDomain, }: DictionaryEntryDisplayProps) { const [localizedStrings] = useLocalizedStrings(LOCALIZED_STRING_KEYS); @@ -84,7 +87,7 @@ export default function DictionaryEntryDisplay({ .map((sense, senseIndex) => (
    {senseIndex + 1} @@ -103,7 +106,10 @@ export default function DictionaryEntryDisplay({
    )} - +
    ))}
    diff --git a/platform.bible-extension/src/components/dictionary-list-item.tsx b/platform.bible-extension/src/components/dictionary-list-item.tsx index 3fcf6d65da..9d674dd112 100644 --- a/platform.bible-extension/src/components/dictionary-list-item.tsx +++ b/platform.bible-extension/src/components/dictionary-list-item.tsx @@ -1,8 +1,9 @@ // Modified from paranext-core/extensions/src/components/dictionary/dictionary-list-item.component.tsx -import type { IEntry } from 'fw-lite-extension'; +import type { IEntry, ISemanticDomain } from 'fw-lite-extension'; import { cn, Separator } from 'platform-bible-react'; -import { entryHeadwordText } from '../utils/entry-display-text'; +import DomainsDisplay from './domains-display'; +import { entryGlossText, entryHeadwordText } from '../utils/entry-display-text'; /** Props for the DictionaryListItem component */ type DictionaryListItemProps = { @@ -12,6 +13,8 @@ type DictionaryListItemProps = { isSelected: boolean; /** Callback function to handle click on the entry */ onClick: () => void; + /** Callback function to handle click on a semantic domain */ + onClickSemanticDomain?: (domain: ISemanticDomain) => void; }; /** @@ -30,6 +33,7 @@ export default function DictionaryListItem({ entry, isSelected, onClick, + onClickSemanticDomain, }: DictionaryListItemProps) { return ( <> @@ -54,13 +58,17 @@ export default function DictionaryListItem({
    -

    - {entry.senses - .map((s) => Object.values(s.gloss).filter(Boolean).join('; ')) - .filter(Boolean) - .join(' | ')} -

    +

    {entryGlossText(entry)}

    + + {onClickSemanticDomain && ( +
    + s.semanticDomains)} + onClickDomain={onClickSemanticDomain} + /> +
    + )} diff --git a/platform.bible-extension/src/components/dictionary-list.tsx b/platform.bible-extension/src/components/dictionary-list.tsx index 9e14189969..d1b9e82a2e 100644 --- a/platform.bible-extension/src/components/dictionary-list.tsx +++ b/platform.bible-extension/src/components/dictionary-list.tsx @@ -1,6 +1,6 @@ // Modified from paranext-core/extensions/src/components/dictionary/dictionary-list.component.tsx -import type { IEntry } from 'fw-lite-extension'; +import type { IEntry, ISemanticDomain } from 'fw-lite-extension'; import { cn, Drawer, @@ -20,6 +20,8 @@ type DictionaryListProps = { dictionaryData: IEntry[]; /** Callback function to handle character press */ onCharacterPress?: (char: string) => void; + /** Callback function to handle click on a semantic domain */ + onClickSemanticDomain?: (domain: ISemanticDomain) => void; }; /** @@ -33,7 +35,11 @@ type DictionaryListProps = { * The component uses the `useListbox` hook from the `listbox-keyboard-navigation.util` module to * handle keyboard navigation of the list. */ -export default function DictionaryList({ dictionaryData, onCharacterPress }: DictionaryListProps) { +export default function DictionaryList({ + dictionaryData, + onCharacterPress, + onClickSemanticDomain, +}: DictionaryListProps) { const isWideScreen = useIsWideScreen(); const [selectedEntryId, setSelectedEntryId] = useState(undefined); const [drawerOpen, setDrawerOpen] = useState(false); @@ -99,6 +105,7 @@ export default function DictionaryList({ dictionaryData, onCharacterPress }: Dic entry={entry} isSelected={isSelected} onClick={() => setSelectedEntryId(entry.id)} + onClickSemanticDomain={onClickSemanticDomain} />
    ); @@ -114,12 +121,12 @@ export default function DictionaryList({ dictionaryData, onCharacterPress }: Dic dictionaryEntry={selectedEntry} handleBackToListButton={handleOptionSelect} onClickScrollToTop={scrollToTop} + onClickSemanticDomain={onClickSemanticDomain} /> ) : ( setSelectedEntryId(undefined)} > @@ -133,6 +140,7 @@ export default function DictionaryList({ dictionaryData, onCharacterPress }: Dic dictionaryEntry={selectedEntry} handleBackToListButton={() => setSelectedEntryId(undefined)} onClickScrollToTop={scrollToTop} + onClickSemanticDomain={onClickSemanticDomain} /> diff --git a/platform.bible-extension/src/components/domains-display.tsx b/platform.bible-extension/src/components/domains-display.tsx index 1e559b17ee..f054596d8f 100644 --- a/platform.bible-extension/src/components/domains-display.tsx +++ b/platform.bible-extension/src/components/domains-display.tsx @@ -1,7 +1,8 @@ // Modified from paranext-core/extensions/src/platform-lexical-tools/src/components/dictionary/domains-display.component.tsx -import { ISemanticDomain } from 'fw-lite-extension'; +import type { ISemanticDomain } from 'fw-lite-extension'; import { Network } from 'lucide-react'; +import { useEffect, useState } from 'react'; import { domainText } from '../utils/entry-display-text'; /** Props for the DomainsDisplay component */ @@ -19,11 +20,20 @@ type DomainsDisplayProps = { * pill is the code of the domain, followed by the label. */ export default function DomainsDisplay({ domains, onClickDomain }: DomainsDisplayProps) { + const [sortedDomains, setSortedDomains] = useState([]); + + useEffect(() => { + const codes = [...new Set(domains.map((domain) => domain.code))].sort(); + // eslint-disable-next-line no-type-assertion/no-type-assertion + setSortedDomains(codes.map((code) => domains.find((domain) => domain.code === code)!)); + }, [domains]); + return (
    - {domains.map((domain) => ( + {sortedDomains.map((domain) => (
    + } + isLoading={isFetching} + hasItems={!!matchingEntries?.length} + /> ); }; - -interface EntriesInSemanticDomainProps { - entries: IEntry[]; - semanticDomain: ISemanticDomain; -} - -function EntriesInSemanticDomain({ - entries, - semanticDomain, -}: EntriesInSemanticDomainProps): ReactElement { - return ( - <> - {`${semanticDomain.code}: ${JSON.stringify(semanticDomain.name)}`} - {entries.length ? ( - entries.map((entry) => ) - ) : ( -

    No entries in this semantic domain.

    - )} - - ); -} diff --git a/platform.bible-extension/src/web-views/index.tsx b/platform.bible-extension/src/web-views/index.tsx index 8cde4c5064..45f479a47d 100644 --- a/platform.bible-extension/src/web-views/index.tsx +++ b/platform.bible-extension/src/web-views/index.tsx @@ -4,7 +4,8 @@ import type { OpenWebViewOptionsWithProjectId, WordWebViewOptions, } from 'fw-lite-extension'; -import mainStyles from '../tailwind.css?inline'; +import mainCssStyles from '../styles.css?inline'; +import tailwindCssStyles from '../tailwind.css?inline'; import { WebViewType } from '../types/enums'; import fwAddWordWindow from './add-word.web-view?inline'; import fwDictionarySelectWindow from './dictionary-select.web-view?inline'; @@ -31,7 +32,7 @@ export const mainWebViewProvider: IWebViewProvider = { allowedFrameSources: ['http://localhost:*'], content: fwMainWindow, iconUrl, - styles: mainStyles, + styles: mainCssStyles, title: '%fwLiteExtension_browseDictionary_title%', }; }, @@ -51,6 +52,7 @@ export const addWordWebViewProvider: IWebViewProvider = { ...options, content: fwAddWordWindow, iconUrl, + styles: tailwindCssStyles, title: '%fwLiteExtension_addWord_title%', }; }, @@ -70,6 +72,7 @@ export const dictionarySelectWebViewProvider: IWebViewProvider = { ...options, content: fwDictionarySelectWindow, iconUrl, + styles: tailwindCssStyles, title: '%fwLiteExtension_selectDictionary_title%', }; }, @@ -89,6 +92,7 @@ export const findWordWebViewProvider: IWebViewProvider = { ...options, content: fwFindWordWindow, iconUrl, + styles: tailwindCssStyles, title: '%fwLiteExtension_findWord_title%', }; }, @@ -108,6 +112,7 @@ export const findRelatedWordsWebViewProvider: IWebViewProvider = { ...options, content: fwFindRelatedWordsWindow, iconUrl, + styles: tailwindCssStyles, title: '%fwLiteExtension_findRelatedWords_title%', }; }, From 2fde7b099653d446c2d28b8f69592ec48e9426f9 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 11 Sep 2025 10:41:48 -0400 Subject: [PATCH 06/18] Finish matching styles of dictionary extension --- .../contributions/localizedStrings.json | 36 +++++------- .../src/components/add-new-entry-button.tsx | 40 +++++++++++++ .../src/components/add-new-entry.tsx | 57 ++++++++++++------- .../components/dictionary-entry-display.tsx | 3 +- .../src/components/domains-display.tsx | 4 +- .../src/components/entry-card.tsx | 39 ------------- .../src/types/localized-string-keys.ts | 7 +++ .../src/web-views/add-word.web-view.tsx | 25 +++++--- .../web-views/find-related-words.web-view.tsx | 4 +- .../src/web-views/find-word.web-view.tsx | 25 ++++---- .../src/web-views/index.tsx | 10 ++-- 11 files changed, 134 insertions(+), 116 deletions(-) create mode 100644 platform.bible-extension/src/components/add-new-entry-button.tsx delete mode 100644 platform.bible-extension/src/components/entry-card.tsx diff --git a/platform.bible-extension/contributions/localizedStrings.json b/platform.bible-extension/contributions/localizedStrings.json index 7507ae0cd9..71c9fa4ea7 100644 --- a/platform.bible-extension/contributions/localizedStrings.json +++ b/platform.bible-extension/contributions/localizedStrings.json @@ -2,19 +2,21 @@ "metadata": {}, "localizedStrings": { "en": { - "%fwLiteExtension_addWord_title%": "Add to FieldWorks", - "%fwLiteExtension_browseDictionary_title%": "FieldWorks Lite", - "%fwLiteExtension_selectDictionary_title%": "Select FieldWorks dictionary", + "%fwLiteExtension_addWord_buttonAdd%": "Add new entry", + "%fwLiteExtension_addWord_buttonSubmit%": "Submit new entry", + "%fwLiteExtension_addWord_title%": "Add entry to FieldWorks", + "%fwLiteExtension_button_cancel%": "Cancel", "%fwLiteExtension_dictionary_backToList%": "Back to list", "%fwLiteExtension_dictionary_loading%": "Loading...", "%fwLiteExtension_dictionary_noResults%": "No results", - "%fwLiteExtension_entryDisplay_partOfSpeech%": "Part of speech: ", + "%fwLiteExtension_entryDisplay_definition%": "Definition", + "%fwLiteExtension_entryDisplay_gloss%": "Gloss", + "%fwLiteExtension_entryDisplay_headword%": "Headword", + "%fwLiteExtension_entryDisplay_partOfSpeech%": "Part of speech", "%fwLiteExtension_entryDisplay_senses%": "Senses", "%fwLiteExtension_error_failedToAddEntry%": "Failed to add entry!", "%fwLiteExtension_error_gettingNetworkObject%": "Error getting network object:", "%fwLiteExtension_error_missingParam%": "Missing required parameter: ", - "%fwLiteExtension_findRelatedWords_title%": "Find related words in FieldWorks", - "%fwLiteExtension_findWord_title%": "Search in FieldWorks", "%fwLiteExtension_findWord_textField%": "Find in dictionary...", "%fwLiteExtension_list_projects_label%": "List all local FieldWorks projects", "%fwLiteExtension_menu_addEntry%": "Add to FieldWorks...", @@ -22,25 +24,15 @@ "%fwLiteExtension_menu_findEntry%": "Search in FieldWorks...", "%fwLiteExtension_menu_findRelatedEntries%": "Search for related words...", "%fwLiteExtension_open_label%": "Open FieldWorks Lite", - "%fwLiteExtension_projectSettings_title%": "FieldWorks Lite settings", "%fwLiteExtension_projectSettings_analysisLanguage%": "Analysis language", "%fwLiteExtension_projectSettings_dictionary%": "FieldWorks dictionary", "%fwLiteExtension_projectSettings_dictionaryDescription%": "The FieldWorks dictionary to use with this project", - "%fwlite_choose_new_entry_button%": "Choose New Entry", - "%fwlite_clear_entry_button%": "Clear Entry", - "%fwlite_clear_selection_button%": "Clear Selection", - "%fwlite_clear_word_selection_button%": "Clear Word Selection", - "%fwlite_definition_label%": "Definition: ", - "%fwlite_form_label%": "Form: ", - "%fwlite_lexical_entries_for_word%": "Lexical Entries for ", - "%fwlite_lexical_information_header%": "Lexical Information for ", - "%fwlite_loading%": "Loading...", - "%fwlite_more_details_placeholder%": "(More details about this lexical entry would go here.)", - "%fwlite_no_entries_found_header%": "No Lexical Entries Found for ", - "%fwlite_no_entries_found_prompt%": "Consider manually adding a link or searching for synonyms.", - "%fwlite_select_entry_button%": "Select Entry", - "%fwlite_select_word_prompt%": "Select a word from the left to view its lexical information or search for entries.", - "%fwlite_words_in_verse_header%": "Words in " + "%fwLiteExtension_projectSettings_title%": "FieldWorks Lite settings", + "%fwLiteExtension_webViewTitle_addWord%": "Add to FieldWorks", + "%fwLiteExtension_webViewTitle_findRelatedWords%": "Find related words in FieldWorks", + "%fwLiteExtension_webViewTitle_findWord%": "Search in FieldWorks", + "%fwLiteExtension_webViewTitle_browseDictionary%": "FieldWorks Lite", + "%fwLiteExtension_webViewTitle_selectDictionary%": "Select FieldWorks dictionary" } } } diff --git a/platform.bible-extension/src/components/add-new-entry-button.tsx b/platform.bible-extension/src/components/add-new-entry-button.tsx new file mode 100644 index 0000000000..ecf733ba40 --- /dev/null +++ b/platform.bible-extension/src/components/add-new-entry-button.tsx @@ -0,0 +1,40 @@ +import { useLocalizedStrings } from '@papi/frontend/react'; +import type { PartialEntry } from 'fw-lite-extension'; +import { Button } from 'platform-bible-react'; +import { type ReactElement, useState } from 'react'; +import AddNewEntry from './add-new-entry'; +import { LOCALIZED_STRING_KEYS } from '../types/localized-string-keys'; + +interface AddNewEntryButtonProps { + addEntry: (entry: PartialEntry) => Promise; + analysisLang: string; + headword?: string; + vernacularLang: string; +} + +export default function AddNewEntryButton({ + addEntry, + analysisLang, + headword, + vernacularLang, +}: AddNewEntryButtonProps): ReactElement { + const [localizedStrings] = useLocalizedStrings(LOCALIZED_STRING_KEYS); + + const [adding, setAdding] = useState(false); + + return adding ? ( +
    + setAdding(false)} + vernacularLang={vernacularLang} + /> +
    + ) : ( + + ); +} diff --git a/platform.bible-extension/src/components/add-new-entry.tsx b/platform.bible-extension/src/components/add-new-entry.tsx index 27c91fad43..c3d39004da 100644 --- a/platform.bible-extension/src/components/add-new-entry.tsx +++ b/platform.bible-extension/src/components/add-new-entry.tsx @@ -1,13 +1,15 @@ import { logger } from '@papi/frontend'; +import { useLocalizedStrings } from '@papi/frontend/react'; import type { PartialEntry } from 'fw-lite-extension'; -import { Button, Card, CardContent, CardHeader, Input, Label } from 'platform-bible-react'; +import { Button, Input, Label } from 'platform-bible-react'; import { type ReactElement, useCallback, useEffect, useState } from 'react'; +import { LOCALIZED_STRING_KEYS } from '../types/localized-string-keys'; interface AddNewEntryProps { addEntry: (entry: PartialEntry) => Promise; analysisLang: string; headword?: string; - isAdding?: boolean; + onCancel?: () => void; vernacularLang: string; } @@ -15,17 +17,16 @@ export default function AddNewEntry({ addEntry, analysisLang, headword, - isAdding, + onCancel, vernacularLang, }: AddNewEntryProps): ReactElement { - const [adding, setAdding] = useState(isAdding); + const [localizedStrings] = useLocalizedStrings(LOCALIZED_STRING_KEYS); + const [definition, setDefinition] = useState(''); const [gloss, setGloss] = useState(''); const [ready, setReady] = useState(false); const [tempHeadword, setTempHeadword] = useState(''); - useEffect(() => setAdding(isAdding), [isAdding]); - useEffect(() => setTempHeadword(headword || ''), [headword]); useEffect(() => { @@ -33,11 +34,11 @@ export default function AddNewEntry({ }, [definition, gloss, tempHeadword, vernacularLang]); const clearEntry = useCallback((): void => { - setAdding(isAdding); setDefinition(''); setGloss(''); setTempHeadword(headword || ''); - }, [headword, isAdding]); + onCancel?.(); + }, [headword, onCancel]); async function onSubmit(): Promise { const entry = createEntry( @@ -52,40 +53,52 @@ export default function AddNewEntry({ .catch((e) => logger.error('Error adding entry:', JSON.stringify(e))); } - return adding ? ( - - Adding new entry - + return ( +
    +

    + {localizedStrings['%fwLiteExtension_addWord_title%']} +

    + +
    - + setTempHeadword(e.target.value)} value={tempHeadword} />
    +
    - + setGloss(e.target.value)} value={gloss} />
    +
    - + setDefinition(e.target.value)} value={definition} />
    -
    + +
    + -
    - - - ) : ( - +
    +
    ); } diff --git a/platform.bible-extension/src/components/dictionary-entry-display.tsx b/platform.bible-extension/src/components/dictionary-entry-display.tsx index 66b3b7b936..b15e4f43a7 100644 --- a/platform.bible-extension/src/components/dictionary-entry-display.tsx +++ b/platform.bible-extension/src/components/dictionary-entry-display.tsx @@ -80,6 +80,7 @@ export default function DictionaryEntryDisplay({

    {localizedStrings['%fwLiteExtension_entryDisplay_senses%']}

    +
    {Object.values(dictionaryEntry.senses) .flat() @@ -102,7 +103,7 @@ export default function DictionaryEntryDisplay({ {sense.partOfSpeech?.id && (
    - {`${localizedStrings['%fwLiteExtension_entryDisplay_partOfSpeech%']}${partOfSpeechText(sense.partOfSpeech)}`} + {`${localizedStrings['%fwLiteExtension_entryDisplay_partOfSpeech%']}: ${partOfSpeechText(sense.partOfSpeech)}`}
    )} diff --git a/platform.bible-extension/src/components/domains-display.tsx b/platform.bible-extension/src/components/domains-display.tsx index f054596d8f..48044e9aed 100644 --- a/platform.bible-extension/src/components/domains-display.tsx +++ b/platform.bible-extension/src/components/domains-display.tsx @@ -28,7 +28,7 @@ export default function DomainsDisplay({ domains, onClickDomain }: DomainsDispla setSortedDomains(codes.map((code) => domains.find((domain) => domain.code === code)!)); }, [domains]); - return ( + return sortedDomains.length ? (
    {sortedDomains.map((domain) => (
    - ); + ) : undefined; } diff --git a/platform.bible-extension/src/components/entry-card.tsx b/platform.bible-extension/src/components/entry-card.tsx deleted file mode 100644 index a50b94f9de..0000000000 --- a/platform.bible-extension/src/components/entry-card.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import type { IEntry, ISemanticDomain } from 'fw-lite-extension'; -import { Button, Card, CardContent, CardHeader } from 'platform-bible-react'; -import type { ReactElement } from 'react'; -import { domainText, entryHeadwordText, partOfSpeechText } from '../utils/entry-display-text'; - -interface EntryCardProps { - entry: IEntry; - onClickSemanticDomain?: (domain: ISemanticDomain) => void; -} - -export default function EntryCard({ entry, onClickSemanticDomain }: EntryCardProps): ReactElement { - return ( - - {entryHeadwordText(entry)} - -

    Senses:

    - {entry.senses.map((sense) => ( -
    - Gloss: {JSON.stringify(sense.gloss)} -

    Definition: {JSON.stringify(sense.definition)}

    - {sense.partOfSpeech &&

    Part of speech: {partOfSpeechText(sense.partOfSpeech)}

    } -

    - Semantic Domains: - {sense.semanticDomains.map((dom) => - onClickSemanticDomain ? ( - - ) : ( - {domainText(dom)} - ), - )} -

    -
    - ))} -
    -
    - ); -} diff --git a/platform.bible-extension/src/types/localized-string-keys.ts b/platform.bible-extension/src/types/localized-string-keys.ts index c570d6fec6..827f8fcfbb 100644 --- a/platform.bible-extension/src/types/localized-string-keys.ts +++ b/platform.bible-extension/src/types/localized-string-keys.ts @@ -1,9 +1,16 @@ import { LocalizeKey } from 'platform-bible-utils'; export const LOCALIZED_STRING_KEYS: LocalizeKey[] = [ + '%fwLiteExtension_addWord_buttonAdd%', + '%fwLiteExtension_addWord_buttonSubmit%', + '%fwLiteExtension_addWord_title%', + '%fwLiteExtension_button_cancel%', '%fwLiteExtension_dictionary_backToList%', '%fwLiteExtension_dictionary_loading%', '%fwLiteExtension_dictionary_noResults%', + '%fwLiteExtension_entryDisplay_definition%', + '%fwLiteExtension_entryDisplay_gloss%', + '%fwLiteExtension_entryDisplay_headword%', '%fwLiteExtension_entryDisplay_partOfSpeech%', '%fwLiteExtension_entryDisplay_senses%', '%fwLiteExtension_error_failedToAddEntry%', diff --git a/platform.bible-extension/src/web-views/add-word.web-view.tsx b/platform.bible-extension/src/web-views/add-word.web-view.tsx index ebf5bd7c4f..fbd3a42491 100644 --- a/platform.bible-extension/src/web-views/add-word.web-view.tsx +++ b/platform.bible-extension/src/web-views/add-word.web-view.tsx @@ -1,8 +1,10 @@ import type { NetworkObject } from '@papi/core'; import papi, { logger } from '@papi/frontend'; +import { useLocalizedStrings } from '@papi/frontend/react'; import type { IEntryService, PartialEntry, WordWebViewOptions } from 'fw-lite-extension'; import { useCallback, useEffect, useState } from 'react'; import AddNewEntry from '../components/add-new-entry'; +import { LOCALIZED_STRING_KEYS } from '../types/localized-string-keys'; /* eslint-disable react-hooks/rules-of-hooks */ @@ -12,6 +14,8 @@ globalThis.webViewComponent = function fwLiteAddWord({ vernacularLanguage, word, }: WordWebViewOptions) { + const [localizedStrings] = useLocalizedStrings(LOCALIZED_STRING_KEYS); + const [fwLiteNetworkObject, setFwLiteNetworkObject] = useState< NetworkObject | undefined >(); @@ -26,14 +30,20 @@ globalThis.webViewComponent = function fwLiteAddWord({ logger.info('Got network object:', networkObject); setFwLiteNetworkObject(networkObject); }) - .catch((e) => logger.error('Error getting network object:', JSON.stringify(e))); - }, []); + .catch((e) => + logger.error( + `${localizedStrings['%fwLiteExtension_error_gettingNetworkObject%']}:`, + JSON.stringify(e), + ), + ); + }, [localizedStrings]); const addEntry = useCallback( async (entry: PartialEntry) => { if (!projectId || !fwLiteNetworkObject) { - if (!projectId) logger.warn('Missing required parameter: projectId'); - if (!fwLiteNetworkObject) logger.warn('Missing required parameter: fwLiteNetworkObject'); + const errMissingParam = localizedStrings['%fwLiteExtension_error_missingParam%']; + if (!projectId) logger.warn(`${errMissingParam}projectId`); + if (!fwLiteNetworkObject) logger.warn(`${errMissingParam}fwLiteNetworkObject`); return; } @@ -46,19 +56,18 @@ globalThis.webViewComponent = function fwLiteAddWord({ setIsSubmitted(true); await papi.commands.sendCommand('fwLiteExtension.displayEntry', projectId, entryId); } else { - logger.error('Failed to add entry!'); + logger.error(`${localizedStrings['%fwLiteExtension_error_failedToAddEntry%']}`); } }, - [fwLiteNetworkObject, projectId], + [fwLiteNetworkObject, localizedStrings, projectId], ); return ( -
    +
    {isSubmitting &&

    Adding entry to FieldWorks...

    } diff --git a/platform.bible-extension/src/web-views/find-related-words.web-view.tsx b/platform.bible-extension/src/web-views/find-related-words.web-view.tsx index c6ed22cf06..01a6cff42e 100644 --- a/platform.bible-extension/src/web-views/find-related-words.web-view.tsx +++ b/platform.bible-extension/src/web-views/find-related-words.web-view.tsx @@ -10,7 +10,7 @@ import type { import { Card, SearchBar } from 'platform-bible-react'; import { debounce } from 'platform-bible-utils'; import { useCallback, useEffect, useMemo, useState } from 'react'; -import AddNewEntry from '../components/add-new-entry'; +import AddNewEntryButton from '../components/add-new-entry-button'; import DictionaryList from '../components/dictionary-list'; import DictionaryListWrapper from '../components/dictionary-list-wrapper'; @@ -145,7 +145,7 @@ globalThis.webViewComponent = function fwLiteFindRelatedWords({ {selectedDomain && (
    - (); const [fwLiteNetworkObject, setFwLiteNetworkObject] = useState< NetworkObject | undefined >(); const [isFetching, setIsFetching] = useState(false); - const [localizedStrings] = useLocalizedStrings(LOCALIZED_STRING_KEYS); const [searchTerm, setSearchTerm] = useState(word ?? ''); useEffect(() => { @@ -45,12 +46,9 @@ globalThis.webViewComponent = function fwLiteFindWord({ const fetchEntries = useCallback( async (untrimmedSurfaceForm: string) => { if (!projectId || !fwLiteNetworkObject) { - if (!projectId) - logger.warn(`${localizedStrings['%fwLiteExtension_error_missingParam%']}projectId`); - if (!fwLiteNetworkObject) - logger.warn( - `${localizedStrings['%fwLiteExtension_error_missingParam%']}fwLiteNetworkObject`, - ); + const errMissingParam = localizedStrings['%fwLiteExtension_error_missingParam%']; + if (!projectId) logger.warn(`${errMissingParam}projectId`); + if (!fwLiteNetworkObject) logger.warn(`${errMissingParam}fwLiteNetworkObject`); return; } @@ -82,12 +80,9 @@ globalThis.webViewComponent = function fwLiteFindWord({ const addEntry = useCallback( async (entry: PartialEntry) => { if (!projectId || !fwLiteNetworkObject) { - if (!projectId) - logger.warn(`${localizedStrings['%fwLiteExtension_error_missingParam%']}projectId`); - if (!fwLiteNetworkObject) - logger.warn( - `${localizedStrings['%fwLiteExtension_error_missingParam%']}fwLiteNetworkObject`, - ); + const errMissingParam = localizedStrings['%fwLiteExtension_error_missingParam%']; + if (!projectId) logger.warn(`${errMissingParam}projectId`); + if (!fwLiteNetworkObject) logger.warn(`${errMissingParam}fwLiteNetworkObject`); return; } @@ -116,7 +111,7 @@ globalThis.webViewComponent = function fwLiteFindWord({
    - Date: Thu, 11 Sep 2025 17:25:58 -0400 Subject: [PATCH 07/18] [FindRelatedWords] More styling and localizing --- .../contributions/localizedStrings.json | 3 + .../src/types/localized-string-keys.ts | 3 + .../web-views/find-related-words.web-view.tsx | 117 ++++++++++-------- 3 files changed, 72 insertions(+), 51 deletions(-) diff --git a/platform.bible-extension/contributions/localizedStrings.json b/platform.bible-extension/contributions/localizedStrings.json index 71c9fa4ea7..de4321f948 100644 --- a/platform.bible-extension/contributions/localizedStrings.json +++ b/platform.bible-extension/contributions/localizedStrings.json @@ -17,6 +17,9 @@ "%fwLiteExtension_error_failedToAddEntry%": "Failed to add entry!", "%fwLiteExtension_error_gettingNetworkObject%": "Error getting network object:", "%fwLiteExtension_error_missingParam%": "Missing required parameter: ", + "%fwLiteExtension_findRelatedWord_noResultsInDomain%": "No entries in this semantic domain.", + "%fwLiteExtension_findRelatedWord_selectInstruction%": "Select a semantic domain for related words in that domain", + "%fwLiteExtension_findRelatedWord_textField%": "Find related words in dictionary...", "%fwLiteExtension_findWord_textField%": "Find in dictionary...", "%fwLiteExtension_list_projects_label%": "List all local FieldWorks projects", "%fwLiteExtension_menu_addEntry%": "Add to FieldWorks...", diff --git a/platform.bible-extension/src/types/localized-string-keys.ts b/platform.bible-extension/src/types/localized-string-keys.ts index 827f8fcfbb..635d46fc15 100644 --- a/platform.bible-extension/src/types/localized-string-keys.ts +++ b/platform.bible-extension/src/types/localized-string-keys.ts @@ -16,5 +16,8 @@ export const LOCALIZED_STRING_KEYS: LocalizeKey[] = [ '%fwLiteExtension_error_failedToAddEntry%', '%fwLiteExtension_error_gettingNetworkObject%', '%fwLiteExtension_error_missingParam%', + '%fwLiteExtension_findRelatedWord_noResultsInDomain%', + '%fwLiteExtension_findRelatedWord_selectInstruction%', + '%fwLiteExtension_findRelatedWord_textField%', '%fwLiteExtension_findWord_textField%', ]; diff --git a/platform.bible-extension/src/web-views/find-related-words.web-view.tsx b/platform.bible-extension/src/web-views/find-related-words.web-view.tsx index 01a6cff42e..30324459d1 100644 --- a/platform.bible-extension/src/web-views/find-related-words.web-view.tsx +++ b/platform.bible-extension/src/web-views/find-related-words.web-view.tsx @@ -1,5 +1,6 @@ import type { NetworkObject } from '@papi/core'; import papi, { logger } from '@papi/frontend'; +import { useLocalizedStrings } from '@papi/frontend/react'; import type { IEntry, IEntryService, @@ -7,12 +8,13 @@ import type { PartialEntry, WordWebViewOptions, } from 'fw-lite-extension'; -import { Card, SearchBar } from 'platform-bible-react'; +import { Label, SearchBar } from 'platform-bible-react'; import { debounce } from 'platform-bible-utils'; import { useCallback, useEffect, useMemo, useState } from 'react'; import AddNewEntryButton from '../components/add-new-entry-button'; import DictionaryList from '../components/dictionary-list'; import DictionaryListWrapper from '../components/dictionary-list-wrapper'; +import { LOCALIZED_STRING_KEYS } from '../types/localized-string-keys'; /* eslint-disable react-hooks/rules-of-hooks */ @@ -22,6 +24,8 @@ globalThis.webViewComponent = function fwLiteFindRelatedWords({ vernacularLanguage, word, }: WordWebViewOptions) { + const [localizedStrings] = useLocalizedStrings(LOCALIZED_STRING_KEYS); + const [fwLiteNetworkObject, setFwLiteNetworkObject] = useState< NetworkObject | undefined >(); @@ -39,8 +43,13 @@ globalThis.webViewComponent = function fwLiteFindRelatedWords({ logger.info('Got network object:', networkObject); setFwLiteNetworkObject(networkObject); }) - .catch((e) => logger.error('Error getting network object:', JSON.stringify(e))); - }, []); + .catch((e) => + logger.error( + `${localizedStrings['%fwLiteExtension_error_gettingNetworkObject%']}:`, + JSON.stringify(e), + ), + ); + }, [localizedStrings]); useEffect(() => { setSelectedDomain(undefined); @@ -52,8 +61,9 @@ globalThis.webViewComponent = function fwLiteFindRelatedWords({ const fetchEntries = useCallback( async (untrimmedSurfaceForm: string) => { if (!projectId || !fwLiteNetworkObject) { - if (!projectId) logger.warn('Missing required parameter: projectId'); - if (!fwLiteNetworkObject) logger.warn('Missing required parameter: fwLiteNetworkObject'); + const errMissingParam = localizedStrings['%fwLiteExtension_error_missingParam%']; + if (!projectId) logger.warn(`${errMissingParam}projectId`); + if (!fwLiteNetworkObject) logger.warn(`${errMissingParam}fwLiteNetworkObject`); return; } @@ -73,14 +83,15 @@ globalThis.webViewComponent = function fwLiteFindRelatedWords({ setIsFetching(false); setMatchingEntries(entries); }, - [fwLiteNetworkObject, projectId], + [fwLiteNetworkObject, localizedStrings, projectId], ); const fetchRelatedEntries = useCallback( async (semanticDomain: string) => { if (!projectId || !fwLiteNetworkObject) { - if (!projectId) logger.warn('Missing required parameter: projectId'); - if (!fwLiteNetworkObject) logger.warn('Missing required parameter: fwLiteNetworkObject'); + const errMissingParam = localizedStrings['%fwLiteExtension_error_missingParam%']; + if (!projectId) logger.warn(`${errMissingParam}projectId`); + if (!fwLiteNetworkObject) logger.warn(`${errMissingParam}fwLiteNetworkObject`); return; } @@ -90,7 +101,7 @@ globalThis.webViewComponent = function fwLiteFindRelatedWords({ setIsFetching(false); setRelatedEntries(entries ?? []); }, - [fwLiteNetworkObject, projectId], + [fwLiteNetworkObject, localizedStrings, projectId], ); useEffect(() => { @@ -110,9 +121,10 @@ globalThis.webViewComponent = function fwLiteFindRelatedWords({ const addEntryInDomain = useCallback( async (entry: PartialEntry) => { if (!fwLiteNetworkObject || !projectId || !selectedDomain || !entry.senses?.length) { - if (!fwLiteNetworkObject) logger.warn('Missing required parameter: fwLiteNetworkObject'); - if (!projectId) logger.warn('Missing required parameter: projectId'); - if (!selectedDomain) logger.warn('Missing required parameter: selectedDomain'); + const errMissingParam = localizedStrings['%fwLiteExtension_error_missingParam%']; + if (!fwLiteNetworkObject) logger.warn(`${errMissingParam}fwLiteNetworkObject`); + if (!projectId) logger.warn(`${errMissingParam}projectId`); + if (!selectedDomain) logger.warn(`${errMissingParam}selectedDomain`); if (!entry.senses?.length) logger.warn('Cannot add entry without senses'); return; } @@ -125,59 +137,62 @@ globalThis.webViewComponent = function fwLiteFindRelatedWords({ onSearch(Object.values(addedEntry.lexemeForm).pop() ?? ''); await papi.commands.sendCommand('fwLiteExtension.displayEntry', projectId, addedEntry.id); } else { - logger.error('Failed to add entry!'); + logger.error(`${localizedStrings['%fwLiteExtension_error_failedToAddEntry%']}`); } }, - [fwLiteNetworkObject, onSearch, projectId, selectedDomain], + [fwLiteNetworkObject, localizedStrings, onSearch, projectId, selectedDomain], ); return ( -
    - +
    +
    +
    + +
    + + {selectedDomain && ( +
    + +
    + )}
    + {matchingEntries && !selectedDomain && ( +

    + {localizedStrings['%fwLiteExtension_findRelatedWord_selectInstruction%']} +

    + )} + {selectedDomain && ( -
    - -
    +

    {`${selectedDomain.code}: ${JSON.stringify(selectedDomain.name)}`}

    )}
    } elementList={ - <> - {matchingEntries && !selectedDomain && ( - <> -

    Select a semantic domain for related words in that domain

    - - - )} - - {selectedDomain && relatedEntries && ( - <> - {`${selectedDomain.code}: ${JSON.stringify(selectedDomain.name)}`} - {relatedEntries.length ? ( - - ) : ( -

    No entries in this semantic domain.

    - )} - - )} - + /* eslint-disable no-nested-ternary */ + !matchingEntries ? undefined : !selectedDomain ? ( + + ) : !relatedEntries?.length ? ( +
    + +
    + ) : ( + + ) } isLoading={isFetching} hasItems={!!matchingEntries?.length} From 942877cd6d1bb367e8fd33603bd8b0353ec1e56f Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 12 Sep 2025 13:35:44 -0400 Subject: [PATCH 08/18] More styling and localizing --- .../contributions/localizedStrings.json | 9 +++ .../src/components/add-new-entry-button.tsx | 14 ++--- .../src/components/add-new-entry.tsx | 24 ++++---- .../src/components/dictionary-combo-box.tsx | 55 ++++++++++++------- .../components/dictionary-entry-display.tsx | 17 +++--- .../src/components/dictionary-list-item.tsx | 13 +++-- .../src/components/dictionary-list.tsx | 17 ++++-- .../src/components/domains-display.tsx | 9 ++- .../src/types/fw-lite-extension.d.ts | 17 ++++-- .../src/types/localized-string-keys.ts | 9 +++ .../src/utils/entry-display-text.ts | 15 ++--- .../src/web-views/add-word.web-view.tsx | 4 +- .../web-views/find-related-words.web-view.tsx | 15 +++-- .../src/web-views/find-word.web-view.tsx | 12 +++- 14 files changed, 147 insertions(+), 83 deletions(-) diff --git a/platform.bible-extension/contributions/localizedStrings.json b/platform.bible-extension/contributions/localizedStrings.json index de4321f948..38553fc336 100644 --- a/platform.bible-extension/contributions/localizedStrings.json +++ b/platform.bible-extension/contributions/localizedStrings.json @@ -9,6 +9,15 @@ "%fwLiteExtension_dictionary_backToList%": "Back to list", "%fwLiteExtension_dictionary_loading%": "Loading...", "%fwLiteExtension_dictionary_noResults%": "No results", + "%fwLiteExtension_dictionarySelect_clear%": "Clear selection", + "%fwLiteExtension_dictionarySelect_confirm%": "Confirm selection", + "%fwLiteExtension_dictionarySelect_loading%": "Loading dictionaries ...", + "%fwLiteExtension_dictionarySelect_noneFound%": "No dictionaries found", + "%fwLiteExtension_dictionarySelect_saved%": "Dictionary selection saved. You can close this window.", + "%fwLiteExtension_dictionarySelect_saveError%": "Error saving dictionary selection:", + "%fwLiteExtension_dictionarySelect_saving%": "Saving dictionary selection", + "%fwLiteExtension_dictionarySelect_select%": "Select a dictionary", + "%fwLiteExtension_dictionarySelect_selected%": "Selected:", "%fwLiteExtension_entryDisplay_definition%": "Definition", "%fwLiteExtension_entryDisplay_gloss%": "Gloss", "%fwLiteExtension_entryDisplay_headword%": "Headword", diff --git a/platform.bible-extension/src/components/add-new-entry-button.tsx b/platform.bible-extension/src/components/add-new-entry-button.tsx index ecf733ba40..efa9c65941 100644 --- a/platform.bible-extension/src/components/add-new-entry-button.tsx +++ b/platform.bible-extension/src/components/add-new-entry-button.tsx @@ -1,22 +1,20 @@ import { useLocalizedStrings } from '@papi/frontend/react'; -import type { PartialEntry } from 'fw-lite-extension'; +import type { DictionaryLanguages, PartialEntry } from 'fw-lite-extension'; import { Button } from 'platform-bible-react'; import { type ReactElement, useState } from 'react'; import AddNewEntry from './add-new-entry'; import { LOCALIZED_STRING_KEYS } from '../types/localized-string-keys'; -interface AddNewEntryButtonProps { +interface AddNewEntryButtonProps extends DictionaryLanguages { addEntry: (entry: PartialEntry) => Promise; - analysisLang: string; headword?: string; - vernacularLang: string; } export default function AddNewEntryButton({ addEntry, - analysisLang, + analysisLanguage, headword, - vernacularLang, + vernacularLanguage, }: AddNewEntryButtonProps): ReactElement { const [localizedStrings] = useLocalizedStrings(LOCALIZED_STRING_KEYS); @@ -26,10 +24,10 @@ export default function AddNewEntryButton({
    setAdding(false)} - vernacularLang={vernacularLang} + vernacularLanguage={vernacularLanguage} />
    ) : ( diff --git a/platform.bible-extension/src/components/add-new-entry.tsx b/platform.bible-extension/src/components/add-new-entry.tsx index c3d39004da..b8b67c7c48 100644 --- a/platform.bible-extension/src/components/add-new-entry.tsx +++ b/platform.bible-extension/src/components/add-new-entry.tsx @@ -1,24 +1,22 @@ import { logger } from '@papi/frontend'; import { useLocalizedStrings } from '@papi/frontend/react'; -import type { PartialEntry } from 'fw-lite-extension'; +import type { DictionaryLanguages, PartialEntry } from 'fw-lite-extension'; import { Button, Input, Label } from 'platform-bible-react'; import { type ReactElement, useCallback, useEffect, useState } from 'react'; import { LOCALIZED_STRING_KEYS } from '../types/localized-string-keys'; -interface AddNewEntryProps { +interface AddNewEntryProps extends DictionaryLanguages { addEntry: (entry: PartialEntry) => Promise; - analysisLang: string; headword?: string; onCancel?: () => void; - vernacularLang: string; } export default function AddNewEntry({ addEntry, - analysisLang, + analysisLanguage, headword, onCancel, - vernacularLang, + vernacularLanguage, }: AddNewEntryProps): ReactElement { const [localizedStrings] = useLocalizedStrings(LOCALIZED_STRING_KEYS); @@ -30,8 +28,8 @@ export default function AddNewEntry({ useEffect(() => setTempHeadword(headword || ''), [headword]); useEffect(() => { - setReady(!!(vernacularLang && tempHeadword.trim() && (gloss.trim() || definition.trim()))); - }, [definition, gloss, tempHeadword, vernacularLang]); + setReady(!!(vernacularLanguage && tempHeadword.trim() && (gloss.trim() || definition.trim()))); + }, [definition, gloss, tempHeadword, vernacularLanguage]); const clearEntry = useCallback((): void => { setDefinition(''); @@ -42,9 +40,9 @@ export default function AddNewEntry({ async function onSubmit(): Promise { const entry = createEntry( - vernacularLang, + vernacularLanguage, tempHeadword.trim(), - analysisLang || 'en', + analysisLanguage || 'en', gloss.trim(), definition.trim(), ); @@ -62,7 +60,7 @@ export default function AddNewEntry({
    setGloss(e.target.value)} value={gloss} />
    setSettingSaved(true)) - .catch((e) => logger.error('Error saving dictionary selection:', JSON.stringify(e))) + .catch((e) => + logger.error( + localizedStrings['%fwLiteExtension_dictionarySelect_saveError%'], + JSON.stringify(e), + ), + ) .finally(() => setSettingSaving(false)); }, - [selectDictionary], + [localizedStrings, selectDictionary], ); if (settingSaving) { - return

    Saving dictionary selection {selectedDictionaryCode}...

    ; + return ( +

    + {localizedStrings['%fwLiteExtension_dictionarySelect_saving%']} {selectedDictionaryCode} ... +

    + ); } if (settingSaved) { - return

    Dictionary selection saved. You can close this window.

    ; + return

    {localizedStrings['%fwLiteExtension_dictionarySelect_saved%']}

    ; } return ( -
    +
    p.code)} - textPlaceholder="Select a dictionary" + textPlaceholder={localizedStrings['%fwLiteExtension_dictionarySelect_select%']} /> + {!!selectedDictionaryCode && ( - <> - - - +
    + + + +
    )}
    ); diff --git a/platform.bible-extension/src/components/dictionary-entry-display.tsx b/platform.bible-extension/src/components/dictionary-entry-display.tsx index b15e4f43a7..a32d006c35 100644 --- a/platform.bible-extension/src/components/dictionary-entry-display.tsx +++ b/platform.bible-extension/src/components/dictionary-entry-display.tsx @@ -1,7 +1,7 @@ // Modified from paranext-core/extensions/src/components/dictionary/dictionary-entry-display.component.tsx import { useLocalizedStrings } from '@papi/frontend/react'; -import type { IEntry, ISemanticDomain } from 'fw-lite-extension'; +import type { DictionaryLanguages, IEntry, ISemanticDomain } from 'fw-lite-extension'; import { ChevronUpIcon } from 'lucide-react'; import { Button, @@ -22,7 +22,7 @@ import { } from '../utils/entry-display-text'; /** Props for the DictionaryEntryDisplay component */ -export type DictionaryEntryDisplayProps = { +export type DictionaryEntryDisplayProps = DictionaryLanguages & { /** Dictionary entry object to display */ dictionaryEntry: IEntry; /** Whether the display is in a drawer or just next to the list */ @@ -41,11 +41,13 @@ export type DictionaryEntryDisplayProps = { * back button to navigate back to the list view. */ export default function DictionaryEntryDisplay({ + analysisLanguage, dictionaryEntry, isDrawer, handleBackToListButton, onClickScrollToTop, onClickSemanticDomain, + vernacularLanguage, }: DictionaryEntryDisplayProps) { const [localizedStrings] = useLocalizedStrings(LOCALIZED_STRING_KEYS); @@ -65,10 +67,10 @@ export default function DictionaryEntryDisplay({
    - {entryHeadwordText(dictionaryEntry)} + {entryHeadwordText(dictionaryEntry, vernacularLanguage)} - {entryGlossText(dictionaryEntry)} + {entryGlossText(dictionaryEntry, analysisLanguage)}
    @@ -92,22 +94,23 @@ export default function DictionaryEntryDisplay({ >
    {senseIndex + 1} - {senseGlossText(sense)} + {senseGlossText(sense, analysisLanguage)}
    {Object.values(sense.definition).some(Boolean) && (
    - {senseDefinitionText(sense)} + {senseDefinitionText(sense, analysisLanguage)}
    )} {sense.partOfSpeech?.id && (
    - {`${localizedStrings['%fwLiteExtension_entryDisplay_partOfSpeech%']}: ${partOfSpeechText(sense.partOfSpeech)}`} + {`${localizedStrings['%fwLiteExtension_entryDisplay_partOfSpeech%']}: ${partOfSpeechText(sense.partOfSpeech, analysisLanguage)}`}
    )} diff --git a/platform.bible-extension/src/components/dictionary-list-item.tsx b/platform.bible-extension/src/components/dictionary-list-item.tsx index 9d674dd112..0f120e6c75 100644 --- a/platform.bible-extension/src/components/dictionary-list-item.tsx +++ b/platform.bible-extension/src/components/dictionary-list-item.tsx @@ -1,12 +1,12 @@ // Modified from paranext-core/extensions/src/components/dictionary/dictionary-list-item.component.tsx -import type { IEntry, ISemanticDomain } from 'fw-lite-extension'; +import type { DictionaryLanguages, IEntry, ISemanticDomain } from 'fw-lite-extension'; import { cn, Separator } from 'platform-bible-react'; import DomainsDisplay from './domains-display'; import { entryGlossText, entryHeadwordText } from '../utils/entry-display-text'; /** Props for the DictionaryListItem component */ -type DictionaryListItemProps = { +type DictionaryListItemProps = DictionaryLanguages & { /** The dictionary entry to display */ entry: IEntry; /** Whether the dictionary entry is selected */ @@ -30,10 +30,12 @@ type DictionaryListItemProps = { * handle keyboard navigation of the list. */ export default function DictionaryListItem({ + analysisLanguage, entry, isSelected, onClick, onClickSemanticDomain, + vernacularLanguage, }: DictionaryListItemProps) { return ( <> @@ -54,16 +56,19 @@ export default function DictionaryListItem({ tabIndex={-1} >
    - {entryHeadwordText(entry)} + {entryHeadwordText(entry, vernacularLanguage)}
    -

    {entryGlossText(entry)}

    +

    + {entryGlossText(entry, analysisLanguage)} +

    {onClickSemanticDomain && (
    s.semanticDomains)} onClickDomain={onClickSemanticDomain} /> diff --git a/platform.bible-extension/src/components/dictionary-list.tsx b/platform.bible-extension/src/components/dictionary-list.tsx index d1b9e82a2e..ac3c94a7c5 100644 --- a/platform.bible-extension/src/components/dictionary-list.tsx +++ b/platform.bible-extension/src/components/dictionary-list.tsx @@ -1,6 +1,6 @@ // Modified from paranext-core/extensions/src/components/dictionary/dictionary-list.component.tsx -import type { IEntry, ISemanticDomain } from 'fw-lite-extension'; +import type { DictionaryLanguages, IEntry, ISemanticDomain } from 'fw-lite-extension'; import { cn, Drawer, @@ -15,7 +15,7 @@ import DictionaryListItem from './dictionary-list-item'; import useIsWideScreen from '../utils/use-is-wide-screen'; /** Props for the DictionaryList component */ -type DictionaryListProps = { +type DictionaryListProps = DictionaryLanguages & { /** Array of dictionary entries */ dictionaryData: IEntry[]; /** Callback function to handle character press */ @@ -36,17 +36,18 @@ type DictionaryListProps = { * handle keyboard navigation of the list. */ export default function DictionaryList({ + analysisLanguage, dictionaryData, onCharacterPress, onClickSemanticDomain, + vernacularLanguage, }: DictionaryListProps) { const isWideScreen = useIsWideScreen(); + const [selectedEntryId, setSelectedEntryId] = useState(undefined); const [drawerOpen, setDrawerOpen] = useState(false); - const options: ListboxOption[] = dictionaryData.map((entry) => ({ - id: entry.id, - })); + const options: ListboxOption[] = dictionaryData.map((entry) => ({ id: entry.id })); const selectedEntry = useMemo(() => { return dictionaryData.find((entry) => entry.id === selectedEntryId); @@ -102,10 +103,12 @@ export default function DictionaryList({ return (
    setSelectedEntryId(entry.id)} onClickSemanticDomain={onClickSemanticDomain} + vernacularLanguage={vernacularLanguage} />
    ); @@ -118,10 +121,12 @@ export default function DictionaryList({
    ) : ( @@ -137,10 +142,12 @@ export default function DictionaryList({
    setSelectedEntryId(undefined)} onClickScrollToTop={scrollToTop} onClickSemanticDomain={onClickSemanticDomain} + vernacularLanguage={vernacularLanguage} />
    diff --git a/platform.bible-extension/src/components/domains-display.tsx b/platform.bible-extension/src/components/domains-display.tsx index 48044e9aed..744f7ae237 100644 --- a/platform.bible-extension/src/components/domains-display.tsx +++ b/platform.bible-extension/src/components/domains-display.tsx @@ -7,6 +7,7 @@ import { domainText } from '../utils/entry-display-text'; /** Props for the DomainsDisplay component */ type DomainsDisplayProps = { + analysisLanguage: string; /** Domains to display */ domains: ISemanticDomain[]; /** Function to trigger when a domain is clicked */ @@ -19,7 +20,11 @@ type DomainsDisplayProps = { * The component displays each domain as a rounded, colored pill with a small icon. The text of the * pill is the code of the domain, followed by the label. */ -export default function DomainsDisplay({ domains, onClickDomain }: DomainsDisplayProps) { +export default function DomainsDisplay({ + analysisLanguage, + domains, + onClickDomain, +}: DomainsDisplayProps) { const [sortedDomains, setSortedDomains] = useState([]); useEffect(() => { @@ -39,7 +44,7 @@ export default function DomainsDisplay({ domains, onClickDomain }: DomainsDispla type="button" > - {domainText(domain)} + {domainText(domain, analysisLanguage)} ))}
    diff --git a/platform.bible-extension/src/types/fw-lite-extension.d.ts b/platform.bible-extension/src/types/fw-lite-extension.d.ts index d63074bcc9..bdac702b1b 100644 --- a/platform.bible-extension/src/types/fw-lite-extension.d.ts +++ b/platform.bible-extension/src/types/fw-lite-extension.d.ts @@ -48,21 +48,26 @@ declare module 'fw-lite-extension' { deleteEntry(projectId: string, id: string): Promise; } - interface OpenWebViewOptionsWithProjectId extends OpenWebViewOptions { + export interface OpenWebViewOptionsWithProjectId extends OpenWebViewOptions { projectId?: string; } - interface BrowseWebViewOptions extends OpenWebViewOptionsWithProjectId { + export interface BrowseWebViewOptions extends OpenWebViewOptionsWithProjectId { url?: string; } - interface OpenWebViewOptionsWithDictionaryInfo extends OpenWebViewOptionsWithProjectId { - analysisLanguage?: string; + export interface DictionaryLanguages { + analysisLanguage: string; + vernacularLanguage: string; + } + + export interface OpenWebViewOptionsWithDictionaryInfo + extends OpenWebViewOptionsWithProjectId, + Partial { dictionaryCode?: string; - vernacularLanguage?: string; } - interface WordWebViewOptions extends OpenWebViewOptionsWithDictionaryInfo { + export interface WordWebViewOptions extends OpenWebViewOptionsWithDictionaryInfo { word?: string; } diff --git a/platform.bible-extension/src/types/localized-string-keys.ts b/platform.bible-extension/src/types/localized-string-keys.ts index 635d46fc15..9377d61ca9 100644 --- a/platform.bible-extension/src/types/localized-string-keys.ts +++ b/platform.bible-extension/src/types/localized-string-keys.ts @@ -8,6 +8,15 @@ export const LOCALIZED_STRING_KEYS: LocalizeKey[] = [ '%fwLiteExtension_dictionary_backToList%', '%fwLiteExtension_dictionary_loading%', '%fwLiteExtension_dictionary_noResults%', + '%fwLiteExtension_dictionarySelect_clear%', + '%fwLiteExtension_dictionarySelect_confirm%', + '%fwLiteExtension_dictionarySelect_loading%', + '%fwLiteExtension_dictionarySelect_noneFound%', + '%fwLiteExtension_dictionarySelect_saved%', + '%fwLiteExtension_dictionarySelect_saveError%', + '%fwLiteExtension_dictionarySelect_saving%', + '%fwLiteExtension_dictionarySelect_select%', + '%fwLiteExtension_dictionarySelect_selected%', '%fwLiteExtension_entryDisplay_definition%', '%fwLiteExtension_entryDisplay_gloss%', '%fwLiteExtension_entryDisplay_headword%', diff --git a/platform.bible-extension/src/utils/entry-display-text.ts b/platform.bible-extension/src/utils/entry-display-text.ts index 730b28fb55..1237d64f9e 100644 --- a/platform.bible-extension/src/utils/entry-display-text.ts +++ b/platform.bible-extension/src/utils/entry-display-text.ts @@ -4,11 +4,8 @@ export function domainText(domain: ISemanticDomain, lang = 'en'): string { return `${domain.code}: ${domain.name[lang] || domain.name.en}`; } -export function entryGlossText(entry: IEntry): string { - return entry.senses - .map((s) => Object.values(s.gloss).filter(Boolean).join('; ')) - .filter(Boolean) - .join(' | '); +export function entryGlossText(entry: IEntry, lang = 'en'): string { + return entry.senses.map((s) => senseGlossText(s, lang)).join(' | '); } export function entryHeadwordText(entry: IEntry, lang = 'en'): string { @@ -25,10 +22,10 @@ export function partOfSpeechText(partOfSpeech: IPartOfSpeech, lang = 'en'): stri return partOfSpeech.name[lang] || partOfSpeech.name.en; } -export function senseDefinitionText(sense: ISense): string { - return JSON.stringify(sense.definition); +export function senseDefinitionText(sense: ISense, lang = 'en'): string { + return sense.definition[lang] || Object.values(sense.definition).join('; '); } -export function senseGlossText(sense: ISense): string { - return JSON.stringify(sense.gloss); +export function senseGlossText(sense: ISense, lang = 'en'): string { + return sense.gloss[lang] || Object.values(sense.gloss).join('; '); } diff --git a/platform.bible-extension/src/web-views/add-word.web-view.tsx b/platform.bible-extension/src/web-views/add-word.web-view.tsx index fbd3a42491..81ea3dbe49 100644 --- a/platform.bible-extension/src/web-views/add-word.web-view.tsx +++ b/platform.bible-extension/src/web-views/add-word.web-view.tsx @@ -66,9 +66,9 @@ globalThis.webViewComponent = function fwLiteAddWord({
    {isSubmitting &&

    Adding entry to FieldWorks...

    } {isSubmitted &&

    Entry added!

    } diff --git a/platform.bible-extension/src/web-views/find-related-words.web-view.tsx b/platform.bible-extension/src/web-views/find-related-words.web-view.tsx index 30324459d1..2cf9f91910 100644 --- a/platform.bible-extension/src/web-views/find-related-words.web-view.tsx +++ b/platform.bible-extension/src/web-views/find-related-words.web-view.tsx @@ -15,6 +15,7 @@ import AddNewEntryButton from '../components/add-new-entry-button'; import DictionaryList from '../components/dictionary-list'; import DictionaryListWrapper from '../components/dictionary-list-wrapper'; import { LOCALIZED_STRING_KEYS } from '../types/localized-string-keys'; +import { domainText } from '../utils/entry-display-text'; /* eslint-disable react-hooks/rules-of-hooks */ @@ -160,9 +161,9 @@ globalThis.webViewComponent = function fwLiteFindRelatedWords({
    )} @@ -175,7 +176,7 @@ globalThis.webViewComponent = function fwLiteFindRelatedWords({ )} {selectedDomain && ( -

    {`${selectedDomain.code}: ${JSON.stringify(selectedDomain.name)}`}

    +

    {domainText(selectedDomain, analysisLanguage)}

    )}
    } @@ -183,15 +184,21 @@ globalThis.webViewComponent = function fwLiteFindRelatedWords({ /* eslint-disable no-nested-ternary */ !matchingEntries ? undefined : !selectedDomain ? ( ) : !relatedEntries?.length ? (
    ) : ( - + ) } isLoading={isFetching} diff --git a/platform.bible-extension/src/web-views/find-word.web-view.tsx b/platform.bible-extension/src/web-views/find-word.web-view.tsx index 0a07850554..7ecc3dc236 100644 --- a/platform.bible-extension/src/web-views/find-word.web-view.tsx +++ b/platform.bible-extension/src/web-views/find-word.web-view.tsx @@ -113,15 +113,21 @@ globalThis.webViewComponent = function fwLiteFindWord({
    } elementList={ - matchingEntries ? : undefined + matchingEntries ? ( + + ) : undefined } isLoading={isFetching} hasItems={!!matchingEntries?.length} From ce35f0b79ea8b0a61c0b238639928b655d16c4a6 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 12 Sep 2025 15:39:08 -0400 Subject: [PATCH 09/18] More styling and localizing --- .../src/components/dictionary-combo-box.tsx | 10 +++++++--- platform.bible-extension/src/main.ts | 2 +- .../src/utils/fw-lite-api.ts | 20 +++++++++++-------- .../src/utils/project-manager.ts | 5 ++++- .../web-views/find-related-words.web-view.tsx | 12 +++++++---- 5 files changed, 32 insertions(+), 17 deletions(-) diff --git a/platform.bible-extension/src/components/dictionary-combo-box.tsx b/platform.bible-extension/src/components/dictionary-combo-box.tsx index 6eaf049368..b8e966feec 100644 --- a/platform.bible-extension/src/components/dictionary-combo-box.tsx +++ b/platform.bible-extension/src/components/dictionary-combo-box.tsx @@ -40,14 +40,18 @@ export default function DictionaryComboBox({ if (settingSaving) { return ( -

    +

    {localizedStrings['%fwLiteExtension_dictionarySelect_saving%']} {selectedDictionaryCode} ... -

    +

    ); } if (settingSaved) { - return

    {localizedStrings['%fwLiteExtension_dictionarySelect_saved%']}

    ; + return ( +

    + {localizedStrings['%fwLiteExtension_dictionarySelect_saved%']} +

    + ); } return ( diff --git a/platform.bible-extension/src/main.ts b/platform.bible-extension/src/main.ts index 9e4da9a21f..3172306d05 100644 --- a/platform.bible-extension/src/main.ts +++ b/platform.bible-extension/src/main.ts @@ -195,7 +195,7 @@ export async function activate(context: ExecutionActivationContext): Promise logger.error('Error fetching writing systems:', JSON.stringify(e))); - const analysisLang = langs?.analysis.pop()?.wsId ?? ''; + const analysisLang = langs?.analysis[0]?.wsId ?? ''; if (analysisLang) { logger.info(`Storing FieldWorks dictionary analysis language '${analysisLang}'`); } else { diff --git a/platform.bible-extension/src/utils/fw-lite-api.ts b/platform.bible-extension/src/utils/fw-lite-api.ts index 0df7173242..2ca4b5e275 100644 --- a/platform.bible-extension/src/utils/fw-lite-api.ts +++ b/platform.bible-extension/src/utils/fw-lite-api.ts @@ -90,14 +90,18 @@ export class FwLiteApi { const projects = (await this.fetchPath('localProjects')) as IProjectModel[]; if (!langTag?.trim()) return projects; - const matches = ( - await Promise.all( - projects.map(async (p) => - (await this.doesProjectMatchLangTag(p.code, langTag)) ? p : undefined, - ), - ) - ).filter((p) => p) as IProjectModel[]; - return matches.length ? matches : projects; + try { + const matches = ( + await Promise.all( + projects.map(async (p) => + (await this.doesProjectMatchLangTag(p.code, langTag)) ? p : undefined, + ), + ) + ).filter((p) => p) as IProjectModel[]; + return matches.length ? matches : projects; + } catch { + return projects; + } } async getWritingSystems(dictionaryCode?: string): Promise { diff --git a/platform.bible-extension/src/utils/project-manager.ts b/platform.bible-extension/src/utils/project-manager.ts index c92adaf58d..01ea177e73 100644 --- a/platform.bible-extension/src/utils/project-manager.ts +++ b/platform.bible-extension/src/utils/project-manager.ts @@ -45,7 +45,10 @@ export class ProjectManager { } logger.info(`FieldWorks dictionary not yet selected for project '${nameOrId}'`); - await this.openWebView(WebViewType.DictionarySelect, { type: 'float' }); + await this.openWebView(WebViewType.DictionarySelect, { + floatSize: { height: 500, width: 400 }, + type: 'float', + }); } async setFwDictionaryCode(dictionaryCode: string): Promise { diff --git a/platform.bible-extension/src/web-views/find-related-words.web-view.tsx b/platform.bible-extension/src/web-views/find-related-words.web-view.tsx index 2cf9f91910..dce5a5215f 100644 --- a/platform.bible-extension/src/web-views/find-related-words.web-view.tsx +++ b/platform.bible-extension/src/web-views/find-related-words.web-view.tsx @@ -8,6 +8,7 @@ import type { PartialEntry, WordWebViewOptions, } from 'fw-lite-extension'; +import { Network } from 'lucide-react'; import { Label, SearchBar } from 'platform-bible-react'; import { debounce } from 'platform-bible-utils'; import { useCallback, useEffect, useMemo, useState } from 'react'; @@ -170,13 +171,16 @@ globalThis.webViewComponent = function fwLiteFindRelatedWords({
    {matchingEntries && !selectedDomain && ( -

    +

    {localizedStrings['%fwLiteExtension_findRelatedWord_selectInstruction%']} -

    + )} {selectedDomain && ( -

    {domainText(selectedDomain, analysisLanguage)}

    +

    + + {domainText(selectedDomain, analysisLanguage)} +

    )}
    } @@ -190,7 +194,7 @@ globalThis.webViewComponent = function fwLiteFindRelatedWords({ vernacularLanguage={vernacularLanguage ?? ''} /> ) : !relatedEntries?.length ? ( -
    +
    ) : ( From c6a38ae278b14442db9c56d18f7772a9f5e20777 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 12 Sep 2025 15:53:36 -0400 Subject: [PATCH 10/18] Remove FWLite browse outside the selected dictionary --- .../contributions/localizedStrings.json | 2 -- .../contributions/menus.json | 6 ----- platform.bible-extension/src/main.ts | 27 +++---------------- .../src/types/fw-lite-extension.d.ts | 2 -- .../src/web-views/main.web-view.tsx | 22 ++------------- 5 files changed, 5 insertions(+), 54 deletions(-) diff --git a/platform.bible-extension/contributions/localizedStrings.json b/platform.bible-extension/contributions/localizedStrings.json index 38553fc336..bb9e27432e 100644 --- a/platform.bible-extension/contributions/localizedStrings.json +++ b/platform.bible-extension/contributions/localizedStrings.json @@ -30,12 +30,10 @@ "%fwLiteExtension_findRelatedWord_selectInstruction%": "Select a semantic domain for related words in that domain", "%fwLiteExtension_findRelatedWord_textField%": "Find related words in dictionary...", "%fwLiteExtension_findWord_textField%": "Find in dictionary...", - "%fwLiteExtension_list_projects_label%": "List all local FieldWorks projects", "%fwLiteExtension_menu_addEntry%": "Add to FieldWorks...", "%fwLiteExtension_menu_browseDictionary%": "Browse FieldWorks dictionary", "%fwLiteExtension_menu_findEntry%": "Search in FieldWorks...", "%fwLiteExtension_menu_findRelatedEntries%": "Search for related words...", - "%fwLiteExtension_open_label%": "Open FieldWorks Lite", "%fwLiteExtension_projectSettings_analysisLanguage%": "Analysis language", "%fwLiteExtension_projectSettings_dictionary%": "FieldWorks dictionary", "%fwLiteExtension_projectSettings_dictionaryDescription%": "The FieldWorks dictionary to use with this project", diff --git a/platform.bible-extension/contributions/menus.json b/platform.bible-extension/contributions/menus.json index 645b321d7c..0de61cc76c 100644 --- a/platform.bible-extension/contributions/menus.json +++ b/platform.bible-extension/contributions/menus.json @@ -47,12 +47,6 @@ "group": "fw-lite-extension.editor", "order": 4, "command": "fwLiteExtension.findRelatedEntries" - }, - { - "label": "%fwLiteExtension_open_label%", - "group": "fw-lite-extension.editor", - "order": 1007, - "command": "fwLiteExtension.openFWLite" } ] } diff --git a/platform.bible-extension/src/main.ts b/platform.bible-extension/src/main.ts index 3172306d05..6ca92502dd 100644 --- a/platform.bible-extension/src/main.ts +++ b/platform.bible-extension/src/main.ts @@ -39,9 +39,7 @@ export async function activate(context: ExecutionActivationContext): Promise urlHolder.baseUrl, - ); - const addEntryCommandPromise = papi.commands.registerCommand( 'fwLiteExtension.addEntry', async (webViewId: string, word: string) => { @@ -114,7 +107,7 @@ export async function activate(context: ExecutionActivationContext): Promise { - await papi.webViews.openWebView(WebViewType.Main); - return { success: true }; - }, - ); - const selectFwDictionaryCommandPromise = papi.commands.registerCommand( 'fwLiteExtension.selectDictionary', async (projectId: string, dictionaryCode: string) => { @@ -221,9 +205,6 @@ export async function activate(context: ExecutionActivationContext): Promise Promise; 'fwLiteExtension.fwDictionaries': (projectId?: string) => Promise; - 'fwLiteExtension.openFWLite': () => Promise; // TODO: Remove before publishing. 'fwLiteExtension.findEntry': (webViewId: string, entry: string) => Promise; 'fwLiteExtension.findRelatedEntries': ( webViewId: string, entry: string, ) => Promise; - 'fwLiteExtension.getBaseUrl': () => string; } export interface ProjectSettingTypes { diff --git a/platform.bible-extension/src/web-views/main.web-view.tsx b/platform.bible-extension/src/web-views/main.web-view.tsx index 7df8297437..6388500cbe 100644 --- a/platform.bible-extension/src/web-views/main.web-view.tsx +++ b/platform.bible-extension/src/web-views/main.web-view.tsx @@ -1,29 +1,11 @@ -import papi from '@papi/frontend'; import type { BrowseWebViewOptions } from 'fw-lite-extension'; -import { Button } from 'platform-bible-react'; -import { useEffect, useRef, useState } from 'react'; +import { useRef } from 'react'; /* eslint-disable react-hooks/rules-of-hooks */ globalThis.webViewComponent = function fwLiteMainWindow({ url }: BrowseWebViewOptions) { - // TODO: Use of baseUrl for development; remove before publishing. - const [baseUrl, setBaseUrl] = useState(''); - const [src, setSrc] = useState(''); - // eslint-disable-next-line no-null/no-null const iframe = useRef(null); - useEffect(() => { - updateUrl(); - }, []); - useEffect(() => setSrc(url || baseUrl), [baseUrl, url]); - - async function updateUrl(): Promise { - setBaseUrl(await papi.commands.sendCommand('fwLiteExtension.getBaseUrl')); - } - - if (!src) { - return ; - } - return