mirror of
https://github.com/docmost/docmost.git
synced 2026-09-11 07:56:54 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d5f7bf4d4 |
@@ -4,7 +4,7 @@
|
||||
"version": "0.95.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"build": "tsc && vite build && node scripts/compress-dist.mjs",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"format": "prettier --write \"src/**/*.tsx\" \"src/**/*.ts\"",
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import { join, extname } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import zlib from "node:zlib";
|
||||
|
||||
const gzip = promisify(zlib.gzip);
|
||||
const brotli = promisify(zlib.brotliCompress);
|
||||
|
||||
const DIST = new URL("../dist", import.meta.url).pathname;
|
||||
// index.html is rewritten by the server at boot, so html must not be precompressed
|
||||
const COMPRESSIBLE = new Set([".js", ".css", ".svg", ".json", ".txt", ".map"]);
|
||||
const MIN_SIZE = 1024;
|
||||
|
||||
async function* walk(dir) {
|
||||
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
|
||||
const path = join(dir, entry.name);
|
||||
if (entry.isDirectory()) yield* walk(path);
|
||||
else yield path;
|
||||
}
|
||||
}
|
||||
|
||||
const files = [];
|
||||
for await (const file of walk(DIST)) {
|
||||
if (!COMPRESSIBLE.has(extname(file))) continue;
|
||||
const { size } = await fs.stat(file);
|
||||
if (size >= MIN_SIZE) files.push(file);
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const content = await fs.readFile(file);
|
||||
const [gz, br] = await Promise.all([
|
||||
gzip(content, { level: 9 }),
|
||||
brotli(content, {
|
||||
params: {
|
||||
[zlib.constants.BROTLI_PARAM_QUALITY]: 11,
|
||||
[zlib.constants.BROTLI_PARAM_SIZE_HINT]: content.length,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
await Promise.all([
|
||||
fs.writeFile(`${file}.gz`, gz),
|
||||
fs.writeFile(`${file}.br`, br),
|
||||
]);
|
||||
}),
|
||||
);
|
||||
|
||||
console.log(`precompressed ${files.length} assets (.gz/.br)`);
|
||||
+77
-45
@@ -1,60 +1,92 @@
|
||||
import { lazy, Suspense, useEffect } from "react";
|
||||
import { Navigate, Route, Routes } from "react-router-dom";
|
||||
import SetupWorkspace from "@/pages/auth/setup-workspace.tsx";
|
||||
import LoginPage from "@/pages/auth/login";
|
||||
import Home from "@/pages/dashboard/home";
|
||||
import Page from "@/pages/page/page";
|
||||
import AccountSettings from "@/pages/settings/account/account-settings";
|
||||
import WorkspaceMembers from "@/pages/settings/workspace/workspace-members";
|
||||
import WorkspaceSettings from "@/pages/settings/workspace/workspace-settings";
|
||||
import Groups from "@/pages/settings/group/groups";
|
||||
import GroupInfo from "./pages/settings/group/group-info";
|
||||
import Spaces from "@/pages/settings/space/spaces.tsx";
|
||||
import { Error404 } from "@/components/ui/error-404.tsx";
|
||||
import AccountPreferences from "@/pages/settings/account/account-preferences.tsx";
|
||||
import SpaceHome from "@/pages/space/space-home.tsx";
|
||||
import PageRedirect from "@/pages/page/page-redirect.tsx";
|
||||
import Layout from "@/components/layouts/global/layout.tsx";
|
||||
import InviteSignup from "@/pages/auth/invite-signup.tsx";
|
||||
import ForgotPassword from "@/pages/auth/forgot-password.tsx";
|
||||
import PasswordReset from "./pages/auth/password-reset";
|
||||
import Billing from "@/ee/billing/pages/billing.tsx";
|
||||
import CloudLogin from "@/ee/pages/cloud-login.tsx";
|
||||
import CreateWorkspace from "@/ee/pages/create-workspace.tsx";
|
||||
import { Error404 } from "@/components/ui/error-404.tsx";
|
||||
import { isCloud } from "@/lib/config.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Security from "@/ee/security/pages/security.tsx";
|
||||
import License from "@/ee/licence/pages/license.tsx";
|
||||
import { useRedirectToCloudSelect } from "@/ee/hooks/use-redirect-to-cloud-select.tsx";
|
||||
import SharedPage from "@/pages/share/shared-page.tsx";
|
||||
import PdfRenderPage from "@/ee/pdf-export/pdf-render-page.tsx";
|
||||
import Shares from "@/pages/settings/shares/shares.tsx";
|
||||
import ShareLayout from "@/features/share/components/share-layout.tsx";
|
||||
import ShareRedirect from "@/pages/share/share-redirect.tsx";
|
||||
import { useTrackOrigin } from "@/hooks/use-track-origin";
|
||||
import SpacesPage from "@/pages/spaces/spaces.tsx";
|
||||
import { MfaChallengePage } from "@/ee/mfa/pages/mfa-challenge-page";
|
||||
import { MfaSetupRequiredPage } from "@/ee/mfa/pages/mfa-setup-required-page";
|
||||
import SpaceTrash from "@/pages/space/space-trash.tsx";
|
||||
import UserApiKeys from "@/ee/api-key/pages/user-api-keys";
|
||||
import WorkspaceApiKeys from "@/ee/api-key/pages/workspace-api-keys";
|
||||
import AiSettings from "@/ee/ai/pages/ai-settings.tsx";
|
||||
import BasePage from "@/ee/base/pages/base-page.tsx";
|
||||
import AuditLogs from "@/ee/audit/pages/audit-logs.tsx";
|
||||
import VerifiedPages from "@/ee/page-verification/pages/verified-pages.tsx";
|
||||
import TemplateList from "@/ee/template/pages/template-list";
|
||||
import TemplateEditor from "@/ee/template/pages/template-editor";
|
||||
import FavoritesPage from "@/pages/favorites/favorites-page";
|
||||
import AiChat from "@/ee/ai-chat/pages/ai-chat.tsx";
|
||||
import VerifyEmail from "@/ee/pages/verify-email.tsx";
|
||||
import LabelPage from "@/pages/label/label-page";
|
||||
|
||||
const SetupWorkspace = lazy(() => import("@/pages/auth/setup-workspace.tsx"));
|
||||
const LoginPage = lazy(() => import("@/pages/auth/login"));
|
||||
const Home = lazy(() => import("@/pages/dashboard/home"));
|
||||
const Page = lazy(() => import("@/pages/page/page"));
|
||||
const AccountSettings = lazy(
|
||||
() => import("@/pages/settings/account/account-settings"),
|
||||
);
|
||||
const WorkspaceMembers = lazy(
|
||||
() => import("@/pages/settings/workspace/workspace-members"),
|
||||
);
|
||||
const WorkspaceSettings = lazy(
|
||||
() => import("@/pages/settings/workspace/workspace-settings"),
|
||||
);
|
||||
const Groups = lazy(() => import("@/pages/settings/group/groups"));
|
||||
const GroupInfo = lazy(() => import("./pages/settings/group/group-info"));
|
||||
const Spaces = lazy(() => import("@/pages/settings/space/spaces.tsx"));
|
||||
const AccountPreferences = lazy(
|
||||
() => import("@/pages/settings/account/account-preferences.tsx"),
|
||||
);
|
||||
const SpaceHome = lazy(() => import("@/pages/space/space-home.tsx"));
|
||||
const PageRedirect = lazy(() => import("@/pages/page/page-redirect.tsx"));
|
||||
const InviteSignup = lazy(() => import("@/pages/auth/invite-signup.tsx"));
|
||||
const ForgotPassword = lazy(() => import("@/pages/auth/forgot-password.tsx"));
|
||||
const PasswordReset = lazy(() => import("./pages/auth/password-reset"));
|
||||
const Billing = lazy(() => import("@/ee/billing/pages/billing.tsx"));
|
||||
const CloudLogin = lazy(() => import("@/ee/pages/cloud-login.tsx"));
|
||||
const CreateWorkspace = lazy(() => import("@/ee/pages/create-workspace.tsx"));
|
||||
const Security = lazy(() => import("@/ee/security/pages/security.tsx"));
|
||||
const License = lazy(() => import("@/ee/licence/pages/license.tsx"));
|
||||
const SharedPage = lazy(() => import("@/pages/share/shared-page.tsx"));
|
||||
const PdfRenderPage = lazy(() => import("@/ee/pdf-export/pdf-render-page.tsx"));
|
||||
const Shares = lazy(() => import("@/pages/settings/shares/shares.tsx"));
|
||||
const ShareLayout = lazy(
|
||||
() => import("@/features/share/components/share-layout.tsx"),
|
||||
);
|
||||
const ShareRedirect = lazy(() => import("@/pages/share/share-redirect.tsx"));
|
||||
const SpacesPage = lazy(() => import("@/pages/spaces/spaces.tsx"));
|
||||
const MfaChallengePage = lazy(() =>
|
||||
import("@/ee/mfa/pages/mfa-challenge-page").then((m) => ({
|
||||
default: m.MfaChallengePage,
|
||||
})),
|
||||
);
|
||||
const MfaSetupRequiredPage = lazy(() =>
|
||||
import("@/ee/mfa/pages/mfa-setup-required-page").then((m) => ({
|
||||
default: m.MfaSetupRequiredPage,
|
||||
})),
|
||||
);
|
||||
const SpaceTrash = lazy(() => import("@/pages/space/space-trash.tsx"));
|
||||
const UserApiKeys = lazy(() => import("@/ee/api-key/pages/user-api-keys"));
|
||||
const WorkspaceApiKeys = lazy(
|
||||
() => import("@/ee/api-key/pages/workspace-api-keys"),
|
||||
);
|
||||
const AiSettings = lazy(() => import("@/ee/ai/pages/ai-settings.tsx"));
|
||||
const BasePage = lazy(() => import("@/ee/base/pages/base-page.tsx"));
|
||||
const AuditLogs = lazy(() => import("@/ee/audit/pages/audit-logs.tsx"));
|
||||
const VerifiedPages = lazy(
|
||||
() => import("@/ee/page-verification/pages/verified-pages.tsx"),
|
||||
);
|
||||
const TemplateList = lazy(() => import("@/ee/template/pages/template-list"));
|
||||
const TemplateEditor = lazy(
|
||||
() => import("@/ee/template/pages/template-editor"),
|
||||
);
|
||||
const FavoritesPage = lazy(() => import("@/pages/favorites/favorites-page"));
|
||||
const AiChat = lazy(() => import("@/ee/ai-chat/pages/ai-chat.tsx"));
|
||||
const VerifyEmail = lazy(() => import("@/ee/pages/verify-email.tsx"));
|
||||
const LabelPage = lazy(() => import("@/pages/label/label-page"));
|
||||
|
||||
export default function App() {
|
||||
const { t } = useTranslation();
|
||||
useRedirectToCloudSelect();
|
||||
useTrackOrigin();
|
||||
|
||||
useEffect(() => {
|
||||
// warm the editor chunk so opening a page doesn't wait on the network
|
||||
const timer = setTimeout(() => import("@/pages/page/page"), 3000);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Suspense fallback={null}>
|
||||
<Routes>
|
||||
<Route index element={<Navigate to="/home" />} />
|
||||
<Route path={"/login"} element={<LoginPage />} />
|
||||
@@ -135,6 +167,6 @@ export default function App() {
|
||||
|
||||
<Route path="*" element={<Error404 />} />
|
||||
</Routes>
|
||||
</>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
import { ActionIcon, Box, Group, ScrollArea, Title, Tooltip } from "@mantine/core";
|
||||
import { IconX } from "@tabler/icons-react";
|
||||
import CommentListWithTabs from "@/features/comment/components/comment-list-with-tabs.tsx";
|
||||
import { useAtom } from "jotai";
|
||||
import { asideStateAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
import React, { ReactNode, useEffect } from "react";
|
||||
import React, { lazy, ReactNode, Suspense, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TableOfContents } from "@/features/editor/components/table-of-contents/table-of-contents.tsx";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { pageEditorAtom } from "@/features/editor/atoms/editor-atoms.ts";
|
||||
import AsideChatPanel from "@/ee/ai-chat/components/aside-chat-panel";
|
||||
import { PageDetailsAside } from "@/features/page-details/components/page-details-aside.tsx";
|
||||
import { ASIDE_PANEL_ID } from "@/hooks/use-toggle-aside.tsx";
|
||||
|
||||
const CommentListWithTabs = lazy(
|
||||
() => import("@/features/comment/components/comment-list-with-tabs.tsx"),
|
||||
);
|
||||
const TableOfContents = lazy(() =>
|
||||
import(
|
||||
"@/features/editor/components/table-of-contents/table-of-contents.tsx"
|
||||
).then((m) => ({ default: m.TableOfContents })),
|
||||
);
|
||||
const AsideChatPanel = lazy(
|
||||
() => import("@/ee/ai-chat/components/aside-chat-panel"),
|
||||
);
|
||||
const PageDetailsAside = lazy(() =>
|
||||
import("@/features/page-details/components/page-details-aside.tsx").then(
|
||||
(m) => ({ default: m.PageDetailsAside }),
|
||||
),
|
||||
);
|
||||
|
||||
export default function Aside() {
|
||||
const [{ tab, isAsideOpen }, setAsideState] = useAtom(asideStateAtom);
|
||||
const { t } = useTranslation();
|
||||
@@ -68,17 +81,19 @@ export default function Aside() {
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{tab === "comments" || tab === "chat" ? (
|
||||
component
|
||||
) : (
|
||||
<ScrollArea
|
||||
style={{ height: "85vh" }}
|
||||
scrollbarSize={5}
|
||||
type="scroll"
|
||||
>
|
||||
<div style={{ paddingBottom: "200px" }}>{component}</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
<Suspense fallback={null}>
|
||||
{tab === "comments" || tab === "chat" ? (
|
||||
component
|
||||
) : (
|
||||
<ScrollArea
|
||||
style={{ height: "85vh" }}
|
||||
scrollbarSize={5}
|
||||
type="scroll"
|
||||
>
|
||||
<div style={{ paddingBottom: "200px" }}>{component}</div>
|
||||
</ScrollArea>
|
||||
)}
|
||||
</Suspense>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -11,7 +11,10 @@ import {
|
||||
sidebarWidthAtom,
|
||||
} from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
import { SpaceSidebar } from "@/features/space/components/sidebar/space-sidebar.tsx";
|
||||
import AiChatSidebar from "@/ee/ai-chat/components/ai-chat-sidebar.tsx";
|
||||
|
||||
const AiChatSidebar = React.lazy(
|
||||
() => import("@/ee/ai-chat/components/ai-chat-sidebar.tsx"),
|
||||
);
|
||||
import { AppHeader } from "@/components/layouts/global/app-header.tsx";
|
||||
import Aside from "@/components/layouts/global/aside.tsx";
|
||||
import classes from "./app-shell.module.css";
|
||||
@@ -126,7 +129,11 @@ export default function GlobalAppShell({
|
||||
)}
|
||||
{isSpaceRoute && <SpaceSidebar />}
|
||||
{isSettingsRoute && <SettingsSidebar />}
|
||||
{isAiRoute && <AiChatSidebar />}
|
||||
{isAiRoute && (
|
||||
<React.Suspense fallback={null}>
|
||||
<AiChatSidebar />
|
||||
</React.Suspense>
|
||||
)}
|
||||
{showGlobalSidebar && <GlobalSidebar />}
|
||||
</AppShell.Navbar>
|
||||
<AppShell.Main id={MAIN_CONTENT_ID} tabIndex={-1}>
|
||||
|
||||
@@ -18,7 +18,6 @@ export type FieldProps = {
|
||||
rowId: string;
|
||||
readOnly: boolean;
|
||||
onChange: (value: unknown) => void;
|
||||
onEditingChange?: (editing: boolean) => void;
|
||||
};
|
||||
|
||||
type FieldShellProps = {
|
||||
@@ -100,10 +99,9 @@ type DetailFieldProps = {
|
||||
row: IBaseRow;
|
||||
readOnly: boolean;
|
||||
onUpdate: (propertyId: string, value: unknown) => void;
|
||||
onEditingChange: (editing: boolean) => void;
|
||||
};
|
||||
|
||||
export function DetailField({ property, row, readOnly, onUpdate, onEditingChange }: DetailFieldProps) {
|
||||
export function DetailField({ property, row, readOnly, onUpdate }: DetailFieldProps) {
|
||||
const descriptor = getDescriptor(property.type);
|
||||
const value = descriptor?.systemAccessor
|
||||
? descriptor.systemAccessor(row)
|
||||
@@ -114,7 +112,6 @@ export function DetailField({ property, row, readOnly, onUpdate, onEditingChange
|
||||
rowId: row.id,
|
||||
readOnly,
|
||||
onChange: (next: unknown) => onUpdate(property.id, next),
|
||||
onEditingChange
|
||||
};
|
||||
|
||||
switch (property.type) {
|
||||
|
||||
@@ -9,13 +9,7 @@ const normalize = (s: string) => {
|
||||
return trimmed.length ? trimmed : null;
|
||||
};
|
||||
|
||||
export function FieldLongText({
|
||||
property,
|
||||
value,
|
||||
readOnly,
|
||||
onChange,
|
||||
onEditingChange,
|
||||
}: FieldProps) {
|
||||
export function FieldLongText({ property, value, readOnly, onChange }: FieldProps) {
|
||||
const text = toText(value);
|
||||
const [draft, setDraft] = useState(text);
|
||||
const [focused, setFocused] = useState(false);
|
||||
@@ -29,7 +23,6 @@ export function FieldLongText({
|
||||
|
||||
const commit = () => {
|
||||
setFocused(false);
|
||||
onEditingChange?.(false);
|
||||
if (cancelRef.current) {
|
||||
cancelRef.current = false;
|
||||
setDraft(text);
|
||||
@@ -57,10 +50,7 @@ export function FieldLongText({
|
||||
className={classes.fieldTextarea}
|
||||
classNames={{ input: classes.fieldTextareaInput }}
|
||||
value={draft}
|
||||
onFocus={() => {
|
||||
setFocused(true);
|
||||
onEditingChange?.(true);
|
||||
}}
|
||||
onFocus={() => setFocused(true)}
|
||||
onChange={(e) => setDraft(e.currentTarget.value)}
|
||||
onBlur={commit}
|
||||
onKeyDown={(e) => {
|
||||
|
||||
@@ -11,13 +11,7 @@ import classes from "@/ee/base/styles/row-detail-modal.module.css";
|
||||
const toDraft = (value: unknown) =>
|
||||
typeof value === "number" ? String(value) : "";
|
||||
|
||||
export function FieldNumber({
|
||||
property,
|
||||
value,
|
||||
readOnly,
|
||||
onChange,
|
||||
onEditingChange,
|
||||
}: FieldProps) {
|
||||
export function FieldNumber({ property, value, readOnly, onChange }: FieldProps) {
|
||||
const typeOptions = property.typeOptions as NumberTypeOptions | undefined;
|
||||
const numValue = typeof value === "number" ? value : null;
|
||||
const [draft, setDraft] = useState(toDraft(value));
|
||||
@@ -42,7 +36,6 @@ export function FieldNumber({
|
||||
|
||||
const commit = () => {
|
||||
setFocused(false);
|
||||
onEditingChange?.(false);
|
||||
if (cancelRef.current) {
|
||||
cancelRef.current = false;
|
||||
setDraft(toDraft(value));
|
||||
@@ -61,7 +54,6 @@ export function FieldNumber({
|
||||
onFocus={() => {
|
||||
setDraft(toDraft(value));
|
||||
setFocused(true);
|
||||
onEditingChange?.(true);
|
||||
}}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
|
||||
@@ -5,13 +5,7 @@ import classes from "@/ee/base/styles/row-detail-modal.module.css";
|
||||
|
||||
const toText = (value: unknown) => (typeof value === "string" ? value : "");
|
||||
|
||||
export function FieldText({
|
||||
property,
|
||||
value,
|
||||
readOnly,
|
||||
onChange,
|
||||
onEditingChange,
|
||||
}: FieldProps) {
|
||||
export function FieldText({ property, value, readOnly, onChange }: FieldProps) {
|
||||
const text = toText(value);
|
||||
const [draft, setDraft] = useState(text);
|
||||
const [focused, setFocused] = useState(false);
|
||||
@@ -26,7 +20,6 @@ export function FieldText({
|
||||
|
||||
const commit = () => {
|
||||
setFocused(false);
|
||||
onEditingChange?.(false);
|
||||
if (cancelRef.current) {
|
||||
cancelRef.current = false;
|
||||
setDraft(text);
|
||||
@@ -61,10 +54,7 @@ export function FieldText({
|
||||
className={classes.fieldInput}
|
||||
value={draft}
|
||||
maxLength={1000}
|
||||
onFocus={() => {
|
||||
setFocused(true);
|
||||
onEditingChange?.(true);
|
||||
}}
|
||||
onFocus={() => setFocused(true)}
|
||||
onChange={(e) => setDraft(e.currentTarget.value)}
|
||||
onBlur={commit}
|
||||
onKeyDown={(e) => {
|
||||
|
||||
@@ -17,7 +17,6 @@ type PropertyRowProps = {
|
||||
onMenuOpenChange: (opened: boolean) => void;
|
||||
onMenuDirtyChange: (dirty: boolean) => void;
|
||||
onUpdate: (propertyId: string, value: unknown) => void;
|
||||
onEditingChange?: (editing: boolean) => void;
|
||||
autoFocusValue?: boolean;
|
||||
onAutoFocused?: () => void;
|
||||
};
|
||||
@@ -30,7 +29,6 @@ export function PropertyRow({
|
||||
onMenuOpenChange,
|
||||
onMenuDirtyChange,
|
||||
onUpdate,
|
||||
onEditingChange,
|
||||
autoFocusValue,
|
||||
onAutoFocused,
|
||||
}: PropertyRowProps) {
|
||||
@@ -114,7 +112,6 @@ export function PropertyRow({
|
||||
row={row}
|
||||
readOnly={!canEdit}
|
||||
onUpdate={onUpdate}
|
||||
onEditingChange={onEditingChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -75,7 +75,6 @@ export function RowDetailModal({
|
||||
|
||||
const isSaving = updateRowMutation.isPending;
|
||||
const opened = !!openRowId;
|
||||
const [editingField, setEditingField] = useState(false);
|
||||
|
||||
// One field menu open at a time, mirroring the grid header's semantics.
|
||||
// The shared closeRequest atom asks an open dirty PropertyMenuContent to
|
||||
@@ -91,7 +90,6 @@ export function RowDetailModal({
|
||||
useEffect(() => {
|
||||
setOpenMenuId(null);
|
||||
menuDirtyRef.current = false;
|
||||
setEditingField(false);
|
||||
}, [openRowId]);
|
||||
|
||||
const handleMenuDirtyChange = useCallback((dirty: boolean) => {
|
||||
@@ -295,7 +293,7 @@ export function RowDetailModal({
|
||||
row={row}
|
||||
primaryProperty={primaryProperty}
|
||||
canEdit={canEdit}
|
||||
onEditingChange={setEditingField}
|
||||
onClose={onClose}
|
||||
onCommit={(value) => {
|
||||
if (!primaryProperty) return;
|
||||
updateRowMutation.mutate({
|
||||
@@ -319,7 +317,6 @@ export function RowDetailModal({
|
||||
autoFocusValue={property.id === newPropertyId}
|
||||
onAutoFocused={clearNewProperty}
|
||||
menuOpened={openMenuId === property.id}
|
||||
onEditingChange={setEditingField}
|
||||
onMenuOpenChange={(nextOpened) =>
|
||||
handleMenuOpenChange(property.id, nextOpened)
|
||||
}
|
||||
@@ -370,38 +367,16 @@ export function RowDetailModal({
|
||||
) : null}
|
||||
</div>
|
||||
<div className={classes.kbdHint}>
|
||||
{editingField ? (
|
||||
{rowIndex >= 0 && rows.length > 1 && (
|
||||
<>
|
||||
<span className={classes.kbdGroup}>
|
||||
<kbd className={classes.kbd}>Ctrl/Cmd</kbd>
|
||||
<span className={classes.kbdPlus} >+</span>
|
||||
<kbd className={classes.kbd}>Enter</kbd>
|
||||
<span>{t("to save")}</span>
|
||||
</span>
|
||||
|
||||
<kbd className={classes.kbd}>↑</kbd>
|
||||
<kbd className={classes.kbd}>↓</kbd>
|
||||
<span>{t("to navigate")}</span>
|
||||
<span className={classes.kbdSeparator} />
|
||||
|
||||
<span className={classes.kbdGroup}>
|
||||
<kbd className={classes.kbd}>Esc</kbd>
|
||||
<span>{t("to reset")}</span>
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{rowIndex >= 0 && rows.length > 1 && (
|
||||
<>
|
||||
<kbd className={classes.kbd}>↑</kbd>
|
||||
<kbd className={classes.kbd}>↓</kbd>
|
||||
<span>{t("to navigate")}</span>
|
||||
<span className={classes.kbdSeparator} />
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<kbd className={classes.kbd}>Esc</kbd>
|
||||
<span>{t("to close")}</span>
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
<kbd className={classes.kbd}>Esc</kbd>
|
||||
<span>{t("to close")}</span>
|
||||
</div>
|
||||
</footer>
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IBaseProperty, IBaseRow } from "@/ee/base/types/base.types";
|
||||
import { timeAgo } from "@/lib/time.ts";
|
||||
@@ -9,7 +9,7 @@ type RowDetailTitleProps = {
|
||||
primaryProperty: IBaseProperty | undefined;
|
||||
canEdit: boolean;
|
||||
onCommit: (value: string) => void;
|
||||
onEditingChange?: (editing: boolean) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export function RowDetailTitle({
|
||||
@@ -17,24 +17,13 @@ export function RowDetailTitle({
|
||||
primaryProperty,
|
||||
canEdit,
|
||||
onCommit,
|
||||
onEditingChange,
|
||||
onClose,
|
||||
}: RowDetailTitleProps) {
|
||||
const { t } = useTranslation();
|
||||
const initial = primaryProperty
|
||||
? (((row.cells ?? {})[primaryProperty.id] as string) ?? "")
|
||||
: "";
|
||||
const [value, setValue] = useState(initial);
|
||||
const cancelRef = useRef(false);
|
||||
|
||||
const commit = () => {
|
||||
onEditingChange?.(false);
|
||||
if (cancelRef.current) {
|
||||
cancelRef.current = false;
|
||||
setValue(initial);
|
||||
return;
|
||||
}
|
||||
if (value !== initial) onCommit(value);
|
||||
};
|
||||
|
||||
// Re-sync when the row changes underneath us (navigation or remote edit).
|
||||
useEffect(() => {
|
||||
@@ -54,18 +43,18 @@ export function RowDetailTitle({
|
||||
aria-label={primaryProperty?.name ?? t("Untitled")}
|
||||
value={value}
|
||||
maxLength={1000}
|
||||
onFocus={() => {
|
||||
onEditingChange?.(true);
|
||||
}}
|
||||
onChange={(e) => setValue(e.currentTarget.value)}
|
||||
onBlur={commit}
|
||||
onBlur={() => {
|
||||
if (value !== initial) onCommit(value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") {
|
||||
cancelRef.current = true;
|
||||
e.currentTarget.blur();
|
||||
} else if (e.key === "Enter") {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
e.currentTarget.blur();
|
||||
(e.currentTarget as HTMLInputElement).blur();
|
||||
} else if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
(e.currentTarget as HTMLInputElement).blur();
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -416,25 +416,9 @@
|
||||
}
|
||||
|
||||
.kbdHint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.kbdGroup {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.kbdPlus {
|
||||
color: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.kbdSeparator {
|
||||
|
||||
@@ -54,7 +54,9 @@ import {
|
||||
import { mobileSidebarAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
|
||||
import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
|
||||
import { searchSpotlight } from "@/features/search/constants";
|
||||
import TemplatePickerModal from "@/ee/template/components/template-picker-modal";
|
||||
const TemplatePickerModal = React.lazy(
|
||||
() => import("@/ee/template/components/template-picker-modal"),
|
||||
);
|
||||
import { useHasFeature } from "@/ee/hooks/use-feature";
|
||||
import { useUpgradeLabel } from "@/ee/hooks/use-upgrade-label";
|
||||
import { Feature } from "@/ee/features";
|
||||
@@ -406,11 +408,13 @@ function SpaceMenu({
|
||||
|
||||
{hasTemplates && templatePickerOpened && (
|
||||
<ErrorBoundary fallbackRender={() => null}>
|
||||
<TemplatePickerModal
|
||||
opened={templatePickerOpened}
|
||||
onClose={closeTemplatePicker}
|
||||
initialSpaceId={spaceId}
|
||||
/>
|
||||
<React.Suspense fallback={null}>
|
||||
<TemplatePickerModal
|
||||
opened={templatePickerOpened}
|
||||
onClose={closeTemplatePicker}
|
||||
initialSpaceId={spaceId}
|
||||
/>
|
||||
</React.Suspense>
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -396,10 +396,7 @@ export class WorkspaceService {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
updateWorkspaceDto.aiSearch &&
|
||||
this.environmentService.getAiVectorDriver() !== 'turbopuffer'
|
||||
) {
|
||||
if (updateWorkspaceDto.aiSearch) {
|
||||
const tableExists = await isPageEmbeddingsTableExists(this.db);
|
||||
if (!tableExists) {
|
||||
throw new BadRequestException(
|
||||
|
||||
+1
-1
Submodule apps/server/src/ee updated: 6dfbcb9241...e13af0ce05
@@ -71,6 +71,16 @@ export class StaticModule implements OnModuleInit {
|
||||
await app.register(fastifyStatic, {
|
||||
root: clientDistPath,
|
||||
wildcard: false,
|
||||
preCompressed: true,
|
||||
setHeaders: (reply: any, pathName: string) => {
|
||||
// Vite content-hashes everything under /assets, so they can be cached forever
|
||||
if (/[\\/]assets[\\/]/.test(pathName)) {
|
||||
reply.header(
|
||||
'Cache-Control',
|
||||
'public, max-age=31536000, immutable',
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
app.get(RENDER_PATH, (req: any, res: any) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis';
|
||||
import { EnvironmentService } from '../environment/environment.service';
|
||||
import { EnvironmentModule } from '../environment/environment.module';
|
||||
import { createRetryStrategy, parseRedisUrl } from '../../common/helpers';
|
||||
import { parseRedisUrl } from '../../common/helpers';
|
||||
import { AUTH_THROTTLER, AI_CHAT_THROTTLER } from './throttler-names';
|
||||
import Redis from 'ioredis';
|
||||
|
||||
@@ -27,8 +27,6 @@ import Redis from 'ioredis';
|
||||
password: redisConfig.password,
|
||||
db: redisConfig.db,
|
||||
family: redisConfig.family,
|
||||
tls: redisConfig.tls,
|
||||
retryStrategy: createRetryStrategy(),
|
||||
keyPrefix: 'throttle:',
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -73,13 +73,9 @@ export const embedProviders: IEmbedProvider[] = [
|
||||
id: "vimeo",
|
||||
name: "Vimeo",
|
||||
regex:
|
||||
/^(https:)?\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)(?:\/([\da-zA-Z]+))?/,
|
||||
getEmbedUrl: (match, url: string) => {
|
||||
// preserve ?h= hash for unlisted videos
|
||||
const hash =
|
||||
match[5] ?? new URL(url, "https://vimeo.com").searchParams.get("h");
|
||||
const base = `https://player.vimeo.com/video/${match[4]}`;
|
||||
return hash ? `${base}?h=${hash}` : base;
|
||||
/^(https:)?\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)/,
|
||||
getEmbedUrl: (match) => {
|
||||
return `https://player.vimeo.com/video/${match[4]}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user