feat(ee): bases

Table and kanban UI, formula engine package, and the base-embed editor extension
This commit is contained in:
Philipinho
2026-06-14 01:29:06 +01:00
parent d86d51c27e
commit 4e5bff6d55
233 changed files with 22278 additions and 141 deletions
@@ -10,7 +10,7 @@ import {
Button,
} from "@mantine/core";
import { Link } from "react-router-dom";
import { buildPageUrl } from "@/features/page/page.utils";
import { buildPageUrl, getPageTitle } from "@/features/page/page.utils";
import { formattedDate } from "@/lib/time";
import { useFavoritesQuery } from "@/features/favorite/queries/favorite-query";
import { IconFileDescription, IconStar } from "@tabler/icons-react";
@@ -82,7 +82,7 @@ export default function FavoritesPage() {
</ThemeIcon>
)}
<Text fw={500} size="md" lineClamp={1}>
{fav.page.title || t("Untitled")}
{getPageTitle(fav.page.title, undefined, t)}
</Text>
</Group>
</UnstyledButton>
+67 -1
View File
@@ -1,6 +1,7 @@
import { useParams } from "react-router-dom";
import { usePageQuery } from "@/features/page/queries/page-query";
import { FullEditor } from "@/features/editor/full-editor";
import { TitleEditor } from "@/features/editor/title-editor";
import HistoryModal from "@/features/page-history/components/history-modal";
import { Helmet } from "react-helmet-async";
import PageHeader from "@/features/page/components/header/page-header.tsx";
@@ -13,7 +14,12 @@ 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 { BaseView } from "@/ee/base/components/base-view";
import { useHasFeature } from "@/ee/hooks/use-feature";
import { Feature } from "@/ee/features";
import { getPageTitle } from "@/features/page/page.utils";
const MemoizedFullEditor = React.memo(FullEditor);
const MemoizedTitleEditor = React.memo(TitleEditor);
const MemoizedPageHeader = React.memo(PageHeader);
const MemoizedHistoryModal = React.memo(HistoryModal);
@@ -52,6 +58,7 @@ function PageContent({ pageSlug }: { pageSlug: string | undefined }) {
} = usePageQuery({ pageId: extractPageSlugId(pageSlug) });
const { data: space } = useGetSpaceBySlugQuery(page?.space?.slug);
const hasBases = useHasFeature(Feature.BASES);
const canEdit = !page?.deletedAt && (page?.permissions?.canEdit ?? false);
const canComment =
canEdit ||
@@ -90,11 +97,70 @@ function PageContent({ pageSlug }: { pageSlug: string | undefined }) {
return <></>;
}
if (page?.isBase) {
return (
<div
className="base-page-root"
style={{
display: "flex",
flexDirection: "column",
// Height: see `.base-page-root` in core.css.
// Clear the fixed PageHeader (breadcrumb) plus a little extra so the
// pinned column-header row isn't tucked half under it.
paddingTop: "calc(var(--page-header-height) + 6px)",
}}
>
<Helmet>
<title>{`${page?.icon || ""} ${getPageTitle(page?.title, page?.isBase, t)}`}</title>
</Helmet>
<MemoizedPageHeader readOnly={!canEdit} />
<div
style={{
flex: 1,
minHeight: 0,
display: "flex",
flexDirection: "column",
paddingInline: 24,
}}
>
<div
style={{
flex: 1,
minHeight: 0,
display: "flex",
flexDirection: "column",
}}
>
<BaseView
pageId={page.id}
editable={hasBases && canEdit}
titleSlot={
<div
className="base-page-title"
style={{ paddingTop: 2, paddingBottom: 6 }}
>
<MemoizedTitleEditor
pageId={page.id}
slugId={page.slugId}
title={page.title}
spaceSlug={page.space?.slug ?? ""}
editable={hasBases && canEdit}
isBase
/>
</div>
}
/>
</div>
</div>
</div>
);
}
return (
page && (
<div>
<Helmet>
<title>{`${page?.icon || ""} ${page?.title || t("untitled")}`}</title>
<title>{`${page?.icon || ""} ${getPageTitle(page?.title, page?.isBase, t)}`}</title>
</Helmet>
<MemoizedPageHeader readOnly={!canEdit} />