From 75644c5c593a0ce4810cdac896d63c3eab7ac848 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Wed, 9 Sep 2026 17:39:23 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7(collaboration)=20make=20the=20vers?= =?UTF-8?q?ion=20history=20granularity=20configurable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Version grouping used a hard-coded 60s window, and its timestamps are minted server-side. We now expose COLLABORATION_VERSION_GRANULARITY_MS through /config, read it in useDocActivity. By doing so, we can control the granularity of version history with settings, it can be adjusted as needed. It will help us to test different version history granularities in our e2e tests. --- CHANGELOG.md | 2 + documentation/env.md | 1 + src/backend/core/api/viewsets.py | 1 + src/backend/core/tests/test_api_config.py | 2 + src/backend/impress/settings.py | 7 + .../app-impress/doc-collaboration.spec.ts | 2 +- .../__tests__/app-impress/doc-version.spec.ts | 150 +++++++++--------- .../app-impress/presenter-mode.spec.ts | 13 +- .../e2e/__tests__/app-impress/utils-common.ts | 20 +-- .../e2e/__tests__/app-impress/utils-export.ts | 4 - .../impress/src/core/config/api/useConfig.tsx | 1 + .../doc-versioning/__tests__/utils.test.ts | 7 +- .../doc-versioning/api/useDocActivity.tsx | 23 ++- .../components/ModalConfirmationVersion.tsx | 8 +- .../src/features/docs/doc-versioning/utils.ts | 4 +- 15 files changed, 119 insertions(+), 126 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eb967317..91d35a0a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ and this project adheres to - ✨(collaboration) notify the backend when the worker persists new content - ✨(collaboration) let a user read the document's editing history - ✨(frontend) fall back to http polling when the websocket cannot be opened. +- 🔧(collaboration) make the version-history granularity configurable through + `COLLABORATION_VERSION_GRANULARITY_MS` ### Changed diff --git a/documentation/env.md b/documentation/env.md index 1b14d7a88..11454b34c 100644 --- a/documentation/env.md +++ b/documentation/env.md @@ -38,6 +38,7 @@ These are the environment variables you can set for the `impress-backend` contai | CACHES_SESSION_IGNORE_EXCEPTIONS | Ignoring exception, behave like a missed cache. Highly recommended to set it as False when used with redis (See https://github.com/jazzband/django-redis#memcached-exceptions-behavior) | False | | CACHES_SESSION_SOCKET_CONNECT_TIMEOUT | Timeout for the connection to be established for the session cache (In seconds) | 0.5 | | CACHES_SESSION_SOCKET_TIMEOUT | Timeout for read and write operations after the connection is established for the session cache (In seconds) | 1 | +| COLLABORATION_VERSION_GRANULARITY_MS | How coarse the document version history is, in milliseconds: edits closer together than this are shown as one version, and none spans more than this. Sent to the collaboration server as the grouping window and re-applied in the browser. | 60000 | | COLLABORATION_WS_INACTIVITY_TIMEOUT | Timeout (in seconds) after which the user is considered inactive when there is no activity. The WebSocket is closed after this inactivity period. `None` means disabled. | None | | COLLABORATION_WS_URL | Collaboration websocket url | | | CONVERSION_API_CONTENT_FIELD | Conversion api content field | content | diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 4351614f2..4280e4660 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -2954,6 +2954,7 @@ class ConfigView(drf.views.APIView): "AI_FEATURE_BLOCKNOTE_ENABLED", "AI_FEATURE_LEGACY_ENABLED", "API_USERS_SEARCH_QUERY_MIN_LENGTH", + "COLLABORATION_VERSION_GRANULARITY_MS", "COLLABORATION_WS_URL", "COLLABORATION_WS_INACTIVITY_TIMEOUT", "CONVERSION_FILE_EXTENSIONS_ALLOWED", diff --git a/src/backend/core/tests/test_api_config.py b/src/backend/core/tests/test_api_config.py index 4eb879910..8bf4b5579 100644 --- a/src/backend/core/tests/test_api_config.py +++ b/src/backend/core/tests/test_api_config.py @@ -24,6 +24,7 @@ pytestmark = pytest.mark.django_db AI_FEATURE_BLOCKNOTE_ENABLED=False, AI_FEATURE_LEGACY_ENABLED=False, API_USERS_SEARCH_QUERY_MIN_LENGTH=6, + COLLABORATION_VERSION_GRANULARITY_MS=45000, COLLABORATION_WS_URL="http://testcollab/", COLLABORATION_WS_INACTIVITY_TIMEOUT=300, CONVERSION_UPLOAD_ENABLED=False, @@ -54,6 +55,7 @@ def test_api_config(is_authenticated): "AI_FEATURE_BLOCKNOTE_ENABLED": False, "AI_FEATURE_LEGACY_ENABLED": False, "API_USERS_SEARCH_QUERY_MIN_LENGTH": 6, + "COLLABORATION_VERSION_GRANULARITY_MS": 45000, "COLLABORATION_WS_URL": "http://testcollab/", "COLLABORATION_WS_INACTIVITY_TIMEOUT": 300, "CONVERSION_FILE_EXTENSIONS_ALLOWED": [".docx", ".md"], diff --git a/src/backend/impress/settings.py b/src/backend/impress/settings.py index 60136b03c..cf98a1f68 100755 --- a/src/backend/impress/settings.py +++ b/src/backend/impress/settings.py @@ -539,6 +539,13 @@ class Base(Configuration): environ_name="COLLABORATION_WS_INACTIVITY_TIMEOUT", environ_prefix=None, ) + # Granularity of the document version history, in milliseconds. + # Increase or decrease this value to adjust the granularity of version history. + COLLABORATION_VERSION_GRANULARITY_MS = values.IntegerValue( + 60000, + environ_name="COLLABORATION_VERSION_GRANULARITY_MS", + environ_prefix=None, + ) # Base url of the collaboration server's REST api, including its route # prefix (e.g. "http://yhub:3002/collaboration"). Server-to-server only: # used with an admin JWT to migrate legacy documents and, later, to kick diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts index ab448455b..a5a05f2cd 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-collaboration.spec.ts @@ -118,7 +118,7 @@ test.describe('Doc Collaboration', () => { await writeInEditor({ page, text: 'Hello after the reset' }); await expect(otherPage.getByText('Hello after the reset')).toBeVisible({ - timeout: 15000, + timeout: 10000, }); await cleanup(); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts index db266cc17..ba8bf3e84 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-version.spec.ts @@ -4,50 +4,22 @@ import { createDoc, goToGridDoc, mockedDocument, - reopenDoc, + overrideConfig, verifyDocName, } from './utils-common'; import { openSuggestionMenu, writeInEditor } from './utils-editor'; -/** - * A version is at least a minute of editing: the collaboration server groups - * changes that are less than a minute apart, and the panel merges what is left - * across authors. That bound is the feature — a history of every few keystrokes - * is not a history — but it does mean a test cannot produce two versions - * without a minute of real time passing. The timestamps come from the server, - * so no clock can be faked to shorten it. - * - * The tests below therefore assert one version wherever one is enough, and only - * the restore test pays for a second one. - */ -const VERSION_GRANULARITY_MS = 60_000; - -test.beforeEach(async ({ page }) => { - await page.goto('/'); -}); +const COLLABORATION_VERSION_GRANULARITY_MS = 2000; test.describe('Doc Version', () => { - test('it displays an empty history', async ({ page, browserName }) => { - // Stubbed, because a document with no history at all cannot be reached - // through the interface: opening one writes to it, and that write is a - // change like any other. Only a document nobody has ever opened has an - // empty timeline. - await page.route('**/collaboration/activity/**', (route) => - route.fulfill({ json: { activity: [] } }), - ); - - await createDoc(page, 'doc-version-empty', browserName, 1); - - await page.getByLabel('Open the document options').click(); - await page.getByRole('menuitem', { name: 'Version history' }).click(); - - const modal = page.getByRole('dialog', { name: 'Version history' }); - await expect(modal.getByLabel('Version list')).toBeVisible(); - await expect(modal.getByText('No versions')).toBeVisible(); - }); - test('it displays the doc versions', async ({ page, browserName }) => { - const [randomDoc] = await createDoc(page, 'doc-version', browserName, 1); + await overrideConfig(page, { + COLLABORATION_VERSION_GRANULARITY_MS: `${COLLABORATION_VERSION_GRANULARITY_MS}`, + }); + + await page.goto('/'); + + await createDoc(page, 'doc-version', browserName, 1); // Initially, there is no version await page.getByLabel('Open the document options').click(); @@ -57,8 +29,15 @@ test.describe('Doc Version', () => { const modal = page.getByRole('dialog', { name: 'Version history' }); const panel = modal.getByLabel('Version list'); + await modal.getByRole('button', { name: 'close' }).click(); + await writeInEditor({ page, text: 'Hello World' }); + await page.waitForTimeout(COLLABORATION_VERSION_GRANULARITY_MS + 10); + + // Write more + await writeInEditor({ page, text: 'It will create a version' }); + const { suggestionMenu } = await openSuggestionMenu({ page }); await suggestionMenu.getByText('Add a callout block').click(); @@ -68,9 +47,12 @@ test.describe('Doc Version', () => { await expect(calloutBlock).toBeVisible(); - await reopenDoc(page, randomDoc); + await page.waitForTimeout(COLLABORATION_VERSION_GRANULARITY_MS + 10); - await expect(page.getByText('Hello World')).toBeVisible(); + // Write more + await writeInEditor({ page, text: 'It will create a second version' }); + + await page.waitForTimeout(COLLABORATION_VERSION_GRANULARITY_MS + 10); await page.getByLabel('Open the document options').click(); await page.getByRole('menuitem', { name: 'History' }).click(); @@ -78,24 +60,44 @@ test.describe('Doc Version', () => { await expect(panel).toBeVisible(); await expect(page.getByText('History', { exact: true })).toBeVisible(); await expect(page.getByRole('status')).toBeHidden(); - - // One entry: opening the document, naming it and typing into it all - // happened inside one minute, and a version is a minute of editing. const items = panel.locator('.version-item'); - await expect(items).toHaveCount(1); + await expect(items).toHaveCount(3); + await items.nth(2).click(); + + await expect(modal.getByText('Hello World')).toBeVisible(); + await expect(modal.getByText('It will create a version')).toBeHidden(); + await expect( + modal.locator('div[data-content-type="callout"]').first(), + ).toBeHidden(); + + await items.nth(1).click(); + + await expect(modal.getByText('Hello World')).toBeVisible(); + await expect(modal.getByText('It will create a version')).toBeVisible(); + await expect( + modal.locator('div[data-content-type="callout"]').first(), + ).toBeVisible(); + await expect( + modal.getByText('It will create a second version'), + ).toBeHidden(); await items.nth(0).click(); - // the preview renders the document as it stood at the end of that version await expect(modal.getByText('Hello World')).toBeVisible(); + await expect(modal.getByText('It will create a version')).toBeVisible(); await expect( modal.locator('div[data-content-type="callout"]').first(), ).toBeVisible(); + await expect( + modal.getByText('It will create a second version'), + ).toBeVisible(); }); test('it does not display the doc versions if not allowed', async ({ page, }) => { + await page.goto('/'); + await mockedDocument(page, { abilities: { versions_list: false, @@ -112,13 +114,13 @@ test.describe('Doc Version', () => { }); test('it restores the doc version', async ({ page, browserName }) => { - // The wait below is the whole reason for this budget, and it is not - // padding: two versions cannot exist any closer together. See the note at - // the top of the file before trying to make this faster. - test.setTimeout(VERSION_GRANULARITY_MS + 120_000); + await overrideConfig(page, { + COLLABORATION_VERSION_GRANULARITY_MS: `${COLLABORATION_VERSION_GRANULARITY_MS}`, + }); - const [randomDoc] = await createDoc(page, 'doc-version', browserName, 1); - await verifyDocName(page, randomDoc); + await page.goto('/'); + + await createDoc(page, 'doc-version', browserName, 1); const editor = await writeInEditor({ page, text: 'Hello' }); @@ -131,20 +133,16 @@ test.describe('Doc Version', () => { await thread.locator('[data-test="save"]').click(); await expect(thread).toBeHidden(); - await reopenDoc(page, randomDoc); - await expect(editor.getByText('Hello')).toBeVisible(); + await page.waitForTimeout(COLLABORATION_VERSION_GRANULARITY_MS + 10); - // Let the first version close before writing the text that has to end up - // in a second one — a minute of silence is what separates them. - await page.waitForTimeout(VERSION_GRANULARITY_MS + 1_000); + await writeInEditor({ page, text: 'World' }); - await page.locator('.bn-block-outer').last().click(); - await page.keyboard.press('Enter'); - await page.locator('.bn-block-outer').last().fill('World'); + await page.waitForTimeout(COLLABORATION_VERSION_GRANULARITY_MS + 10); - await reopenDoc(page, randomDoc); - - await expect(page.getByText('World')).toBeVisible(); + await editor.getByText('Hello').click(); + await thread.getByText('This is a comment').first().hover(); + await thread.locator('[data-test="resolve"]').click(); + await expect(thread).toBeHidden(); await page.getByLabel('Open the document options').click(); await page.getByRole('menuitem', { name: 'History' }).click(); @@ -152,14 +150,12 @@ test.describe('Doc Version', () => { const modal = page.getByRole('dialog', { name: 'Version history' }); const panel = modal.getByLabel('Version list'); await expect(panel).toBeVisible(); + await expect(page.getByText('History', { exact: true })).toBeVisible(); - - // newest first: the second item is the version that predates 'World' const items = panel.locator('.version-item'); - await expect(items).toHaveCount(2); - await items.nth(1).click(); + await expect(items).toHaveCount(3); + await items.nth(2).click(); - await expect(modal.getByText('Hello')).toBeVisible(); await expect(modal.getByText('World')).toBeHidden(); await page.getByRole('button', { name: 'Restore', exact: true }).click(); @@ -171,21 +167,23 @@ test.describe('Doc Version', () => { await page.getByLabel('Restore', { exact: true }).click(); - // The collaboration server applies the rollback and pushes it back over the - // connection this editor is already holding — nothing is reloaded here. const mainEditor = page.getByLabel('Document editor'); await expect(mainEditor.getByText('Hello')).toBeVisible(); await expect(mainEditor.getByText('World')).toBeHidden(); - // The comment survives, and that is the point: a restore undoes the changes - // made after the chosen version rather than replacing the document with an - // old copy of it. This comment belongs to the version being restored, so - // nothing about it was undone. - await expect(mainEditor.getByText('Hello')).toHaveClass('bn-thread-mark'); + // The old comment is not restored + await expect(mainEditor.getByText('Hello')).toHaveCSS( + 'background-color', + 'rgba(0, 0, 0, 0)', + ); - // and the document is still live afterwards - await mainEditor.getByText('Hello').click(); - await expect(thread.getByText('This is a comment').first()).toBeVisible(); + // We can add a new comment + await mainEditor.getByText('Hello').selectText(); + await page.getByRole('button', { name: 'Add comment' }).click(); + + await thread.getByRole('paragraph').first().fill('This is a comment'); + await thread.locator('[data-test="save"]').click(); + await expect(mainEditor.getByText('Hello')).toHaveClass('bn-thread-mark'); }); }); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts index b9245c95e..92c252007 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/presenter-mode.spec.ts @@ -3,7 +3,7 @@ import path from 'path'; import { Locator, Page, expect, test } from '@playwright/test'; import { PDFParse } from 'pdf-parse'; -import { createDoc, mockedDocument, reopenDoc } from './utils-common'; +import { createDoc, mockedDocument } from './utils-common'; import { openSuggestionMenu, tryFocusEditorContent, @@ -427,19 +427,10 @@ test.describe('Presenter Mode', () => { page, browserName, }) => { - const [docTitle] = await createDoc( - page, - 'presenter-deeplink', - browserName, - 1, - ); + await createDoc(page, 'presenter-deeplink', browserName, 1); await writeMultiSlideDoc(page); const docId = getDocIdFromUrl(page); - // Leave and come back before reloading through the deep-link, so what the - // deep-link opens is what the collaboration server kept, not what this tab - // still had in memory. - await reopenDoc(page, docTitle); await page.goto(`/docs/${docId}/?view=present&slide=3`); const overlay = page.getByRole('dialog', { name: 'Presenter mode' }); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts index af865f943..ea81b63bf 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-common.ts @@ -18,6 +18,7 @@ export const CONFIG = { AI_FEATURE_BLOCKNOTE_ENABLED: false, AI_FEATURE_LEGACY_ENABLED: true, API_USERS_SEARCH_QUERY_MIN_LENGTH: 3, + COLLABORATION_VERSION_GRANULARITY_MS: 60000, COLLABORATION_WS_INACTIVITY_TIMEOUT: 15, COLLABORATION_WS_URL: process.env.COLLABORATION_WS_URL, CONVERSION_UPLOAD_ENABLED: true, @@ -277,25 +278,6 @@ export const waitForResponseCreateDoc = (page: Page) => { ); }; -/** - * Leaves the doc and comes back to it, so that what follows reads the document - * from the collaboration server rather than from the editor that just wrote it. - * - * There is nothing to save: the collaboration server receives every change as - * it is typed, and the backend holds no copy of the content to be pushed to. - * (This used to wait for a `PATCH /content/`, which stopped existing with the - * migration — and so waited forever.) What makes the round trip meaningful is - * therefore the assertion that follows it: content still on the page after - * leaving and returning is content the server kept. - */ -export const reopenDoc = async (page: Page, title: string) => { - await page.getByRole('button', { name: 'Back to homepage' }).click(); - await expect(page.getByTestId('docs-grid')).toBeVisible(); - await expect(page.getByTestId('grid-loader')).toBeHidden(); - - await goToGridDoc(page, { title }); -}; - export const mockedDocument = async (page: Page, data: object) => { // document/[ID]/ or document/[ID]/tree/ routes let uuid: string | undefined; diff --git a/src/frontend/apps/e2e/__tests__/app-impress/utils-export.ts b/src/frontend/apps/e2e/__tests__/app-impress/utils-export.ts index ddd66983f..487a0246a 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/utils-export.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/utils-export.ts @@ -78,10 +78,6 @@ export const overrideDocContent = async ({ await page.goto(`/docs/${docId}/`); // the seed has to be on screen before anything is added after it - await expect(page.getByText('Hello Heading 1')).toBeVisible({ - timeout: 15000, - }); - await expect(page.getByText('copy/pasting out of doc')).toBeVisible(); // Add Image SVG diff --git a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx index be5d56224..b309c0764 100644 --- a/src/frontend/apps/impress/src/core/config/api/useConfig.tsx +++ b/src/frontend/apps/impress/src/core/config/api/useConfig.tsx @@ -48,6 +48,7 @@ export interface ConfigResponse { AI_FEATURE_BLOCKNOTE_ENABLED?: boolean; AI_FEATURE_LEGACY_ENABLED?: boolean; API_USERS_SEARCH_QUERY_MIN_LENGTH?: number; + COLLABORATION_VERSION_GRANULARITY_MS?: number; COLLABORATION_WS_URL?: string; COLLABORATION_WS_INACTIVITY_TIMEOUT?: number | null; CONVERSION_FILE_EXTENSIONS_ALLOWED: string[]; diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/__tests__/utils.test.ts b/src/frontend/apps/impress/src/features/docs/doc-versioning/__tests__/utils.test.ts index 350895db2..f22e9e55a 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-versioning/__tests__/utils.test.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/__tests__/utils.test.ts @@ -1,7 +1,10 @@ import { describe, expect, it } from 'vitest'; import { ActivityEntry } from '../types'; -import { VERSION_GRANULARITY_MS, mergeActivityEntries } from '../utils'; +import { + VERSION_GRANULARITY_MS_FALLBACK as G, + mergeActivityEntries, +} from '../utils'; /** * `mergeActivityEntries` is the only part of the history policy that lives in @@ -15,8 +18,6 @@ const entry = (from: number, to: number, by: string | null): ActivityEntry => ({ by, }); -const G = VERSION_GRANULARITY_MS; - describe('mergeActivityEntries', () => { it('has nothing to say about an empty timeline', () => { expect(mergeActivityEntries([])).toEqual([]); diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/api/useDocActivity.tsx b/src/frontend/apps/impress/src/features/docs/doc-versioning/api/useDocActivity.tsx index 294c2f254..309b63239 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-versioning/api/useDocActivity.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/api/useDocActivity.tsx @@ -6,12 +6,13 @@ import { UseQueryOptionsAPI, fetchCollaborationAPI, } from '@/api'; +import { useConfig } from '@/core/config/api'; import { useCollaborationTarget } from '@/core/config/hooks/useCollaborationUrl'; import { APIActivity, DocVersion } from '../types'; import { UNGROUPED_AUTHORS, - VERSION_GRANULARITY_MS, + VERSION_GRANULARITY_MS_FALLBACK, mergeActivityEntries, } from '../utils'; @@ -35,6 +36,7 @@ export type DocActivityParam = { const getDocActivity = async ( target: CollaborationTarget, { docId }: DocActivityParam, + granularityMs: number, ): Promise => { const { activity } = await fetchCollaborationAPI( target, @@ -43,8 +45,8 @@ const getDocActivity = async ( { query: { group: true, - groupMaxGap: VERSION_GRANULARITY_MS, - groupMaxDuration: VERSION_GRANULARITY_MS, + groupMaxGap: granularityMs, + groupMaxDuration: granularityMs, // the imported history is shown save by save — see UNGROUPED_AUTHORS. // The panel applies this again, on both sides of an entry. groupExclude: UNGROUPED_AUTHORS.join(','), @@ -53,7 +55,7 @@ const getDocActivity = async ( ); // ascending from the server, newest first for the panel - return mergeActivityEntries(activity).reverse(); + return mergeActivityEntries(activity, granularityMs).reverse(); }; export const KEY_DOC_ACTIVITY = 'doc-activity'; @@ -63,12 +65,17 @@ export function useDocActivity( queryConfig?: Omit, 'queryKey' | 'queryFn'>, ) { const target = useCollaborationTarget(); + const { data: conf } = useConfig(); + const granularityMs = + conf?.COLLABORATION_VERSION_GRANULARITY_MS ?? + VERSION_GRANULARITY_MS_FALLBACK; return useQuery({ - // `target` belongs in the key: it arrives with the configuration, so a - // query started before it resolved must not be reused after - queryKey: [KEY_DOC_ACTIVITY, params, target], - queryFn: () => getDocActivity(target as CollaborationTarget, params), + // `target` and `granularityMs` belong in the key: both arrive with the + // configuration, so a query started before they resolved must not be reused + queryKey: [KEY_DOC_ACTIVITY, params, target, granularityMs], + queryFn: () => + getDocActivity(target as CollaborationTarget, params, granularityMs), enabled: !!target, /** * Against the application's three-minute default, which is wrong for this diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalConfirmationVersion.tsx b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalConfirmationVersion.tsx index 5b5ac8497..c4d86c255 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalConfirmationVersion.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/ModalConfirmationVersion.tsx @@ -9,10 +9,11 @@ import { useTranslation } from 'react-i18next'; import { createGlobalStyle } from 'styled-components'; import { Box, Text } from '@/components'; -import { Doc } from '@/docs/doc-management/'; +import { useThreadStore } from '@/docs/doc-comments/stores/useThreadStore'; +import { type Doc } from '@/docs/doc-management/'; import { useRestoreDocVersion } from '../api'; -import { DocVersion } from '../types'; +import { type DocVersion } from '../types'; const ModalStyle = createGlobalStyle` .c__modal__title { @@ -35,6 +36,7 @@ export const ModalConfirmationVersion = ({ }: ModalConfirmationVersionProps) => { const { t } = useTranslation(); const { toast } = useToastProvider(); + const { threadStore } = useThreadStore(); /** * The collaboration server undoes everything after this version and hands the @@ -45,6 +47,8 @@ export const ModalConfirmationVersion = ({ onSuccess: () => { toast(t('Version restored successfully'), VariantType.SUCCESS); onSuccess(); + + threadStore?.refreshThreads(); }, }); diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/utils.ts b/src/frontend/apps/impress/src/features/docs/doc-versioning/utils.ts index 192c69e00..d8d6bc773 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-versioning/utils.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/utils.ts @@ -9,7 +9,7 @@ import { ActivityEntry, DocVersion } from './types'; * at typing speed is far finer than anything worth listing — a history of every * few keystrokes is not a history. */ -export const VERSION_GRANULARITY_MS = 60_000; +export const VERSION_GRANULARITY_MS_FALLBACK = 60_000; /** * Authors whose changes are never merged into a version with anything else. @@ -56,7 +56,7 @@ export const UNGROUPED_AUTHORS = ['system']; */ export const mergeActivityEntries = ( activity: ActivityEntry[], - granularityMs: number = VERSION_GRANULARITY_MS, + granularityMs: number = VERSION_GRANULARITY_MS_FALLBACK, ): DocVersion[] => { const versions: DocVersion[] = []; const authors: Set[] = [];