diff --git a/platform.bible-extension/contributions/localizedStrings.json b/platform.bible-extension/contributions/localizedStrings.json index fda3ce5f44..bb9e27432e 100644 --- a/platform.bible-extension/contributions/localizedStrings.json +++ b/platform.bible-extension/contributions/localizedStrings.json @@ -2,37 +2,47 @@ "metadata": {}, "localizedStrings": { "en": { - "%fwLiteExtension_addWord_title%": "Add to FieldWorks", - "%fwLiteExtension_browseDictionary_title%": "FieldWorks Lite", - "%fwLiteExtension_selectDictionary_title%": "Select FieldWorks dictionary", - "%fwLiteExtension_findRelatedWords_title%": "Find related words in FieldWorks", - "%fwLiteExtension_findWord_title%": "Search in FieldWorks", - "%fwLiteExtension_find_in_dictionary%": "Find in FieldWorks Lite", - "%fwLiteExtension_list_projects_label%": "List all local FieldWorks projects", + "%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_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", + "%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_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_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_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/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/components/add-new-entry-button.tsx b/platform.bible-extension/src/components/add-new-entry-button.tsx new file mode 100644 index 0000000000..cf66d19816 --- /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 { 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'; + +/** Props for the AddNewEntryButton component */ +interface AddNewEntryButtonProps extends DictionaryLanguages { + addEntry: (entry: PartialEntry) => Promise; + headword?: string; +} + +/** A button that, when clicked, expands to the AddNewEntry component. */ +export default function AddNewEntryButton({ + addEntry, + analysisLanguage, + headword, + vernacularLanguage, +}: AddNewEntryButtonProps): ReactElement { + const [localizedStrings] = useLocalizedStrings(LOCALIZED_STRING_KEYS); + + const [adding, setAdding] = useState(false); + + return adding ? ( +
+ setAdding(false)} + 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 27c91fad43..f195d3e971 100644 --- a/platform.bible-extension/src/components/add-new-entry.tsx +++ b/platform.bible-extension/src/components/add-new-entry.tsx @@ -1,49 +1,50 @@ import { logger } from '@papi/frontend'; -import type { PartialEntry } from 'fw-lite-extension'; -import { Button, Card, CardContent, CardHeader, Input, Label } from 'platform-bible-react'; +import { useLocalizedStrings } from '@papi/frontend/react'; +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 { +/** Props for the AddNewEntry component */ +interface AddNewEntryProps extends DictionaryLanguages { addEntry: (entry: PartialEntry) => Promise; - analysisLang: string; headword?: string; - isAdding?: boolean; - vernacularLang: string; + onCancel?: () => void; } +/** A panel for creating a simple entry: headword with gloss and/or definition. */ export default function AddNewEntry({ addEntry, - analysisLang, + analysisLanguage, headword, - isAdding, - vernacularLang, + onCancel, + vernacularLanguage, }: 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(() => { - 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 => { - setAdding(isAdding); setDefinition(''); setGloss(''); setTempHeadword(headword || ''); - }, [headword, isAdding]); + onCancel?.(); + }, [headword, onCancel]); async function onSubmit(): Promise { const entry = createEntry( - vernacularLang, + vernacularLanguage, tempHeadword.trim(), - analysisLang || 'en', + analysisLanguage || 'en', gloss.trim(), definition.trim(), ); @@ -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/back-to-list-button.tsx b/platform.bible-extension/src/components/back-to-list-button.tsx new file mode 100644 index 0000000000..de3ebb552e --- /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 default 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-combo-box.tsx b/platform.bible-extension/src/components/dictionary-combo-box.tsx index b92c6bcb67..9d210af276 100644 --- a/platform.bible-extension/src/components/dictionary-combo-box.tsx +++ b/platform.bible-extension/src/components/dictionary-combo-box.tsx @@ -1,17 +1,23 @@ import { logger } from '@papi/frontend'; +import { useLocalizedStrings } from '@papi/frontend/react'; import type { IProjectModel } from 'fw-lite-extension'; -import { ComboBox } from 'platform-bible-react'; +import { Button, ComboBox } from 'platform-bible-react'; import { type ReactElement, useCallback, useState } from 'react'; +import { LOCALIZED_STRING_KEYS } from '../types/localized-string-keys'; +/** Props for the DictionaryComboBox component */ interface DictionaryComboBoxProps { dictionaries?: IProjectModel[]; selectDictionary: (dictionaryCode: string) => Promise; } +/** A combo-box for selecting a FieldWorks dictionary for a project. */ export default function DictionaryComboBox({ dictionaries, selectDictionary, }: DictionaryComboBoxProps): ReactElement { + const [localizedStrings] = useLocalizedStrings(LOCALIZED_STRING_KEYS); + const [selectedDictionaryCode, setSelectedDictionaryCode] = useState(''); const [settingSaved, setSettingSaved] = useState(false); const [settingSaving, setSettingSaving] = useState(false); @@ -23,49 +29,64 @@ export default function DictionaryComboBox({ // eslint-disable-next-line promise/catch-or-return selectDictionary(code) .then(() => 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 new file mode 100644 index 0000000000..24501343f1 --- /dev/null +++ b/platform.bible-extension/src/components/dictionary-entry-display.tsx @@ -0,0 +1,131 @@ +// Modified from paranext-core/extensions/src/components/dictionary/dictionary-entry-display.component.tsx + +import { useLocalizedStrings } from '@papi/frontend/react'; +import type { DictionaryLanguages, IEntry, ISemanticDomain } 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 = DictionaryLanguages & { + /** 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; + /** Callback function to handle click on a semantic domain */ + onClickSemanticDomain?: (domain: ISemanticDomain) => 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 default function DictionaryEntryDisplay({ + analysisLanguage, + dictionaryEntry, + isDrawer, + handleBackToListButton, + onClickScrollToTop, + onClickSemanticDomain, + vernacularLanguage, +}: 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, vernacularLanguage)} + + + {entryGlossText(dictionaryEntry, analysisLanguage)} + + +
+
+ + + +
+

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

+ +
+ {dictionaryEntry.senses.filter(Boolean).map((sense, senseIndex) => ( +
+
+ {senseIndex + 1} + {senseGlossText(sense, analysisLanguage)} +
+ + {Object.values(sense.definition).some(Boolean) && ( +
+ {senseDefinitionText(sense, analysisLanguage)} +
+ )} + + {sense.partOfSpeech?.id && ( +
+ {`${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 new file mode 100644 index 0000000000..0f120e6c75 --- /dev/null +++ b/platform.bible-extension/src/components/dictionary-list-item.tsx @@ -0,0 +1,81 @@ +// Modified from paranext-core/extensions/src/components/dictionary/dictionary-list-item.component.tsx + +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 = DictionaryLanguages & { + /** 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; + /** Callback function to handle click on a semantic domain */ + onClickSemanticDomain?: (domain: ISemanticDomain) => 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 default function DictionaryListItem({ + analysisLanguage, + entry, + isSelected, + onClick, + onClickSemanticDomain, + vernacularLanguage, +}: 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, vernacularLanguage)} +
    + +
    +

    + {entryGlossText(entry, analysisLanguage)} +

    +
    + + {onClickSemanticDomain && ( +
    + s.semanticDomains)} + onClickDomain={onClickSemanticDomain} + /> +
    + )} +
  • + + + ); +} 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..fcc451c45c --- /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 wrapper layout with a sticky header and a loading/no-results/list body. */ +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 new file mode 100644 index 0000000000..ac3c94a7c5 --- /dev/null +++ b/platform.bible-extension/src/components/dictionary-list.tsx @@ -0,0 +1,158 @@ +// Modified from paranext-core/extensions/src/components/dictionary/dictionary-list.component.tsx + +import type { DictionaryLanguages, IEntry, ISemanticDomain } 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 = DictionaryLanguages & { + /** Array of dictionary entries */ + 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; +}; + +/** + * 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({ + 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 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)} + onClickSemanticDomain={onClickSemanticDomain} + vernacularLanguage={vernacularLanguage} + /> +
      + ); + })} +
    +
    + {selectedEntryId && + selectedEntry && + (isWideScreen ? ( +
    + +
    + ) : ( + setSelectedEntryId(undefined)} + > + +
    + + +
    + 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 new file mode 100644 index 0000000000..ea4cd8a623 --- /dev/null +++ b/platform.bible-extension/src/components/domains-display.tsx @@ -0,0 +1,50 @@ +// Modified from paranext-core/extensions/src/platform-lexical-tools/src/components/dictionary/domains-display.component.tsx + +import type { ISemanticDomain } from 'fw-lite-extension'; +import { Network } from 'lucide-react'; +import { useMemo } from 'react'; +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 */ + 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 default function DomainsDisplay({ + analysisLanguage, + domains, + onClickDomain, +}: DomainsDisplayProps) { + const sortedDomains = useMemo(() => { + const domainsByCode = new Map(); + domains.forEach((d) => d.code && !domainsByCode.has(d.code) && domainsByCode.set(d.code, d)); + return Array.from(domainsByCode.values()).sort((a, b) => a.code.localeCompare(b.code)); + }, [domains]); + + 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 b27903f1de..0000000000 --- a/platform.bible-extension/src/components/entry-card.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import type { IEntry, IPartOfSpeech, 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; -} - -interface EntryCardProps { - entry: IEntry; - onClickSemanticDomain?: (domain: ISemanticDomain) => void; -} - -export default function EntryCard({ entry, onClickSemanticDomain }: EntryCardProps): ReactElement { - return ( - - - {Object.keys(entry.citationForm).length - ? JSON.stringify(entry.citationForm) - : JSON.stringify(entry.lexemeForm)} - - -

    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/main.ts b/platform.bible-extension/src/main.ts index 9e4da9a21f..7cf7ed8ab2 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) => { @@ -195,7 +179,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 { @@ -221,9 +205,6 @@ export async function activate(context: ExecutionActivationContext): Promise { return true; } -function launchFwLiteFwLiteWeb(context: ExecutionActivationContext) { +function launchFwLiteWeb(context: ExecutionActivationContext) { const binaryPath = 'fw-lite/FwLiteWeb.exe'; if (context.elevatedPrivileges.createProcess === undefined) { throw new Error('FieldWorks Lite requires createProcess elevated privileges'); 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..93a07df80e 100644 --- a/platform.bible-extension/src/types/fw-lite-extension.d.ts +++ b/platform.bible-extension/src/types/fw-lite-extension.d.ts @@ -17,7 +17,7 @@ declare module 'fw-lite-extension' { export type ProjectSettingKey = import('./enums.ts').ProjectSettingKey; export type WebViewType = import('./enums.ts').WebViewType; - type PartialEntry = Omit, 'senses'> & { + export type PartialEntry = Omit, 'senses'> & { senses?: Partial[]; }; @@ -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; } @@ -79,13 +84,11 @@ declare module 'papi-shared-types' { dictionaryCode: string, ) => 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/types/localized-string-keys.ts b/platform.bible-extension/src/types/localized-string-keys.ts new file mode 100644 index 0000000000..9377d61ca9 --- /dev/null +++ b/platform.bible-extension/src/types/localized-string-keys.ts @@ -0,0 +1,32 @@ +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_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%', + '%fwLiteExtension_entryDisplay_partOfSpeech%', + '%fwLiteExtension_entryDisplay_senses%', + '%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/utils/entry-display-text.ts b/platform.bible-extension/src/utils/entry-display-text.ts new file mode 100644 index 0000000000..fd013db234 --- /dev/null +++ b/platform.bible-extension/src/utils/entry-display-text.ts @@ -0,0 +1,31 @@ +import type { 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, lang = 'en'): string { + return entry.senses.map((s) => senseGlossText(s, lang)).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, lang = 'en'): string { + return sense.definition[lang] || Object.values(sense.definition).join('; '); +} + +export function senseGlossText(sense: ISense, lang = 'en'): string { + return sense.gloss[lang] || Object.values(sense.gloss).join('; '); +} 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 b3d1a8570b..01ea177e73 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, @@ -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 { @@ -79,13 +82,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 +100,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; } 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..d04bf549bc --- /dev/null +++ b/platform.bible-extension/src/utils/use-is-wide-screen.tsx @@ -0,0 +1,21 @@ +import { useEffect, useState } from 'react'; + +// Copied from paranext-core/extensions/src/platform-lexical-tools/src/utils/dictionary.utils.ts +export default 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/add-word.web-view.tsx b/platform.bible-extension/src/web-views/add-word.web-view.tsx index ebf5bd7c4f..ab113ae4b9 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,17 @@ 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%']}`, 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,20 +53,19 @@ 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...

    } {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 5660b2584d..f84a8048a9 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,11 +8,15 @@ import type { PartialEntry, WordWebViewOptions, } from 'fw-lite-extension'; -import { Card, SearchBar } from 'platform-bible-react'; +import { Network } from 'lucide-react'; +import { Label, SearchBar } from 'platform-bible-react'; import { debounce } from 'platform-bible-utils'; -import { type ReactElement, useCallback, useEffect, useMemo, useState } from 'react'; -import AddNewEntry from '../components/add-new-entry'; -import EntryCard from '../components/entry-card'; +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'; +import { domainText } from '../utils/entry-display-text'; /* eslint-disable react-hooks/rules-of-hooks */ @@ -21,6 +26,8 @@ globalThis.webViewComponent = function fwLiteFindRelatedWords({ vernacularLanguage, word, }: WordWebViewOptions) { + const [localizedStrings] = useLocalizedStrings(LOCALIZED_STRING_KEYS); + const [fwLiteNetworkObject, setFwLiteNetworkObject] = useState< NetworkObject | undefined >(); @@ -38,8 +45,10 @@ 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%']}`, e), + ); + }, [localizedStrings]); useEffect(() => { setSelectedDomain(undefined); @@ -51,8 +60,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; } @@ -72,14 +82,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; } @@ -89,7 +100,7 @@ globalThis.webViewComponent = function fwLiteFindRelatedWords({ setIsFetching(false); setRelatedEntries(entries ?? []); }, - [fwLiteNetworkObject, projectId], + [fwLiteNetworkObject, localizedStrings, projectId], ); useEffect(() => { @@ -109,9 +120,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; } @@ -124,67 +136,74 @@ 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 ( -
    - - - {isFetching &&

    Loading...

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

    No matching entries with a semantic domain.

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

    Select a semantic domain for related words in that domain

    - {matchingEntries.map((e) => ( - - ))} - - )} - - {selectedDomain && relatedEntries && ( - - )} - - {selectedDomain && ( - - )} -
    + +
    +
    + +
    + + {selectedDomain && ( +
    + +
    + )} +
    + + {matchingEntries && !selectedDomain && ( +

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

    + )} + + {selectedDomain && ( +

    + + {domainText(selectedDomain, analysisLanguage)} +

    + )} +
    + } + elementList={ + /* eslint-disable no-nested-ternary */ + !matchingEntries ? undefined : !selectedDomain ? ( + + ) : !relatedEntries?.length ? ( +
    + +
    + ) : ( + + ) + } + 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/find-word.web-view.tsx b/platform.bible-extension/src/web-views/find-word.web-view.tsx index 926b1bed3f..2b502beb9d 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,14 @@ 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 { 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 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 */ @@ -15,6 +18,8 @@ globalThis.webViewComponent = function fwLiteFindWord({ vernacularLanguage, word, }: WordWebViewOptions) { + const [localizedStrings] = useLocalizedStrings(LOCALIZED_STRING_KEYS); + const [matchingEntries, setMatchingEntries] = useState(); const [fwLiteNetworkObject, setFwLiteNetworkObject] = useState< NetworkObject | undefined @@ -30,14 +35,17 @@ 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%']}`, 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'); + const errMissingParam = localizedStrings['%fwLiteExtension_error_missingParam%']; + if (!projectId) logger.warn(`${errMissingParam}projectId`); + if (!fwLiteNetworkObject) logger.warn(`${errMissingParam}fwLiteNetworkObject`); return; } @@ -53,7 +61,7 @@ globalThis.webViewComponent = function fwLiteFindWord({ setIsFetching(false); setMatchingEntries(entries ?? []); }, - [fwLiteNetworkObject, projectId], + [fwLiteNetworkObject, localizedStrings, projectId], ); const debouncedFetchEntries = useMemo(() => debounce(fetchEntries, 500), [fetchEntries]); @@ -69,8 +77,9 @@ 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'); + const errMissingParam = localizedStrings['%fwLiteExtension_error_missingParam%']; + if (!projectId) logger.warn(`${errMissingParam}projectId`); + if (!fwLiteNetworkObject) logger.warn(`${errMissingParam}fwLiteNetworkObject`); return; } @@ -80,28 +89,45 @@ 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], ); return ( -
    - - - {isFetching &&

    Loading...

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

    No matching entries

    } - {matchingEntries?.map((entry) => ( - - ))} + +
    + +
    - -
    +
    + +
    +
    + } + 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..f4b08d4a57 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 '../styles.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,8 +32,8 @@ export const mainWebViewProvider: IWebViewProvider = { allowedFrameSources: ['http://localhost:*'], content: fwMainWindow, iconUrl, - styles: mainStyles, - title: '%fwLiteExtension_browseDictionary_title%', + styles: mainCssStyles, + title: '%fwLiteExtension_webViewTitle_browseDictionary%', }; }, }; @@ -51,7 +52,8 @@ export const addWordWebViewProvider: IWebViewProvider = { ...options, content: fwAddWordWindow, iconUrl, - title: '%fwLiteExtension_addWord_title%', + styles: tailwindCssStyles, + title: '%fwLiteExtension_webViewTitle_addWord%', }; }, }; @@ -70,7 +72,8 @@ export const dictionarySelectWebViewProvider: IWebViewProvider = { ...options, content: fwDictionarySelectWindow, iconUrl, - title: '%fwLiteExtension_selectDictionary_title%', + styles: tailwindCssStyles, + title: '%fwLiteExtension_webViewTitle_selectDictionary%', }; }, }; @@ -89,7 +92,8 @@ export const findWordWebViewProvider: IWebViewProvider = { ...options, content: fwFindWordWindow, iconUrl, - title: '%fwLiteExtension_findWord_title%', + styles: tailwindCssStyles, + title: '%fwLiteExtension_webViewTitle_findWord%', }; }, }; @@ -108,7 +112,8 @@ export const findRelatedWordsWebViewProvider: IWebViewProvider = { ...options, content: fwFindRelatedWordsWindow, iconUrl, - title: '%fwLiteExtension_findRelatedWords_title%', + styles: tailwindCssStyles, + title: '%fwLiteExtension_webViewTitle_findRelatedWords%', }; }, }; 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