From d489467005b8fa348eed3d3fbbb9133d06570f8a Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 30 Jun 2026 11:47:30 -0400 Subject: [PATCH 01/10] Add createProject to FwLiteApi, fix empty-body fetch handling `fetchUrl` now reads the response as text before conditionally parsing JSON, so endpoints that return an empty 200 (like DELETE entry and the new project/create route) no longer throw a SyntaxError. `FwLiteApi.createProject` wraps `POST /api/project/create`, which was added in #2281 to resolve #1920. Closes #2061. Co-Authored-By: Claude Opus 4.8 --- platform.bible-extension/src/utils/fw-lite-api.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/platform.bible-extension/src/utils/fw-lite-api.ts b/platform.bible-extension/src/utils/fw-lite-api.ts index d6ca905b17..f42edd0d07 100644 --- a/platform.bible-extension/src/utils/fw-lite-api.ts +++ b/platform.bible-extension/src/utils/fw-lite-api.ts @@ -31,7 +31,9 @@ async function fetchUrl(input: string, init?: RequestInit): Promise { if (!results.ok) { throw new Error(`Failed to fetch: ${results.statusText}`); } - return await results.json(); + const text = await results.text(); + // eslint-disable-next-line no-type-assertion/no-type-assertion + return text ? (JSON.parse(text) as unknown) : undefined; } export function getBrowseUrl(baseUrl: string, lexiconCode: string, entryId?: string): string { @@ -131,6 +133,17 @@ export class FwLiteApi { /* eslint-enable no-type-assertion/no-type-assertion */ + async createProject( + name: string, + code: string, + vernacularWs: string, + analysisWs?: string, + ): Promise { + const params = new URLSearchParams({ name, code, vernacularWs }); + if (analysisWs) params.append('analysisWs', analysisWs); + await this.fetchPath(`project/create?${params.toString()}`, 'POST'); + } + private checkLexiconCode(lexiconCode?: string): LexiconRef { const code = sanitizeUrlComponent(lexiconCode || this.lexiconCode); return { code, type: 'FwData' }; From 833ca0c951af2f939afcab05fcc5934ee96ea7e6 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 30 Jun 2026 15:12:26 -0400 Subject: [PATCH 02/10] Add Create new lexicon option to SelectLexicon WebView Adds an end-to-end flow for creating a new FW Lite CRDT project from within the SelectLexicon dialog: - New `lexicon.createLexicon` PAPI command wrapping FwLiteApi.createProject - New CreateLexicon form component (name, auto-derived code, vernacular WS pre-filled from the Paratext project language, optional analysis WS) - "Create new lexicon" button in LexiconComboBox opens the form - On successful creation the new lexicon is auto-selected and the dialog shows the standard "Lexicon selection saved" confirmation Co-Authored-By: Claude Sonnet 4.6 --- .../contributions/localizedStrings.json | 8 ++ .../src/components/create-lexicon.tsx | 125 ++++++++++++++++++ .../src/components/lexicon-combo-box.tsx | 8 ++ platform.bible-extension/src/main.ts | 14 ++ .../src/types/lexicon.d.ts | 6 + .../src/types/localized-string-keys.ts | 8 ++ .../src/utils/project-manager.ts | 10 +- .../src/web-views/select-lexicon.web-view.tsx | 86 ++++++++++-- 8 files changed, 250 insertions(+), 15 deletions(-) create mode 100644 platform.bible-extension/src/components/create-lexicon.tsx diff --git a/platform.bible-extension/contributions/localizedStrings.json b/platform.bible-extension/contributions/localizedStrings.json index 65febacf79..663a1d8f53 100644 --- a/platform.bible-extension/contributions/localizedStrings.json +++ b/platform.bible-extension/contributions/localizedStrings.json @@ -3,6 +3,14 @@ "localizedStrings": { "en": { "%lexicon_addWord_buttonAdd%": "Add new entry", + "%lexicon_createLexicon_analysisWs%": "Analysis language tag (optional)", + "%lexicon_createLexicon_button%": "Create new lexicon", + "%lexicon_createLexicon_code%": "Code", + "%lexicon_createLexicon_error%": "Error creating lexicon:", + "%lexicon_createLexicon_name%": "Name", + "%lexicon_createLexicon_submit%": "Create", + "%lexicon_createLexicon_title%": "Create new lexicon", + "%lexicon_createLexicon_vernacularWs%": "Vernacular language tag", "%lexicon_addWord_buttonSubmit%": "Submit new entry", "%lexicon_addWord_title%": "Add entry to lexicon", "%lexicon_button_cancel%": "Cancel", diff --git a/platform.bible-extension/src/components/create-lexicon.tsx b/platform.bible-extension/src/components/create-lexicon.tsx new file mode 100644 index 0000000000..a86af9dc51 --- /dev/null +++ b/platform.bible-extension/src/components/create-lexicon.tsx @@ -0,0 +1,125 @@ +import { logger } from '@papi/frontend'; +import { useLocalizedStrings } from '@papi/frontend/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'; + +const CODE_PATTERN = /^[a-z\d][a-z\d-]*$/; + +function deriveCode(name: string): string { + return name + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^a-z0-9-]/g, '') + .replace(/-+/g, '-') + .replace(/^[^a-z0-9]+/, '') + .replace(/[^a-z0-9]+$/, ''); +} + +interface CreateLexiconProps { + createLexicon: ( + name: string, + code: string, + vernacularWs: string, + analysisWs?: string, + ) => Promise; + defaultVernacularWs?: string; + onCancel: () => void; + onCreated: (code: string) => Promise; +} + +/** A form for creating a new FW Lite CRDT project from the blank template. */ +export default function CreateLexicon({ + createLexicon, + defaultVernacularWs, + onCancel, + onCreated, +}: CreateLexiconProps): ReactElement { + const [localizedStrings] = useLocalizedStrings(LOCALIZED_STRING_KEYS); + + const [name, setName] = useState(''); + const [code, setCode] = useState(''); + const [codeEdited, setCodeEdited] = useState(false); + const [vernacularWs, setVernacularWs] = useState(defaultVernacularWs ?? ''); + const [analysisWs, setAnalysisWs] = useState(''); + const [creating, setCreating] = useState(false); + + useEffect(() => { + if (!codeEdited) setCode(deriveCode(name)); + }, [codeEdited, name]); + + const isValid = !!(name.trim() && CODE_PATTERN.test(code) && vernacularWs.trim()); + + const handleSubmit = useCallback(async () => { + if (!isValid) return; + setCreating(true); + try { + await createLexicon(name.trim(), code, vernacularWs.trim(), analysisWs.trim() || undefined); + await onCreated(code); + } catch (e) { + logger.error(localizedStrings['%lexicon_createLexicon_error%'], JSON.stringify(e)); + } finally { + setCreating(false); + } + }, [analysisWs, code, createLexicon, isValid, localizedStrings, name, onCreated, vernacularWs]); + + return ( +
+

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

+ +
+ + setName(e.target.value)} value={name} /> +
+ +
+ + { + setCode(e.target.value); + setCodeEdited(true); + }} + value={code} + /> +
+ +
+ + setVernacularWs(e.target.value)} + value={vernacularWs} + /> +
+ +
+ + setAnalysisWs(e.target.value)} + value={analysisWs} + /> +
+ +
+ + +
+
+ ); +} diff --git a/platform.bible-extension/src/components/lexicon-combo-box.tsx b/platform.bible-extension/src/components/lexicon-combo-box.tsx index cc8ce37b52..f13f146c37 100644 --- a/platform.bible-extension/src/components/lexicon-combo-box.tsx +++ b/platform.bible-extension/src/components/lexicon-combo-box.tsx @@ -8,12 +8,14 @@ import { LOCALIZED_STRING_KEYS } from '../types/localized-string-keys'; /** Props for the LexiconComboBox component */ interface LexiconComboBoxProps { lexicons?: IProjectModel[]; + onCreateNew?: () => void; selectLexicon: (lexiconCode: string) => Promise; } /** A combo-box for selecting a lexicon for a project. */ export default function LexiconComboBox({ lexicons, + onCreateNew, selectLexicon, }: LexiconComboBoxProps): ReactElement { const [localizedStrings] = useLocalizedStrings(LOCALIZED_STRING_KEYS); @@ -85,6 +87,12 @@ export default function LexiconComboBox({ )} + + {!!onCreateNew && ( + + )} ); } diff --git a/platform.bible-extension/src/main.ts b/platform.bible-extension/src/main.ts index 4fb6ed70ac..2f69df7eae 100644 --- a/platform.bible-extension/src/main.ts +++ b/platform.bible-extension/src/main.ts @@ -197,6 +197,19 @@ export async function activate(context: ExecutionActivationContext): Promise { + try { + await fwLiteApi.createProject(name, code, vernacularWs, analysisWs); + return { success: true }; + } catch (e) { + logger.error('Error creating lexicon:', JSON.stringify(e)); + return { success: false }; + } + }, + ); + const lexiconsCommandPromise = papi.commands.registerCommand( 'lexicon.lexicons', async (projectId?: string) => { @@ -224,6 +237,7 @@ export async function activate(context: ExecutionActivationContext): Promise Promise; + 'lexicon.createLexicon': ( + name: string, + code: string, + vernacularWs: string, + analysisWs?: string, + ) => Promise; 'lexicon.browseLexicon': (webViewId: string) => Promise; 'lexicon.displayEntry': (projectId: string, entryId: string) => Promise; 'lexicon.findEntry': (webViewId: string, entry: string) => Promise; diff --git a/platform.bible-extension/src/types/localized-string-keys.ts b/platform.bible-extension/src/types/localized-string-keys.ts index 89d3043849..d4f054b581 100644 --- a/platform.bible-extension/src/types/localized-string-keys.ts +++ b/platform.bible-extension/src/types/localized-string-keys.ts @@ -2,6 +2,14 @@ import { LocalizeKey } from 'platform-bible-utils'; export const LOCALIZED_STRING_KEYS: LocalizeKey[] = [ '%lexicon_addWord_buttonAdd%', + '%lexicon_createLexicon_analysisWs%', + '%lexicon_createLexicon_button%', + '%lexicon_createLexicon_code%', + '%lexicon_createLexicon_error%', + '%lexicon_createLexicon_name%', + '%lexicon_createLexicon_submit%', + '%lexicon_createLexicon_title%', + '%lexicon_createLexicon_vernacularWs%', '%lexicon_addWord_buttonSubmit%', '%lexicon_addWord_title%', '%lexicon_button_cancel%', diff --git a/platform.bible-extension/src/utils/project-manager.ts b/platform.bible-extension/src/utils/project-manager.ts index dff0d463e9..51c15edd0b 100644 --- a/platform.bible-extension/src/utils/project-manager.ts +++ b/platform.bible-extension/src/utils/project-manager.ts @@ -41,10 +41,12 @@ export class ProjectManager { } logger.info(`Lexicon not yet selected for project '${nameOrId}'`); - await this.openWebView(WebViewType.SelectLexicon, { - floatSize: { height: 500, width: 400 }, - type: 'float', - }); + const vernacularLanguage = await this.getLanguageTag(); + await this.openWebView( + WebViewType.SelectLexicon, + { floatSize: { height: 500, width: 400 }, type: 'float' }, + { vernacularLanguage }, + ); } async setLexiconCode(lexiconCode: string): Promise { diff --git a/platform.bible-extension/src/web-views/select-lexicon.web-view.tsx b/platform.bible-extension/src/web-views/select-lexicon.web-view.tsx index 3f0d1db5a3..f3d4468aa1 100644 --- a/platform.bible-extension/src/web-views/select-lexicon.web-view.tsx +++ b/platform.bible-extension/src/web-views/select-lexicon.web-view.tsx @@ -1,11 +1,31 @@ -import type { WebViewProps } from '@papi/core'; import { commands, logger } from '@papi/frontend'; -import type { IProjectModel } from 'lexicon'; +import { useLocalizedStrings } from '@papi/frontend/react'; +import type { IProjectModel, LexiconWebViewProps } from 'lexicon'; import { useCallback, useEffect, useState } from 'react'; +import CreateLexicon from '../components/create-lexicon'; import LexiconComboBox from '../components/lexicon-combo-box'; +import { LOCALIZED_STRING_KEYS } from '../types/localized-string-keys'; -globalThis.webViewComponent = function LexiconSelect({ projectId }: WebViewProps) { +globalThis.webViewComponent = function LexiconSelect({ + projectId, + vernacularLanguage, +}: LexiconWebViewProps) { + const [localizedStrings] = useLocalizedStrings(LOCALIZED_STRING_KEYS); const [lexicons, setLexicons] = useState(); + const [showCreate, setShowCreate] = useState(false); + const [done, setDone] = useState(false); + + const fetchLexicons = useCallback(() => { + commands + .sendCommand('lexicon.lexicons', projectId) + .then(setLexicons) + .catch((e) => logger.error('Error fetching lexicons:', JSON.stringify(e))); + }, [projectId]); + + useEffect(() => { + logger.info(`This WebView was opened for project '${projectId}'`); + fetchLexicons(); + }, [fetchLexicons, projectId]); const selectLexicon = useCallback( async (code: string): Promise => { @@ -14,13 +34,57 @@ globalThis.webViewComponent = function LexiconSelect({ projectId }: WebViewProps [projectId], ); - useEffect(() => { - logger.info(`This WebView was opened for project '${projectId}'`); - commands - .sendCommand('lexicon.lexicons', projectId) - .then(setLexicons) - .catch((e) => logger.error('Error fetching lexicons:', JSON.stringify(e))); - }, [projectId]); + const createLexicon = useCallback( + async ( + name: string, + code: string, + vernacularWs: string, + analysisWs?: string, + ): Promise => { + const result = await commands.sendCommand( + 'lexicon.createLexicon', + name, + code, + vernacularWs, + analysisWs, + ); + if (!result?.success) throw new Error('Failed to create lexicon'); + }, + [], + ); + + const onCreated = useCallback( + async (code: string): Promise => { + await selectLexicon(code); + setDone(true); + }, + [selectLexicon], + ); - return ; + if (done) { + return ( +

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

+ ); + } + + if (showCreate) { + return ( + setShowCreate(false)} + onCreated={onCreated} + /> + ); + } + + return ( + setShowCreate(true)} + selectLexicon={selectLexicon} + /> + ); }; From 323c3f90e0b4dbc2c730e48899ab1ccca8a7fc2a Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 30 Jun 2026 15:39:58 -0400 Subject: [PATCH 03/10] Address review: fix CRDT type routing, error bodies, tag validation, key order - fw-lite-api: add static projectTypeByCode cache (shared across FwLiteApi instances including EntryService) so CRDT projects created via createProject route through mini-lcm/Harmony/... instead of mini-lcm/FwData/... getProjects() populates the cache; createProject() seeds 'Harmony' immediately so the writingSystems call in selectLexicon succeeds without a re-fetch - fw-lite-api: include response body text in error messages so backend validation failures (duplicate code, invalid lang tag, etc.) reach the logger - create-lexicon: validate vernacular and analysis language tags with Intl.getCanonicalLocales() before enabling the submit button - localized-string-keys / localizedStrings.json: restore alphabetical ordering Co-Authored-By: Claude Sonnet 4.6 --- .../contributions/localizedStrings.json | 6 +++--- .../src/components/create-lexicon.tsx | 15 ++++++++++++++- .../src/types/localized-string-keys.ts | 6 +++--- .../src/utils/fw-lite-api.ts | 19 ++++++++++++++----- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/platform.bible-extension/contributions/localizedStrings.json b/platform.bible-extension/contributions/localizedStrings.json index 663a1d8f53..c41b10f3e3 100644 --- a/platform.bible-extension/contributions/localizedStrings.json +++ b/platform.bible-extension/contributions/localizedStrings.json @@ -3,6 +3,9 @@ "localizedStrings": { "en": { "%lexicon_addWord_buttonAdd%": "Add new entry", + "%lexicon_addWord_buttonSubmit%": "Submit new entry", + "%lexicon_addWord_title%": "Add entry to lexicon", + "%lexicon_button_cancel%": "Cancel", "%lexicon_createLexicon_analysisWs%": "Analysis language tag (optional)", "%lexicon_createLexicon_button%": "Create new lexicon", "%lexicon_createLexicon_code%": "Code", @@ -11,9 +14,6 @@ "%lexicon_createLexicon_submit%": "Create", "%lexicon_createLexicon_title%": "Create new lexicon", "%lexicon_createLexicon_vernacularWs%": "Vernacular language tag", - "%lexicon_addWord_buttonSubmit%": "Submit new entry", - "%lexicon_addWord_title%": "Add entry to lexicon", - "%lexicon_button_cancel%": "Cancel", "%lexicon_entryDisplay_definition%": "Definition", "%lexicon_entryDisplay_gloss%": "Gloss", "%lexicon_entryDisplay_headword%": "Headword", diff --git a/platform.bible-extension/src/components/create-lexicon.tsx b/platform.bible-extension/src/components/create-lexicon.tsx index a86af9dc51..ef38602ea4 100644 --- a/platform.bible-extension/src/components/create-lexicon.tsx +++ b/platform.bible-extension/src/components/create-lexicon.tsx @@ -6,6 +6,14 @@ import { LOCALIZED_STRING_KEYS } from '../types/localized-string-keys'; const CODE_PATTERN = /^[a-z\d][a-z\d-]*$/; +function isValidLangTag(tag: string): boolean { + try { + return Intl.getCanonicalLocales(tag).length === 1; + } catch { + return false; + } +} + function deriveCode(name: string): string { return name .toLowerCase() @@ -48,7 +56,12 @@ export default function CreateLexicon({ if (!codeEdited) setCode(deriveCode(name)); }, [codeEdited, name]); - const isValid = !!(name.trim() && CODE_PATTERN.test(code) && vernacularWs.trim()); + const isValid = !!( + name.trim() && + CODE_PATTERN.test(code) && + isValidLangTag(vernacularWs.trim()) && + (!analysisWs.trim() || isValidLangTag(analysisWs.trim())) + ); const handleSubmit = useCallback(async () => { if (!isValid) return; diff --git a/platform.bible-extension/src/types/localized-string-keys.ts b/platform.bible-extension/src/types/localized-string-keys.ts index d4f054b581..b21aa8375b 100644 --- a/platform.bible-extension/src/types/localized-string-keys.ts +++ b/platform.bible-extension/src/types/localized-string-keys.ts @@ -2,6 +2,9 @@ import { LocalizeKey } from 'platform-bible-utils'; export const LOCALIZED_STRING_KEYS: LocalizeKey[] = [ '%lexicon_addWord_buttonAdd%', + '%lexicon_addWord_buttonSubmit%', + '%lexicon_addWord_title%', + '%lexicon_button_cancel%', '%lexicon_createLexicon_analysisWs%', '%lexicon_createLexicon_button%', '%lexicon_createLexicon_code%', @@ -10,9 +13,6 @@ export const LOCALIZED_STRING_KEYS: LocalizeKey[] = [ '%lexicon_createLexicon_submit%', '%lexicon_createLexicon_title%', '%lexicon_createLexicon_vernacularWs%', - '%lexicon_addWord_buttonSubmit%', - '%lexicon_addWord_title%', - '%lexicon_button_cancel%', '%lexicon_entryList_backToList%', '%lexicon_entryList_loading%', '%lexicon_entryList_noResults%', diff --git a/platform.bible-extension/src/utils/fw-lite-api.ts b/platform.bible-extension/src/utils/fw-lite-api.ts index f42edd0d07..c09ff68937 100644 --- a/platform.bible-extension/src/utils/fw-lite-api.ts +++ b/platform.bible-extension/src/utils/fw-lite-api.ts @@ -29,7 +29,8 @@ async function fetchUrl(input: string, init?: RequestInit): Promise { } const results = await papi.fetch(input, init); if (!results.ok) { - throw new Error(`Failed to fetch: ${results.statusText}`); + const errorBody = await results.text(); + throw new Error(errorBody || `Failed to fetch: ${results.statusText}`); } const text = await results.text(); // eslint-disable-next-line no-type-assertion/no-type-assertion @@ -43,6 +44,9 @@ export function getBrowseUrl(baseUrl: string, lexiconCode: string, entryId?: str } export class FwLiteApi { + // Shared across all instances (EntryService, main) — all talk to the same backend process. + private static readonly projectTypeByCode = new Map(); + private readonly baseUrl: string; private lexiconCode?: string; constructor(baseUrl: string, lexiconCode?: string) { @@ -98,11 +102,13 @@ export class FwLiteApi { } async getProjects(): Promise { - return (await this.fetchPath('localProjects')) as IProjectModel[]; + const projects = (await this.fetchPath('localProjects')) as IProjectModel[]; + projects.forEach((p) => FwLiteApi.projectTypeByCode.set(p.code, p.crdt ? 'Harmony' : 'FwData')); + return projects; } async getProjectsMatchingLanguage(langTag?: string): Promise { - const projects = (await this.fetchPath('localProjects')) as IProjectModel[]; + const projects = await this.getProjects(); if (!langTag?.trim()) return projects; try { @@ -142,11 +148,14 @@ export class FwLiteApi { const params = new URLSearchParams({ name, code, vernacularWs }); if (analysisWs) params.append('analysisWs', analysisWs); await this.fetchPath(`project/create?${params.toString()}`, 'POST'); + FwLiteApi.projectTypeByCode.set(code, 'Harmony'); } private checkLexiconCode(lexiconCode?: string): LexiconRef { - const code = sanitizeUrlComponent(lexiconCode || this.lexiconCode); - return { code, type: 'FwData' }; + const rawCode = lexiconCode || this.lexiconCode; + const code = sanitizeUrlComponent(rawCode); + const type = FwLiteApi.projectTypeByCode.get(rawCode ?? '') ?? 'FwData'; + return { code, type }; } private getUrl(path: string): string { From af9e9537e1b352e944a953b614e4ced4fd238529 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 30 Jun 2026 16:00:31 -0400 Subject: [PATCH 04/10] Address review: deriveCode parity with backend, resilient onCreated - deriveCode now replaces invalid characters with '-' instead of removing them, matching the backend SanitizeProjectCode behavior - WebView onCreated catches selectLexicon failure: logs the error, refreshes the lexicon list, and falls back to the combo box so the user can manually select the project that was created on disk Co-Authored-By: Claude Sonnet 4.6 --- .../src/components/create-lexicon.tsx | 2 +- .../src/web-views/select-lexicon.web-view.tsx | 14 +++++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/platform.bible-extension/src/components/create-lexicon.tsx b/platform.bible-extension/src/components/create-lexicon.tsx index ef38602ea4..013dec0702 100644 --- a/platform.bible-extension/src/components/create-lexicon.tsx +++ b/platform.bible-extension/src/components/create-lexicon.tsx @@ -18,7 +18,7 @@ function deriveCode(name: string): string { return name .toLowerCase() .replace(/\s+/g, '-') - .replace(/[^a-z0-9-]/g, '') + .replace(/[^a-z0-9-]/g, '-') .replace(/-+/g, '-') .replace(/^[^a-z0-9]+/, '') .replace(/[^a-z0-9]+$/, ''); diff --git a/platform.bible-extension/src/web-views/select-lexicon.web-view.tsx b/platform.bible-extension/src/web-views/select-lexicon.web-view.tsx index f3d4468aa1..0af577e621 100644 --- a/platform.bible-extension/src/web-views/select-lexicon.web-view.tsx +++ b/platform.bible-extension/src/web-views/select-lexicon.web-view.tsx @@ -55,10 +55,18 @@ globalThis.webViewComponent = function LexiconSelect({ const onCreated = useCallback( async (code: string): Promise => { - await selectLexicon(code); - setDone(true); + try { + await selectLexicon(code); + setDone(true); + } catch (e) { + // Lexicon was created but auto-selection failed; go back to the combo box so + // the user can select it manually from the refreshed list. + logger.error('Error auto-selecting created lexicon:', JSON.stringify(e)); + fetchLexicons(); + setShowCreate(false); + } }, - [selectLexicon], + [fetchLexicons, selectLexicon], ); if (done) { From 5bb55e1aceca06c07466f55d345cb4f5ba820a32 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Tue, 30 Jun 2026 16:18:32 -0400 Subject: [PATCH 05/10] Fix getBrowseUrl to use correct path segment for CRDT projects The Svelte frontend routes FwData projects via /paratext/fwdata/{code} and CRDT/Harmony projects via /paratext/project/{code}. The standalone getBrowseUrl always used 'fwdata', producing a broken URL for any Harmony lexicon. Move getBrowseUrl into FwLiteApi as a method so it can consult the projectTypeByCode cache and emit 'project' vs 'fwdata' accordingly. Co-Authored-By: Claude Sonnet 4.6 --- platform.bible-extension/src/main.ts | 6 +++--- platform.bible-extension/src/utils/fw-lite-api.ts | 14 ++++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/platform.bible-extension/src/main.ts b/platform.bible-extension/src/main.ts index 2f69df7eae..ec415b084c 100644 --- a/platform.bible-extension/src/main.ts +++ b/platform.bible-extension/src/main.ts @@ -5,7 +5,7 @@ import type { BrowseWebViewOptions } from 'lexicon'; import { Stream } from 'stream'; import { EntryService } from './services/entry-service'; import { WebViewType } from './types/enums'; -import { FwLiteApi, getBrowseUrl } from './utils/fw-lite-api'; +import { FwLiteApi } from './utils/fw-lite-api'; import { ProjectManagers } from './utils/project-managers'; import * as webViewProviders from './web-views'; @@ -111,7 +111,7 @@ export async function activate(context: ExecutionActivationContext): Promise { return text ? (JSON.parse(text) as unknown) : undefined; } -export function getBrowseUrl(baseUrl: string, lexiconCode: string, entryId?: string): string { - let url = `${baseUrl}/paratext/fwdata/${sanitizeUrlComponent(lexiconCode)}`; - if (entryId) url += `/browse?entryId=${validateUrlComponent(entryId)}&entryOpen=true`; - return url; -} - export class FwLiteApi { // Shared across all instances (EntryService, main) — all talk to the same backend process. private static readonly projectTypeByCode = new Map(); @@ -139,6 +133,14 @@ export class FwLiteApi { /* eslint-enable no-type-assertion/no-type-assertion */ + getBrowseUrl(lexiconCode: string, entryId?: string): string { + const type = FwLiteApi.projectTypeByCode.get(lexiconCode) ?? 'FwData'; + const segment = type === 'Harmony' ? 'project' : 'fwdata'; + let url = `${this.baseUrl}/paratext/${segment}/${sanitizeUrlComponent(lexiconCode)}`; + if (entryId) url += `/browse?entryId=${validateUrlComponent(entryId)}&entryOpen=true`; + return url; + } + async createProject( name: string, code: string, From 77b6250c1bbb0e65eea383063b4f4ccdb9961e24 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Thu, 2 Jul 2026 16:51:26 -0400 Subject: [PATCH 06/10] Add en placeholder to analysis WS input Hints the default analysis writing system on the Create new lexicon form, per review feedback. Co-Authored-By: Claude Opus 4.8 (1M context) --- platform.bible-extension/src/components/create-lexicon.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/platform.bible-extension/src/components/create-lexicon.tsx b/platform.bible-extension/src/components/create-lexicon.tsx index 013dec0702..cdcb03040f 100644 --- a/platform.bible-extension/src/components/create-lexicon.tsx +++ b/platform.bible-extension/src/components/create-lexicon.tsx @@ -121,6 +121,7 @@ export default function CreateLexicon({ setAnalysisWs(e.target.value)} + placeholder="en" value={analysisWs} /> From 1f723bddf20e2db89a96d64c4c307f6256d05144 Mon Sep 17 00:00:00 2001 From: Danny Rorabaugh Date: Fri, 3 Jul 2026 13:43:11 -0400 Subject: [PATCH 07/10] Surface lexicon creation errors in the form Display the backend error message (e.g. duplicate code, invalid language tag) instead of only logging it, per review feedback. Also drop the now- pointless useCallback around handleSubmit. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/components/create-lexicon.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/platform.bible-extension/src/components/create-lexicon.tsx b/platform.bible-extension/src/components/create-lexicon.tsx index cdcb03040f..6036849fa3 100644 --- a/platform.bible-extension/src/components/create-lexicon.tsx +++ b/platform.bible-extension/src/components/create-lexicon.tsx @@ -1,7 +1,7 @@ import { logger } from '@papi/frontend'; import { useLocalizedStrings } from '@papi/frontend/react'; import { Button, Input, Label } from 'platform-bible-react'; -import { type ReactElement, useCallback, useEffect, useState } from 'react'; +import { type ReactElement, useEffect, useState } from 'react'; import { LOCALIZED_STRING_KEYS } from '../types/localized-string-keys'; const CODE_PATTERN = /^[a-z\d][a-z\d-]*$/; @@ -51,6 +51,7 @@ export default function CreateLexicon({ const [vernacularWs, setVernacularWs] = useState(defaultVernacularWs ?? ''); const [analysisWs, setAnalysisWs] = useState(''); const [creating, setCreating] = useState(false); + const [error, setError] = useState(''); useEffect(() => { if (!codeEdited) setCode(deriveCode(name)); @@ -63,18 +64,20 @@ export default function CreateLexicon({ (!analysisWs.trim() || isValidLangTag(analysisWs.trim())) ); - const handleSubmit = useCallback(async () => { + const handleSubmit = async () => { if (!isValid) return; setCreating(true); + setError(''); try { await createLexicon(name.trim(), code, vernacularWs.trim(), analysisWs.trim() || undefined); await onCreated(code); } catch (e) { logger.error(localizedStrings['%lexicon_createLexicon_error%'], JSON.stringify(e)); + setError(e instanceof Error ? e.message : String(e)); } finally { setCreating(false); } - }, [analysisWs, code, createLexicon, isValid, localizedStrings, name, onCreated, vernacularWs]); + }; return (
@@ -126,6 +129,8 @@ export default function CreateLexicon({ />
+ {error &&

{error}

} +