FEAT: Moment Plugin Framework & Search Suggestions#192
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a moment plugin framework (including a new public ./plugins entrypoint) and adds first-class search suggestions + facet serialization support for CardsBrowser-driven collection search, including an “empty landing” start mode.
Changes:
- Add moment plugin support via
MomentPlugintypes, configurablemomentPluginsonRootStore, and a dedicated plugins export entry (src/plugins.ts) with Rollup packaging updates. - Implement search suggestions (input + landing), committed-vs-draft query handling, URL facet serialization/deserialization, and empty-landing behavior for API collections.
- Update UI/stores/build tooling: CardsBrowser suggestion UI, new localization strings, MobX action wrapping, deck.gl package split, and Rollup shims for browser bundling.
Reviewed changes
Copilot reviewed 52 out of 54 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/timelineUtils.ts | Adds reusable timeline direction + stable-ish key helpers. |
| src/utils/searchSuggestionUtils.ts | Adds shared key helper for rendering suggestion lists. |
| src/utils/searchFacetUtils.ts | Adds selected-facet deserialization for router/query-string integration. |
| src/utils/searchFacetUtils.test.ts | Tests round-trip selected facet serialization/deserialization. |
| src/utils/momentConfigUtils.ts | Builds moment config map with plugin overrides. |
| src/types.ts | Introduces Moment plugin types/config map and search suggestion/facet query types. |
| src/state/storyStore.ts | Wraps state mutations in runInAction; adds updateOptions. |
| src/state/storiesStore.ts | Uses StoryStore.updateOptions instead of direct mutation. |
| src/state/searchStore.ts | Adds suggestions, committedQuery, empty landing mode, and improved concurrency handling. |
| src/state/searchStore.test.ts | Expands concurrency tests; adds suggestion behavior tests. |
| src/state/rootStore.ts | Accepts momentPlugins; builds config map via utility; renames StoryId action plugin type. |
| src/state/moments/iframeMomentStore.ts | Makes message access tolerant of missing iframe data. |
| src/state/momentStore.ts | Wraps a few mutations (reset, reloadComponent, setMuiTheme) in runInAction. |
| src/state/localeStore.ts | Wraps locale mutation in runInAction. |
| src/state/geoMapStore.ts | Switches to @deck.gl/core imports; wraps several mutations in runInAction. |
| src/state/collectionsStore.ts | Removes async runInAction; updates collection delete to be action-wrapped. |
| src/state/collectionStore.ts | Adds emptyLanding/search default behavior + suggestion config; refactors init/search flows. |
| src/state/collectionStore.test.ts | Tests emptyLanding initial-load deferral and default-query behavior. |
| src/state/avStore.ts | Wraps isPlaying mutation in runInAction. |
| src/plugins.ts | New public plugins entrypoint exports moment components/stores for consumers. |
| src/hooks/useElementIsVisible.ts | Fixes observer lifecycle/cleanup and adds effect deps. |
| src/hooks/useCardsBrowserSearch.ts | New hook managing keyboard nav + animated transitions for suggestions. |
| src/hooks/index.ts | Exports useCardsBrowserSearch. |
| src/configs/momentConfig.ts | Moves MomentConfig types to types.ts; uses shared config map type. |
| src/configs/localizationConfig.ts | Adds new translation keys for search placeholders/suggestion headings. |
| src/components/UI/Timeline/Timeline.tsx | Uses extracted timeline utils and improved event keys. |
| src/components/UI/StoryId/StoryId.types.ts | Renames StoryId action type to StoryIdActionPlugin. |
| src/components/UI/StoryId/StoryId.helpers.ts | Renames StoryId action type to StoryIdActionPlugin. |
| src/components/UI/MenuTooltip/MenuTooltip.tsx | Prevents popover open state when there’s no anchor element. |
| src/components/UI/GeoMap/GeoMap.types.ts | Switches PickingInfo import to @deck.gl/core. |
| src/components/UI/CardsBrowser/CardsBrowserSearch.tsx | Adds input/landing suggestion UI + inline loader; wires new hook. |
| src/components/UI/CardsBrowser/CardsBrowserLandingSuggestions.tsx | New landing suggestions component (chips). |
| src/components/UI/CardsBrowser/CardsBrowserInputSuggestions.tsx | New input suggestions overlay (keyboard + hover). |
| src/components/UI/CardsBrowser/CardsBrowser.types.ts | Adds suggestion-related prop types; adds isLoading prop to search. |
| src/components/UI/CardsBrowser/CardsBrowser.tsx | Passes loading state into search; adjusts loader behavior. |
| src/components/UI/CardsBrowser/CardsBrowser.styles.ts | Adds styles/animations for suggestion overlays and landing chips. |
| src/components/StoryMoment/StoryMoment.tsx | Supports plugin-provided moment components (string lazy import vs direct component). |
| src/components/StoriesAPICollection/StoriesAPICollection.tsx | Reads/writes facets query param; passes default facets/query into CollectionStore. |
| src/components/Moments/IFrameMoment/index.ts | Adds index barrel export for plugin consumption. |
| src/components/Moments/CardsBaseMoment/index.ts | Adds index barrel export for plugin consumption. |
| src/components/Moments/BaseMoment/index.ts | Adds index barrel export for plugin consumption. |
| src/components/Moments/AVBaseMoment/index.ts | Adds index barrel export for plugin consumption. |
| src/components/Collections/Collections.stories.tsx | Refactors story to build CollectionStores with a real RootStore. |
| src/components/CollectionStoriesList/CollectionStoriesList.tsx | Hides stories list when collection indicates it should be hidden. |
| src/components/CollectionSection/CollectionSection.tsx | Applies new badge style override via sx. |
| src/components/CollectionSection/CollectionSection.styles.ts | Adds badge styling to inherit font size. |
| src/components/CollectionSection/CollectionSection.stories.tsx | Adds Storybook coverage for CollectionSection with a real store. |
| src/components/CollectionLayout/CollectionLayout.tsx | Avoids rendering single-story card when story is missing; uses new header gating flag. |
| src/components/Collection/Collection.stories.tsx | Adds single-story “tool layout” story to validate list hiding. |
| rollup.config.mjs | Adds loaders.gl shims, better externals, dynamic import vars support, and plugins build output + dts. |
| package.json | Updates deps (split deck.gl packages, bump maplibre, add ./plugins export, version bump). |
| configs/loadersGlNodeShimPlugin.mjs | New Rollup plugin to shim node built-ins leaking from loaders.gl worker-utils. |
| .gitignore | Ignores .npmrc. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| '@keyframes cardsBrowserSuggestionFadeIn': { | ||
| from: { | ||
| opacity: 0, | ||
| transform: 'translateY(6px)', | ||
| }, |
There was a problem hiding this comment.
The @keyframes definitions are embedded inside searchSuggestionOption(...), which is called for every rendered suggestion option. This can lead to duplicate keyframe injection and unnecessary style churn. Consider moving these keyframes to a static/top-level style definition (or a shared @global/keyframes() helper) and referencing them from each option.
| <SearchInput | ||
| fullWidth | ||
| onBlur={handleSearchInputBlur} | ||
| onFocus={handleSearchInputFocus} | ||
| onKeyDown={handleSearchInputKeyDown} | ||
| placeholder={search.placeholder} | ||
| search={search} | ||
| sx={styles.searchInput} | ||
| variant="standard" | ||
| /> |
There was a problem hiding this comment.
SearchInput defines its own Enter/Shift+Enter handling (Shift+Enter bypasses cache). Passing onKeyDown={handleSearchInputKeyDown} here overrides that internal handler (because SearchInput spreads ...textFieldProps after its own props), so Shift+Enter bypass-cache behavior is lost. Consider either updating handleSearchInputKeyDown to preserve the existing behavior (e.g., when no suggestion is selected, call search.submit(false, event.shiftKey)), or adjusting SearchInput to compose/chain its internal onKeyDown with a provided handler instead of being overridden.
| const applySuggestionWithTransition = async ( | ||
| suggestion: SearchSuggestion, | ||
| location: SuggestionLocation, | ||
| ) => { | ||
| if (isTransitioningSuggestion) { | ||
| return; | ||
| } | ||
|
|
||
| const key = getSuggestionKey(location, suggestion); | ||
| setTransitioningSuggestionLocation(location); | ||
| setTransitioningSuggestionKey(key); | ||
|
|
||
| if (location === 'input') { | ||
| // Close the input menu immediately once a selection is made. | ||
| search.setFocused(false); | ||
| setActiveSuggestionIndex(-1); | ||
| } | ||
|
|
||
| if (location === 'landing') { | ||
| setTransitionLandingSuggestions([...search.landingSuggestions]); | ||
| } | ||
|
|
||
| setTransitionPhase('itemFade'); | ||
| await delay(ITEM_FADE_DELAY_MS); | ||
|
|
||
| // Phase 2: search fires, loader appears while section stays visible. | ||
| setTransitionPhase('searchWait'); | ||
|
|
||
| try { | ||
| await search.applySuggestion(suggestion); | ||
|
|
||
| // Phase 3: search is done; section fades out before results render. | ||
| setTransitionPhase('sectionFade'); | ||
| await delay(SECTION_FADE_DELAY_MS); | ||
| } finally { | ||
| setTransitionPhase(null); | ||
| setTransitioningSuggestionLocation(null); | ||
| setTransitioningSuggestionKey(null); | ||
| setTransitionLandingSuggestions([]); | ||
| } |
There was a problem hiding this comment.
applySuggestionWithTransition awaits delay(...) and then calls multiple setState calls. If the component using this hook unmounts during the transition/search, these pending timeouts/promises can still resolve and trigger state updates on an unmounted component. Consider adding a cancellation/"isMounted" guard (or clearing timeouts via refs) so the transition aborts cleanly on unmount.
| reset() { | ||
| this.collection = deepCopy(this.initialCollection); | ||
| this.init(); | ||
| this.isEdited = false; | ||
| runInAction(() => { | ||
| this.collection = deepCopy(this.initialCollection); | ||
| this.init(); | ||
| this.isEdited = false; | ||
| }); |
There was a problem hiding this comment.
reset() calls the async this.init() inside runInAction. Invoking async work inside runInAction is fragile (the action is meant to be synchronous) and can make MobX action boundaries harder to reason about. Consider limiting the action to synchronous state updates (collection/isEdited) and then calling this.init() outside the runInAction block (and optionally handling the returned promise).
No description provided.