Commit Graph

1188 Commits

Author SHA1 Message Date
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
Philipinho bfa85b9835 fix(base): scope per-base UI atoms by pageId to prevent embed render loops
Two BaseTable instances on the same page (e.g. multiple base embeds in
one document) shared the same global jotai atoms for activeViewId,
editingCell, property menu state, and row selection. Each instance's
useEffect that synced activeViewId would clobber the other's value
every render, pinning React into a "Maximum update depth exceeded"
loop.

Convert every UI atom in base-atoms.ts to an atomFamily keyed by pageId
so each base owns its own scope, and thread pageId through the grid
component tree (GridRow, GridCell, GridHeaderCell, RowNumberCell,
RowNumberHeaderCell, PropertyMenuContent) plus useRowSelection so each
consumer reaches the per-base atom. use-base-socket already had pageId
in scope; its store.get/store.set calls now resolve through
selectedRowIdsAtomFamily(pageId) too.
2026-04-27 21:35:39 +01:00
Philipinho cd8d1e0ed8 feat(base): make column headers sticky in standalone and inline contexts
Split the unified .gridWrapper into a sticky band (containing column
headers, plus banner + toolbar in inline) and a body grid that owns
horizontal scroll. The band's vertical sticky anchor is automatic CSS:
the table's scrollport in standalone, the page in inline. A small
useHorizontalScrollSync hook mirrors body scrollLeft onto the header
and turns wheel-on-header into pan-on-body.
2026-04-27 20:25:13 +01:00
Philipinho 1cfd0fb2c4 style(base): add tableScrollport / stickyBand / headerGrid / bodyGrid classes 2026-04-27 14:07:20 +01:00
Philipinho fc13520322 feat(base): add useHorizontalScrollSync hook 2026-04-27 13:59:07 +01:00
Philipinho 3ad666adad refactor(base): use --page-header-height var in standalone layout 2026-04-27 13:56:24 +01:00
Philipinho a645ab947b style: introduce --page-header-height variable 2026-04-27 13:52:49 +01:00
Philipinho 6a89212e32 fix(base): strip global ProseMirror 3rem horizontal padding from full-page base title
core.css globally pads .ProseMirror with 3rem on each side, so the
TitleEditor's content sat 48px further in than the table below. Add
a .base-page-title scope that resets the padding to 0 (only on the
full-page base title wrapper), so the outer 24px paddingInline is
the single source of horizontal positioning. Doc pages and the
embedded BaseTable are unaffected — neither uses the new class.
2026-04-27 11:29:17 +01:00
Philipinho 701ae5c78d fix(base): align title and table at the same left edge with shared horizontal padding
Wrap both the title editor and the BaseTable in one outer flex
container that owns the horizontal padding (24px on each side).
Each child only sets vertical spacing — they no longer fight over
their own left positions, so the title's left edge now lines up
with the toolbar / first column header below.
2026-04-27 05:10:01 +01:00
Philipinho 1de982c95b fix(base): add horizontal breathing space around full-page base title and table 2026-04-27 05:06:31 +01:00
Philipinho 9eee7bc12c fix(base): left-align full-page base title with the table below 2026-04-27 05:04:38 +01:00
Philipinho d476d0b8ba feat(base): add editable page title above the full-page base view
Mirror the doc-page layout — the base name is editable via the same
TitleEditor used on document pages, sitting in a centered Container
above the table. The base table fills the remaining space below via
flex: 1, so the toolbar and grid stay anchored to the bottom of the
viewport instead of fighting the page header.
2026-04-27 05:02:13 +01:00
Philipinho 361afc2426 feat(editor-ext): block typing/paste from replacing a selected base embed
Add a ProseMirror plugin with handleTextInput and handlePaste that
return true (handled, no-op) when the current selection is a
NodeSelection of the base embed. Pairs with the existing Backspace/
Delete keyboard guard — between them the two accidental-deletion
paths (focus + delete key, focus + type a character) are blocked.

Other deletion routes still work: range selections covering the
node, programmatic deletes, and so on. Pressing an arrow key
deselects the node so the user can type elsewhere.
2026-04-27 04:53:36 +01:00
Philipinho 144838aa89 feat(editor-ext): block accidental delete of selected base embed
Add Backspace/Delete keyboard shortcuts on the BaseEmbed node that
return true (handled, no-op) when the current selection is a
NodeSelection of this node — the 'click the embed and hit delete'
accidental path. Other deletion paths (range selections covering
the node, programmatic transactions, sync) still go through normally.
2026-04-27 04:51:18 +01:00
Philipinho 00e9d1d724 feat(base): add right-side scroll headroom on inline embed grid
Mirror the leftward padding with --embed-grid-pad-right = extendRight,
applied as padding-right on .grid. The user can now pan past the last
column into empty space — same shape as Notion's behavior. Standalone
full-page bases are unaffected (var unset → 0).
2026-04-27 04:44:47 +01:00
Philipinho 583d2d37c4 fix(base): pull AddRowButton out of grid so sticky-left actually sticks
As a child of .grid with grid-column: 1/-1, the button took the full
row width and the sticky-left offset got swallowed by the grid layout
— it scrolled along with the cells instead of anchoring to the left.

Move it to a sibling of .grid inside .gridWrapper, restyle as a
small inline-flex chip with width: max-content. Sticky-left now
keeps it pinned to page-content-left while the user scrolls
horizontally, matching the toolbar.

Add padding-bottom: 6px on .gridWrapper and a small vertical margin
on the button so the horizontal scrollbar no longer overlaps it.
2026-04-27 04:39:29 +01:00
Philipinho a992fa8a63 feat(base): keep New row button sticky during horizontal scroll
Add position: sticky + inset-inline-start to .addRowButton so the
'New row' affordance stays at the page-content edge while the user
scrolls horizontally — same shape as the toolbar staying put. The
sticky offset uses the existing --embed-grid-pad-left var: in embed
mode that's the leftward extension distance (so it sticks at
page-content-left); in standalone mode the var is 0 (sticks at the
grid's natural left edge, no visible effect when there's no scroll).

Add a body-color background and z-index: 4 so cells don't bleed
through during scroll.
2026-04-27 04:30:04 +01:00
Philipinho 7a119010c7 fix(base): suppress ProseMirror atom-node selection outline on inline embed 2026-04-27 04:23:16 +01:00
Philipinho 678c6f5f3f fix(base): drop outer panel border on inline embed grid
The .grid had a 1px outer border + small radius drawing a rounded
rectangle around the whole table. In standalone full-page mode that
reads as a panel and is fine. In an inline embed it looked like a
floating box, especially with the leftward padded area inside the
border.

Make the outer border and radius CSS vars; the embed wrapper sets
them to none/0 so inline databases get only cell-level gridlines,
matching Notion's document-style rendering. Standalone bases get
the default 1px panel frame as before.
2026-04-27 04:20:58 +01:00
Philipinho 030d3e878a feat(base): extend inline embed scroll viewport leftward, anchor first cell with grid padding
Bring back the leftward extension via negative margin-left so the
scroll viewport reaches AppShell.Main's left edge, then offset the
.grid with padding-left = extendLeft so the first cell still lines
up with page-content on load. The extended area becomes scrollable
empty space the user can pan into — same shape as Notion's inline
databases.

Done with one new CSS var (--embed-grid-pad-left) consumed by the
.grid in grid.module.css. Standalone full-page bases never set the
var, so it's a no-op there.
2026-04-27 04:13:34 +01:00
Philipinho a7105267ed fix(base): keep inline embed left edge at page-content, extend right only
Removing the leftward extension. With negative margin-left, the
grid grew leftward past page-content and the first column appeared
near the sidebar edge on load — wrong; Notion keeps the first column
aligned with page text. Drop margin-left and the extendLeft
calculation; only extend rightward toward AppShell.Main's right edge.

Leftward viewport extension (so frozen columns can lock at the
sidebar edge during scroll) becomes meaningful once we add frozen
columns. Deferred until that feature lands.
2026-04-27 04:05:18 +01:00
Philipinho 67f45ee61b fix(base): anchor inline embed extension to AppShell main, not first wider parent
The findWiderAncestor walk often stopped at a slightly-larger
intermediate (NodeViewWrapper, drag-handle wrapper, etc.) whose
left edge was almost the same as the wrapper's, so the leftward
extension came out as ~0 and the grid only grew to the right.

Use closest('main') instead — that's AppShell.Main, the layout
container whose bounds already reflect the navbar width and
sidebar collapse state. Both sides now extend to its edges.
2026-04-27 03:57:45 +01:00
Philipinho d73ac010af feat(base): use negative margin to extend inline grid past parent
The previous approach (position: relative + explicit width +
inset-inline-start) didn't physically grow the box in some flex
contexts, so the grid stayed at the parent's width. Switch to
negative margin-left / margin-right on the grid wrapper only —
with width: auto, the rendered width becomes parent + |margin|,
extending the box past the parent without any positioning hacks.

The toolbar keeps its natural parent-constrained width (no extension)
so it stays aligned with the page text above. Two CSS vars,
--embed-extend-l / --embed-extend-r, are computed on mount + on
ResizeObserver from the wrapper and the closest wider ancestor.
2026-04-27 03:51:21 +01:00
Philipinho 8132b171f4 fix(base): apply embed width-extension after base loads, not just on mount
useEffect ran once on mount with empty deps, but the wrapper div was
inside a conditional branch that only renders after the base query
resolves. Result: the ref was null when the effect ran, so nothing
extended. Move the wrapper outside the conditional so the ref is
always set, and re-run the effect when isLoading / isError / pageId
change so the extension applies once the table mounts.
2026-04-27 03:43:38 +01:00
Philipinho e0e87329f4 feat(base): inline embed extends grid + toolbar beyond page width
When a base is embedded inline in a doc page, measure the parent
container's available area and extend toolbar + grid sections to fill
it via CSS variables (--embed-width / --embed-shift / --embed-pad).
Inner content is re-padded so toolbar buttons and the first column
visually align with the page text, while the box itself reaches the
viewport edges for horizontal scroll headroom on wide databases.
Sticky inset-inline-start keeps the toolbar pinned to the page-content
edge during horizontal scroll. Standalone full-page bases are
unaffected (the embedded prop defaults to false).
2026-04-27 03:39:03 +01:00
Philipinho 8aabf86abb fix(base): make property type-picker popover scrollable with responsive width 2026-04-27 02:20:42 +01:00
Philipinho adae526627 fix type 2026-04-27 02:12:34 +01:00
Philipinho e66e18cd60 fix(base): rename remaining baseId references in attachment upload and ws utility 2026-04-27 01:57:18 +01:00
Philipinho 7f8ed733a3 feat(base): inline base embed — node registration, slash command, and renderer view 2026-04-27 01:54:59 +01:00
Philipinho 3826e5a50d feat(base): add /bases/inline-embed endpoint that creates a child base page 2026-04-27 01:54:00 +01:00
Philipinho 4a15c805f1 feat(editor-ext): add BaseEmbed TipTap node with pageId attribute 2026-04-27 01:52:58 +01:00
Philipinho 69e7bd73f2 feat(base): render table icon in sidebar for is_base=true pages
- Add isBase to SpaceTreeNode type
- Forward isBase from IPage in buildTree utility
- In the sidebar Node renderer, show IconTable when node.data.isBase
  is true and no custom emoji icon is set
2026-04-27 01:50:05 +01:00
Philipinho 19821d3aeb feat(base): page renderer dispatches to base view when page.isBase
- Add isBase to page repo baseFields so it is always selected
- Add isBase to sidebar pages query select list
- Add isBase to client IPage type
- In PageContent, render <BaseTable pageId={page.id} /> when page.isBase
  is true instead of the TipTap editor path
2026-04-27 01:49:54 +01:00
Philipinho 060bd1048f refactor(base): rename /base/:baseId param to :pageId 2026-04-27 01:44:37 +01:00
Philipinho f56af5e6b4 refactor(base): rename baseId to pageId in client components 2026-04-27 01:44:11 +01:00
Philipinho 3e9f26a8dd refactor(base): rename baseId to pageId in client atoms, hooks, types 2026-04-27 01:42:54 +01:00
Philipinho 4510543510 refactor(base): rename baseId to pageId in client services and queries 2026-04-27 01:41:17 +01:00
Philipinho 25dfb44774 refactor(base): rename baseId to pageId across WS, processors, tasks, formula, events
Renames baseId → pageId in:
- Domain event types (BaseEventBase) and all derived event types
- WS Zod schemas (wire-protocol field names now emit pageId)
- BaseWsService, BaseWsConsumers, BasePresenceService
- FormulaLockService, FormulaService
- BullMQ job interfaces (IBaseTypeConversionJob, IBaseCellGcJob, IBaseFormulaRecomputeJob)
- BaseQueueProcessor and all task functions
- Service emit-payload keys in base.service, base-property.service,
  base-row.service, base-view.service, base-csv-export.service
- Formula spec test fixtures
2026-04-27 01:38:11 +01:00
Philipinho 78d450a238 refactor(base): use PageAccessService for BaseViewController 2026-04-27 01:29:23 +01:00
Philipinho 2e7fe5bbb4 refactor(base): use PageAccessService for BaseRowController 2026-04-27 01:28:59 +01:00
Philipinho 43cf1665f5 refactor(base): use PageAccessService for BasePropertyController 2026-04-27 01:28:28 +01:00
Philipinho fd6a208235 refactor(base): use PageAccessService for BaseController permissions 2026-04-27 01:28:03 +01:00
Philipinho a53dabae70 refactor(base): rename baseId to pageId in DTOs, schemas, and services 2026-04-27 01:27:10 +01:00
Philipinho 54de3a7791 refactor(base): update task files to use renamed repo methods 2026-04-27 01:22:09 +01:00
Philipinho 16161d793b refactor(base): rename baseId to pageId in csv-export and page-resolver services 2026-04-27 01:21:07 +01:00
Philipinho 42f950e11d refactor(base): rename baseId to pageId in BaseViewService 2026-04-27 01:20:34 +01:00