Compare commits
No commits in common. "731d7f5e93c74486eb6a74cf8fbbf5419bb7d42e" and "5e0f5cf49edf4762c7595d1052b164db9bb82273" have entirely different histories.
731d7f5e93
...
5e0f5cf49e
3 changed files with 1 additions and 112 deletions
|
|
@ -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,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -23,7 +23,6 @@ import { modals } from "@mantine/modals";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useViewData } from "../hooks/use-view-data";
|
import { useViewData } from "../hooks/use-view-data";
|
||||||
import { useUpdateRow } from "../hooks/use-update-row";
|
import { useUpdateRow } from "../hooks/use-update-row";
|
||||||
import { useCreateRow } from "../hooks/use-create-row";
|
|
||||||
import { useDatabaseRealtimeUpdates } from "../hooks/use-database-realtime-updates";
|
import { useDatabaseRealtimeUpdates } from "../hooks/use-database-realtime-updates";
|
||||||
import { usePermissions } from "../hooks/use-permissions";
|
import { usePermissions } from "../hooks/use-permissions";
|
||||||
import { useAcadenicePermissions } from "@/features/acadenice/rbac/hooks/use-acadenice-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 canAdminTables = acadenicePerms.hasPermission("tables:write");
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const updateRow = useUpdateRow({ tableId, viewId, bridgeUrl });
|
const updateRow = useUpdateRow({ tableId, viewId, bridgeUrl });
|
||||||
const createRow = useCreateRow({ tableId, viewId, bridgeUrl });
|
|
||||||
|
|
||||||
// Field admin modal state.
|
// Field admin modal state.
|
||||||
const [fieldModalOpen, setFieldModalOpen] = useState(false);
|
const [fieldModalOpen, setFieldModalOpen] = useState(false);
|
||||||
|
|
@ -354,19 +352,6 @@ export function TableRenderer({ tableId, viewId, bridgeUrl }: TableRendererProps
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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. */}
|
{/* Pagination — only shown when there is more than one page. */}
|
||||||
{(page > 1 || hasNextPage) && (
|
{(page > 1 || hasNextPage) && (
|
||||||
<div className={styles.pagination}>
|
<div className={styles.pagination}>
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,12 @@ import {
|
||||||
Get,
|
Get,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
type MessageEvent,
|
|
||||||
Param,
|
Param,
|
||||||
ParseUUIDPipe,
|
ParseUUIDPipe,
|
||||||
Patch,
|
Patch,
|
||||||
Post,
|
Post,
|
||||||
Sse,
|
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
||||||
import { fromEvent, interval, merge, type Observable } from 'rxjs';
|
|
||||||
import { filter, map } from 'rxjs/operators';
|
|
||||||
import {
|
import {
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
ApiBody,
|
ApiBody,
|
||||||
|
|
@ -29,10 +24,6 @@ import { AuthUser } from '../../../../common/decorators/auth-user.decorator';
|
||||||
import { AuthWorkspace } from '../../../../common/decorators/auth-workspace.decorator';
|
import { AuthWorkspace } from '../../../../common/decorators/auth-workspace.decorator';
|
||||||
import { User, Workspace } from '@docmost/db/types/entity.types';
|
import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||||
import { SyncBlocksService } from '../services/sync-blocks.service';
|
import { SyncBlocksService } from '../services/sync-blocks.service';
|
||||||
import {
|
|
||||||
SYNC_BLOCK_UPDATED_EVENT,
|
|
||||||
type SyncBlockUpdatedPayload,
|
|
||||||
} from '../services/sync-block-broadcast.service';
|
|
||||||
import {
|
import {
|
||||||
CreateSyncBlockDto,
|
CreateSyncBlockDto,
|
||||||
UpdateSyncBlockDto,
|
UpdateSyncBlockDto,
|
||||||
|
|
@ -59,51 +50,7 @@ import {
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Controller('v1/sync-blocks')
|
@Controller('v1/sync-blocks')
|
||||||
export class SyncBlocksController {
|
export class SyncBlocksController {
|
||||||
constructor(
|
constructor(private readonly syncBlocksService: SyncBlocksService) {}
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiOperation({ summary: 'Create sync block', description: 'Creates a master sync block that can be embedded by reference in multiple pages.' })
|
@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)' })
|
@ApiBody({ schema: { type: 'object', properties: { content: { type: 'object', description: 'ProseMirror JSON content' } } }, description: 'Initial content (optional)' })
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue