From f75779951e5c836248479f57848ff3d54fbc048d Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Sun, 24 May 2026 13:00:20 +0100 Subject: [PATCH] refactor(bases): split BaseTable into BaseView shell + ViewRenderer --- .../features/base/components/base-table.tsx | 424 +++--------------- .../features/base/components/base-view.tsx | 404 +++++++++++++++++ .../base/components/views/view-renderer.tsx | 43 ++ .../components/base-embed/base-embed-view.tsx | 4 +- apps/client/src/pages/base/base-page.tsx | 4 +- apps/client/src/pages/page/page.tsx | 4 +- 6 files changed, 503 insertions(+), 380 deletions(-) create mode 100644 apps/client/src/features/base/components/base-view.tsx create mode 100644 apps/client/src/features/base/components/views/view-renderer.tsx diff --git a/apps/client/src/features/base/components/base-table.tsx b/apps/client/src/features/base/components/base-table.tsx index d0674e715..8de15dff1 100644 --- a/apps/client/src/features/base/components/base-table.tsx +++ b/apps/client/src/features/base/components/base-table.tsx @@ -1,391 +1,67 @@ -import { useCallback, useEffect, useMemo, useRef } from "react"; -import { Text, Stack } from "@mantine/core"; -import { notifications } from "@mantine/notifications"; -import { useAtom } from "jotai"; -import { IconDatabase } from "@tabler/icons-react"; -import { useTranslation } from "react-i18next"; -import { reorder } from "@atlaskit/pragmatic-drag-and-drop/reorder"; -import { generateJitteredKeyBetween } from "fractional-indexing-jittered"; -import { useBaseQuery } from "@/features/base/queries/base-query"; -import { useBaseSocket } from "@/features/base/hooks/use-base-socket"; -import { - FilterGroup, - ViewSortConfig, -} from "@/features/base/types/base.types"; -import { - useBaseRowsQuery, - useBaseRowsCountQuery, - flattenRows, -} from "@/features/base/queries/base-row-query"; -import { useUpdateRowMutation } from "@/features/base/queries/base-row-query"; -import { useCreateRowMutation } from "@/features/base/queries/base-row-query"; -import { useReorderRowMutation } from "@/features/base/queries/base-row-query"; -import { - useCreateViewMutation, - useUpdateViewMutation, -} from "@/features/base/queries/base-view-query"; -import { activeViewIdAtomFamily } from "@/features/base/atoms/base-atoms"; -import { useBaseTable } from "@/features/base/hooks/use-base-table"; -import { useRowSelection } from "@/features/base/hooks/use-row-selection"; -import useCurrentUser from "@/features/user/hooks/use-current-user"; -import { useViewDraft } from "@/features/base/hooks/use-view-draft"; -import { useSpaceQuery } from "@/features/space/queries/space-query"; -import { useSpaceAbility } from "@/features/space/permissions/use-space-ability"; -import { - SpaceCaslAction, - SpaceCaslSubject, -} from "@/features/space/permissions/permissions.type"; import { GridContainer } from "@/features/base/components/grid/grid-container"; -import { BaseToolbar } from "@/features/base/components/base-toolbar"; -import { BaseViewDraftBanner } from "@/features/base/components/base-view-draft-banner"; -import { BaseTableSkeleton } from "@/features/base/components/base-table-skeleton"; -import classes from "@/features/base/styles/grid.module.css"; +import { Table } from "@tanstack/react-table"; +import { + IBase, + IBaseRow, + IBaseView, +} from "@/features/base/types/base.types"; type BaseTableProps = { + base: IBase; + rows: IBaseRow[]; + effectiveView: IBaseView | undefined; + table: Table; pageId: string; embedded?: boolean; + hasNextPage: boolean; + isFetchingNextPage: boolean; + onFetchNextPage: () => void; + onCellUpdate: (rowId: string, propertyId: string, value: unknown) => void; + onAddRow: () => void; + onColumnReorder: (columnId: string, finishIndex: number) => void; + onResizeEnd: () => void; + onRowReorder: ( + rowId: string, + targetRowId: string, + dropPosition: "above" | "below", + ) => void; + persistViewConfig: () => void; + scrollportRef: React.RefObject; + stickyBandPrelude?: React.ReactNode; }; -export function BaseTable({ pageId, embedded }: BaseTableProps) { - const { t } = useTranslation(); - // Subscribe to the base's realtime room so other clients' edits, - // schema changes, and async-job completions reconcile into our cache. - useBaseSocket(pageId); - const { data: base, isLoading: baseLoading, error: baseError } = useBaseQuery(pageId); - - const [activeViewId, setActiveViewId] = useAtom(activeViewIdAtomFamily(pageId)) as unknown as [string | null, (val: string | null) => void]; - - const views = base?.views ?? []; - const activeView = useMemo(() => { - if (!views.length) return undefined; - return views.find((v) => v.id === activeViewId) ?? views[0]; - }, [views, activeViewId]); - - const { data: currentUser } = useCurrentUser(); - const { - draft: _draft, - effectiveFilter, - effectiveSorts, - isDirty, - setFilter: setDraftFilter, - setSorts: setDraftSorts, - reset: resetDraft, - buildPromotedConfig, - } = useViewDraft({ - userId: currentUser?.user.id, - pageId, - viewId: activeView?.id, - baselineFilter: activeView?.config?.filter, - baselineSorts: activeView?.config?.sorts, - }); - - // Render view: baseline merged with any local draft. Passed to - // `useBaseTable` (for table state seeding) and to the toolbar (for badge - // counts). The real `activeView` is still used as the auto-persist - // baseline so drafts can't leak into column-layout writes. - const effectiveView = useMemo( - () => - activeView - ? { - ...activeView, - config: { - ...activeView.config, - filter: effectiveFilter, - sorts: effectiveSorts, - }, - } - : undefined, - [activeView, effectiveFilter, effectiveSorts], - ); - - // Effective values drive the row query and the client-side position - // sort guard below. The old `activeView.config` reads are no longer the - // source of truth once drafts are involved. - const activeFilter = effectiveFilter; - const activeSorts = effectiveSorts; - - // `useSpaceQuery` is guarded by `enabled: !!spaceId` internally, so - // passing `""` when `base` hasn't loaded yet is safe. See - // use-history-restore.tsx for the same pattern. - const { data: space } = useSpaceQuery(base?.spaceId ?? ""); - const spaceAbility = useSpaceAbility(space?.membership?.permissions); - // Bases are pages — gate save on the same Page subject the rest of - // the app uses; the dedicated Base subject was redundant. - const canSave = spaceAbility.can( - SpaceCaslAction.Edit, - SpaceCaslSubject.Page, - ); - - // Hold the rows query until `base` has loaded. Otherwise the query - // fires once with `activeFilter` / `activeSorts` still undefined - // (a "bland" list request), then fires a second time as soon as the - // active view's config resolves — doubling network traffic on every - // base open for any view that has sort or filter. - const { data: rowsData, isLoading: rowsLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = - useBaseRowsQuery(base ? pageId : undefined, activeFilter, activeSorts); - - // Fire the count request alongside the rows query. Not rendered yet — - // this mounts the query so its cache is warm for when the toolbar - // consumes it. Gate on `currentUser` too so `useViewDraft` has had a - // chance to hydrate the persisted draft from localStorage; otherwise - // the first post-refresh count would race ahead of the user's saved - // filter and fire with baseline-only (or nothing). - const canFetchCount = !!base && !!currentUser; - useBaseRowsCountQuery( - canFetchCount ? pageId : undefined, - activeFilter, - ); - - const updateRowMutation = useUpdateRowMutation(); - const createRowMutation = useCreateRowMutation(); - const reorderRowMutation = useReorderRowMutation(); - const createViewMutation = useCreateViewMutation(); - const updateViewMutation = useUpdateViewMutation(); - - useEffect(() => { - if (activeView && activeViewId !== activeView.id) { - setActiveViewId(activeView.id); - } - }, [activeView, activeViewId, setActiveViewId]); - - const { clear: clearSelection } = useRowSelection(pageId); - useEffect(() => { - clearSelection(); - }, [pageId, activeView?.id, clearSelection]); - - const scrollportRef = useRef(null); - - const rows = useMemo(() => { - const flat = flattenRows(rowsData); - // When a sort is active, the server returns rows in the requested - // sort order via keyset pagination. Re-sorting by `position` on the - // client would override that with fractional-index order — visibly - // breaking the sort as more pages load. Only apply the position - // sort when no view sort is active (where it keeps - // optimistically-created and ws-pushed rows in place without a - // refetch). - if (activeSorts && activeSorts.length > 0) { - return flat; - } - return flat.sort((a, b) => - a.position < b.position ? -1 : a.position > b.position ? 1 : 0, - ); - }, [rowsData, activeSorts]); - - const { table, persistViewConfig } = useBaseTable(base, rows, effectiveView, { - baselineConfig: activeView?.config, - }); - - const handleCellUpdate = useCallback( - (rowId: string, propertyId: string, value: unknown) => { - updateRowMutation.mutate({ - rowId, - pageId, - cells: { [propertyId]: value }, - }); - }, - [pageId, updateRowMutation], - ); - - const handleAddRow = useCallback(() => { - createRowMutation.mutate({ pageId }); - }, [pageId, createRowMutation]); - - const handleViewChange = useCallback( - (viewId: string) => { - setActiveViewId(viewId); - }, - [setActiveViewId], - ); - - const handleAddView = useCallback(() => { - createViewMutation.mutate({ - pageId, - name: t("New view"), - type: "table", - }); - }, [pageId, createViewMutation, t]); - - const handleColumnReorder = useCallback( - (columnId: string, finishIndex: number) => { - const order = table.getState().columnOrder; - const startIndex = order.indexOf(columnId); - if (startIndex === -1 || startIndex === finishIndex) return; - table.setColumnOrder(reorder({ list: order, startIndex, finishIndex })); - persistViewConfig(); - }, - [table, persistViewConfig], - ); - - const handleResizeEnd = useCallback(() => { - persistViewConfig(); - }, [persistViewConfig]); - - const handleDraftSortsChange = useCallback( - (sorts: ViewSortConfig[] | undefined) => { - setDraftSorts(sorts && sorts.length > 0 ? sorts : undefined); - }, - [setDraftSorts], - ); - - const handleDraftFiltersChange = useCallback( - (filter: FilterGroup | undefined) => { - setDraftFilter(filter); - }, - [setDraftFilter], - ); - - const handleSaveDraft = useCallback(async () => { - if (!activeView || !base) return; - // `buildPromotedConfig` preserves all non-draft baseline fields - // (widths/order/visibility) and only overwrites filter/sorts when the - // draft has divergent values. - const config = buildPromotedConfig(activeView.config); - try { - await updateViewMutation.mutateAsync({ - viewId: activeView.id, - pageId: base.id, - config, - }); - resetDraft(); - notifications.show({ message: t("View updated for everyone") }); - } catch { - // `useUpdateViewMutation` already shows a red toast on error and - // rolls back the optimistic cache; keep the draft so the user can - // retry without re-typing. - } - }, [ - activeView, - base, - buildPromotedConfig, - resetDraft, - t, - updateViewMutation, - ]); - - const handleRowReorder = useCallback( - (rowId: string, targetRowId: string, dropPosition: "above" | "below") => { - const remainingRows = rows.filter((r) => r.id !== rowId); - const targetIndex = remainingRows.findIndex((r) => r.id === targetRowId); - if (targetIndex === -1) return; - - let lowerPos: string | null = null; - let upperPos: string | null = null; - - if (dropPosition === "above") { - lowerPos = targetIndex > 0 ? remainingRows[targetIndex - 1]?.position : null; - upperPos = remainingRows[targetIndex]?.position ?? null; - } else { - lowerPos = remainingRows[targetIndex]?.position ?? null; - upperPos = targetIndex < remainingRows.length - 1 ? remainingRows[targetIndex + 1]?.position : null; - } - - try { - let newPosition: string; - if (lowerPos && upperPos && lowerPos === upperPos) { - newPosition = generateJitteredKeyBetween(lowerPos, null); - } else { - newPosition = generateJitteredKeyBetween(lowerPos, upperPos); - } - - reorderRowMutation.mutate({ - rowId, - pageId, - position: newPosition, - }); - } catch { - // Position computation failed — skip silently - } - }, - [rows, pageId, reorderRowMutation], - ); - - if (baseLoading || rowsLoading) { - return ; - } - - if (baseError) { - return ( - - - {t("Failed to load base")} - - ); - } - - if (!base) return null; - - const banner = ( - - ); - - const toolbar = ( - - ); - - const grid = ( +export function BaseTable({ + base, + rows: _rows, + table, + pageId, + embedded, + hasNextPage, + isFetchingNextPage, + onFetchNextPage, + onCellUpdate, + onAddRow, + onColumnReorder, + onResizeEnd, + onRowReorder, + scrollportRef, + stickyBandPrelude, +}: BaseTableProps) { + return ( - {banner} - {toolbar} - - ) : null - } + stickyBandPrelude={stickyBandPrelude ?? null} /> ); - - if (embedded) { - // Inline: banner + toolbar live inside the StickyBand (passed via - // stickyBandPrelude). The page is the vertical scroll container. - return grid; - } - - // Standalone: banner + toolbar sit above the .tableScrollport, which - // is the vertical scroll container. StickyBand inside contains only - // the column-header row. - return ( -
- {banner} - {toolbar} -
- {grid} -
-
- ); } diff --git a/apps/client/src/features/base/components/base-view.tsx b/apps/client/src/features/base/components/base-view.tsx new file mode 100644 index 000000000..69d5f774e --- /dev/null +++ b/apps/client/src/features/base/components/base-view.tsx @@ -0,0 +1,404 @@ +import { useCallback, useEffect, useMemo, useRef } from "react"; +import { Text, Stack } from "@mantine/core"; +import { useAtom } from "jotai"; +import { IconDatabase } from "@tabler/icons-react"; +import { useTranslation } from "react-i18next"; +import { notifications } from "@mantine/notifications"; +import { reorder } from "@atlaskit/pragmatic-drag-and-drop/reorder"; +import { generateJitteredKeyBetween } from "fractional-indexing-jittered"; +import { useBaseQuery } from "@/features/base/queries/base-query"; +import { useBaseSocket } from "@/features/base/hooks/use-base-socket"; +import { + FilterGroup, + ViewSortConfig, +} from "@/features/base/types/base.types"; +import { + useBaseRowsQuery, + useBaseRowsCountQuery, + flattenRows, + useCreateRowMutation, + useUpdateRowMutation, + useReorderRowMutation, +} from "@/features/base/queries/base-row-query"; +import { + useCreateViewMutation, + useUpdateViewMutation, +} from "@/features/base/queries/base-view-query"; +import { activeViewIdAtomFamily } from "@/features/base/atoms/base-atoms"; +import { useBaseTable } from "@/features/base/hooks/use-base-table"; +import { useRowSelection } from "@/features/base/hooks/use-row-selection"; +import useCurrentUser from "@/features/user/hooks/use-current-user"; +import { useViewDraft } from "@/features/base/hooks/use-view-draft"; +import { useSpaceQuery } from "@/features/space/queries/space-query"; +import { useSpaceAbility } from "@/features/space/permissions/use-space-ability"; +import { + SpaceCaslAction, + SpaceCaslSubject, +} from "@/features/space/permissions/permissions.type"; +import { BaseToolbar } from "@/features/base/components/base-toolbar"; +import { BaseViewDraftBanner } from "@/features/base/components/base-view-draft-banner"; +import { BaseTableSkeleton } from "@/features/base/components/base-table-skeleton"; +import { ViewRenderer } from "@/features/base/components/views/view-renderer"; +import classes from "@/features/base/styles/grid.module.css"; + +type BaseViewProps = { + pageId: string; + embedded?: boolean; +}; + +export function BaseView({ pageId, embedded }: BaseViewProps) { + const { t } = useTranslation(); + // Subscribe to the base's realtime room so other clients' edits, + // schema changes, and async-job completions reconcile into our cache. + useBaseSocket(pageId); + const { data: base, isLoading: baseLoading, error: baseError } = + useBaseQuery(pageId); + + const [activeViewId, setActiveViewId] = useAtom( + activeViewIdAtomFamily(pageId), + ) as unknown as [string | null, (val: string | null) => void]; + + const views = base?.views ?? []; + const activeView = useMemo(() => { + if (!views.length) return undefined; + return views.find((v) => v.id === activeViewId) ?? views[0]; + }, [views, activeViewId]); + + const { data: currentUser } = useCurrentUser(); + const { + effectiveFilter, + effectiveSorts, + isDirty, + setFilter: setDraftFilter, + setSorts: setDraftSorts, + reset: resetDraft, + buildPromotedConfig, + } = useViewDraft({ + userId: currentUser?.user.id, + pageId, + viewId: activeView?.id, + baselineFilter: activeView?.config?.filter, + baselineSorts: activeView?.config?.sorts, + }); + + // Render view: baseline merged with any local draft. Passed to + // `useBaseTable` (for table state seeding) and to the toolbar (for badge + // counts). The real `activeView` is still used as the auto-persist + // baseline so drafts can't leak into column-layout writes. + const effectiveView = useMemo( + () => + activeView + ? { + ...activeView, + config: { + ...activeView.config, + filter: effectiveFilter, + sorts: effectiveSorts, + }, + } + : undefined, + [activeView, effectiveFilter, effectiveSorts], + ); + + // Effective values drive the row query and the client-side position + // sort guard below. The old `activeView.config` reads are no longer the + // source of truth once drafts are involved. + const activeFilter = effectiveFilter; + const activeSorts = effectiveSorts; + + // `useSpaceQuery` is guarded by `enabled: !!spaceId` internally, so + // passing `""` when `base` hasn't loaded yet is safe. See + // use-history-restore.tsx for the same pattern. + const { data: space } = useSpaceQuery(base?.spaceId ?? ""); + const spaceAbility = useSpaceAbility(space?.membership?.permissions); + // Bases are pages — gate save on the same Page subject the rest of + // the app uses; the dedicated Base subject was redundant. + const canSave = spaceAbility.can( + SpaceCaslAction.Edit, + SpaceCaslSubject.Page, + ); + + // Hold the rows query until `base` has loaded. Otherwise the query + // fires once with `activeFilter` / `activeSorts` still undefined + // (a "bland" list request), then fires a second time as soon as the + // active view's config resolves — doubling network traffic on every + // base open for any view that has sort or filter. + const { + data: rowsData, + isLoading: rowsLoading, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useBaseRowsQuery(base ? pageId : undefined, activeFilter, activeSorts); + + // Fire the count request alongside the rows query. Not rendered yet — + // this mounts the query so its cache is warm for when the toolbar + // consumes it. Gate on `currentUser` too so `useViewDraft` has had a + // chance to hydrate the persisted draft from localStorage; otherwise + // the first post-refresh count would race ahead of the user's saved + // filter and fire with baseline-only (or nothing). + const canFetchCount = !!base && !!currentUser; + useBaseRowsCountQuery(canFetchCount ? pageId : undefined, activeFilter); + + const updateRowMutation = useUpdateRowMutation(); + const createRowMutation = useCreateRowMutation(); + const reorderRowMutation = useReorderRowMutation(); + const createViewMutation = useCreateViewMutation(); + const updateViewMutation = useUpdateViewMutation(); + + useEffect(() => { + if (activeView && activeViewId !== activeView.id) { + setActiveViewId(activeView.id); + } + }, [activeView, activeViewId, setActiveViewId]); + + const { clear: clearSelection } = useRowSelection(pageId); + useEffect(() => { + clearSelection(); + }, [pageId, activeView?.id, clearSelection]); + + const scrollportRef = useRef(null); + + const rows = useMemo(() => { + const flat = flattenRows(rowsData); + // When a sort is active, the server returns rows in the requested + // sort order via keyset pagination. Re-sorting by `position` on the + // client would override that with fractional-index order — visibly + // breaking the sort as more pages load. Only apply the position + // sort when no view sort is active (where it keeps + // optimistically-created and ws-pushed rows in place without a + // refetch). + if (activeSorts && activeSorts.length > 0) { + return flat; + } + return flat.sort((a, b) => + a.position < b.position ? -1 : a.position > b.position ? 1 : 0, + ); + }, [rowsData, activeSorts]); + + const { table, persistViewConfig } = useBaseTable(base, rows, effectiveView, { + baselineConfig: activeView?.config, + }); + + const handleCellUpdate = useCallback( + (rowId: string, propertyId: string, value: unknown) => { + updateRowMutation.mutate({ + rowId, + pageId, + cells: { [propertyId]: value }, + }); + }, + [pageId, updateRowMutation], + ); + + const handleAddRow = useCallback(() => { + createRowMutation.mutate({ pageId }); + }, [pageId, createRowMutation]); + + const handleViewChange = useCallback( + (viewId: string) => { + setActiveViewId(viewId); + }, + [setActiveViewId], + ); + + const handleAddView = useCallback(() => { + createViewMutation.mutate({ + pageId, + name: t("New view"), + type: "table", + }); + }, [pageId, createViewMutation, t]); + + const handleColumnReorder = useCallback( + (columnId: string, finishIndex: number) => { + const order = table.getState().columnOrder; + const startIndex = order.indexOf(columnId); + if (startIndex === -1 || startIndex === finishIndex) return; + table.setColumnOrder(reorder({ list: order, startIndex, finishIndex })); + persistViewConfig(); + }, + [table, persistViewConfig], + ); + + const handleResizeEnd = useCallback(() => { + persistViewConfig(); + }, [persistViewConfig]); + + const handleDraftSortsChange = useCallback( + (sorts: ViewSortConfig[] | undefined) => { + setDraftSorts(sorts && sorts.length > 0 ? sorts : undefined); + }, + [setDraftSorts], + ); + + const handleDraftFiltersChange = useCallback( + (filter: FilterGroup | undefined) => { + setDraftFilter(filter); + }, + [setDraftFilter], + ); + + const handleSaveDraft = useCallback(async () => { + if (!activeView || !base) return; + // `buildPromotedConfig` preserves all non-draft baseline fields + // (widths/order/visibility) and only overwrites filter/sorts when the + // draft has divergent values. + const config = buildPromotedConfig(activeView.config); + try { + await updateViewMutation.mutateAsync({ + viewId: activeView.id, + pageId: base.id, + config, + }); + resetDraft(); + notifications.show({ message: t("View updated for everyone") }); + } catch { + // `useUpdateViewMutation` already shows a red toast on error and + // rolls back the optimistic cache; keep the draft so the user can + // retry without re-typing. + } + }, [ + activeView, + base, + buildPromotedConfig, + resetDraft, + t, + updateViewMutation, + ]); + + const handleRowReorder = useCallback( + (rowId: string, targetRowId: string, dropPosition: "above" | "below") => { + const remainingRows = rows.filter((r) => r.id !== rowId); + const targetIndex = remainingRows.findIndex((r) => r.id === targetRowId); + if (targetIndex === -1) return; + + let lowerPos: string | null = null; + let upperPos: string | null = null; + if (dropPosition === "above") { + lowerPos = + targetIndex > 0 ? remainingRows[targetIndex - 1]?.position : null; + upperPos = remainingRows[targetIndex]?.position ?? null; + } else { + lowerPos = remainingRows[targetIndex]?.position ?? null; + upperPos = + targetIndex < remainingRows.length - 1 + ? remainingRows[targetIndex + 1]?.position + : null; + } + + try { + let newPosition: string; + if (lowerPos && upperPos && lowerPos === upperPos) { + newPosition = generateJitteredKeyBetween(lowerPos, null); + } else { + newPosition = generateJitteredKeyBetween(lowerPos, upperPos); + } + reorderRowMutation.mutate({ rowId, pageId, position: newPosition }); + } catch { + // Position computation failed — skip silently. + } + }, + [rows, pageId, reorderRowMutation], + ); + + if (baseLoading || rowsLoading) { + return ; + } + if (baseError) { + return ( + + + {t("Failed to load base")} + + ); + } + if (!base) return null; + + const banner = ( + + ); + + const toolbar = ( + + ); + + if (embedded) { + // Inline: banner + toolbar live inside the StickyBand (passed via + // stickyBandPrelude). The page is the vertical scroll container. + return ( + + {banner} + {toolbar} + + } + /> + ); + } + + // Standalone: banner + toolbar sit above the .tableScrollport, which + // is the vertical scroll container. StickyBand inside contains only + // the column-header row. + return ( +
+ {banner} + {toolbar} +
+ +
+
+ ); +} diff --git a/apps/client/src/features/base/components/views/view-renderer.tsx b/apps/client/src/features/base/components/views/view-renderer.tsx new file mode 100644 index 000000000..f2a164f54 --- /dev/null +++ b/apps/client/src/features/base/components/views/view-renderer.tsx @@ -0,0 +1,43 @@ +import { Table } from "@tanstack/react-table"; +import { + IBase, + IBaseRow, + IBaseView, +} from "@/features/base/types/base.types"; +import { BaseTable } from "@/features/base/components/base-table"; + +type ViewRendererProps = { + base: IBase; + rows: IBaseRow[]; + effectiveView: IBaseView | undefined; + table: Table; + pageId: string; + embedded?: boolean; + hasNextPage: boolean; + isFetchingNextPage: boolean; + onFetchNextPage: () => void; + onCellUpdate: (rowId: string, propertyId: string, value: unknown) => void; + onAddRow: () => void; + onColumnReorder: (columnId: string, finishIndex: number) => void; + onResizeEnd: () => void; + onRowReorder: ( + rowId: string, + targetRowId: string, + dropPosition: "above" | "below", + ) => void; + persistViewConfig: () => void; + scrollportRef: React.RefObject; + stickyBandPrelude?: React.ReactNode; +}; + +export function ViewRenderer(props: ViewRendererProps) { + const viewType = props.effectiveView?.type ?? "table"; + + if (viewType === "table") { + return ; + } + + // Kanban added in a later task; until then, fall back to the table so + // selecting a kanban view never produces a blank page. + return ; +} diff --git a/apps/client/src/features/editor/components/base-embed/base-embed-view.tsx b/apps/client/src/features/editor/components/base-embed/base-embed-view.tsx index a0a7ebfa2..7845e1a26 100644 --- a/apps/client/src/features/editor/components/base-embed/base-embed-view.tsx +++ b/apps/client/src/features/editor/components/base-embed/base-embed-view.tsx @@ -1,7 +1,7 @@ import { NodeViewWrapper, NodeViewProps } from "@tiptap/react"; import { Box, Text } from "@mantine/core"; import { useEffect, useRef } from "react"; -import { BaseTable } from "@/features/base/components/base-table"; +import { BaseView } from "@/features/base/components/base-view"; import { BaseTableSkeleton } from "@/features/base/components/base-table-skeleton"; import { useBaseQuery } from "@/features/base/queries/base-query"; @@ -99,7 +99,7 @@ export function BaseEmbedView({ node }: NodeViewProps) { ); } else { - content = ; + content = ; } return ( diff --git a/apps/client/src/pages/base/base-page.tsx b/apps/client/src/pages/base/base-page.tsx index ef3c42baf..2fb5a5c4f 100644 --- a/apps/client/src/pages/base/base-page.tsx +++ b/apps/client/src/pages/base/base-page.tsx @@ -1,6 +1,6 @@ import { useParams } from "react-router-dom"; import { Container, Title, Text, Stack } from "@mantine/core"; -import { BaseTable } from "@/features/base/components/base-table"; +import { BaseView } from "@/features/base/components/base-view"; import { useBaseQuery } from "@/features/base/queries/base-query"; export default function BasePage() { @@ -26,7 +26,7 @@ export default function BasePage() { {base.icon ? `${base.icon} ` : ""}{base.name} )} - + ); } diff --git a/apps/client/src/pages/page/page.tsx b/apps/client/src/pages/page/page.tsx index 029716ff1..a0242e7a9 100644 --- a/apps/client/src/pages/page/page.tsx +++ b/apps/client/src/pages/page/page.tsx @@ -14,7 +14,7 @@ import { IconAlertTriangle, IconFileOff } from "@tabler/icons-react"; import { Button } from "@mantine/core"; import { Link } from "react-router-dom"; import { ErrorBoundary } from "react-error-boundary"; -import { BaseTable } from "@/features/base/components/base-table"; +import { BaseView } from "@/features/base/components/base-view"; const MemoizedFullEditor = React.memo(FullEditor); const MemoizedTitleEditor = React.memo(TitleEditor); const MemoizedPageHeader = React.memo(PageHeader); @@ -139,7 +139,7 @@ function PageContent({ pageSlug }: { pageSlug: string | undefined }) { flexDirection: "column", }} > - +