Commit Graph

741 Commits

Author SHA1 Message Date
Philipinho eeb84e97c9 refactor(base): migrate column reorder from dnd-kit to pragmatic-drag-and-drop 2026-05-24 02:24:54 +01:00
Philipinho 218b5755a9 feat(base): add drop-edge indicator component for pragmatic-dnd 2026-05-24 02:18:31 +01:00
Philipinho 911bf53924 Merge branch 'main' into base-formula 2026-05-24 01:47:59 +01:00
Philipinho d7c4f0551e fix: strip html styles on paste 2026-05-22 19:00:30 +01:00
Philipinho f010f6a83a fix: internal links 2026-05-21 17:01:20 +01:00
Philipinho 0402420fbc fix visibility 2026-05-20 18:20:52 +01:00
Philip Okugbe 2be5e0d4ee New Crowdin updates (#2220) 2026-05-20 18:20:02 +01:00
Philipinho adb1f27767 v0.90.0 2026-05-20 16:55:23 +01:00
Philip Okugbe 92c0e36e46 fix(a11y): WCAG 2.1 AA fixes (#2219) 2026-05-20 16:47:25 +01:00
Olivier Lambert 1c166c4736 feat(editor): add alt text support for images (#2097)
* feat(editor): add alt text support for images
* feat:  extend alt text support to videos and diagrams

---------
Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2026-05-20 16:45:59 +01:00
Philip Okugbe 66a754c9eb Revert "fix: prevent browser tab fallback in editor (#2123)" (#2216)
This reverts commit 1d2486455f.
2026-05-19 14:07:07 +01:00
Philip Okugbe 6cf8101ab3 feat(ee): templates (#2215)
* feat(ee): templates
* fix tree
* fix
2026-05-19 02:41:52 +01:00
Philip Okugbe b7b99cb3b2 fix: code splitting and editor fixes (#2211)
* fix table

* fix code splitting

* fix: editor ready check

* fix codeblock/mermaid gap cursor

* fix callout
2026-05-15 02:46:54 +01:00
Philipinho e41518a93d fix type 2026-05-14 14:49:02 +01:00
Peter Tripp 932c1ad5b7 Better trash (#2190)
* Better trash

I recently lost a bunch of time editing and searching for pages that were actually in the Trash. Docmost intentionally tries to not link to Trashed pages, but the url of that Trashed page and any inbound links still work.  This makes it clearer when a page you are interacting with is in the Trash.

- /trash
  - Refactored banner into `trash-banner.tsx`
  - Refactored "Restore" modal into `use-restore-page-modal.tsx`
- Page (when isDeleted)
  - Add: `trash-banner.tsx`
  - Add breadcrumbs: `Parent / Child / Page (Deleted)`
  - Change: Deleted Pages are read-only
  - Replace "Move to Trash" with "Restore" in page menu (invokes `use-restore-page-modal`)

I tried very hard to keep this simple and re-use existing translation strings wherever possible.

* cleanup

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2026-05-14 14:41:10 +01:00
Julien Fontanet 82d065669d fix: page mode toggle no longer overwrites default preference (#1996)
The header edit/read toggle now controls only the current session's mode
without saving it as the user's preference. The saved preference (set in
profile settings) is applied once on initial load and sticks across page
navigations within the session, so navigating to a new page no longer
resets the mode mid-session.

Fixes #1693
2026-05-14 13:15:03 +01:00
Philip Okugbe f4af4c3fc0 feat(editor): add page break node (#2202) 2026-05-14 03:48:13 +01:00
Philip Okugbe 299a9ca3c8 fix: bug fixes (#2201)
* fix(editor): hide transclusion borders and reset spacing in read-only mode

* feat(share): add full width toggle for shared pages

* feat(share): support resizing sidebar on shared pages

* fix: auto redirect if there is only one SSO provider.
- fix tighten sso redirect
- fix share tree margin

* sync

* package overrides
2026-05-14 02:54:00 +01:00
Philip Okugbe cea9be7926 feat: table enhancement (#2191) 2026-05-14 00:37:44 +01:00
Philip Okugbe 31ed0df3f7 feat(tree): replace sidebar tree (react-aborist) with custom tree implementation (#2199)
* feat(tree): replace react-arborist with custom tree implementation

* feat(tree): keyboard arrow navigation between rows

* feat(emoji-picker): focus search input on open

* refactor(emoji): switch to @slidoapp/emoji-mart fork for accessibility

* feat(tree): Home/End and typeahead keyboard navigation

* feat(tree): roving tabindex and * to expand sibling subtrees

* feat(tree): Space activation and ARIA refinements

* fix(tree): move treeitem role to focusable row + aria-current
2026-05-13 23:01:04 +01:00
Philip Okugbe a689cca7a0 feat: page labels/tags (#2188)
* feat: labels (WIP)
* full implementation
2026-05-10 18:14:15 +01:00
Philip Okugbe 537e45bc11 feat: page details section and backlinks (#2186)
* feat: page details section and backlinks
2026-05-09 17:03:08 +01:00
Philip Okugbe bdc369fce0 feat(editor): fixed toolbar preference (#2185)
* feat(editor): fixed toolbar preference

* remove key

* cleanup translation strings

* update axios
2026-05-09 13:27:03 +01:00
Philip Okugbe 2d8b470495 feat(editor): indentation (#2174)
* switch to default codeblock tab handling

* feat(editor): indentation
2026-05-08 21:40:37 +01:00
David Gallardo c66c08fa78 fix: ignore emoji when deriving avatar initials (#2167) 2026-05-08 21:36:10 +01:00
David Gallardo 6046d04375 feat(editor): replace emoji picker with browse + search (#2171)
* feat(editor): show emoji name in suggestion list

Replace the fixed-column emoji grid with a vertical list that displays
each emoji alongside its :shortcode: name. This makes the picker more
discoverable—users can see and learn shortcodes without prior knowledge.

Changes:
- EmojiList: switch from SimpleGrid/ActionIcon to UnstyledButton list
  rows showing emoji glyph + monospace 🆔 label
- Navigation simplified to ArrowUp/ArrowDown (list has no columns)
- Results capped at 8 items for a focused, scannable dropdown
- CSS module: rename menuBtn -> menuItem, tighten padding

* feat(editor): replace SearchIndex with name/id includes search

Port the exact search algorithm from the original extension:
- Build a flat index from @emoji-mart/data: { id, name (lowercase), native }
- Filter with name.includes(q) || id.includes(q) — predictable, no
  keyword indirection
- Results capped at 5 (same as extension)
- Frequently-used emojis (sorted by usage) shown when query is empty
- Remove emoji-mart init() / SearchIndex / getEmojiDataFromNative
  dependencies; index is built lazily and cached in memory
- Remove unused GRID_COLUMNS constant

* feat(editor): emoji picker with browse and search modes

When the query is empty the picker shows a category bar with 8 tabs
(people, nature, food…) and a scrollable emoji grid. Typing after ':'
switches to a compact list that shows the glyph and :shortcode: side by
side, making it easy to discover emoji names while you type.

- Category data is loaded lazily from @emoji-mart/data and cached, so
  opening the picker more than once has no overhead
- Grid keyboard nav: arrow keys move by cell/row, Enter picks
- List keyboard nav: up/down through results, Enter picks
- Mouse hover syncs the keyboard selection index in both modes
- incrementEmojiUsage tracks picks so frequently used ones bubble up
  in future sessions

* fix(editor): polish emoji picker copy and loading

* feat: add emoji to slash command

* Add keyboard support to emoji group navigation

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2026-05-08 21:33:43 +01:00
David Gallardo 5d8c11e741 fix: sync html lang with current user locale (#2165) 2026-05-08 21:15:04 +01:00
Philip Okugbe de60aa7e61 feat: synced blocks (transclusion) (#2163)
* feat: synced blocks (transclusion)

* fix:remove name

* make placeholders smaller

* feat: enforce strict transclusion schema

* fix: scope synced blocks to workspace, gate unsync on edit permission

* fix collab module error
2026-05-08 13:23:16 +01:00
Peter Tripp c9fa6e20b3 Add alias: /toc and /ol (#2161) 2026-05-08 01:20:27 +01:00
Philipinho 3227bc6059 fix: a11y 2026-05-04 23:04:26 +01:00
Philip Okugbe dbe6c2d6ba feat: A11y fixes (#2148) 2026-05-04 21:21:37 +01:00
Philip Okugbe c247d4c1e3 feat(ee): PDF import (#2142)
* feat: replace pdfjs-dist with firecrawl-pdf-inspector

* use modified firecrawl-pdf-inspector

* feat: pdf import

* increase single file upload size limit

* use npm package

* sync

* update package
2026-05-01 14:56:39 +01:00
Philip Okugbe 641ce142df feat(ee): SCIM (#1347)
* SCIM - init (EE)

* accept db transaction

* sync

* Content parser support for scim+json

* patch scimmy

* sync

* return early if userIds is empty

* sync

* SCIM db table

* fixes

* scim tokens

* backfill

* feat(audit): add scim token events

* rename scim migration

* fix

* fix translation

* cleanup
2026-05-01 14:53:30 +01:00
Sarthak Chaturvedi 1d2486455f fix: prevent browser tab fallback in editor (#2123) 2026-05-01 13:58:51 +01:00
Philip Okugbe 09c69d7a0f feat: properly preserve table width (#2143) 2026-05-01 00:49:31 +01:00
Sarthak Chaturvedi 9943e104a5 fix(i18n): Correct German column count label rendering (#2131) 2026-05-01 00:37:59 +01:00
Peter Tripp b16f1e5a55 fix: ctrl-k behavior on macOS (#2052)
* Improve cmd-k / ctrl-k behavior

Use cmd-k on macOS/iOS for search and keep ctrl-k everywhere else.

Fixes a bug where ctrl-k on macOS, which cuts to the end of the line,
was also triggering the search prompt.

* comment submit: cmd-enter (mac) / ctrl-enter (win/linux)
2026-05-01 00:36:40 +01:00
Philipinho 01ba1add5d fix(base): restore position: relative on .headerCell
The sticky-headers refactor (cd8d1e0e) moved the header row's sticky
behavior up to .stickyBand and dropped the position rule from
.headerCell entirely — but the cell still has two absolutely-positioned
children that need it as their containing block:

  - .resizeHandle (right: 0) — without a per-cell containing block, all
    handles resolve up to .stickyBand and stack at the band's right
    edge, so only one is visible.
  - Popover.Target (inset: 0) — every header's popover anchor collapses
    to the same band-relative box, so the property menu opens at one
    fixed spot regardless of which header was clicked.

.headerCellPinned still establishes a containing block via position:
sticky, which is why the primary Title column kept working. Re-adding
position: relative on the base .headerCell fixes both the resize handle
and the property-menu popover for non-pinned columns; pinned cells are
unaffected because position: sticky later in the cascade still wins.
2026-04-29 13:47:25 +01:00
Philip Okugbe 24be90b95f fix: duplicate PDF uploads (#2139) 2026-04-29 10:01:47 +01:00
Olivier Lambert 3ecf27c6b0 fix(page-permission): make people-with-access list scroll past 4 entries (#2137)
The "People with access" list in the page share modal used
<ScrollArea mah={250}>, which caps the container height but does not
make the inner viewport scroll (no fixed height is given to the
viewport). Items beyond ~4 entries were rendered correctly but clipped
out of view.

Switches to <ScrollArea.Autosize mah={400}>, which is Mantine's
dedicated primitive for "grow with content up to a max, then scroll".

Closes #2135
2026-04-29 09:36:38 +01:00
Philipinho 5069573e8a refactor(base): fold /bases/inline-embed into /bases/create with parentPageId
The inline-embed endpoint was a thin wrapper around baseService.create:
its only differences from /bases/create were that it derived the
spaceId/workspaceId from a parent page, gated permission via
pageAccessService.validateCanEdit on that parent, and seeded inline
defaults (Title + Text 1 + Text 2 + one row). All of that fits
naturally on /bases/create as a parentPageId branch.

  - CreateBaseDto: spaceId is now optional. Either spaceId or
    parentPageId must be supplied; the controller validates the
    cross-field requirement and 400s otherwise.
  - /bases/create: with parentPageId, derive workspaceId/spaceId from
    the parent, validateCanEdit on the parent, apply inline defaults.
    Without parentPageId, gate on space-level Create, Page (the
    standalone path).
  - InlineEmbedBaseDto + createInlineEmbed deleted.
  - Slash command in the editor now POSTs /bases/create with
    { parentPageId } — the server picks up the inline branch.
2026-04-28 20:29:07 +01:00
Philipinho 936c0de7fe refactor(base): drop SpaceCaslSubject.Base, route base permissions through Page
Bases are pages (isBase=true) and the casl rules already granted the
exact same Manage/Read level on the Base subject as on Page for every
space role (admin, writer, reader). The `Base` subject was therefore
pure duplication: any caller that needed to check base access either
went through pageAccessService (which uses Page internally) or did a
direct Page-equivalent ability.cannot(..., Base) check that produced
the same outcome as Page would have.

Drop SpaceCaslSubject.Base entirely — server enum, server union,
server factory rules, client enum, client union — and switch the two
remaining direct callers to Page:

  - base.controller.ts `create` and list checks now use Page (matching
    page.controller.ts's create/list).
  - base-table.tsx's `canSave` now reads Page edit ability.

Net effect: one source of truth for "can this user view/edit/manage
content in this space," whether the content is a regular page or a
base. Existing role assignments behave identically; no migration
needed because permissions are computed per-request from the role,
not stored.
2026-04-28 19:21:46 +01:00
Philipinho 0ec30ba804 fix(base): keep view-tab context menu right-click-only, close on outside / Esc
Switching to <Menu> in the previous fix made left-click on a tab
also open the menu — Mantine Menu auto-toggles via its Target's
click handler in controlled mode, which we don't want (left-click
should switch view, only right-click should open the context menu).

Switch back to <Popover> (no auto-toggle on Target click) and wire
outside-click / Escape close paths manually with a useEffect that's
active only while the menu is open. Capture-phase mousedown so we
run before grid-container's outside-click logic. Left-click on the
tab now calls onClick (switch view) and dismisses the menu in the
same gesture if it happens to be open.
2026-04-28 19:05:39 +01:00
Philipinho 38f7ffefe0 fix(base): replace Popover with Menu for view-tab context menu
The Rename / Delete-view popover (right-click on a view tab) wasn't
closing on outside click or Escape. The container was a <Popover>
with hand-rolled <UnstyledButton> menu items — closeOnClickOutside
and closeOnEscape on Mantine Popover only fire onClose when focus is
inside the dropdown, which never happens here because the popover
opens via a context-menu (focus stays on body) and there's no
trapFocus.

Switch to Mantine <Menu>, which is purpose-built for this pattern:
closeOnClickOutside / closeOnEscape work without focus being inside,
closeOnItemClick removes the manual setMenuOpened(false) wiring on
each item, and the keyboard arrow-key navigation is free.
2026-04-28 18:41:59 +01:00
Philipinho 4b75383460 fix(base): store file-cell URLs in the editor's /api/files/{id}/{name} shape
The file-cell value was storing the raw S3 storage path returned by
attachmentService (e.g. "01944.../files/019dd.../AlgoExpert_Receipt.pdf"),
which the browser can't fetch as-is — and the cell never built it
into a clickable link anyway. Match the editor's attachment node-view
pattern: store url as "/api/files/{id}/{fileName}" on upload and
resolve it through getFileUrl when rendering. The dropdown's file
rows are now <a target="_blank"> links so the user can actually open
the attached file.

`buildFileUrl` falls back to constructing the path from id+fileName
for any pre-existing values that still carry the old shape, so
already-uploaded attachments don't break.
2026-04-28 18:34:12 +01:00
Philipinho 7dfb172d45 style(base): slim the body-grid horizontal scrollbar
The browser-default-thick scrollbar at the bottom of an inline-embed's
bodyGrid was visually competing with the table content. Drop it to an
8px thumb with a transparent track in WebKit and `scrollbar-width:
thin` in Firefox. Tokens picked from the Mantine theme so dark mode
inverts cleanly.
2026-04-28 18:30:06 +01:00
Philipinho 79394f93f5 refactor(base): drop dedicated /bases/files/upload endpoint, reuse /files/upload
Base file-cell uploads were hitting a parallel POST /bases/files/upload
endpoint that re-implemented the multipart parse, the size limit
handling, and the spaceId resolution that the standard page-attachment
endpoint (POST /files/upload) already does. The two diverged on minor
points (no audit log, no attachmentId support, slightly different
permission check) without good reason — bases are pages (isBase=true),
so the existing endpoint already handles them correctly.

Server: delete uploadBaseFile and the now-unused BaseRepo injection.

Client: route the file-cell uploader through the existing uploadFile
helper in page-service. The base's pageId is a valid page id, so the
server's pageRepo.findById succeeds and pageAccessService.validateCanEdit
runs — which lines up with the Base edit ability at the space-role
level (Manage Page and Manage Base track together for admins/writers,
Read for readers).
2026-04-28 15:51:06 +01:00
Philipinho 0532253034 fix(base): commit cell edit on click-outside, not just on Enter
CellNumber/CellText/CellEmail/CellUrl all commit their draft via
onBlur. The grid-container's document mousedown handler was clearing
editingCell synchronously when the user clicked outside, which made
React unmount the input before the native blur event reached its
onBlur listener — so the edit was silently dropped, and pressing
Enter was the only way to save.

Trigger blur() on the active element first; the cell's onBlur runs,
commits, and clears editingCell as part of its normal flow. The
trailing setEditingCell(null) is now a safety net for the case
where the active element wasn't a cell editor (no double-commit
risk because each cell guards with committedRef).
2026-04-28 15:44:22 +01:00
Philipinho 7e35936544 fix(base): align inline-embed AddRowButton to page-content edge like standalone
In embed mode the "+ New row" button rendered to the right of the
data columns instead of at the page-content edge (where it lives in
standalone). Two compounding causes:

  - position: sticky with inset-inline-start:
    var(--embed-grid-pad-left, 0). Sticky offsets are measured from
    the scroll-port, not the bodyGrid's outer edge — and the scroll-
    port already starts at the page-content edge (bodyGrid's negative
    margins and equal padding cancel out). With the variable set to
    ~200px in embed, sticky shifted the button 200px *into* the
    scroll-port. inset-inline-start: 0 keeps it at the scroll-port's
    start in both modes.
  - max-content grid item with default justify-self: stretch on a
    grid-column: 1 / -1 area has surprising placement; `justify-self:
    start` makes the inline-start anchoring explicit.

Standalone behavior is unchanged (the variable was 0 there anyway).
2026-04-28 11:51:06 +01:00
Philipinho edc7143f77 fix(base): match inline-embed placeholder skeleton to seeded 3 col / 1 row shape
The placeholder rendered a default 10×6 BaseTableSkeleton while
waiting on the create-base API, then swapped to the real table once
the response landed. Because the inline-embed flow now seeds
Title + Text 1 + Text 2 with one default row, the real table is 3×1
— the swap visibly collapsed a large fake table down to a small
empty one. The scroll didn't jump (initialOffset takes care of that)
but the flicker was jarring.

Re-introduce rows + columns props on BaseTableSkeleton (default still
10 / 6 so other call sites are unaffected) and pass rows=1 columns=3
from the inline-embed placeholder so the swap is visually stable.
2026-04-28 11:34:08 +01:00