Commit Graph

1114 Commits

Author SHA1 Message Date
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 ec51ca7815 fix request ip 2026-05-07 22:09:32 +01:00
Philipinho 2b63137217 mail 2026-05-07 18:13:24 +01:00
Philipinho 3227bc6059 fix: a11y 2026-05-04 23:04:26 +01:00
Philip Okugbe 73dc62bca3 update react-email (#2149) 2026-05-04 22:26:53 +01:00
Philipinho 3c74bb3dee update package 2026-05-04 22:09:19 +01:00
Philip Okugbe dbe6c2d6ba feat: A11y fixes (#2148) 2026-05-04 21:21:37 +01:00
Sarthak Chaturvedi fe18f22dc6 fix: prevent code block deletion when adding inline comments in read mode (#2146) 2026-05-04 21:14:21 +01:00
Philipinho fcef0c6b96 fix: S3 2026-05-04 20:57:35 +01:00
Philipinho 17f3158a3b update aws packages 2026-05-01 20:00:20 +01:00
Philipinho b74ca00bfd sync 2026-05-01 14:57:32 +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
Philipinho a0aea43e25 feat(saml): allow disabling RequestedAuthnContext via env var
Adds SAML_DISABLE_REQUESTED_AUTHN_CONTEXT env var, passed through
    to the SAML strategy's disableRequestedAuthnContext option.
    Defaults to existing behavior (element sent). Set to true to omit
    the element when the IdP authenticates the user with a method that
    does not match (e.g. MFA, FIDO, passwordless), which would
    otherwise cause AADSTS75011 with Microsoft Entra ID.
2026-05-01 11:47:03 +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 d6575a1cf8 chore(base): drop redundant 'private' section divider comment in base-ws 2026-04-28 20:31:07 +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 0615bcf222 refactor(base): route WS subscribe through pageAccessService
BaseWsService.subscribe was the last surface that didn't go through
the page-permission system. It checked authorization with a bespoke
canReadBaseSpace(userId, spaceId) — which queried space membership
directly and accepted ANY space role — so a user with a per-base
restriction (revoked access via pagePermissionRepo) could still
stream live updates and presence for a base they couldn't otherwise
read.

Replace it with pageAccessService.validateCanView(base, user) — the
same gate the HTTP endpoints (info, list, rows query, etc.) and the
page collab WS already use. Bases are pages structurally (isBase=true),
so reusing the page validator keeps them on a single permission code
path.

Drops the now-unused SpaceMemberRepo / findHighestUserSpaceRole
imports; injects UserRepo + PageAccessService instead (both are
globally provided modules, no DI changes needed).
2026-04-28 19:28:52 +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
Philipinho dac65914ed chore(base): drop pre-virtualizer-fix scroll-jump workarounds
Two pieces of code added under the wrong height-mismatch theory of
the inline-embed scroll jump are no longer load-bearing now that the
real cause (virtual-core's _willUpdate calling _scrollToOffset(NaN))
is fixed by the initialOffset seed in grid-container.tsx:

  - BaseTableSkeleton's `rows` prop, whose only consumer was the
    "creating database" placeholder passing rows=0 to "match" the
    eventual empty-base height. Reverted to a fixed 10-row skeleton.
  - The Database slash command's React Query cache prefill of
    ["bases", id] and ["base-rows", id, …] (~30 lines), which
    existed to skip BaseTable's own loading skeleton on swap. The
    create endpoint return type is back to { id }.

The placeholder approach (pendingKey + setNodeMarkup patch) stays —
that's what gives the user visible state during the create request,
unrelated to the scroll jump.
2026-04-28 11:29:28 +01:00
Philipinho e9926d5cef feat(base): seed inline-embed bases with two extra text columns and one row
A freshly-created inline-embed used to render with only the primary
"Title" column and zero rows — visually it looks more like a broken
widget than a database, so users always had to do at least three
manual setup steps before the embed conveyed its purpose.

BaseService.create now accepts an optional `defaults` arg for
`extraTextProperties` and `defaultRows`; both extras are inserted in
the same transaction as the page/property/view so a half-built base
can never slip out. The inline-embed controller passes
{ extraTextProperties: 2, defaultRows: 1 } so the embed lands as
"Title + Text 1 + Text 2", with one empty row ready to type into.

Standalone base creation goes through the same code path with no
defaults, so its existing single-Title-column shape is unchanged.
2026-04-28 11:25:11 +01:00
Philipinho 4a25e787fa prevent create property popover flash 2026-04-28 10:46:27 +01:00
Philipinho 86809fc0dc fix scroll bug 2026-04-28 10:39:20 +01:00
Philipinho b411f52c18 fix(base): keep editor scroll stable when inline-embed creation completes
The placeholder rendered the full 10-row BaseTableSkeleton (~440px)
while waiting for the create response, then BaseTable mounted, ran its
own queries, and rendered the same 10-row skeleton again until rows
loaded. The actual content for a freshly-created empty base is ~112px
— so the swap shrank the doc by ~330px and on a short page the
browser clamped scrollY past the new doc bottom, manifesting as a
"jump to top of editor."

Two changes to keep the height constant end-to-end:

1. BaseTableSkeleton now accepts a `rows` prop (default 10). The
   placeholder in BaseEmbedView passes `rows={0}` so the skeleton
   matches the height of the eventual empty base shell — header row +
   AddRow button, no fake body rows.

2. The Database slash command now seeds `["bases", id]` and the
   `["base-rows", id, undefined, undefined, undefined]` infinite-query
   cache from the create response (the endpoint already returns the
   full base with properties + views; the typed return was just too
   narrow). BaseTable mounts with baseLoading/rowsLoading already
   false and skips its own skeleton — no transient grow-then-shrink
   between placeholder and final content.

End state: placeholder height ≈ rendered-empty-base height, and no
intermediate skeleton appears while BaseTable is "loading." The
scrollY clamp can't fire because the doc never shrinks.
2026-04-27 22:33:08 +01:00
Philipinho c8086b33e4 fix(base): show skeleton while inline embed is being created, anchor at slash position
The Database slash command deleted the trigger text and then awaited
the create-base API before inserting the embed. During the wait the
editor sat empty (no visible state), and by the time the embed was
inserted the editor's selection had often drifted — so the new node
landed in the wrong place and the page appeared to "jump up" when the
response arrived.

Insert a placeholder baseEmbed node synchronously at the slash position
with pageId: null and a unique pendingKey. BaseEmbedView renders
BaseTableSkeleton while pendingKey is set — same skeleton BaseTable
shows on its own initial load, so the swap to the real table is a
visual no-op. Once the API responds, look up the placeholder by its
pendingKey and patch in the real pageId via setNodeMarkup. On API
failure, remove the placeholder and surface a toast. The pendingKey
attribute is non-serializing (parseHTML returns null, renderHTML
returns {}) so a placeholder can't survive a page reload as orphan.
2026-04-27 22:02:42 +01:00