feat(sidebar): show logged-in user's name in sidebar header#2
feat(sidebar): show logged-in user's name in sidebar header#2NagariaHussain wants to merge 76 commits into
Conversation
Use frappe-ui's usePageMeta (plugin was already registered but unused) in each leaf route component so the tab title reflects what's open: the page/draft title with its space name, the change request title, or the static section name. Titles live only in leaf components (not SpaceDetails) since each usePageMeta watcher independently writes document.title and the last one to fire wins — a parent title arriving late would clobber the child's. Space names are read with getCachedDocumentResource because createDocumentResource re-fetches on cache hit when auto is set. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tles feat(editor): dynamic browser tab titles
Adds a Wiki Setting (on by default) that converts PNG/JPEG images uploaded in the editor to WebP server-side via Pillow, reducing page weight. Editor uploads route through a dedicated `wiki.api.upload_wiki_asset` endpoint; conversion is skipped (and the original kept) when the setting is off or the image can't be decoded. Uploads now show an inline preview with a loading state and swap to the optimized URL on success. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e-conversion feat(editor): auto-convert uploaded images to WebP
Add a PDF embed block to the wiki editor and public reader. Editor (TipTap/Vue): - New `pdfBlock` node + PdfBlockView (inline scrollable all-pages viewer, loading/error states) + PdfViewerModal (full-screen zoom/scroll). - Toolbar button, `/pdf` slash command, and drag-drop, all routed through insertAndUploadPdf (optimistic loading; reuses upload_wiki_asset). - Stored as ``, disambiguated by extension; the image tokenizer yields to PDFs (as it already does to videos). Public reader (Jinja + vanilla JS): - markdown.py renders a `.wiki-pdf-embed` card (parallel to the video block); pdf-viewer.js hydrates an inline scrollable all-pages viewer + full-screen modal. Lazy via IntersectionObserver. - pdfjs-dist vendored under public/js/vendor/pdfjs and served directly (no CDN). Rendering uses PDF.js (vue-pdf-embed in the editor, pdfjs-dist on the public side, pinned/matched at 4.10.38). Also add get_asset_hash() jinja helper to cache-bust the public JS includes (pdf-viewer/image-viewer/code-blocks) so structural changes don't break against stale browser-cached scripts.
`get_asset_hash` is only ever called with hardcoded literal paths from our own templates (layout.html), never with user input, and resolves under the wiki app root. Add a targeted nosemgrep suppression so the Linters CI passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…beds
Opening any page containing a PDF embed in the editor froze the browser
tab. CPU profiling showed the main thread pegged in an infinite markdown
serialize/parse loop (normalizeMarkdown), not in PDF rendering — PDF.js
renders the file in ~130-380ms.
Root cause: `savedContent` (the "confirmed saved" baseline handed to the
editor for dirty detection) fell back to `editorContent`, which reflects
the draft store's `localContent`. That created a reactive cycle:
watch(savedContent) -> emitContentReady -> reconcileEditorContent
-> sets localContent -> editorContent recomputes -> savedContent
recomputes -> watch fires again -> ...
The cycle only settles when the editor's serialized content equals the
normalized saved content. Plain prose reaches that fixed point in one
step, so it never looped. A PDF embed serializes to a form that differs
from the stored markdown, so the comparison never converged and the
watcher re-fired forever, each iteration running the expensive
parse+serialize round-trip seen in the profile.
Fix: `savedContent` now mirrors `editorContent` MINUS the `localContent`
branch, so the saved baseline never reflects the editor's unsaved local
snapshot and the watcher can't be re-triggered by the reconcile output it
feeds. This is also semantically correct — a "saved" baseline should not
include unsaved edits.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The earlier savedContent change (53c865a..e0a3899) did not fix the freeze; this reverts it and fixes the actual root cause. A page with two or more PDF embeds froze the editor tab. CPU profiling showed an infinite markdown serialize/parse loop, and instrumenting the live editor revealed why: the markdown round-trip was non-idempotent. `PdfBlock.renderMarkdown` appended "\n\n", but the markdown serializer already inserts a blank-line block separator between block nodes. The two stack up, and the PreserveBlankLines extension then re-parses the extra blanks into empty paragraphs, which add yet another separator on the next cycle — so the gap between consecutive embeds grows without bound: \n\n\n\n (cycle 1) \n\n\n\n\n\n (cycle 2) ... `WikiDocumentPanel`'s content reconcile writes that normalized value back into the store baseline that feeds the editor's `savedContent` watcher, so a round-trip that never reaches a fixed point re-fires the watcher forever and pegs the main thread. Fix: drop the trailing "\n\n" from `PdfBlock.renderMarkdown`, exactly like the image extension (which is idempotent for this reason). The round-trip now converges in one step and the reconcile loop terminates. Verified on the two-PDF demo page: editor mounts, both PDFs render, tab stays responsive. Note: video-block.js and iframe-block.js share the same "\n\n" pattern and the same latent freeze for two consecutive embeds; left untouched here to keep this change scoped to the reported PDF regression. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…embeds video-block.js and iframe-block.js had the identical bug fixed for PDFs in 6dcaac7: their renderMarkdown appended "\n\n" on top of the serializer's own block separator, so the markdown round-trip grew blank lines without bound between two consecutive embeds and froze the editor in an infinite reconcile loop. Drop the trailing "\n\n" from both. Verified the round-trip now converges in one step for two consecutive video and two consecutive iframe embeds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(editor): embed PDFs with inline scrollable viewer
Wiki Document.route is looked up on the main page-serving paths (WikiDocumentRenderer.can_render, get_page_data, download_pdf) but had no index, causing full table scans on every page view. Add search_index so route lookups use a ref index instead. Same class of fix as frappe#651 (Wiki Revision Item index). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add (change_request, status) on Wiki Merge Conflict and (wiki_space,
status) on Wiki Change Request via on_doctype_update hooks. These filter
combinations were doing full table scans:
- open/resolved conflict counts & listings filter by {change_request, status}
- _find_existing_draft / list_change_requests filter by {wiki_space, status}
modified timestamps bumped so the doctypes re-sync and the hooks run on
next migrate. Same class of fix as frappe#651.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CR diff/merge and get_descendants_of range-scan the nested set via WHERE lft >= ? AND rgt <= ?, previously a full table scan. Mirrors how ERPNext indexes lft/rgt on its tree masters (Account, Item Group, etc.). modified bumped so the doctype re-syncs on next migrate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
perf: add missing DB indexes for hot lookups (closes frappe#651-class scans)
The public PDF viewer dynamically imports the vendored pdfjs bundle as an
ES module. It worked locally because the werkzeug dev server resolves .mjs
to text/javascript, but production nginx has no MIME mapping for .mjs and
serves it as application/octet-stream. Browsers enforce strict MIME checking
for module scripts and refuse to execute it, so the import failed
("Failed to fetch dynamically imported module ... pdf.min.mjs") and the
public cards never rendered.
Vendor the files as .js instead. .js is reliably served as text/javascript,
and dynamic import() keys off the MIME type rather than the extension, so the
files still load as ES modules. The internal
`GlobalWorkerOptions.workerSrc ||= "./pdf.worker.mjs"` default in the bundle
is never hit because pdf-viewer.js sets workerSrc explicitly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(pdf-embed): vendor pdfjs as .js so production serves it as JS
Configure per-space Read/Write access via existing Frappe Roles, enforced across the Vue SPA, Desk, and the public portal by leaning on the framework permission layer. - Schema: new Wiki Space Role child doctype; roles table on Wiki Space; denormalized wiki_space link on Wiki Document; remove per-page is_private. - Permissions: wiki/permissions.py helpers (can_read_space/can_write_space) + permission_query_conditions and has_permission hooks for Wiki Space, Wiki Document, Wiki Change Request. - Content gates: check_space_access 404s unauthorized/guest hits across portal HTML, text/markdown, and PDF; wiki_space auto-stamped on update and re-stamped on reorder (live get_wiki_space fallback keeps gating correct). - Change Requests: merge gated by per-space Write (_can_merge); Read-tier users can raise CRs, with a privileged bootstrap for fresh spaces. - Migration: backfill_space_access patch denormalizes wiki_space and translates legacy is_private into Guest/All Read rows (raw-SQL read, has_column-guarded, idempotent). - Frontend: SPA roles editor in Space Settings; Merge UI gated by get_space_capabilities; remove page-level Private toggle and lock icon. - Search: post-filter hits by can_read_space so restricted titles/snippets don't leak. - Reconcile existing tests to the new model; reconcile the spec to as-built. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Type-hint the `roles` arg of `update_space_roles` (missing-argument-type-hint). - Annotate the privileged bootstrap `set_user` calls with `# nosemgrep` (frappe-setuser) — read access is already verified and the user is restored in `finally`. - Annotate the already-guest-whitelisted `search` endpoint with `# nosemgrep` (guest-whitelisted-method), matching the existing convention in wiki_document.py; it now post-filters hits by read access. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s, not set_user `frappe.set_user` swaps the whole session (user, roles, cache) mid-request and attributes the seed revision to Administrator. Match the first-party idiom (Builder/Helpdesk: per-doc `insert(ignore_permissions=True)`) instead: thread an `ignore_permissions` flag through `create_revision_from_live_tree` and have `_bootstrap_main_revision` use it. The session stays intact and the bootstrap revision keeps the real `created_by`. Removes the frappe-setuser Semgrep finding. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add CLAUDE.md (Claude Code project guidance). - Add specs/automatic_webp_image_optimization.md (as-built doc for the already-shipped WebP feature). - Remove stale NOTES.md / TODO.md scratch files. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n spec Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…roles editor Add wiki/test_permissions.py covering the full read/write matrix (manager, open, read-role, write-role, wrong-role, Guest/public), query-condition filtering via get_list, the has_permission hooks, and the role-editor API — replacing the temporary bench harness used during development. Gate the SPA Access Control editor controls behind get_space_capabilities .can_write so read-tier users see roles read-only instead of controls that would 403 on save. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-based role defaults Refactor Space Settings into a Builder-style dialog with a vertical nav (General / Permissions). Permissions renders roles as a CRM/LMS-styled table with an inline Read/Write select, a hover trash action, and an in-flow searchable role picker (select -> Add) that stays clickable inside the reka-ui modal. Save sits below the table, disabled until dirty, with an "Unsaved changes" badge next to the heading. Seed sensible access defaults: new spaces (published by default) start with Guest + All read; a patch makes existing empty spaces explicit from is_published (published -> Guest + All read, unpublished -> All read), skipping already-configured spaces. Empty roles still mean open-to-logged-in as a backend fallback; managers bypass via _is_manager. Also drop the sidebar resize handle's z-10 so the modal overlay covers it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
frappe.get_roles() returns "Guest" for every user (anonymous and logged-in), so a Guest Read row already makes a space readable by everyone; only the anonymous Guest user lacks the "All" role. The paired "All" row on published spaces was therefore redundant and inconsistent with the existing backfill convention (Guest = public, All = login-only). Map published -> Guest, unpublished -> All in both the seed patch and the new-space UI default, and correct the patch docstring. Verified: 27 permission tests pass; patch unit-tested (published->Guest, unpublished->All, configured untouched, idempotent); migrate seeded 540 spaces with 0 empty / 0 published-but-login-only mismatches. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…PDATE Replace the raw SQL bulk UPDATE in the backfill patch with frappe.qb on the real wiki_space field. The is_private read stays raw SQL — it queries a column removed from the DocType meta via a conditional aggregate, which the query builder can't express cleanly (documented in the module). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…space-access-control feat: role-based access control for Wiki Spaces
Plan for a ```mermaid fenced-block feature spanning both render stacks: editor (TipTap node with code pane + live SVG preview) and the public server-rendered reader (markdown.py fence interception + lazy client-side mermaid render). Mirrors the PDF-embed precedent; client-side render, securityLevel:'strict', vendored mermaid (.js not .mjs). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
(cherry picked from commit e61c09f)
(cherry picked from commit 3f4f7f9)
(cherry picked from commit 4ca6e07)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bullet 5 of the change-request review revamp adds the notification plumbing. - _notify_cr_owner: realtime ping (publish_realtime, after_commit) plus a best-effort Notification Log entry to the CR author. No-op when the actor is the author (self-serve approve & merge shouldn't notify yourself). - Wired into approve / request_changes / reject and _finalize_merge (the single chokepoint for both merge paths). - Frontend: global socket listener in App.vue toasts the author live; the Notification Log is the durable copy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bullet 6 of the change-request review revamp lets reviewers see the proposed pages rendered exactly as they will publish, not just a markdown diff. The live reader renders markdown server-side (wiki/markdown.py), so preview parity comes from reusing that pipeline rather than a Vue component: - get_cr_preview_context: runs the CR head-revision markdown through the same render_markdown_with_toc used by Wiki Document.get_web_context. - ChangeRequestPreview page: tree sidebar (get_cr_tree) + rendered HTML pane; content styles lifted from the reader's main.css into shared wiki-rendered.css. - Entry points: Preview button in the review header (any status) and a per-row Diff/Preview toggle that renders each page inline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The editor banner's one-click Merge (managers, Draft/Changes Requested) called merge_change_request directly, but Bullet 1 tightened merge to require Approved — breaking one-click publish from the editor. Wire it to walk the CR up the state machine in one click via a new store action approveAndMergeChangeRequest() (submit -> approve -> merge, skipping already-satisfied steps); on a merge-conflict ValidationError, route to the review page's conflict-resolution UI. Rewrite change-request-flow.spec.ts for the new flow: replace removed request_review calls with submit_change_request, drive the Approve & Merge confirm dialog, and add submit->assign->approve->merge (two-person), request changes->revise->resubmit, and reject (terminal) scenarios. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The preview page's "Back to review" pushes a new history entry, while the review page's Back used router.back() — so the two pinged between each other and you could never leave the flow. The review page's Back now navigates absolutely to the Change Requests list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The outer p-4 plus frappe-ui Tabs' own px-5 inset gave the title, tab labels, and list header three different left edges, and the ListView's header butted straight against the tab bar with no gap. Drop the conflicting p-4/gap-4 and pad each section to a consistent px-5, with top/bottom padding on the panel for breathing room. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clicking a page in the sidebar pushed a new history entry, so browser-back
walked back to the bare space landing ("select a page") and could
ping-pong between pages and the landing. Navigate with router.replace when
already inside the same space, so back returns to wherever the user entered
the space from.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refine the change-request review experience: - Inline Diff/Preview toggle: Preview now renders Current vs Proposed side-by-side via a read-only WikiContentViewer (same TipTap config as the editor) so syntax highlighting and custom blocks match the reader. - Review actions: header primary is Approve -> then Merge; combined Approve & Merge moved into the three-dot menu. - Assignees: CRM-style overlapping avatars in the list rows and review header. Fetch _assign via frappe.client.get_value (the document resource's frappe.client.get strips it). - List: fix Assign button navigating to the CR (router-link anchor needs @click.stop.prevent, not just .stop). - Reorder: show a structural before/after (path + sibling position) instead of an empty content diff; detect reorder by real position so order_index renumbering no longer flags unmoved pages; drop the structural root group from the breadcrumb. - Navigation: Back buttons pop history (router.back) so the originating list tab/query is preserved and the review<->preview loop is gone; preview page-switching uses replace. Tests: backend unit tests for reorder location/position and churn-skip; e2e tests for assign-no-navigate, tab-preserving back, and the reorder structural view. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…library chore(deps): update @pierre/diffs to 1.2.11
The Wiki Spaces list now has an actions column with a View button on published spaces that opens the public-facing reader (`/<route>`, served at the site root outside the `/wiki` editor SPA) in a new tab. Uses @click.stop.prevent so the click doesn't bubble into the row's router-link navigation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The View action sat far right with a wide gap from the route. Render it inline in the Route cell instead, right after the route text, so it's adjacent to what it opens. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Move View back into its own left-aligned column so the buttons form a straight column instead of jagging with each route's length; a wide last column keeps it near the route rather than at the far edge. - Show a skeleton while the first page loads so the empty state no longer flashes on reload. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The review header primary is now Approve-only with Approve & Merge in the three-dots menu, so specs that published a page by clicking a "Merge" button (which used to substring-match "Approve & Merge") broke. Add a publishChangeRequestFromReview helper that drives the new menu + confirm flow, and use it across the public-reader specs. API-merge specs (mobile-view, local-first-store) now approve before merge since merge_change_request requires an Approved decision; also replace the removed request_review call with submit_change_request. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ptor-driven The Contributions tabs filtered on `userStore.user`, a property the user store never exposes. The value was always `undefined`, so the inbox filtered on `_assign LIKE %undefined%` (and "My Change Requests" on `owner = undefined`) and showed nothing. Rework the page around a single `tabDefs` descriptor list instead of three hand-wired resources plus parallel options objects: - `filters` is now a getter read at fetch time, so the session user can never be frozen in as `undefined` again. - Lists are created and fetched lazily, only when a tab first becomes active (the resource `auto` flag is not reactive, so fetching is explicit). This drops the eager double-fetch on page load and the always-on `allInReview` query for non-managers. Add an e2e regression test asserting a CR assigned to the current user shows up under the "Assigned to me" tab. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…the space Once the "Assigned to me"/"My Change Requests" filter was repaired, the My tab finally returned rows — exposing a latent bug: a Draft/Changes-Requested row routes to the space editor via `row.wiki_space`, but only `wiki_space.space_name` was selected, so `spaceId` was undefined and vue-router threw "Missing required param". Select the `wiki_space` link id alongside the display name. Add an e2e regression test asserting the My tab renders a draft row linking to its space without a router param error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Narrow Author and Status, give Space more room, and move "Submitted" to the end as a compact clock-marked date column: drop the in-cell time (the width hog) to a date only, drop the header text in favour of the icon, and surface the full timestamp on hover. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…itor styles The preview rendered server HTML via v-html, so it had no syntax highlighting and didn't match the editor. Render it through the same read-only WikiContentViewer (lowlight, callouts, tables) the editor/diff use: - expose the raw markdown `content` from get_cr_preview_context - the content/code/highlight CSS lived in WikiEditor.vue's lazy chunk, so it was absent on the preview page; extract it to a global wiki-editor-content.css imported in main.js, and move the editing-surface box (border + min-height + padding) behind an `.is-editable` modifier the editor opts into - hide the code-block language picker when the editor is read-only - make the preview full-bleed and drop the redundant page title (it's in the sidebar) Extend the preview e2e test to assert lowlight highlighting renders and the language picker does not leak into the read-only view. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-revamp feat: change request review flow revamp
Diagrams rendered with Mermaid's stock light/dark palette, which clashed with the wiki's look. Replace the "default"/"dark" theme switch with a single `base` theme whose themeVariables are resolved live from the Frappe UI gray/surface/ink tokens (read as hex via getComputedStyle). Because the tokens flip with `<html data-theme>`, one mapping renders correctly in both modes — no more separate dark/default branch. The config lives in one place (wikiMermaidThemeConfig in the shared loader) and is reused by the public renderer and the editor preview, so the in-editor diagram matches the published page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts: # frontend/src/pages/ContributionReview.vue # frontend/src/pages/SpaceDetails.vue
The change-request preview (full page + split-pane) renders markdown through WikiContentViewer, which lacked the MermaidBlock extension — so mermaid fences fell through to a plain code block and never rendered. Register MermaidBlock in the viewer and give MermaidBlockView a read-only branch: when the editor isn't editable it drops the split-pane editing chrome and shows only the rendered diagram as a centered figure, matching the published page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat: Mermaid diagram editing & embedding
Surface the logged-in user's full name as the sidebar header subtitle, matching the placement used in the LMS sidebar. Uses the existing `subtitle` prop on frappe-ui's SidebarHeader. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
Reopening against frappe/wiki upstream. |
Why?
The sidebar header showed only the app name, with no indication of who is logged in. The LMS sidebar already surfaces the logged-in user's name in this spot, so we want parity in Wiki.
What?
Show the logged-in user's full name as the subtitle in the sidebar header, right below "Frappe Wiki".
How?
frappe-ui's
SidebarHeaderalready supports asubtitleprop rendered below the title. The header object inSidebar.vueis now a computed that setssubtitle: userStore.data?.full_name, so the name fills in reactively once the user resource loads.🤖 Generated with Claude Code