feat(core): mode in query string (#7904)

This commit is contained in:
EYHN
2024-08-16 10:59:43 +00:00
parent 83716c2fd9
commit c822594882
16 changed files with 174 additions and 29 deletions
+1
View File
@@ -81,6 +81,7 @@
"mixpanel-browser": "^2.49.0",
"nanoid": "^5.0.7",
"next-themes": "^0.3.0",
"query-string": "^9.1.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-error-boundary": "^4.0.13",
@@ -1,5 +1,7 @@
import { Entity, LiveData } from '@toeverything/infra';
import type { Location, To } from 'history';
import { isEqual } from 'lodash-es';
import queryString from 'query-string';
import { Observable } from 'rxjs';
import { createNavigableHistory } from '../../../utils/navigable-history';
@@ -77,6 +79,52 @@ export class View extends Entity<{
icon$ = new LiveData(this.props.icon ?? 'allDocs');
queryString$<T extends Record<string, unknown>>({
parseNumbers = true,
}: { parseNumbers?: boolean } = {}) {
return this.location$.map(
location =>
queryString.parse(location.search, {
parseBooleans: true,
parseNumbers: parseNumbers,
}) as Partial<T>
);
}
updateQueryString<T extends Record<string, unknown>>(
patch: Partial<T>,
{
forceUpdate,
parseNumbers,
replace,
}: {
forceUpdate?: boolean;
parseNumbers?: boolean;
replace?: boolean;
} = {}
) {
const oldQueryStrings = queryString.parse(location.search, {
parseBooleans: true,
parseNumbers: parseNumbers,
});
const newQueryStrings = { ...oldQueryStrings, ...patch };
if (forceUpdate || !isEqual(oldQueryStrings, newQueryStrings)) {
const search = queryString.stringify(newQueryStrings);
const newState = {
...this.history.location,
search,
};
if (replace) {
this.history.replace(newState);
} else {
this.history.push(newState);
}
}
}
push(path: To) {
this.history.push(path);
}
@@ -10,7 +10,7 @@ import type { Editor } from '@affine/core/modules/editor';
import { EditorService, EditorsService } from '@affine/core/modules/editor';
import { RecentDocsService } from '@affine/core/modules/quicksearch';
import { ViewService } from '@affine/core/modules/workbench/services/view';
import type { PageRootService } from '@blocksuite/blocks';
import type { DocMode, PageRootService } from '@blocksuite/blocks';
import {
BookmarkBlockService,
customImageProxyMiddleware,
@@ -330,9 +330,25 @@ export const DetailPage = ({ pageId }: { pageId: string }): ReactElement => {
const docRecordList = docsService.list;
const docListReady = useLiveData(docRecordList.isReady$);
const docRecord = useLiveData(docRecordList.doc$(pageId));
const viewService = useService(ViewService);
const queryString = useLiveData(
viewService.view.queryString$<{
mode?: string;
}>()
);
const queryStringMode =
queryString.mode && ['edgeless', 'page'].includes(queryString.mode)
? (queryString.mode as DocMode)
: null;
// We only read the querystring mode when entering, so use useState here.
const [initialQueryStringMode] = useState(() => queryStringMode);
const [doc, setDoc] = useState<Doc | null>(null);
const [editor, setEditor] = useState<Editor | null>(null);
const editorMode = useLiveData(editor?.mode$);
useLayoutEffect(() => {
if (!docRecord) {
@@ -351,12 +367,26 @@ export const DetailPage = ({ pageId }: { pageId: string }): ReactElement => {
}
const editor = doc.scope
.get(EditorsService)
.createEditor(doc.getPrimaryMode() || 'page');
.createEditor(initialQueryStringMode || doc.getPrimaryMode() || 'page');
setEditor(editor);
return () => {
editor.dispose();
};
}, [doc]);
}, [doc, initialQueryStringMode]);
// update editor mode to queryString
useEffect(() => {
if (editorMode) {
viewService.view.updateQueryString(
{
mode: editorMode,
},
{
replace: true,
}
);
}
}, [editorMode, viewService.view]);
// set sync engine priority target
useEffect(() => {
+1
View File
@@ -310,6 +310,7 @@ export enum ErrorNames {
MAILER_SERVICE_IS_NOT_CONFIGURED = 'MAILER_SERVICE_IS_NOT_CONFIGURED',
MEMBER_QUOTA_EXCEEDED = 'MEMBER_QUOTA_EXCEEDED',
MISSING_OAUTH_QUERY_PARAMETER = 'MISSING_OAUTH_QUERY_PARAMETER',
NOT_FOUND = 'NOT_FOUND',
NOT_IN_WORKSPACE = 'NOT_IN_WORKSPACE',
NO_COPILOT_PROVIDER_AVAILABLE = 'NO_COPILOT_PROVIDER_AVAILABLE',
OAUTH_ACCOUNT_ALREADY_CONNECTED = 'OAUTH_ACCOUNT_ALREADY_CONNECTED',
@@ -16,6 +16,7 @@ import {
filterTags,
removeSelectedTag,
} from '@affine-test/kit/utils/properties';
import { getCurrentDocIdFromUrl } from '@affine-test/kit/utils/url';
import { expect, type Page } from '@playwright/test';
const searchAndCreateTag = async (page: Page, name: string) => {
@@ -64,7 +65,7 @@ test('New a page and open it ,then open info modal in the title bar more action
});
test('New a page, then open info modal from all doc', async ({ page }) => {
const newPageId = page.url().split('/').reverse()[0];
const newPageId = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
const cell = getPageByTitle(page, 'this is a new page');
@@ -83,7 +84,7 @@ test('New a page, then open info modal from all doc', async ({ page }) => {
test('New a page and add to favourites, then open info modal from sidebar', async ({
page,
}) => {
const newPageId = page.url().split('/').reverse()[0];
const newPageId = getCurrentDocIdFromUrl(page);
await clickPageMoreActions(page);
await page.getByTestId('editor-option-menu-favorite').click();
+9 -5
View File
@@ -8,6 +8,10 @@ import {
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { clickSideBarAllPageButton } from '@affine-test/kit/utils/sidebar';
import {
getCurrentCollectionIdFromUrl,
getCurrentDocIdFromUrl,
} from '@affine-test/kit/utils/url';
import type { Locator, Page } from '@playwright/test';
import { expect } from '@playwright/test';
@@ -32,7 +36,7 @@ const createCollection = async (page: Page, name: string) => {
await expect(input).toBeVisible();
await input.fill(name);
await page.getByTestId('save-collection').click();
const newCollectionId = page.url().split('/').reverse()[0];
const newCollectionId = getCurrentCollectionIdFromUrl(page);
const collection = page.getByTestId(`explorer-collection-${newCollectionId}`);
await expect(collection).toBeVisible();
return collection;
@@ -85,7 +89,7 @@ test('drag a page from "All pages" list to favourites, then drag to trash', asyn
const title = 'this is a new page to drag';
await waitForEditorLoad(page);
await createPage(page, title);
const pageId = page.url().split('/').reverse()[0];
const pageId = getCurrentDocIdFromUrl(page);
await clickSideBarAllPageButton(page);
await page.waitForTimeout(500);
@@ -133,7 +137,7 @@ test('drag a page from favourites to collection', async ({ page }) => {
const title = 'this is a new page to drag';
await createPage(page, title);
const pageId = page.url().split('/').reverse()[0];
const pageId = getCurrentDocIdFromUrl(page);
await clickSideBarAllPageButton(page);
await page.waitForTimeout(500);
@@ -152,7 +156,7 @@ test('drag a collection to favourites', async ({ page }) => {
await clickSideBarAllPageButton(page);
await page.waitForTimeout(500);
const collection = await createCollection(page, 'test collection');
const collectionId = page.url().split('/').reverse()[0];
const collectionId = getCurrentCollectionIdFromUrl(page);
await dragToFavourites(page, collection, collectionId, 'collection');
});
@@ -167,7 +171,7 @@ test('items in favourites can be reordered by dragging', async ({ page }) => {
{
const collection = await createCollection(page, 'test collection');
const collectionId = page.url().split('/').reverse()[0];
const collectionId = getCurrentCollectionIdFromUrl(page);
await dragToFavourites(page, collection, collectionId, 'collection');
}
@@ -7,6 +7,7 @@ import {
getPageOperationButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { getCurrentDocIdFromUrl } from '@affine-test/kit/utils/url';
import { expect } from '@playwright/test';
test('page delete -> refresh page -> it should be disappear', async ({
@@ -18,7 +19,7 @@ test('page delete -> refresh page -> it should be disappear', async ({
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page delete');
const newPageId = page.url().split('/').reverse()[0];
const newPageId = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
const cell = page.getByRole('cell', {
name: 'this is a new page delete',
@@ -54,7 +55,7 @@ test('page delete -> create new page -> refresh page -> new page should be appea
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page delete');
const newPageDeleteId = page.url().split('/').reverse()[0];
const newPageDeleteId = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
const cellDelete = page.getByRole('cell', {
name: 'this is a new page delete',
@@ -80,13 +81,13 @@ test('page delete -> create new page -> refresh page -> new page should be appea
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page1');
await page.waitForTimeout(1000);
const newPageId1 = page.url().split('/').reverse()[0];
const newPageId1 = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page2');
await page.waitForTimeout(1000);
const newPageId2 = page.url().split('/').reverse()[0];
const newPageId2 = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
await page.reload();
await getPageItem(page, newPageId1).click();
@@ -109,13 +110,13 @@ test('delete multiple pages -> create multiple pages -> refresh', async ({
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page1');
const newPageId1 = page.url().split('/').reverse()[0];
const newPageId1 = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
// create 2nd page
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page2');
const newPageId2 = page.url().split('/').reverse()[0];
const newPageId2 = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
// 1st cell to be deleted
@@ -10,6 +10,7 @@ import {
waitForEditorLoad,
waitForEmptyEditor,
} from '@affine-test/kit/utils/page-logic';
import { getCurrentDocIdFromUrl } from '@affine-test/kit/utils/url';
import { expect } from '@playwright/test';
test('Show favorite items in sidebar', async ({ page, workspace }) => {
@@ -18,7 +19,7 @@ test('Show favorite items in sidebar', async ({ page, workspace }) => {
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite');
const newPageId = page.url().split('/').reverse()[0];
const newPageId = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
const cell = getPageByTitle(page, 'this is a new page to favorite');
await expect(cell).toBeVisible();
@@ -50,7 +51,7 @@ test('Show favorite reference in sidebar', async ({ page, workspace }) => {
await createLinkedPage(page, 'Another page');
const newPageId = page.url().split('/').reverse()[0];
const newPageId = getCurrentDocIdFromUrl(page);
await clickPageMoreActions(page);
@@ -89,7 +90,7 @@ test("Deleted page's reference will not be shown in sidebar", async ({
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to favorite');
const newPageId = page.url().split('/').reverse()[0];
const newPageId = getCurrentDocIdFromUrl(page);
// goes to main content
await page.keyboard.press('Enter', { delay: 50 });
@@ -108,7 +109,7 @@ test("Deleted page's reference will not be shown in sidebar", async ({
page.locator('.doc-title-container:has-text("Another page")')
).toBeVisible();
const anotherPageId = page.url().split('/').reverse()[0];
const anotherPageId = getCurrentDocIdFromUrl(page);
const favItemTestId = 'explorer-doc-' + newPageId;
@@ -5,14 +5,15 @@ import {
getBlockSuiteEditorTitle,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { getCurrentDocIdFromUrl } from '@affine-test/kit/utils/url';
import { expect } from '@playwright/test';
test('click btn new page', async ({ page, workspace }) => {
await openHomePage(page);
await waitForEditorLoad(page);
const originPageId = page.url().split('/').reverse()[0];
const originPageId = getCurrentDocIdFromUrl(page);
await clickNewPageButton(page);
const newPageId = page.url().split('/').reverse()[0];
const newPageId = getCurrentDocIdFromUrl(page);
expect(newPageId).not.toBe(originPageId);
const currentWorkspace = await workspace.current();
@@ -6,6 +6,7 @@ import {
getPageOperationButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { getCurrentDocIdFromUrl } from '@affine-test/kit/utils/url';
import { expect } from '@playwright/test';
test('click btn new page and open in tab', async ({ page, workspace }) => {
@@ -15,7 +16,7 @@ test('click btn new page and open in tab', async ({ page, workspace }) => {
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page');
const newPageUrl = page.url();
const newPageId = page.url().split('/').reverse()[0];
const newPageId = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
@@ -6,6 +6,7 @@ import {
getPageOperationButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { getCurrentDocIdFromUrl } from '@affine-test/kit/utils/url';
import { expect } from '@playwright/test';
test('New a page , then delete it in all pages, restore it', async ({
@@ -17,7 +18,7 @@ test('New a page , then delete it in all pages, restore it', async ({
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to restore');
const newPageId = page.url().split('/').reverse()[0];
const newPageId = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
const cell = page.getByRole('cell', {
name: 'this is a new page to restore',
@@ -7,6 +7,7 @@ import {
getPageOperationButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { getCurrentDocIdFromUrl } from '@affine-test/kit/utils/url';
import { expect } from '@playwright/test';
test('New a page ,then open it and show delete modal', async ({
@@ -44,7 +45,7 @@ test('New a page ,then go to all pages and show delete modal', async ({
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to delete');
const newPageId = page.url().split('/').reverse()[0];
const newPageId = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
const cell = page.getByRole('cell', {
name: 'this is a new page to delete',
@@ -6,6 +6,7 @@ import {
getPageOperationButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { getCurrentDocIdFromUrl } from '@affine-test/kit/utils/url';
import { expect } from '@playwright/test';
test('New a page , then delete it in all pages, finally find it in trash', async ({
@@ -17,7 +18,7 @@ test('New a page , then delete it in all pages, finally find it in trash', async
await clickNewPageButton(page);
await getBlockSuiteEditorTitle(page).click();
await getBlockSuiteEditorTitle(page).fill('this is a new page to delete');
const newPageId = page.url().split('/').reverse()[0];
const newPageId = getCurrentDocIdFromUrl(page);
await page.getByTestId('all-pages').click();
const cell = page.getByRole('cell', {
name: 'this is a new page to delete',
+4 -3
View File
@@ -6,9 +6,10 @@ import { expect } from '@playwright/test';
test('goto not found page', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
const currentUrl = page.url();
const invalidUrl = currentUrl.concat('invalid');
await page.goto(invalidUrl);
const currentUrl = new URL(page.url());
const invalidUrl = (currentUrl.pathname =
currentUrl.pathname.concat('invalid'));
await page.goto(invalidUrl.toString());
await expect(page.getByTestId('not-found')).toBeVisible({
timeout: 10000,
});
+19
View File
@@ -0,0 +1,19 @@
import type { Page } from '@playwright/test';
export function getCurrentDocIdFromUrl(page: Page) {
const pathname = new URL(page.url()).pathname;
const match = pathname.match(/\/workspace\/([^/]+)\/([^/]+)\/?/);
if (match && match[2]) {
return match[2];
}
throw new Error('Failed to get doc id from url');
}
export function getCurrentCollectionIdFromUrl(page: Page) {
const pathname = new URL(page.url()).pathname;
const match = pathname.match(/\/workspace\/([^/]+)\/collection\/([^/]+)\/?/);
if (match && match[2]) {
return match[2];
}
throw new Error('Failed to get collection id from url');
}
+33
View File
@@ -464,6 +464,7 @@ __metadata:
mixpanel-browser: "npm:^2.49.0"
nanoid: "npm:^5.0.7"
next-themes: "npm:^0.3.0"
query-string: "npm:^9.1.0"
react: "npm:18.3.1"
react-dom: "npm:18.3.1"
react-error-boundary: "npm:^4.0.13"
@@ -20679,6 +20680,13 @@ __metadata:
languageName: node
linkType: hard
"decode-uri-component@npm:^0.4.1":
version: 0.4.1
resolution: "decode-uri-component@npm:0.4.1"
checksum: 10/74eec26f7bec3767164e37d526ef19bc1214cb0bbeeeea1c4f0ceb79299e5c38d3ba734e7243d829842aa140f24e5d020f54cc25b17c7082461c8eead8a72ce3
languageName: node
linkType: hard
"decompress-response@npm:^6.0.0":
version: 6.0.0
resolution: "decompress-response@npm:6.0.0"
@@ -23414,6 +23422,13 @@ __metadata:
languageName: node
linkType: hard
"filter-obj@npm:^5.1.0":
version: 5.1.0
resolution: "filter-obj@npm:5.1.0"
checksum: 10/8f6dab6d8d8855f686e8cc6be289bbbd64a80be52c660124e36e982f78017cf5dae7de95f79ec167fbf62101d6aab93067a3105ae8f56251785a721e678d6b07
languageName: node
linkType: hard
"finalhandler@npm:1.2.0":
version: 1.2.0
resolution: "finalhandler@npm:1.2.0"
@@ -32984,6 +32999,17 @@ __metadata:
languageName: node
linkType: hard
"query-string@npm:^9.1.0":
version: 9.1.0
resolution: "query-string@npm:9.1.0"
dependencies:
decode-uri-component: "npm:^0.4.1"
filter-obj: "npm:^5.1.0"
split-on-first: "npm:^3.0.0"
checksum: 10/87a0ea851f62d6abe5c6c60bc0114e5df1af73f6dbdfd3086ab82624330cafe92310029d834ee6117d92e3de877f2959cb634c31a55a1aef4acddaa11ad8be20
languageName: node
linkType: hard
"querystring@npm:0.2.0":
version: 0.2.0
resolution: "querystring@npm:0.2.0"
@@ -35446,6 +35472,13 @@ __metadata:
languageName: node
linkType: hard
"split-on-first@npm:^3.0.0":
version: 3.0.0
resolution: "split-on-first@npm:3.0.0"
checksum: 10/75dc27ecbac65cfbeab9a3b90cf046307220192d3d7a30e46aa0f19571cc9b4802aac813f3de2cc9b16f2e46aae72f275659b5d2614bb5369c77724d739e5f73
languageName: node
linkType: hard
"split2@npm:^4.0.0":
version: 4.2.0
resolution: "split2@npm:4.2.0"