Core: Split bundled-extension web views into presentational components + interactive Storybook stories#2321
Core: Split bundled-extension web views into presentational components + interactive Storybook stories#2321Sebastian-ubs wants to merge 23 commits into
Conversation
efc9c24 to
eb3b76e
Compare
…ories
Establish the webview-split pattern: each production webview becomes a thin
data-loader plus a same-named presentational component covered by an interactive
Storybook story.
- Add shared .storybook/story.utils.ts:
- alertCommand(command, args): happy-path callbacks announce the real command
name + arguments via alert()
- rejectingMock(businessError): error-handling stories simulate a business
failure (not "backend unavailable")
- home: surface action errors in a destructive Alert (await onSendReceiveProject);
upgrade story alerts to state real commands + args; add SendReceiveError story
- get-resources: extract GetResources presentational component (filters stay
controlled by the webview to preserve useWebViewState persistence; text/sort
internal); webview becomes a data-loader. Stories: Default, Loading, Empty,
ResourcesError, InstallError, RemoveError
- new-tab: add story rendering Home in new-tab config (no redundant component)
Verified: npm run lint:scripts, typecheck, and storybook:build all clean.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Apply the webview-split pattern to platform-scripture-editor's model text panel: the webview becomes a thin data-loader that resolves PAPI data and computes a `status`, and a new same-named presentational ModelTextPanel component renders each state from props. - Extract ModelTextPanel (pure, props-driven): renders the noProject / loadingModelTexts / noModelText / unknownResource / installing / loadingText / active states. Owns the editor ref, options, and read-only setUsj effect. - model-text-panel.web-view.tsx: keep all PAPI data resolution; collapse the render branches into a single `status` value passed to the component. - Add model-text-panel.stories.tsx: one story per state, with the active state rendering the read-only Scripture editor on a sample USJ. Verified: typecheck, extensions lint, and storybook:build all clean. Co-Authored-By: Claude Code <noreply@anthropic.com>
Apply the webview-split pattern to legacy-comment-manager's comment list: the web view stays a thin data-loader (PAPI threads, registration, PDP callbacks) and a new CommentListPanel presentational component renders the toolbar + list from props. - Extract CommentListPanel (pure, props-driven): filter toolbar + loading skeletons + empty states + the platform-bible-react CommentList. Forwarded prop types are derived from CommentList so they stay in sync. The comment/ scope filter constants, types, and guards move here and are re-exported. - comment-list.web-view.tsx: keep all PAPI wiring; render <CommentListPanel> with the resolved threads, controlled filters, and callbacks. - Add comment-list.stories.tsx: Loading, Populated, Empty, and EmptyFiltered states with sample threads. Verified: typecheck, extensions lint, and storybook:build all clean. Co-Authored-By: Claude Code <noreply@anthropic.com>
Apply the webview-split pattern to paratext-registration's Internet settings: the web view stays the data-loader (load/save settings via PAPI commands, the save-and-restart flow, useWebViewState persistence) and a new InternetSettingsForm presentational component renders the controlled form. - Extract InternetSettingsForm (pure, props-driven): internet-use selector, optional proxy settings card, server selector, success/error alerts, and the save-and-restart button. The server/internet-use/proxy option lists, localize key helpers, and the string-keys list move here and are re-exported. - internet-settings.web-view.tsx: keep all PAPI wiring and derived disabled/ unsaved-changes logic; render <InternetSettingsForm> with the values. - Add internet-settings.stories.tsx: Default, ProxyOnly, Restarting, SaveError. Verified: typecheck, extensions lint, and storybook:build all clean. Co-Authored-By: Claude Code <noreply@anthropic.com>
Separate paratext-registration's RegistrationForm into a data/logic container and a pure presentational view, so the form can be exercised in Storybook. The form's heavy logic (debounced backend validation, save + restart, and the webview-only updateWebViewDefinition title side-effect) is unsafe to move into the web view wholesale, so the container keeps it and a new RegistrationFormView holds only the JSX, fully controlled via props. - Extract RegistrationFormView (pure, props-driven): editable/read-only name + code fields, validation hint, success/error alerts, and the change / save-and-restart buttons. The code-format regex + length constants used by the inputs move here and are re-exported for the container's validation. - registration-form.component.tsx: keep all state, validation, and PAPI calls; render <RegistrationFormView> with the values and handlers. - Add registration-form-view.stories.tsx: InitialRegistration, ExistingRegistration, ValidRegistration, SaveError, Restarting. Verified: typecheck, extensions lint, and storybook:build all clean. Co-Authored-By: Claude Code <noreply@anthropic.com>
This reverts commit 109b404.
The demo scripture-editor stylesheet (loaded in Storybook via ten-layout-shared and the footnote/scripture-editor stories) declared bare `table`, `td`, `td.markercell`, and `rt` element selectors. Once the stylesheet loaded, those rules styled every table on the page — notably giving the Get Resources / Home tables a solid black border after an editor-based story had been viewed. Scope them to the editor's `.usfm` content root so editor tables keep their styling but unrelated UI is unaffected. The upstream editor package ships the same unscoped rules. Co-Authored-By: Claude Code <noreply@anthropic.com>
Storybook opens the first exported story by default, so lead with the data-present state instead of loading/empty: - comment-list: Populated first (was Loading) - registration-form-view: ExistingRegistration first (was the empty InitialRegistration) Loading/empty remain as later stories. Co-Authored-By: Claude Code <noreply@anthropic.com>
…ve picker story Redo the model-text-panel split so the story exercises the real component through the same interface the webview uses, with a thin in-memory backend (per the agreed pattern). The component now owns the orchestration instead of receiving a derived `status`/`usj`. - ModelTextPanel: owns resolution (configured model text → match DBL resource → auto-install → load USJ). Receives raw data as props (effectiveModelTexts, dblResources, adminModelTexts, canWriteProjectSettings, scrRef) and operations as callbacks (installResource, setAdminModelTexts, setUserModelTexts, showResourcePicker, and getResourceChapter — a callback because the resource project to read is resolved inside the component). No @papi imports. - webview: thin data-loader wiring PAPI to those props/callbacks; getResourceChapter and showResourcePicker use the imperative papi.projectDataProviders.get / papi.dialogs.showDialog APIs. - story: thin in-memory CRUD service (resources + admin/user model-text lists); renders the REAL ResourcePickerDialog inline for showResourcePicker; install flips state; settings writes are console-logged. Active story leads; NoModelText is fully interactive (pick → install → render). Verified: typecheck, extensions lint, and storybook:build all clean. Co-Authored-By: Claude Code <noreply@anthropic.com>
…story Apply the webview-split pattern to platform-lexical-tools' dictionary: a new Dictionary presentational component owns the orchestration (scope/search/selection state, text filtering, single-entry-vs-list derivation) and the webview becomes a thin data-loader. - Dictionary component (pure, no @papi): renders the scope/search header, loading/ error/no-results states, and DictionaryEntryDisplay or DictionaryList. Dependent reads are callbacks (getEntries, getFullEntry) run in effects. - Lift the internal useLocalizedStrings out of dictionary-entry-display and dictionary-list into a localizedStrings prop; make dictionary-list-item's @papi/core import a type-only import so the render tree is @papi-free. - webview: thin loader; getEntries/getFullEntry via the imperative papi.dataProviders.get('platformLexicalTools.lexicalReferenceService') API; scrRef from the scroll group. - story: thin in-memory service with seed entries; getEntries filters by scope; occurrence-nav announced via alertCommand. EntryList leads; SingleEntry, Loading, NoResults, DataError follow. Verified: typecheck, extensions lint, and storybook:build all clean. Co-Authored-By: Claude Code <noreply@anthropic.com>
Make the four inventory components (character, marker, punctuation, repeated-words) pure so they render in Storybook, and add interactive stories driven by a thin in-memory service. - Lift each component's internal useLocalizedStrings into a localized-strings prop. Marker also used useProjectData + logger for marker names; lift that to a markerNames prop so the component is fully @papi-free (the webview now loads marker names and passes them down). - inventory.web-view.tsx: resolve each type's table-header strings (and marker names) and pass them into the rendered component; everything else (useInventory, settings reads/writes, occurrence loading) unchanged. - Add inventory.stories.tsx: a thin in-memory CRUD service seeds items + approved/unapproved lists; approve/unapprove move items between lists so the UI reflects the change; occurrences load on selection; valid/invalid persistence is console-logged. Stories for Character (default), RepeatedWords, Markers, plus Loading and Empty. Verified: typecheck, extensions lint, and storybook:build all clean. Co-Authored-By: Claude Code <noreply@anthropic.com>
…tory Extract a presentational ChecksSidePanel component from the checks side panel webview and add an interactive story. The async check-job lifecycle (begin/poll/ stop/abandon, network invalidation, PDP writes, editor navigation) stays in the webview; the component owns the presentational logic and is fed via props. - ChecksSidePanel component (pure, no @papi): config bar (project/scope/check-type filters), the results list of CheckCards, progress/status bar, and empty/loading states. Receives checkResults, checksInfo, projects, filters, and job status as props and allow/deny/navigate/settings as callbacks. - check-card: lift its internal useLocalizedStrings into a localizedStrings prop. - checks-side-panel.utils: move getProjectNames (the only @papi user) out to the webview so the util is @papi-free and importable by the component + story. - webview: thin loader — keeps all PAPI/job/event orchestration; maps results + progress to props and wires callbacks to PDP writes/navigation. - story: thin in-memory service seeds check results; allow/deny mutate them so the cards update; navigation/settings announced via alertCommand; Running shows progress; WriteFailure uses rejectingMock. Populated leads. Verified: typecheck, extensions lint, and storybook:build all clean. Co-Authored-By: Claude Code <noreply@anthropic.com>
Extract a presentational Find component from the find/replace webview and add an interactive story. The find-job lifecycle, replace/revert, version-history commits, and external-change detection stay in the webview; the component is fed via props. - Find component (pure, no @papi): search input + recent searches + scope selector + FindFilters + find/replace mode toggle + replace row + grouped results list + progress/empty states. Owns presentational derivations + keyboard result navigation, calling back for every action. - Make the result children @papi-free: search-result drops its logger (keeps the graceful catch); search-results-in-book takes a getBookUsj callback instead of calling useProjectData, building the UsjReaderWriter from the result. - webview: keeps all orchestration; renders <Find> with the state/callbacks; getBookUsj via the imperative papi.projectDataProviders.get(...).getBookUSJ API. - story: thin in-memory service seeds results across two books with verse context; replace/replace-all/cancel/hide mutate the seed so the UI reflects it; commit/editor-nav announced via alertCommand. Populated leads; ReplaceMode, InProgress, NoResults, Replaced follow. Verified: typecheck, extensions lint, and storybook:build all clean. Co-Authored-By: Claude Code <noreply@anthropic.com>
Upgrade the comment-list story from announce-only callbacks to a thin in-memory CRUD store: add/edit/delete/read-status changes mutate the threads in state so the list reflects them, matching the real app (and the other split stories). Editor navigation stays announced via alertCommand. Verified: typecheck, extensions lint, and storybook:build all clean. Co-Authored-By: Claude Code <noreply@anthropic.com>
The find story imported `Canon` from platform-bible-utils, which only re-exports it as a type — so `Canon` was undefined at runtime and `Canon.allBookIds` threw when the story rendered (typecheck/build passed; only runtime caught it). Import Canon from @sillsdev/scripture, as the webview does. Co-Authored-By: Claude Code <noreply@anthropic.com>
This reverts commit 63e0b05.
…ground Stories now behave like the running app instead of rendering a fixed snapshot: - preview.ts: wrap every story in a full-height bg-background/text-foreground surface so each renders on the app panel color. - get-resources: get/remove transition through the spinner state and flip the resource installed flag; add a 100-languages story. - comment-list: derive the visible threads from the comment/scope filters (mirroring the web view's query); add a cross-chapter thread for the scope demo. - checks-side-panel: derive the results from the selected check types + scope. - find: add a small in-memory search engine over the seed corpus so the term, match-case, word restriction, regex, and scope all re-run the search and update the highlighting; replace/replace-all announce the command, show the replaced state, then commit (the result drops out as a re-find would); Cancel reverts. - inventory: announce the occurrence-row navigation and the approve/unapprove status change via alert. - model-text-panel: import the editor's usj-nodes/nodes-menu CSS and hide the read-only toolbar so the context menu is styled and the stray current-marker label no longer shows above the editor. - registration-form-view story: wire "Change" to enter edit mode, Cancel to revert, and Save to persist back to the read-only view. - Add .storybook/STORYBOOK-INTERACTIVITY.md documenting these conventions. Co-Authored-By: Claude Code <noreply@anthropic.com>
…ntory status alignment Component (app-facing) fixes flagged while reviewing the stories: - internet-settings: make the form scroll so the proxy-settings card no longer overflows the fixed-height panel with no scrollbar, and keep the "Paratext servers" label left-aligned (the shared Grid right-aligns label columns, which pushed this standalone label far right on a wide panel). - registration-form-view: use shadcn's field-validation styling (aria-invalid) on the registration name and code inputs instead of the bespoke invalid border. - inventory status column: center the status buttons in the cell to match the centered column header (they were left-aligned). Co-Authored-By: Claude Code <noreply@anthropic.com>
The registration form's field validation is computed by the RegistrationForm container (showInvalidCode, registrationIsValid, the save-disabled state) and fed to the view as props. The story never reproduced that, so unlike the app on main, typing an invalid code showed no red field or length warning and the save button never gated. The decorator now derives those flags from the entered name/code (mirroring the container): a non-empty code that doesn't match the registration-code format is flagged with the destructive field styling and the length warning, the registration is "valid" only with a name and a well-formed code (success alert + enabled save), and Save is disabled while invalid or unchanged. Co-Authored-By: Claude Code <noreply@anthropic.com>
The previous commit reproduced the field validation in the story, which was the wrong place — UI logic must live in the component, and the story may only stand in for backend behavior. RegistrationFormView now derives its own field-level validation: a debounced malformed-code check (the invalid-code hint) and the save-enabled gating (unsaved + backend-confirmed + not mid-operation). The RegistrationForm container no longer computes/passes showInvalidCode or the save-disabled flag (it only does the backend validation and passes registrationIsValid). The story now mocks just the backend validate command (the sample registration is recognized) and feeds raw data + callbacks — no validation logic. Co-Authored-By: Claude Code <noreply@anthropic.com>
…backend Adds a "Where logic lives" section drawing the line the story must not cross (client-side rules → component; backend results → story mock), clarifies that the "reproduce the derivation" guidance refers to the web view's PAPI query/data layer (not UI logic), and adds a verification step to catch UI logic that snuck into a story. Uses the registration-form validation as the cautionary example. Co-Authored-By: Claude Code <noreply@anthropic.com>
- Wrap Select onValueChange to narrow string to DictionaryScope - Guard against undefined lexical reference data provider in getEntries/getFullEntry Co-Authored-By: Claude Code <noreply@anthropic.com>
eb3b76e to
4d1d0a5
Compare
irahopkinson
left a comment
There was a problem hiding this comment.
@irahopkinson made 1 comment.
Reviewable status: 0 of 47 files reviewed, 1 unresolved discussion (waiting on Sebastian-ubs).
-- commits at r2:
I've been trying to get to review this. Now I finally have and it isn't ready for review. There are failing CI checks that need to be addressed. Also you need to run the Claude /review-paratext command. If you have the github plugin it will automatically update the PR description with its results (if not you will need to do that manually). Note the review command usually catches and fixes CI issues so do that first.
|
@irahopkinson Sebastian's posts (multiple) in Discord do not have a :code-review emoji reaction from you. Are you still hoping/planning to review this? |
Summary
Splits bundled-extension web views into a thin data-loading
*.web-view.tsx(the PAPI container) plus a presentational*.component.tsx, each covered by a Storybook story that behaves like the real app — filter, select, edit, install, save, and fail — driven through the same interface the web view feeds the component, but with a thin in-memory Storybook service standing in for PAPI.This continues the home/get-resources split already on the branch and extends the pattern to every remaining PAPI-coupled web view that can be made Storybook-compatible.
Architecture (per feature)
<name>.component.tsx— presentational, owns its orchestration, no@papi. Props = raw data +localizedStrings+ operation callbacks (on*/handle*,get*for dependent reads,show*for in-app sub-UIs).<name>.web-view.tsx— thin container: PAPI hooks / commands / project data providers → the component's props. Keeps web-view-only primitives (useWebViewState, scroll-group scrRef, controllers,updateWebViewDefinition).<name>.stories.tsx— container backed by a thin in-memory CRUD store (seeduseState+ mutating callbacks), real localized strings viagetLocalizedStrings, and real in-app sub-components wired inline (e.g. the resource picker dialog).Story conventions: save settings →
console.log; opens a completely different UI →alertCommand; calls another in-app UI → wire the real component; business-level failure →rejectingMock(only failures the web view already produces). Default story leads with the populated/data-present state.To make components Storybook-compatible, internal
@papiusage (useLocalizedStrings,logger, data hooks) was lifted up to the web view so the component is pure.Features split
ResourcePickerDialogand a thin install/settings store.check-cardlocalization; madechecks-side-panel.utils.ts@papi-free; allow/deny mutate the store.SearchResultlogger, madeSearchResultsInBookpure (USJ via callback); story drives a seed result set.Fixes
usj-nodes.cssbaretable/td/rtselectors to.usfmto stop a global table-border style leak that appeared after viewing an editor story.Canon(a runtime value) from@sillsdev/scripturerather thanplatform-bible-utils(which only re-exports it as a type).Verification
tsc) and ESLint clean on changed files.npm run storybook:buildsucceeds — the real gate confirming components compile without@papi.Deferred
The main
platform-scripture-editor.web-view.tsx(~1900 lines) is a follow-up.🤖 Generated with Claude Code
This change is