Compare commits

..

No commits in common. "731d7f5e93c74486eb6a74cf8fbbf5419bb7d42e" and "5e0f5cf49edf4762c7595d1052b164db9bb82273" have entirely different histories.

3 changed files with 1 additions and 112 deletions

View file

@ -1,43 +0,0 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { getBridgeClient, resolveBridgeUrl } from "../services/bridge-client";
import type { BridgeRow } from "../types/database-view.types";
import { VIEW_DATA_QUERY_KEY } from "./use-view-data";
interface UseCreateRowOptions {
tableId: string;
viewId: string;
bridgeUrl?: string | null;
}
/**
* Create a new row in the table via the bridge, then invalidate the view
* cache so the row appears.
*
* Server-first (no optimistic insert): the bridge assigns the row id, and the
* inline editor PATCHes by id afterwards fabricating a temporary id here
* would desync the first edit. An empty payload creates a blank row (Baserow
* fills defaults); callers may pass initial field values.
*/
export function useCreateRow({ tableId, viewId, bridgeUrl }: UseCreateRowOptions) {
const queryClient = useQueryClient();
const url = resolveBridgeUrl(bridgeUrl);
return useMutation<BridgeRow, Error, Record<string, unknown> | void>({
mutationFn: async (fields) => {
const client = getBridgeClient(url);
return (await (client.post(
`/api/v1/tables/${tableId}/rows`,
fields ?? {},
) as unknown)) as BridgeRow;
},
onSettled: () => {
// Reconcile with the server. The bridge also emits an SSE row.created
// event which triggers the same invalidation — idempotent.
queryClient.invalidateQueries({
queryKey: [VIEW_DATA_QUERY_KEY, viewId],
exact: false,
});
},
});
}

View file

@ -23,7 +23,6 @@ import { modals } from "@mantine/modals";
import { useQueryClient } from "@tanstack/react-query";
import { useViewData } from "../hooks/use-view-data";
import { useUpdateRow } from "../hooks/use-update-row";
import { useCreateRow } from "../hooks/use-create-row";
import { useDatabaseRealtimeUpdates } from "../hooks/use-database-realtime-updates";
import { usePermissions } from "../hooks/use-permissions";
import { useAcadenicePermissions } from "@/features/acadenice/rbac/hooks/use-acadenice-permissions";
@ -187,7 +186,6 @@ export function TableRenderer({ tableId, viewId, bridgeUrl }: TableRendererProps
const canAdminTables = acadenicePerms.hasPermission("tables:write");
const queryClient = useQueryClient();
const updateRow = useUpdateRow({ tableId, viewId, bridgeUrl });
const createRow = useCreateRow({ tableId, viewId, bridgeUrl });
// Field admin modal state.
const [fieldModalOpen, setFieldModalOpen] = useState(false);
@ -354,19 +352,6 @@ export function TableRenderer({ tableId, viewId, bridgeUrl }: TableRendererProps
</table>
</div>
{canWriteRows && (
<Button
size="xs"
variant="subtle"
leftSection={<IconPlus size={14} />}
onClick={() => createRow.mutate()}
loading={createRow.isPending}
mt="xs"
>
{t("database_view.table.add_row", "Ajouter une ligne")}
</Button>
)}
{/* Pagination — only shown when there is more than one page. */}
{(page > 1 || hasNextPage) && (
<div className={styles.pagination}>

View file

@ -5,17 +5,12 @@ import {
Get,
HttpCode,
HttpStatus,
type MessageEvent,
Param,
ParseUUIDPipe,
Patch,
Post,
Sse,
UseGuards,
} from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { fromEvent, interval, merge, type Observable } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import {
ApiBearerAuth,
ApiBody,
@ -29,10 +24,6 @@ import { AuthUser } from '../../../../common/decorators/auth-user.decorator';
import { AuthWorkspace } from '../../../../common/decorators/auth-workspace.decorator';
import { User, Workspace } from '@docmost/db/types/entity.types';
import { SyncBlocksService } from '../services/sync-blocks.service';
import {
SYNC_BLOCK_UPDATED_EVENT,
type SyncBlockUpdatedPayload,
} from '../services/sync-block-broadcast.service';
import {
CreateSyncBlockDto,
UpdateSyncBlockDto,
@ -59,51 +50,7 @@ import {
@UseGuards(JwtAuthGuard)
@Controller('v1/sync-blocks')
export class SyncBlocksController {
constructor(
private readonly syncBlocksService: SyncBlocksService,
private readonly eventEmitter: EventEmitter2,
) {}
/**
* SSE stream of updates for a single sync block.
*
* The client (use-sync-block-realtime) opens an EventSource here and
* invalidates its cache on `sync-block.updated`. Auth is the controller's
* JwtAuthGuard EventSource cannot set headers but the JWT strategy reads
* the `authToken` cookie, sent automatically same-origin.
*
* A 25s heartbeat keeps the connection alive through proxies that drop idle
* streams (~30s). The client ignores the unknown `ping` event.
*/
@ApiOperation({
summary: 'Sync block realtime stream (SSE)',
description:
'Server-sent events: emits `sync-block.updated` when this block changes.',
})
@ApiParam({ name: 'id', description: 'Sync block UUID', type: 'string' })
@Sse(':id/events')
events(
@Param('id', ParseUUIDPipe) id: string,
): Observable<MessageEvent> {
const updates = fromEvent<SyncBlockUpdatedPayload>(
this.eventEmitter,
SYNC_BLOCK_UPDATED_EVENT,
).pipe(
filter((payload) => payload?.masterId === id),
map(
(payload): MessageEvent => ({
type: 'sync-block.updated',
data: { masterId: payload.masterId },
}),
),
);
const heartbeat = interval(25_000).pipe(
map((): MessageEvent => ({ type: 'ping', data: {} })),
);
return merge(updates, heartbeat);
}
constructor(private readonly syncBlocksService: SyncBlocksService) {}
@ApiOperation({ summary: 'Create sync block', description: 'Creates a master sync block that can be embedded by reference in multiple pages.' })
@ApiBody({ schema: { type: 'object', properties: { content: { type: 'object', description: 'ProseMirror JSON content' } } }, description: 'Initial content (optional)' })