mirror of
https://github.com/suitenumerique/docs.git
synced 2026-09-12 12:47:55 +02:00
🔧(collaboration) make the version history granularity configurable
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.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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([]);
|
||||
|
||||
@@ -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<DocVersion[]> => {
|
||||
const { activity } = await fetchCollaborationAPI<APIActivity>(
|
||||
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<UseQueryOptionsAPI<DocVersion[]>, 'queryKey' | 'queryFn'>,
|
||||
) {
|
||||
const target = useCollaborationTarget();
|
||||
const { data: conf } = useConfig();
|
||||
const granularityMs =
|
||||
conf?.COLLABORATION_VERSION_GRANULARITY_MS ??
|
||||
VERSION_GRANULARITY_MS_FALLBACK;
|
||||
|
||||
return useQuery<DocVersion[], APIError, DocVersion[]>({
|
||||
// `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
|
||||
|
||||
+6
-2
@@ -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();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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<string>[] = [];
|
||||
|
||||
Reference in New Issue
Block a user