From c82259488203b35d5a0f46d10bed9a088e424dd5 Mon Sep 17 00:00:00 2001 From: EYHN Date: Fri, 16 Aug 2024 10:59:43 +0000 Subject: [PATCH] feat(core): mode in query string (#7904) --- packages/frontend/core/package.json | 1 + .../src/modules/workbench/entities/view.ts | 48 +++++++++++++++++++ .../workspace/detail-page/detail-page.tsx | 36 ++++++++++++-- packages/frontend/graphql/src/schema.ts | 1 + tests/affine-local/e2e/doc-info-modal.spec.ts | 5 +- tests/affine-local/e2e/drag-page.spec.ts | 14 ++++-- .../e2e/local-first-delete-page.spec.ts | 13 ++--- .../e2e/local-first-favorites-items.spec.ts | 9 ++-- .../e2e/local-first-new-page.spec.ts | 5 +- .../e2e/local-first-openpage-newtab.spec.ts | 3 +- .../e2e/local-first-restore-page.spec.ts | 3 +- .../e2e/local-first-show-delete-modal.spec.ts | 3 +- .../e2e/local-first-trash-page.spec.ts | 3 +- tests/affine-local/e2e/router.spec.ts | 7 +-- tests/kit/utils/url.ts | 19 ++++++++ yarn.lock | 33 +++++++++++++ 16 files changed, 174 insertions(+), 29 deletions(-) create mode 100644 tests/kit/utils/url.ts diff --git a/packages/frontend/core/package.json b/packages/frontend/core/package.json index dc27f6edee..23a0fe5902 100644 --- a/packages/frontend/core/package.json +++ b/packages/frontend/core/package.json @@ -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", diff --git a/packages/frontend/core/src/modules/workbench/entities/view.ts b/packages/frontend/core/src/modules/workbench/entities/view.ts index f037f67693..a0c04e3639 100644 --- a/packages/frontend/core/src/modules/workbench/entities/view.ts +++ b/packages/frontend/core/src/modules/workbench/entities/view.ts @@ -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$>({ + parseNumbers = true, + }: { parseNumbers?: boolean } = {}) { + return this.location$.map( + location => + queryString.parse(location.search, { + parseBooleans: true, + parseNumbers: parseNumbers, + }) as Partial + ); + } + + updateQueryString>( + patch: Partial, + { + 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); } diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx index 539f2f6ca3..ae4b8a8638 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx @@ -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(null); const [editor, setEditor] = useState(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(() => { diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts index 509d1b9745..ae8c81cae6 100644 --- a/packages/frontend/graphql/src/schema.ts +++ b/packages/frontend/graphql/src/schema.ts @@ -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', diff --git a/tests/affine-local/e2e/doc-info-modal.spec.ts b/tests/affine-local/e2e/doc-info-modal.spec.ts index 0f5ecab12a..78fec7a533 100644 --- a/tests/affine-local/e2e/doc-info-modal.spec.ts +++ b/tests/affine-local/e2e/doc-info-modal.spec.ts @@ -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(); diff --git a/tests/affine-local/e2e/drag-page.spec.ts b/tests/affine-local/e2e/drag-page.spec.ts index c2920f43df..862079bfac 100644 --- a/tests/affine-local/e2e/drag-page.spec.ts +++ b/tests/affine-local/e2e/drag-page.spec.ts @@ -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'); } diff --git a/tests/affine-local/e2e/local-first-delete-page.spec.ts b/tests/affine-local/e2e/local-first-delete-page.spec.ts index 2510e03ac4..69b670f1bc 100644 --- a/tests/affine-local/e2e/local-first-delete-page.spec.ts +++ b/tests/affine-local/e2e/local-first-delete-page.spec.ts @@ -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 diff --git a/tests/affine-local/e2e/local-first-favorites-items.spec.ts b/tests/affine-local/e2e/local-first-favorites-items.spec.ts index 6bbc8443f3..ae82eaae59 100644 --- a/tests/affine-local/e2e/local-first-favorites-items.spec.ts +++ b/tests/affine-local/e2e/local-first-favorites-items.spec.ts @@ -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; diff --git a/tests/affine-local/e2e/local-first-new-page.spec.ts b/tests/affine-local/e2e/local-first-new-page.spec.ts index b43d0bb947..aa96c887a4 100644 --- a/tests/affine-local/e2e/local-first-new-page.spec.ts +++ b/tests/affine-local/e2e/local-first-new-page.spec.ts @@ -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(); diff --git a/tests/affine-local/e2e/local-first-openpage-newtab.spec.ts b/tests/affine-local/e2e/local-first-openpage-newtab.spec.ts index c66ebb087d..b04d25fa52 100644 --- a/tests/affine-local/e2e/local-first-openpage-newtab.spec.ts +++ b/tests/affine-local/e2e/local-first-openpage-newtab.spec.ts @@ -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(); diff --git a/tests/affine-local/e2e/local-first-restore-page.spec.ts b/tests/affine-local/e2e/local-first-restore-page.spec.ts index 95a57e2268..3bd9f2d9f7 100644 --- a/tests/affine-local/e2e/local-first-restore-page.spec.ts +++ b/tests/affine-local/e2e/local-first-restore-page.spec.ts @@ -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', diff --git a/tests/affine-local/e2e/local-first-show-delete-modal.spec.ts b/tests/affine-local/e2e/local-first-show-delete-modal.spec.ts index 92ccd9f5ab..52465d897e 100644 --- a/tests/affine-local/e2e/local-first-show-delete-modal.spec.ts +++ b/tests/affine-local/e2e/local-first-show-delete-modal.spec.ts @@ -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', diff --git a/tests/affine-local/e2e/local-first-trash-page.spec.ts b/tests/affine-local/e2e/local-first-trash-page.spec.ts index c35739264c..255b37687c 100644 --- a/tests/affine-local/e2e/local-first-trash-page.spec.ts +++ b/tests/affine-local/e2e/local-first-trash-page.spec.ts @@ -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', diff --git a/tests/affine-local/e2e/router.spec.ts b/tests/affine-local/e2e/router.spec.ts index 9505d87dd0..5f3d1e2419 100644 --- a/tests/affine-local/e2e/router.spec.ts +++ b/tests/affine-local/e2e/router.spec.ts @@ -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, }); diff --git a/tests/kit/utils/url.ts b/tests/kit/utils/url.ts new file mode 100644 index 0000000000..89d64f5421 --- /dev/null +++ b/tests/kit/utils/url.ts @@ -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'); +} diff --git a/yarn.lock b/yarn.lock index 6bc7d7ab91..f48d656729 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"