modernization(query-tool) #35709: migrate Query Tool portlet from Dojo JSP to Angular#35792
modernization(query-tool) #35709: migrate Query Tool portlet from Dojo JSP to Angular#35792hmoreras wants to merge 11 commits into
Conversation
…gular Replace the Dojo/Dijit query-tool JSP with a new Angular portlet under libs/portlets/dot-query-tool/, mirroring the layout and conventions of the ES Search portlet shipped in #35701. - Two-pane splitter: Monaco (plain-text Lucene) + collapsible Parameters panel on the left; stats bar (resultsSize / queryTook / contentTook) and Results / Raw tabs on the right. - Help popover with four canonical example Lucene queries. - URL query-param state sync (q / offset / limit / sort / userId) replacing the legacy session-side rememberQuery flag. - Result title click routes through DotContentDriveNavigationService, which picks the new vs legacy edit-content UI per content type's CONTENT_EDITOR2_ENABLED flag. - Consumes POST /api/v1/content/_search (the v1 wrapper, preferred per ContentResource JavaDoc). - portlet.xml: query-tool now points at PortletController; legacy JSP preserved as query-tool-legacy for live-toggle rollback. - PortletID: add QUERY_TOOL_LEGACY (mirrors TAGS_LEGACY precedent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… polish
Replace DotContentDriveNavigationService with a direct DotContentTypeService
lookup so the click flow no longer pollutes the URL with CD_* query params
(which were triggering the post-close redirect to content-drive via
DotContentletWrapperComponent.onClose).
- Result row click now window.open(_blank) so query-tool keeps its full
state (query, offset, limit, results, scroll) regardless of which editor
the target contentlet opens in.
- HTML pages → /dotAdmin/#/edit-page/content?url=...&language_id=...
- New editor (CONTENT_EDITOR2_ENABLED=true) → /dotAdmin/#/content/{inode}
- Legacy editor → /dotAdmin/#/c/content/{inode}
- about:blank is opened synchronously inside the click handler to avoid
popup-blocker rejection, then location.href is assigned after the
async getContentType() call resolves.
i18n tweaks:
- queryTool.action.run: "Submit" → "Run"
- Add com.dotcms.repackage.javax.portlet.title.query-tool-legacy="Query
Tool Legacy" so the legacy twin shows up in Roles → Tools (parity with
es-search-legacy / tags-legacy).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e URL sync UX/copy: - Run button: always enabled (no [disabled]); onRun() short-circuits when query is empty (matches ES Search behavior). - "Submit" → "Run" everywhere (button label, empty-state hint copy). - Help tooltip + aria-label: "Click for query examples". - New Language.properties entry com.dotcms.repackage.javax.portlet.title .query-tool-legacy so the legacy twin shows up under Roles → Tools. Limit cap (parity with ES Search MAX_HITS): - New MAX_RESULTS=1000 constant + limitWasCapped state field. - runSearch caps limit at 1000 when the user asks for more and surfaces a <p-message severity="warn"> between stats bar and tabs. - setLimit resets limitWasCapped so the warning clears when the user adjusts the input. Reactive URL sync (fixes duplicate-request bug): - Replace the imperative syncUrl() / Router.navigate flow with an effect() that pipes store signals through Router.createUrlTree + Location.go — the canonical pattern from dot-content-drive-shell.component.ts. Updates the address bar without triggering a router navigation, so the component no longer re-mounts (which was firing a second auto-run with isAdmin=false and dropping userId from the payload). - userId gate removed from runSearch: forward userId whenever set; the server is the authoritative admin gate. Tests: 27 passing (added cap behavior + Location.go assertions). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…elpers
Query-tool stats bar now matches the ES Search format ("Showing X of Y
results · N ms"), with Share/Export buttons that mirror ES Search.
Tab count-badges removed from both portlets; Raw editor gains bottom
padding so the closing brace no longer sits flush against the viewport.
Cross-portlet refactors land the duplicated copy/download/snippet code
into shared modules:
- libs/utils: buildCurlSnippet, buildFetchSnippet (POSIX-shell-safe)
- libs/ui: DOT_MONACO_BASE_OPTIONS, DOT_MONACO_RAW_OPTIONS
- query-tool store: apiRequestBody computed (single source of truth for
the search payload; runSearch and the snippets read from the same
computed so they cannot drift)
Component cleanup in both portlets:
- DotClipboardUtil replaces hand-rolled navigator.clipboard.writeText
- getDownloadLink replaces hand-rolled <a> download anchors
- copyQuery dropped; copyToClipboard is the single entry point
- DEFAULT_LIMIT / DEFAULT_OFFSET exposed on the component to replace
magic numbers in template (?? 0 / ?? 20)
- aria-label added to icon-only copy buttons
- URL-sync effect skips no-op Location.go writes via lastSyncedUrl guard;
comment trimmed to the why (no cross-file references)
- Showing-of i18n restored to a self-contained sentence:
"Showing {0} of {1} results"
Tests:
- New unit tests for buildCurlSnippet / buildFetchSnippet in libs/utils
- Brittle private-method probes replaced with menu-driven tests
(exportItems[i].command() + clipboard spy)
- DotClipboardUtil added to componentProviders in both specs
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Claude finished @hmoreras's task in 4m 0s —— View job ReviewMostly clean. The big pieces (store, shared resolver, URL sync, portlet wiring, tests) hang together well. The findings below are minor — none are blockers, but a few merit a second look before merge. Worth a second look1.
2. Popup-blocker rejection is silent const '_blank');
if (!placeholder) return;When a popup blocker rejects the placeholder open, the click does nothing visible. At minimum show a 3. Race: in-flight
limit: wasCapped ? MAX_RESULTS : userLimit…which overwrites the user's current input with the value they had when they clicked Run. For a long-running query against a big index, this is a confusing UX paper-cut. Consider only patching 4. Share-URL vs cURL/fetch snippets are sourced from different states
So after a user edits the offset but doesn't re-run, the Share-URL and the cURL snippet disagree. Probably worth either (a) syncing the URL on input change, or (b) sourcing the share URL from 5. PR description vs code drift: 6. License-gate claim in DoD Smaller stuff
Tasks
|
…ce limit cap Address PR review feedback (Claude bot, comment 4510539091): - Switch URL sync from Location.go (history.pushState) to Location.replaceState so per-keystroke store-signal changes do not pollute the browser back stack. - Skip the URL sync when the freshly computed URL already matches router.url, removing the no-op replaceState on first mount. - Use !== DEFAULT_OFFSET for offset URL pruning, matching the limit comparator (was a falsy check that only worked because 0 happened to be the default). - Make limitWasCapped a computed derived from state, drop the runSearch patchState mutation that visibly snapped a user-typed limit (5000 -> 1000). apiRequestBody remains the single source of truth for the capped value sent to the API; the user's typed value is preserved in state and the warning surfaces immediately on type rather than only after Run. - Update store + component tests for the new no-mutation contract and the replaceState wiring.
…rlService Move "where in dotAdmin should I open this contentlet?" out of the Query Tool page component and into a new shared service in @dotcms/data-access. The component now owns only the synchronous window.open (popup-blocker constraint) and the URL assignment; the resolver owns the HTTP call, the feature-flag inspection, and the three URL shapes (HTMLPAGE / new editor / legacy editor). Why a new service instead of inlining or storing it: - The same logic is currently re-implemented inline at three other call sites (libs/new-block-editor/.../contentlet-edit-url.service.ts, libs/block-editor/.../dot-bubble-menu.component.ts, libs/portlets/dot-content-drive/.../dot-content-drive-navigation.service.ts). - Centralising it now gives future portlets a canonical, documented target, even though the three existing duplicates are intentionally left alone in this PR to keep its review surface focused on Query Tool. Migrating those is tracked as a follow-up. The service is providedIn: 'root' with an app-wide per-content-type cache, so a list view of N contentlets of the same type only fetches the content type metadata once. Errors fall back to the legacy editor URL rather than propagating, matching the prior inline behavior. Tests: 7 cases on the new service (HTMLPAGE branch, both editor branches, graceful fallback, cache scoping by content type). Query Tool component spec updated to mock the resolver instead of DotContentTypeService.
Three issues found during browser QA on the limit-cap flow: 1. Warning bar appeared the moment the user typed an oversized limit, before any search ran. The warning describes the *displayed* results, not the pending input, so it now flips only when a search settles (LOADED), and persists across input edits until the next non-capped run replaces those results. 2. After a capped run, hitting Run again while 1000 result rows were on screen froze the UI for 1–2 seconds. Root cause: tearing down 1000 PrimeNG rows (each with two p-button + tooltip children) is synchronous and expensive. Switched the results table to PrimeNG virtual scrolling (virtualScrollItemSize=46) so only the visible window mounts. Run-again is now instant; typing in the limit input is responsive again. 3. URL was syncing on every keystroke (query/offset/limit/sort/userId). The address bar is only meaningful once a search has actually executed, so the sync effect now tracks `status` and fires only on LOADED/ERROR. Input signals are read via `untracked()` so they no longer drive the effect. Typing never touches the URL; both successful and failing runs do. Tests updated for the new contracts: - Store: limitWasCapped persists across edits, clears synchronously on next Run, snaps limit + raises warning together with rendered results. - Component: URL sync fires on LOADED, on ERROR, and not on INIT/LOADING.
…Test SerializationHelperTest.testFromXmlFile hard-codes the expected portlet count from portlet.xml. Adding `query-tool-legacy` (the rollback safety net for the Query Tool migration) bumped the count from 52 to 53 and broke JVM Unit Tests in CI. Updated the count and added an explicit assertion for the `query-tool-legacy` entry — mirrors how `es-search-legacy`, `categories-legacy`, and `plugins-legacy` are already pinned. This way a future accidental removal of the rollback portlet fails loudly, not silently.
Summary
Migrates the Query Tool portlet from the legacy Dojo/Dijit JSP to a new Angular + PrimeNG implementation, with a
query-tool-legacyrollback entry preserved inportlet.xml.Resolves #35709 (sub-task of epic #34735).
What changed
New Angular portlet —
core-web/libs/portlets/dot-query-tool/DotQueryToolStore(NgRx SignalStore) + data-access serviceq,offset,limit,sort,userId) replacing the legacyrememberQuerysession flag — implemented as a reactiveeffect()usingRouter.createUrlTree+Location.go(canonicaldot-content-drive-shellpattern) so address-bar updates do not re-mount the componentMAX_RESULTS = 1000cap mirroring ES Search'sMAX_HITS, with a<p-message>warn when the user-supplied limit is capped/dotAdmin/#/edit-page/content?url=…&language_id=…CONTENT_EDITOR2_ENABLED=true) →/dotAdmin/#/content/{inode}/dotAdmin/#/c/content/{inode}about:blankis opened synchronously inside the click handler to avoid popup-blocker rejection, thenlocation.hrefis assigned once the asyncgetContentType()lookup resolvesPOST /api/v1/content/_search(v1 wrapper, preferred perContentResourceJavaDoc)Cross-portlet shared helpers (consumed by both Query Tool and ES Search)
libs/utils:buildCurlSnippet,buildFetchSnippet(POSIX-shell-safe) + unit testslibs/ui:DOT_MONACO_BASE_OPTIONS,DOT_MONACO_RAW_OPTIONSapiRequestBodycomputed — single source of truth for the search payload sorunSearchand the curl/fetch snippets cannot driftDotClipboardUtilreplaces hand-rollednavigator.clipboard.writeText;getDownloadLinkreplaces hand-rolled<a>download anchorsES Search alignment
Showing X of Y results · N msBackend wiring
portlet.xml:query-toolnow points atPortletController(Angular shell);query-tool-legacyentry preserved as live-toggle rollback to the existing JSP without redeployPortletID:QUERY_TOOL_LEGACYadded (mirrorsTAGS_LEGACYprecedent)Language.properties:com.dotcms.repackage.javax.portlet.title.query-tool-legacy = Query Tool Legacyso the legacy twin shows up under Roles → Tools (parity withes-search-legacy/tags-legacy)queryTool.action.run:Submit→Run(button label + empty-state hint copy); help tooltip / aria-label:Click for query examplesTests
Location.goURL-sync assertions, menu-driven export tests viaexportItems[i].command()+ clipboard spy, error paths throughhttpErrorManager.handlebuildCurlSnippet/buildFetchSnippetinlibs/utilsDefinition of Done (from epic #34735)
contentTook/queryTooktimingshttpErrorManager; results table unchangeduserIdforwarded only when set)query-tool-legacyportlet entrylimit-size pages with URL-synced offsetTest plan
yarn nx test dot-query-tool— all unit tests passyarn nx test dot-es-search— ES Search regression suite passes (shared helpers)yarn nx lint dot-query-toolcleanyarn nx serve dotcms-ui→ open Query Tool portlet as admin:q,offset,limit,sort,userId) round-trip on reloadportlet.xmltoquery-tool-legacy→ legacy JSP renders without redeploy🤖 Generated with Claude Code