Skip to content

Feature/search keyboard shortcut#4397

Merged
thorsten merged 16 commits into
mainfrom
feature/search-keyboard-shortcut
Jun 20, 2026
Merged

Feature/search keyboard shortcut#4397
thorsten merged 16 commits into
mainfrom
feature/search-keyboard-shortcut

Conversation

@thorsten

@thorsten thorsten commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

Release Notes

  • New Features
    • Added platform-aware search keyboard shortcut (Cmd+K / Ctrl+K) with an inline hint badge
    • Enhanced autocomplete with recent searches and “popular searches” suggestions (including counts)
    • Added a search modal fallback for when the inline search isn’t available
    • Improved suggestion rendering with query highlighting and richer “empty/no results” states
  • Bug Fixes
    • Hardens popular-search loading by safely filtering malformed data and failing gracefully to empty results
  • Documentation
    • Updated coding patterns guidance in AGENTS.md
  • Tests
    • Added/expanded unit and web tests for the new search UI and API behavior

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 78b72fcb-1fb4-4ac4-a1bd-d291c0284b79

📥 Commits

Reviewing files that changed from the base of the PR and between c68ca38 and 79c86c4.

📒 Files selected for processing (9)
  • phpmyfaq/assets/src/api/popularSearches.test.ts
  • phpmyfaq/assets/src/api/popularSearches.ts
  • phpmyfaq/assets/src/search/autocomplete.test.ts
  • phpmyfaq/assets/src/search/autocomplete.ts
  • phpmyfaq/assets/src/search/recentSearches.test.ts
  • phpmyfaq/assets/src/search/recentSearches.ts
  • phpmyfaq/assets/src/search/searchModal.ts
  • phpmyfaq/assets/src/utils/platform.test.ts
  • tests/bootstrap.php
🚧 Files skipped from review as they are similar to previous changes (6)
  • tests/bootstrap.php
  • phpmyfaq/assets/src/search/searchModal.ts
  • phpmyfaq/assets/src/search/recentSearches.test.ts
  • phpmyfaq/assets/src/search/recentSearches.ts
  • phpmyfaq/assets/src/search/autocomplete.test.ts
  • phpmyfaq/assets/src/search/autocomplete.ts

📝 Walkthrough

Walkthrough

Adds a GET /searches/popular PHP API endpoint backed by PopularSearchesController, then builds a full enhanced search UX in TypeScript: recent searches persisted in localStorage, query match highlighting, a platform-aware ⌘K/Ctrl+K shortcut with hint badge, a Bootstrap fallback modal, and an expanded autocomplete with recent/popular/empty states. Translations, SCSS, and a Twig template kbd element complete the feature.

Changes

Enhanced Search Experience

Layer / File(s) Summary
PHP popular searches API, DI wiring, and tests
phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/PopularSearchesController.php, phpmyfaq/src/services.php, tests/phpMyFAQ/Controller/Frontend/Api/PopularSearchesControllerDirectTest.php, tests/phpMyFAQ/Controller/Frontend/Api/PopularSearchesControllerWebTest.php
PopularSearchesController defines GET /searches/popular returning 200/404 JSON, wired into the service container with phpmyfaq.search injected; direct and web tests validate results, empty list, configured count, and zero-fallback.
TypeScript data contracts and popular-searches API client
phpmyfaq/assets/src/interfaces/PopularSearch.ts, phpmyfaq/assets/src/interfaces/suggestionItem.ts, phpmyfaq/assets/src/interfaces/index.ts, phpmyfaq/assets/src/api/popularSearches.ts, phpmyfaq/assets/src/api/index.ts, phpmyfaq/assets/src/api/popularSearches.test.ts
Adds PopularSearch interface and PopularSearchResponse type; updates SuggestionItem with optional fields and SuggestionType union; implements fetchPopularSearches with normalizePopularSearches validation; re-exports through barrel files.
Platform detection utility
phpmyfaq/assets/src/utils/platform.ts, phpmyfaq/assets/src/utils/index.ts, phpmyfaq/assets/src/utils/platform.test.ts
isMacPlatform() reads navigator.userAgentData.platform with fallback; getShortcutHintLabel() returns ⌘ K or Ctrl K; exported via utils barrel and tested across platforms.
Recent searches localStorage persistence
phpmyfaq/assets/src/search/recentSearches.ts, phpmyfaq/assets/src/search/recentSearches.test.ts
getRecentSearches, addRecentSearch (dedup, cap at 5, silent error swallow), and clearRecentSearches backed by localStorage; tests cover ordering, deduplication, cap, blank-term rejection, and throw-resilience.
Query match highlighting utility
phpmyfaq/assets/src/search/highlight.ts, phpmyfaq/assets/src/search/highlight.test.ts
highlightMatch(text, query) returns a DocumentFragment wrapping matched substrings in <strong> using case-insensitive, metacharacter-safe regex; tested for case, no-match, empty query, and HTML escaping.
Enhanced autocomplete with recent/popular/empty states, SCSS, template, and translations
phpmyfaq/assets/src/search/autocomplete.ts, phpmyfaq/assets/src/search/autocomplete.test.ts, phpmyfaq/assets/scss/layout/_autocomplete.scss, phpmyfaq/assets/templates/default/index.twig, phpmyfaq/translations/language_de.php, phpmyfaq/translations/language_en.php
Refactors autocomplete into attachAutocomplete; adds buildEmptyStateItems (recent+popular), renderItem (icons/badges/highlighting for all suggestion types), normalized query handling; includes hint-label SCSS, modal shadow override, kbd element, and EN/DE translation strings.
Bootstrap search modal singleton
phpmyfaq/assets/src/search/searchModal.ts, phpmyfaq/assets/src/search/searchModal.test.ts
openSearchModal() lazily builds modal DOM, attaches autocomplete, focuses input on shown.bs.modal, and holds a singleton Modal instance; tests verify DOM creation, autocomplete wiring, show call, and reuse.
Cmd+K shortcut and hint badge
phpmyfaq/assets/src/search/searchShortcut.ts, phpmyfaq/assets/src/search/searchShortcut.test.ts, phpmyfaq/assets/src/search/searchShortcutBadge.ts, phpmyfaq/assets/src/search/searchShortcutBadge.test.ts
initSearchShortcut registers a guarded keydown listener for Cmd+K/Ctrl+K (focus inline input or open modal) and Escape (blur); initSearchShortcutBadge sets badge text and toggles d-none on focus/blur/input; both fully tested.
Frontend entry point wiring and barrel exports
phpmyfaq/assets/src/frontend.ts, phpmyfaq/assets/src/frontend.test.ts, phpmyfaq/assets/src/search/index.ts
frontend.ts calls initSearchShortcut() and initSearchShortcutBadge() on DOMContentLoaded; search/index.ts adds barrel re-exports for searchModal, searchShortcut, and searchShortcutBadge; test mocks updated.

Supporting Changes

Layer / File(s) Summary
AGENTS.md coding patterns
AGENTS.md
Adds a "Coding Patterns" section with bullet guidelines and a PHP subsection on enums, constructor invariants, readonly value objects, and null reduction.
PHPUnit bootstrap path validation
tests/bootstrap.php
canReusePreparedTestDatabase now validates the stored SQLite DB['server'] path against the current $databasePath before reusing the prepared database.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant Frontend as frontend.ts (DOMContentLoaded)
  participant initSearchShortcut
  participant initSearchShortcutBadge
  participant attachAutocomplete
  participant openSearchModal

  Frontend->>initSearchShortcut: init()
  Frontend->>initSearchShortcutBadge: init()
  Frontend->>attachAutocomplete: handleAutoComplete() → attachAutocomplete(input)

  User->>initSearchShortcut: keydown Cmd+K
  alt inline input exists
    initSearchShortcut->>attachAutocomplete: input.focus() / scrollIntoView
  else no inline input
    initSearchShortcut->>openSearchModal: openSearchModal()
    openSearchModal->>attachAutocomplete: attachAutocomplete(modalInput)
  end

  User->>attachAutocomplete: types query
  rect rgba(59, 130, 246, 0.5)
    note over attachAutocomplete: query non-empty
    attachAutocomplete->>attachAutocomplete: fetchAutoCompleteData(query)
  end
  rect rgba(16, 185, 129, 0.5)
    note over attachAutocomplete: query empty
    attachAutocomplete->>attachAutocomplete: buildEmptyStateItems() → recent + popular
  end
  attachAutocomplete-->>User: rendered suggestion list

  User->>attachAutocomplete: selects item
  attachAutocomplete->>attachAutocomplete: addRecentSearch(term)
  attachAutocomplete-->>User: navigate to item.url
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 A rabbit tapped ⌘ K with glee,
And up popped searches — one, two, three!
Recent hops and popular trails,
Highlighted terms, no empty wails.
The shortcut badge now winks just right —
hop hop hop — what a search delight! 🔍

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary feature being added: a keyboard shortcut for search functionality that spans frontend and backend.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/search-keyboard-shortcut

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (5)
phpmyfaq/assets/src/utils/platform.ts (1)

26-26: ⚡ Quick win

Move shortcut hint labels into the translation system.

'⌘ K' / 'Ctrl K' are hard-coded in TypeScript on Line 26. Please read them via translation keys to stay consistent with i18n rules and locale-specific labels (e.g., Ctrl naming).

As per coding guidelines, "Do not hard-code strings — use the translation system".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@phpmyfaq/assets/src/utils/platform.ts` at line 26, The getShortcutHintLabel
function currently returns hard-coded strings '⌘ K' and 'Ctrl K' instead of
using the translation system. Replace the hard-coded string literals with
translation key lookups using your i18n system. Create appropriate translation
keys for both the Mac platform shortcut hint (using ⌘) and the non-Mac platform
shortcut hint (using Ctrl), then call your translation function within
getShortcutHintLabel to fetch and return the locale-specific labels based on the
platform condition.

Source: Coding guidelines

phpmyfaq/assets/src/utils/platform.test.ts (1)

14-37: ⚡ Quick win

Add coverage for navigator.userAgentData.platform branch and empty-string fallback.

Current tests only cover navigator.platform. Please add cases where userAgentData.platform is set and where it is '', so the fallback behavior is locked in and prevents regressions in isMacPlatform().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@phpmyfaq/assets/src/utils/platform.test.ts` around lines 14 - 37, The current
test suite for isMacPlatform() and getShortcutHintLabel() only covers the
navigator.platform path via setPlatform(). Add new test cases to cover the
navigator.userAgentData.platform branch by testing scenarios where
userAgentData.platform contains macOS indicators (like 'MacOS') and non-macOS
values, as well as test cases where the platform value is an empty string to
verify the fallback behavior functions correctly. This ensures all code branches
in isMacPlatform() are exercised and prevents regressions when either platform
detection method is used.
phpmyfaq/assets/src/interfaces/PopularSearch.ts (1)

4-4: ⚡ Quick win

Normalize API ambiguity at the boundary instead of exporting number | string downstream.

Using number | string in the shared interface propagates backend inconsistency to all consumers. Prefer a raw API type plus a normalized app-facing type (count: number) converted in the API client.

Based on learnings: "Isolate external dependencies behind an interface you own, so they can be swapped or mocked at the boundary" and "Make illegal states unrepresentable — encode invariants in types and constructors instead of re-validating them everywhere."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@phpmyfaq/assets/src/interfaces/PopularSearch.ts` at line 4, The PopularSearch
interface exports a `number` property typed as `number | string`, which forces
all consumers to handle both types. Instead, create two types: keep a raw API
response type (RawPopularSearch or similar) that matches the backend response
with the union type, and normalize the PopularSearch interface to only have
`number: number`. In the API client code that fetches popular searches, convert
the raw API response to the normalized PopularSearch type by coercing the number
field to an actual number, ensuring all downstream consumers see a consistent
numeric type.

Source: Learnings

phpmyfaq/assets/src/api/popularSearches.test.ts (1)

13-43: ⚡ Quick win

Add a malformed-success-payload test case for boundary robustness.

Please add a case where fetch returns 200 with an invalid JSON shape and assert the fallback behavior ([] or filtered-normalized output). Current tests don’t lock this contract.

Based on learnings: "Always write tests for new features and bug fixes."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@phpmyfaq/assets/src/api/popularSearches.test.ts` around lines 13 - 43, Add a
new test case to the fetchPopularSearches test suite that verifies the behavior
when the fetch returns a 200 status code but with an invalid or malformed JSON
payload that doesn't conform to the expected data shape. Mock the response with
a 200 status but provide data that is either malformed JSON, missing expected
fields, or has incorrect types. Assert that the fetchPopularSearches function
returns an empty array or applies appropriate filtering and normalization to
handle this edge case, ensuring the function gracefully handles payload
mismatches without breaking.

Source: Learnings

phpmyfaq/assets/src/interfaces/suggestionItem.ts (1)

5-11: ⚡ Quick win

Model SuggestionItem as a discriminated union to prevent invalid variant shapes.

The current optional-property interface allows impossible states. Define per-type variants (e.g., result, recent, popular, empty) with required fields per variant so misuse is caught at compile time.

Based on learnings: "Make illegal states unrepresentable — encode invariants in types and constructors instead of re-validating them everywhere."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@phpmyfaq/assets/src/interfaces/suggestionItem.ts` around lines 5 - 11, The
SuggestionItem interface allows invalid state combinations through excessive
optional properties. Refactor it into a discriminated union by creating separate
interface variants for each SuggestionType value (such as result, recent,
popular, empty), where each variant has a literal type field and its own
required fields (for example, the result variant requires question and category
while empty requires none). Then export a union type that combines all variants
so TypeScript enforces that only valid field combinations are allowed per type.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@phpmyfaq/assets/src/api/popularSearches.ts`:
- Line 34: The function at line 34 unsafely casts the JSON response directly to
PopularSearchResponse using the `as` operator without any runtime validation.
This allows malformed or incorrect data from the backend to propagate into the
autocomplete rendering. Add validation logic before the return statement to
validate the array items and their shape, verify that numeric fields are
properly coerced to numbers, and throw an error or return a default value if
validation fails. This ensures that only well-formed data matching the expected
PopularSearchResponse structure is returned from this function.

In `@phpmyfaq/assets/src/search/autocomplete.ts`:
- Around line 103-127: The fetch callback does not handle errors from
fetchAutoCompleteData(), which throws on non-OK responses and causes the
rejection to bubble up, preventing update() from being called and leaving the
autocomplete UI stuck. Wrap the fetchAutoCompleteData(query) call in a try-catch
block to gracefully handle errors. When an error is caught, ensure update() is
still invoked with an appropriate fallback state (such as an empty suggestion
list or similar) so the UI can recover from transient network or backend
failures.

In `@phpmyfaq/assets/src/search/recentSearches.ts`:
- Around line 26-27: The getRecentSearches() function needs to normalize and cap
parsed entries before returning them. In the return statement where you
currently filter for string types, you must also trim whitespace from each item
using the trim() method, filter out any empty strings after trimming, and cap
the resulting array to a reasonable maximum length (consider defining a constant
for this limit). This ensures that externally modified localStorage cannot
inject blank entries or arbitrarily large arrays into the autocomplete
rendering.

In `@phpmyfaq/assets/src/search/searchModal.ts`:
- Around line 30-31: The searchModal.ts file contains hard-coded user-facing
strings in the placeholder and aria-label properties instead of using the
translation system, which violates the coding guidelines. Replace the hard-coded
string 'Search …' in the placeholder property and the hard-coded string 'Search'
in the aria-label property with appropriate translation function calls that
retrieve these strings from the project's translation system, ensuring
consistency with the rest of the search UI localization.

In `@tests/bootstrap.php`:
- Around line 96-107: Add an `is_readable()` check before the dynamic include
statement for `$databaseConfigPath` in the code block around the `include
$databaseConfigPath;` line. This will satisfy the static analysis warning about
dynamic file inclusion by validating that the configuration file is readable in
addition to the existing `file_exists()` check. This follows the same defensive
pattern already implemented in DatabaseConfiguration.php.

---

Nitpick comments:
In `@phpmyfaq/assets/src/api/popularSearches.test.ts`:
- Around line 13-43: Add a new test case to the fetchPopularSearches test suite
that verifies the behavior when the fetch returns a 200 status code but with an
invalid or malformed JSON payload that doesn't conform to the expected data
shape. Mock the response with a 200 status but provide data that is either
malformed JSON, missing expected fields, or has incorrect types. Assert that the
fetchPopularSearches function returns an empty array or applies appropriate
filtering and normalization to handle this edge case, ensuring the function
gracefully handles payload mismatches without breaking.

In `@phpmyfaq/assets/src/interfaces/PopularSearch.ts`:
- Line 4: The PopularSearch interface exports a `number` property typed as
`number | string`, which forces all consumers to handle both types. Instead,
create two types: keep a raw API response type (RawPopularSearch or similar)
that matches the backend response with the union type, and normalize the
PopularSearch interface to only have `number: number`. In the API client code
that fetches popular searches, convert the raw API response to the normalized
PopularSearch type by coercing the number field to an actual number, ensuring
all downstream consumers see a consistent numeric type.

In `@phpmyfaq/assets/src/interfaces/suggestionItem.ts`:
- Around line 5-11: The SuggestionItem interface allows invalid state
combinations through excessive optional properties. Refactor it into a
discriminated union by creating separate interface variants for each
SuggestionType value (such as result, recent, popular, empty), where each
variant has a literal type field and its own required fields (for example, the
result variant requires question and category while empty requires none). Then
export a union type that combines all variants so TypeScript enforces that only
valid field combinations are allowed per type.

In `@phpmyfaq/assets/src/utils/platform.test.ts`:
- Around line 14-37: The current test suite for isMacPlatform() and
getShortcutHintLabel() only covers the navigator.platform path via
setPlatform(). Add new test cases to cover the navigator.userAgentData.platform
branch by testing scenarios where userAgentData.platform contains macOS
indicators (like 'MacOS') and non-macOS values, as well as test cases where the
platform value is an empty string to verify the fallback behavior functions
correctly. This ensures all code branches in isMacPlatform() are exercised and
prevents regressions when either platform detection method is used.

In `@phpmyfaq/assets/src/utils/platform.ts`:
- Line 26: The getShortcutHintLabel function currently returns hard-coded
strings '⌘ K' and 'Ctrl K' instead of using the translation system. Replace the
hard-coded string literals with translation key lookups using your i18n system.
Create appropriate translation keys for both the Mac platform shortcut hint
(using ⌘) and the non-Mac platform shortcut hint (using Ctrl), then call your
translation function within getShortcutHintLabel to fetch and return the
locale-specific labels based on the platform condition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c687b525-dbdc-41a6-9ee7-a4f059c5b1bd

📥 Commits

Reviewing files that changed from the base of the PR and between 00f5ace and c68ca38.

📒 Files selected for processing (34)
  • AGENTS.md
  • phpmyfaq/assets/scss/layout/_autocomplete.scss
  • phpmyfaq/assets/src/api/index.ts
  • phpmyfaq/assets/src/api/popularSearches.test.ts
  • phpmyfaq/assets/src/api/popularSearches.ts
  • phpmyfaq/assets/src/frontend.test.ts
  • phpmyfaq/assets/src/frontend.ts
  • phpmyfaq/assets/src/interfaces/PopularSearch.ts
  • phpmyfaq/assets/src/interfaces/index.ts
  • phpmyfaq/assets/src/interfaces/suggestionItem.ts
  • phpmyfaq/assets/src/search/autocomplete.test.ts
  • phpmyfaq/assets/src/search/autocomplete.ts
  • phpmyfaq/assets/src/search/highlight.test.ts
  • phpmyfaq/assets/src/search/highlight.ts
  • phpmyfaq/assets/src/search/index.ts
  • phpmyfaq/assets/src/search/recentSearches.test.ts
  • phpmyfaq/assets/src/search/recentSearches.ts
  • phpmyfaq/assets/src/search/searchModal.test.ts
  • phpmyfaq/assets/src/search/searchModal.ts
  • phpmyfaq/assets/src/search/searchShortcut.test.ts
  • phpmyfaq/assets/src/search/searchShortcut.ts
  • phpmyfaq/assets/src/search/searchShortcutBadge.test.ts
  • phpmyfaq/assets/src/search/searchShortcutBadge.ts
  • phpmyfaq/assets/src/utils/index.ts
  • phpmyfaq/assets/src/utils/platform.test.ts
  • phpmyfaq/assets/src/utils/platform.ts
  • phpmyfaq/assets/templates/default/index.twig
  • phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/PopularSearchesController.php
  • phpmyfaq/src/services.php
  • phpmyfaq/translations/language_de.php
  • phpmyfaq/translations/language_en.php
  • tests/bootstrap.php
  • tests/phpMyFAQ/Controller/Frontend/Api/PopularSearchesControllerDirectTest.php
  • tests/phpMyFAQ/Controller/Frontend/Api/PopularSearchesControllerWebTest.php

Comment thread phpmyfaq/assets/src/api/popularSearches.ts Outdated
Comment thread phpmyfaq/assets/src/search/autocomplete.ts
Comment thread phpmyfaq/assets/src/search/recentSearches.ts Outdated
Comment thread phpmyfaq/assets/src/search/searchModal.ts Outdated
Comment thread tests/bootstrap.php
@thorsten thorsten merged commit 2aeb0e3 into main Jun 20, 2026
9 checks passed
@thorsten thorsten deleted the feature/search-keyboard-shortcut branch June 20, 2026 12:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant