Compare commits

..

1 Commits

Author SHA1 Message Date
Valentino Hudhra 9ef655c946 Fix tracking page views when running inside an iframe 2024-07-15 16:04:03 +02:00
39 changed files with 92 additions and 450 deletions
-8
View File
@@ -1,8 +0,0 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
-11
View File
@@ -1,11 +0,0 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.0.2/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
-35
View File
@@ -1,35 +0,0 @@
name: Publish
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
publish:
name: Publish
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v3
with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0
- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.18
- name: Install dependencies
run: bun install --frozen-lockfile
env:
PUPPETEER_SKIP_DOWNLOAD: 1
- name: Create Release Pull Request or Publish to npm
id: changesets
uses: changesets/action@v1
with:
publish: npm run release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
BIN
View File
Binary file not shown.
+3 -8
View File
@@ -129,7 +129,7 @@ const testCases: TestsCase[] = [
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant // It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
await page.waitForURL( await page.waitForURL(
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions/v/2.0/reference/api-reference/pets?fallback=true', 'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions/v/2.0/reference/api-reference/pets',
); );
}, },
}, },
@@ -152,7 +152,7 @@ const testCases: TestsCase[] = [
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant // It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
await page.waitForURL( await page.waitForURL(
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-share-links/bRfQbzwsK8rbN1GRxx7K/v/2.0/reference/api-reference/pets?fallback=true', 'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-share-links/bRfQbzwsK8rbN1GRxx7K/v/2.0/reference/api-reference/pets',
); );
}, },
}, },
@@ -187,7 +187,7 @@ const testCases: TestsCase[] = [
// It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant // It should keep the current page path, i.e "reference/api-reference/pets" when navigating to the new variant
await page.waitForURL( await page.waitForURL(
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-va/v/2.0/reference/api-reference/pets?fallback=true', 'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-va/v/2.0/reference/api-reference/pets',
); );
}, },
}, },
@@ -363,11 +363,6 @@ const testCases: TestsCase[] = [
name: 'Page options', name: 'Page options',
baseUrl: 'https://gitbook.gitbook.io/test-1-1/', baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
tests: [ tests: [
{
name: 'Hidden',
url: 'page-options/page-hidden',
run: waitForCookiesDialog,
},
{ {
name: 'With cover', name: 'With cover',
url: 'page-options/page-with-cover', url: 'page-options/page-with-cover',
+1 -4
View File
@@ -13,8 +13,6 @@
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"unit": "bun test {src,packages}", "unit": "bun test {src,packages}",
"e2e": "playwright test", "e2e": "playwright test",
"changeset": "changeset",
"release": "changeset publish",
"postinstall": "rm -rf ./public/~gitbook/static/mathjax@3.2.2 && mkdir -p ./public/~gitbook/static/ && cp -R node_modules/mathjax/es5 ./public/~gitbook/static/mathjax@3.2.2" "postinstall": "rm -rf ./public/~gitbook/static/mathjax@3.2.2 && mkdir -p ./public/~gitbook/static/ && cp -R node_modules/mathjax/es5 ./public/~gitbook/static/mathjax@3.2.2"
}, },
"workspaces": [ "workspaces": [
@@ -22,7 +20,7 @@
], ],
"dependencies": { "dependencies": {
"@geist-ui/icons": "^1.0.2", "@geist-ui/icons": "^1.0.2",
"@gitbook/api": "^0.56.0", "@gitbook/api": "^0.53.0",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",
"@sentry/nextjs": "^7.94.1", "@sentry/nextjs": "^7.94.1",
@@ -67,7 +65,6 @@
}, },
"devDependencies": { "devDependencies": {
"@argos-ci/playwright": "^2.0.0", "@argos-ci/playwright": "^2.0.0",
"@changesets/cli": "^2.27.7",
"@cloudflare/next-on-pages": "^1.11.3", "@cloudflare/next-on-pages": "^1.11.3",
"@cloudflare/workers-types": "^4.20231218.0", "@cloudflare/workers-types": "^4.20231218.0",
"@playwright/test": "^1.42.1", "@playwright/test": "^1.42.1",
-13
View File
@@ -1,13 +0,0 @@
# @gitbook/emoji-codepoints
## 0.2.0
### Minor Changes
- 57adb3e: Second release to fix publishing with changeset
## 0.1.0
### Minor Changes
- 5f8a8fe: Initial release
-1
View File
@@ -1,7 +1,6 @@
{ {
"name": "@gitbook/emoji-codepoints", "name": "@gitbook/emoji-codepoints",
"description": "Optimized mapping of codepoints to the fully qualified emoji codepoints", "description": "Optimized mapping of codepoints to the fully qualified emoji codepoints",
"version": "0.2.0",
"private": true, "private": true,
"exports": "./index.ts", "exports": "./index.ts",
"dependencies": {}, "dependencies": {},
-13
View File
@@ -1,13 +0,0 @@
# @gitbook/react-contentkit
## 0.2.0
### Minor Changes
- 57adb3e: Second release to fix publishing with changeset
## 0.1.0
### Minor Changes
- 5f8a8fe: Initial release
-1
View File
@@ -1,6 +1,5 @@
{ {
"name": "@gitbook/react-contentkit", "name": "@gitbook/react-contentkit",
"version": "0.2.0",
"exports": "./src/index.ts", "exports": "./src/index.ts",
"dependencies": { "dependencies": {
"classnames": "^2.5.1", "classnames": "^2.5.1",
-13
View File
@@ -1,13 +0,0 @@
# @gitbook/react-math
## 0.2.0
### Minor Changes
- 57adb3e: Second release to fix publishing with changeset
## 0.1.0
### Minor Changes
- 5f8a8fe: Initial release
-1
View File
@@ -1,7 +1,6 @@
{ {
"name": "@gitbook/react-math", "name": "@gitbook/react-math",
"exports": "./src/index.ts", "exports": "./src/index.ts",
"version": "0.2.0",
"dependencies": { "dependencies": {
"object-hash": "^3.0.0" "object-hash": "^3.0.0"
}, },
-19
View File
@@ -1,19 +0,0 @@
# @gitbook/react-openapi
## 0.3.0
### Minor Changes
- bd0ca5b: Fix missing react imports
## 0.2.0
### Minor Changes
- 57adb3e: Second release to fix publishing with changeset
## 0.1.0
### Minor Changes
- 5f8a8fe: Initial release
+9 -2
View File
@@ -1,5 +1,12 @@
# `@gitbook/react-openapi` # `@gitbook/react-openapi`
Style-less React components to render OpenAPI operation blocks. React components to render OpenAPI operations.
Note: The package is a TypeScript package and do not expose JS built files. ## Features
- Generate code samples for the request
- Support custom cde samples with `x-codeSamples` (Redocly syntax)
## TODO
- Support for trying out the request
-1
View File
@@ -1,7 +1,6 @@
{ {
"name": "@gitbook/react-openapi", "name": "@gitbook/react-openapi",
"exports": "./src/index.ts", "exports": "./src/index.ts",
"version": "0.3.0",
"dependencies": { "dependencies": {
"@scalar/api-client-react": "^0.3.7", "@scalar/api-client-react": "^0.3.7",
"@scalar/oas-utils": "0.1.6", "@scalar/oas-utils": "0.1.6",
-1
View File
@@ -1,4 +1,3 @@
import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
export function Markdown(props: { source: string; className?: string }) { export function Markdown(props: { source: string; className?: string }) {
@@ -1,4 +1,4 @@
import * as React from 'react'; import { OpenAPIV3 } from 'openapi-types';
import { CodeSampleInput, codeSampleGenerators } from './code-samples'; import { CodeSampleInput, codeSampleGenerators } from './code-samples';
import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation'; import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation';
@@ -1,4 +1,3 @@
import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation'; import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation';
@@ -1,5 +1,3 @@
import * as React from 'react';
import { OpenAPIV3 } from 'openapi-types'; import { OpenAPIV3 } from 'openapi-types';
import { OpenAPIRootSchema } from './OpenAPISchema'; import { OpenAPIRootSchema } from './OpenAPISchema';
import { noReference } from './utils'; import { noReference } from './utils';
@@ -1,4 +1,3 @@
import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { OpenAPIV3 } from 'openapi-types'; import { OpenAPIV3 } from 'openapi-types';
import { OpenAPIRootSchema, OpenAPISchemaProperties } from './OpenAPISchema'; import { OpenAPIRootSchema, OpenAPISchemaProperties } from './OpenAPISchema';
@@ -1,4 +1,3 @@
import * as React from 'react';
import { InteractiveSection } from './InteractiveSection'; import { InteractiveSection } from './InteractiveSection';
import { OpenAPIOperationData } from './fetchOpenAPIOperation'; import { OpenAPIOperationData } from './fetchOpenAPIOperation';
import { generateSchemaExample } from './generateSchemaExample'; import { generateSchemaExample } from './generateSchemaExample';
@@ -1,4 +1,3 @@
import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { OpenAPIV3 } from 'openapi-types'; import { OpenAPIV3 } from 'openapi-types';
import { noReference } from './utils'; import { noReference } from './utils';
@@ -1,4 +1,3 @@
import * as React from 'react';
import { OpenAPIV3 } from 'openapi-types'; import { OpenAPIV3 } from 'openapi-types';
import { OpenAPIClientContext } from './types'; import { OpenAPIClientContext } from './types';
import { InteractiveSection } from './InteractiveSection'; import { InteractiveSection } from './InteractiveSection';
@@ -1,4 +1,3 @@
import * as React from 'react';
import { OpenAPIV3 } from 'openapi-types'; import { OpenAPIV3 } from 'openapi-types';
import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable'; import { OpenAPIServerURLVariable } from './OpenAPIServerURLVariable';
@@ -1,8 +1,8 @@
'use client'; 'use client';
import * as React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { OpenAPIV3 } from 'openapi-types'; import { OpenAPIV3 } from 'openapi-types';
import React from 'react';
/** /**
* Interactive component to show the value of a server variable and let the user change it. * Interactive component to show the value of a server variable and let the user change it.
@@ -1,6 +1,5 @@
'use client'; 'use client';
import * as React from 'react';
import { OpenAPIV3 } from 'openapi-types'; import { OpenAPIV3 } from 'openapi-types';
import { OpenAPIOperationData, fromJSON } from './fetchOpenAPIOperation'; import { OpenAPIOperationData, fromJSON } from './fetchOpenAPIOperation';
@@ -1,8 +1,5 @@
'use client'; 'use client';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import React from 'react';
import { useScrollToHash } from '@/components/hooks'; import { useScrollToHash } from '@/components/hooks';
/** /**
@@ -12,29 +9,5 @@ export function PageClientLayout(props: {}) {
// We use this hook in the page layout to ensure the elements for the blocks // We use this hook in the page layout to ensure the elements for the blocks
// are rendered before we scroll to the hash. // are rendered before we scroll to the hash.
useScrollToHash(); useScrollToHash();
useStripFallbackQueryParam();
return null; return null;
} }
/**
* Strip the fallback query parameter from current URL.
*
* When the user switches variants using the space dropdown, we pass a fallback=true parameter.
* This parameter indicates that we should redirect to the root page if the path from the
* previous variant doesn't exist in the new variant. If the path does exist, no redirect occurs,
* so we need to remove the fallback parameter.
*/
function useStripFallbackQueryParam() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
React.useEffect(() => {
if (searchParams.has('fallback')) {
const params = new URLSearchParams(searchParams.toString());
params.delete('fallback');
router.push(`${pathname}?${params.toString()}${window.location.hash ?? ''}`);
}
}, [router, pathname, searchParams]);
}
@@ -6,7 +6,7 @@ import React from 'react';
import { PageAside } from '@/components/PageAside'; import { PageAside } from '@/components/PageAside';
import { PageBody, PageCover } from '@/components/PageBody'; import { PageBody, PageCover } from '@/components/PageBody';
import { PageHrefContext, absoluteHref, pageHref } from '@/lib/links'; import { PageHrefContext, absoluteHref, pageHref } from '@/lib/links';
import { getPagePath, resolveFirstDocument } from '@/lib/pages'; import { getPagePath } from '@/lib/pages';
import { ContentRefContext } from '@/lib/references'; import { ContentRefContext } from '@/lib/references';
import { tcls } from '@/lib/tailwind'; import { tcls } from '@/lib/tailwind';
import { getContentTitle } from '@/lib/utils'; import { getContentTitle } from '@/lib/utils';
@@ -19,12 +19,10 @@ export const runtime = 'edge';
/** /**
* Fetch and render a page. * Fetch and render a page.
*/ */
export default async function Page(props: { export default async function Page(props: { params: PagePathParams }) {
params: PagePathParams; const { params } = props;
searchParams: { fallback?: string };
}) {
const { params, searchParams } = props;
const rawPathname = getPathnameParam(params);
const { const {
content: contentPointer, content: contentPointer,
contentTarget, contentTarget,
@@ -34,14 +32,9 @@ export default async function Page(props: {
pages, pages,
page, page,
document, document,
} = await getPageDataWithFallback({ } = await fetchPageData(params);
pagePathParams: params,
searchParams,
redirectOnFallback: true,
});
const linksContext: PageHrefContext = {}; const linksContext: PageHrefContext = {};
const rawPathname = getPathnameParam(params);
if (!page) { if (!page) {
const pathname = normalizePathname(rawPathname); const pathname = normalizePathname(rawPathname);
if (pathname !== rawPathname) { if (pathname !== rawPathname) {
@@ -121,18 +114,8 @@ export async function generateViewport({ params }: { params: PagePathParams }):
}; };
} }
export async function generateMetadata({ export async function generateMetadata({ params }: { params: PagePathParams }): Promise<Metadata> {
params, const { space, pages, page, customization, parent } = await fetchPageData(params);
searchParams,
}: {
params: PagePathParams;
searchParams: { fallback?: string };
}): Promise<Metadata> {
const { space, pages, page, customization, parent } = await getPageDataWithFallback({
pagePathParams: params,
searchParams,
});
if (!page) { if (!page) {
notFound(); notFound();
} }
@@ -153,34 +136,3 @@ export async function generateMetadata({
}, },
}; };
} }
/**
* Fetches the page data matching the requested pathname and fallback to root page when page is not found.
*/
async function getPageDataWithFallback(args: {
pagePathParams: PagePathParams;
searchParams: { fallback?: string };
redirectOnFallback?: boolean;
}) {
const { pagePathParams, searchParams, redirectOnFallback = false } = args;
const { pages, page: targetPage, ...otherPageData } = await fetchPageData(pagePathParams);
let page = targetPage;
const canFallback = !!searchParams.fallback;
if (!page && canFallback) {
const rootPage = resolveFirstDocument(pages, []);
if (redirectOnFallback && rootPage?.page) {
redirect(pageHref(pages, rootPage?.page));
}
page = rootPage?.page;
}
return {
...otherPageData,
pages,
page,
};
}
+3 -6
View File
@@ -15,7 +15,7 @@ export const runtime = 'edge';
*/ */
export async function GET(req: NextRequest) { export async function GET(req: NextRequest) {
const { pages: rootPages } = await getSpaceContentData(getContentPointer()); const { pages: rootPages } = await getSpaceContentData(getContentPointer());
const pages = flattenPages(rootPages, (page) => !page.hidden); const pages = flattenPages(rootPages);
const urls = pages.map(({ page, depth }) => { const urls = pages.map(({ page, depth }) => {
// Decay priority with depth // Decay priority with depth
const priority = Math.pow(2, -0.25 * depth); const priority = Math.pow(2, -0.25 * depth);
@@ -66,16 +66,13 @@ export async function GET(req: NextRequest) {
type FlatPageEntry = { page: RevisionPageDocument; depth: number }; type FlatPageEntry = { page: RevisionPageDocument; depth: number };
function flattenPages( function flattenPages(rootPags: RevisionPage[]): FlatPageEntry[] {
rootPags: RevisionPage[],
filter: (page: RevisionPageDocument) => boolean,
): FlatPageEntry[] {
const flattenPage = ( const flattenPage = (
page: RevisionPageDocument | RevisionPageGroup, page: RevisionPageDocument | RevisionPageGroup,
depth: number, depth: number,
): FlatPageEntry[] => { ): FlatPageEntry[] => {
return [ return [
...(page.type === 'document' && filter(page) ? [{ page, depth }] : []), ...(page.type === 'document' ? [{ page, depth }] : []),
...page.pages.flatMap((child) => ...page.pages.flatMap((child) =>
child.type === 'link' ? [] : flattenPage(child, depth + 1), child.type === 'link' ? [] : flattenPage(child, depth + 1),
), ),
+6 -23
View File
@@ -4,7 +4,6 @@ import {
Revision, Revision,
RevisionPageDocument, RevisionPageDocument,
RevisionPageGroup, RevisionPageGroup,
RevisionPageLink,
SiteCustomizationSettings, SiteCustomizationSettings,
Space, Space,
} from '@gitbook/api'; } from '@gitbook/api';
@@ -314,17 +313,9 @@ function selectPages(
): FlatPageEntry[] => { ): FlatPageEntry[] => {
return [ return [
{ page, depth }, { page, depth },
...page.pages.flatMap((child) => { ...page.pages.flatMap((child) =>
if (child.type === 'link') { child.type === 'link' ? [] : flattenPage(child, depth + 1),
return []; ),
}
if (child.hidden) {
return [];
}
return flattenPage(child, depth + 1);
}),
]; ];
}; };
@@ -349,16 +340,8 @@ function selectPages(
return limitTo(flattenPage(found.page, 0)); return limitTo(flattenPage(found.page, 0));
} }
const allPages = rootPages.flatMap((page) => { const allPages = rootPages.flatMap((page) =>
if (page.type === 'link') { page.type === 'link' ? [] : flattenPage(page, 0),
return []; );
}
if (page.hidden) {
return [];
}
return flattenPage(page, 0);
});
return limitTo(allPages); return limitTo(allPages);
} }
@@ -18,12 +18,8 @@ export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>
return null; return null;
} }
const isContentInOtherSpace =
context.contentRefContext?.space &&
'space' in block.data.ref &&
context.contentRefContext.space.id !== block.data.ref.space;
const kind = block?.data?.ref?.kind; const kind = block?.data?.ref?.kind;
if ((resolved.active && kind === 'space') || isContentInOtherSpace) { if (resolved.active && kind === 'space') {
return <SpaceRefCard {...props} resolved={resolved} />; return <SpaceRefCard {...props} resolved={resolved} />;
} }
@@ -65,7 +61,6 @@ async function SpaceRefCard(
} }
href={resolved.href} href={resolved.href}
title={resolved.text} title={resolved.text}
postTitle={resolved.subText}
style={style} style={style}
/> />
); );
@@ -10,7 +10,6 @@ function useVariantSpaceHref(variantSpaceUrl: string) {
const targetUrl = new URL(variantSpaceUrl); const targetUrl = new URL(variantSpaceUrl);
targetUrl.pathname += `/${currentPathname}`; targetUrl.pathname += `/${currentPathname}`;
targetUrl.pathname = targetUrl.pathname.replace(/\/{2,}/g, '/').replace(/\/$/, ''); targetUrl.pathname = targetUrl.pathname.replace(/\/{2,}/g, '/').replace(/\/$/, '');
targetUrl.searchParams.set('fallback', 'true');
return targetUrl.toString(); return targetUrl.toString();
} }
+16
View File
@@ -63,6 +63,17 @@ async function sendSiteTrackPageViewRequest(args: {
let latestPageId: string | undefined | null = null; let latestPageId: string | undefined | null = null;
/**
* Check if the current page is in an iframe.
* This is used to avoid tracking page views in iframes.
*/
function isInIframe(): boolean {
try {
return window.self !== window.top || window.frameElement !== null;
} catch (e) {
return true;
}
}
/** /**
* Track the page view for the current page to GitBook. * Track the page view for the current page to GitBook.
* We don't use the API client to avoid shipping 80kb of JS to the client. * We don't use the API client to avoid shipping 80kb of JS to the client.
@@ -75,6 +86,11 @@ async function trackPageView(args: {
pageId: string | undefined; pageId: string | undefined;
}) { }) {
const { apiHost, sitePointer, pageId, spaceId } = args; const { apiHost, sitePointer, pageId, spaceId } = args;
if (isInIframe()) {
return;
}
if (pageId === latestPageId) { if (pageId === latestPageId) {
// The hook can be called multiple times, we only want to track once. // The hook can be called multiple times, we only want to track once.
return; return;
-1
View File
@@ -96,7 +96,6 @@ export const SearchResults = React.forwardRef(function SearchResults(
const fetchedResults = await (parent const fetchedResults = await (parent
? searchParentContent(parent, query) ? searchParentContent(parent, query)
: searchSpaceContent(spaceId, revisionId, query)); : searchSpaceContent(spaceId, revisionId, query));
setResults(withAsk ? withQuestionResult(fetchedResults, query) : fetchedResults); setResults(withAsk ? withQuestionResult(fetchedResults, query) : fetchedResults);
}, 250); }, 250);
+34 -106
View File
@@ -6,9 +6,9 @@ import {
SearchAIAnswer, SearchAIAnswer,
SearchPageResult, SearchPageResult,
Site, Site,
SiteSpace,
Space, Space,
} from '@gitbook/api'; } from '@gitbook/api';
import { headers } from 'next/headers';
import { getContentPointer } from '@/app/(space)/fetch'; import { getContentPointer } from '@/app/(space)/fetch';
import { streamResponse } from '@/lib/actions'; import { streamResponse } from '@/lib/actions';
@@ -52,61 +52,6 @@ export interface AskAnswerResult {
hasAnswer: boolean; hasAnswer: boolean;
} }
export async function searchSiteContent(args: {
query: string;
siteSpaceIds?: string[];
cacheBust?: string;
}): Promise<OrderedComputedResult[]> {
const { siteSpaceIds, query, cacheBust } = args;
const pointer = getContentPointer();
if (siteSpaceIds?.length === 0) {
// if we have no siteSpaces to search in then we won't find anything. skip the call.
return [];
}
if ('siteId' in pointer && 'organizationId' in pointer) {
const [searchResults, allSiteSpaces] = await Promise.all([
api.searchSiteContent(
pointer.organizationId,
pointer.siteId,
query,
siteSpaceIds,
cacheBust,
),
siteSpaceIds
? null
: api.getSiteSpaces({
organizationId: pointer.organizationId,
siteId: pointer.siteId,
siteShareKey: pointer.siteShareKey,
}),
]);
if (!siteSpaceIds) {
// We are searching all of this Site's content
return searchResults.items
.map((spaceItem) => {
const siteSpace = allSiteSpaces?.find(
(siteSpace) => siteSpace.space.id === spaceItem.id,
);
return spaceItem.pages.map((item) => transformSitePageResult(item, siteSpace));
})
.flat(2);
}
return searchResults.items
.map((spaceItem) => {
return spaceItem.pages.map((item) => transformPageResult(item));
})
.flat(2);
}
// This should never happen
return [];
}
/** /**
* Server action to search content in a space * Server action to search content in a space
*/ */
@@ -115,16 +60,6 @@ export async function searchSpaceContent(
revisionId: string, revisionId: string,
query: string, query: string,
): Promise<OrderedComputedResult[]> { ): Promise<OrderedComputedResult[]> {
const pointer = getContentPointer();
if ('siteId' in pointer && 'organizationId' in pointer) {
const siteSpaceIds = pointer.siteSpaceId ? [pointer.siteSpaceId] : []; // if we don't have a siteSpaceID search all content
// This is a site so use a different function which we can eventually call directly
// We also want to break cache for this specific space if the revisionId is different so use it as a cache busting key
return await searchSiteContent({ siteSpaceIds, query, cacheBust: revisionId });
}
const data = await api.searchSpaceContent(spaceId, revisionId, query); const data = await api.searchSpaceContent(spaceId, revisionId, query);
return data.items.map((item) => transformPageResult(item, undefined)).flat(); return data.items.map((item) => transformPageResult(item, undefined)).flat();
} }
@@ -137,18 +72,36 @@ export async function searchParentContent(
query: string, query: string,
): Promise<OrderedComputedResult[]> { ): Promise<OrderedComputedResult[]> {
const pointer = getContentPointer(); const pointer = getContentPointer();
const isSite = 'siteId' in pointer;
if (isSite) { const [data, collectionSpaces, siteSpaces] = await Promise.all([
return searchSiteContent({ query });
}
const [data, collectionSpaces] = await Promise.all([
api.searchParentContent(parent.id, query), api.searchParentContent(parent.id, query),
parent.object === 'collection' ? api.getCollectionSpaces(parent.id) : null, parent.object === 'collection' ? api.getCollectionSpaces(parent.id) : null,
parent.object === 'site' && 'organizationId' in pointer
? api.getSiteSpaces({
organizationId: pointer.organizationId,
siteId: parent.id,
siteShareKey: pointer.siteShareKey,
})
: null,
]); ]);
let spaces: Space[] = collectionSpaces ? collectionSpaces : []; let spaces: Space[] = [];
if (collectionSpaces) {
spaces = collectionSpaces;
} else if (siteSpaces) {
spaces = Object.values(
siteSpaces.reduce(
(acc, siteSpace) => {
acc[siteSpace.space.id] = siteSpace.space;
// replace the published url for the "space" with the site's published url
acc[siteSpace.space.id].urls.published = siteSpace.urls.published;
return acc;
},
{} as Record<string, Space>,
),
);
}
return data.items return data.items
.map((spaceItem) => { .map((spaceItem) => {
@@ -230,24 +183,19 @@ function transformAnswer(
}; };
} }
function transformSectionsAndPage(args: { function transformPageResult(item: SearchPageResult, space?: Space) {
item: SearchPageResult;
space?: Space;
spaceURL?: string;
}): [ComputedPageResult, ComputedSectionResult[]] {
const { item, space, spaceURL } = args;
// Resolve a relative path to an absolute URL // Resolve a relative path to an absolute URL
// if the search result is relative to another space, we use the space URL // if the search result is relative to another space, we use the space URL
const getURL = (path: string, spaceURL?: string) => { const getURL = (path: string) => {
if (spaceURL) { if (space) {
if (!spaceURL.endsWith('/')) { let url = space.urls.published ?? space.urls.app;
spaceURL += '/'; if (!url.endsWith('/')) {
url += '/';
} }
if (path.startsWith('/')) { if (path.startsWith('/')) {
path = path.slice(1); path = path.slice(1);
} }
return spaceURL + path; return url + path;
} else { } else {
return absoluteHref(path); return absoluteHref(path);
} }
@@ -258,7 +206,7 @@ function transformSectionsAndPage(args: {
type: 'section', type: 'section',
id: item.id + '/' + section.id, id: item.id + '/' + section.id,
title: section.title, title: section.title,
href: getURL(section.path, spaceURL), href: getURL(section.path),
body: section.body, body: section.body,
})) ?? []; })) ?? [];
@@ -270,25 +218,5 @@ function transformSectionsAndPage(args: {
spaceTitle: space?.title, spaceTitle: space?.title,
}; };
return [page, sections];
}
function transformSitePageResult(item: SearchPageResult, siteSpace?: SiteSpace) {
const [page, sections] = transformSectionsAndPage({
item,
space: siteSpace?.space,
spaceURL: siteSpace?.urls.published,
});
return [page, ...sections];
}
function transformPageResult(item: SearchPageResult, space?: Space) {
const [page, sections] = transformSectionsAndPage({
item,
space,
spaceURL: space?.urls.published ?? space?.urls.app,
});
return [page, ...sections]; return [page, ...sections];
} }
+2 -8
View File
@@ -19,14 +19,6 @@ export function PagesList(props: {
return ( return (
<ul className={tcls('flex', 'flex-1', 'flex-col', 'gap-y-0.5', style)}> <ul className={tcls('flex', 'flex-1', 'flex-col', 'gap-y-0.5', style)}>
{pages.map((page) => { {pages.map((page) => {
if (page.type === 'link') {
return <PageLinkItem key={page.id} page={page} context={context} />;
}
if (page.hidden) {
return null;
}
if (page.type === 'group') { if (page.type === 'group') {
return ( return (
<PageGroupItem <PageGroupItem
@@ -37,6 +29,8 @@ export function PagesList(props: {
context={context} context={context}
/> />
); );
} else if (page.type === 'link') {
return <PageLinkItem key={page.id} page={page} context={context} />;
} }
return ( return (
-35
View File
@@ -1046,41 +1046,6 @@ export const searchParentContent = cache(
}, },
); );
/**
* Search content in a Site or specific SiteSpaces.
*/
export const searchSiteContent = cache(
'api.searchSiteContent',
async (
organizationId: string,
siteId: string,
query: string,
siteSpaceIds?: string[],
/** A cache bust param to avoid revalidating lot of cache entries by tags */
cacheBust?: string,
options?: CacheFunctionOptions,
) => {
const response = await api().orgs.searchSiteContent(
organizationId,
siteId,
{
query,
siteSpaceIds,
},
undefined,
{
...noCacheFetchOptions,
signal: options?.signal,
},
);
return cacheResponse(response, {
ttl: 60 * 60,
tags: [],
});
},
);
/** /**
* Get a list of recommended questions in a space. * Get a list of recommended questions in a space.
*/ */
+3 -14
View File
@@ -75,13 +75,12 @@ export function resolvePageId(
/** /**
* Resolve the next/previous page before another one. * Resolve the next/previous page before another one.
* It ignores hidden pages as this is used for navigation purpose.
*/ */
export function resolvePrevNextPages( export function resolvePrevNextPages(
rootPages: Revision['pages'], rootPages: Revision['pages'],
page: RevisionPageDocument, page: RevisionPageDocument,
): { previous?: RevisionPageDocument; next?: RevisionPageDocument } { ): { previous?: RevisionPageDocument; next?: RevisionPageDocument } {
const flat = flattenPages(rootPages, (page) => !page.hidden); const flat = flattenPages(rootPages);
const currentIndex = flat.findIndex((p) => p.id === page.id); const currentIndex = flat.findIndex((p) => p.id === page.id);
if (currentIndex === -1) { if (currentIndex === -1) {
@@ -153,27 +152,17 @@ function resolvePageDocument(
return { page, ancestors }; return { page, ancestors };
} }
/** function flattenPages(pages: RevisionPage[]): RevisionPageDocument[] {
* Flatten a list of pages into a list of page documents.
*/
function flattenPages(
pages: RevisionPage[],
filter?: (page: RevisionPageDocument | RevisionPageGroup) => boolean,
): RevisionPageDocument[] {
const result: RevisionPageDocument[] = []; const result: RevisionPageDocument[] = [];
for (const page of pages) { for (const page of pages) {
if (page.type === 'link') { if (page.type === 'link') {
continue; continue;
} }
if (filter && !filter(page)) {
continue;
}
if (page.type === 'document') { if (page.type === 'document') {
result.push(page); result.push(page);
} }
result.push(...flattenPages(page.pages, filter)); result.push(...flattenPages(page.pages));
} }
return result; return result;
+4 -25
View File
@@ -18,8 +18,6 @@ import { getPagePath, resolvePageId } from './pages';
export interface ResolvedContentRef { export interface ResolvedContentRef {
/** Text to render in the content ref */ /** Text to render in the content ref */
text: string; text: string;
/** Additional sub text to render in the content ref */
subText?: string;
/** Emoji associated with the reference */ /** Emoji associated with the reference */
emoji?: string; emoji?: string;
/** URL to open for the content ref */ /** URL to open for the content ref */
@@ -105,14 +103,10 @@ export async function resolveContentRef(
return resolveContentRefInSpace(contentRef.space, contentRef); return resolveContentRefInSpace(contentRef.space, contentRef);
} }
const resolvePageResult = const page =
!contentRef.page || contentRef.page === activePage?.id !contentRef.page || contentRef.page === activePage?.id
? activePage ? activePage
? { page: activePage, ancestors: [] } : resolvePageId(pages, contentRef.page)?.page;
: undefined
: resolvePageId(pages, contentRef.page);
const page = resolvePageResult?.page;
if (!page) { if (!page) {
return null; return null;
} }
@@ -140,13 +134,7 @@ export async function resolveContentRef(
} }
} }
} else { } else {
const parentPage = (resolvePageResult?.ancestors || []).slice(-1).pop(); text = page.title;
// When the looked up ref was a page group we use the page group title as resolved ref text.
// Otherwise use the resolved page title.
text =
parentPage && contentRef.page === parentPage.id && parentPage.type === 'group'
? parentPage.title
: page.title;
emoji = isCurrentPage ? undefined : page.emoji; emoji = isCurrentPage ? undefined : page.emoji;
} }
@@ -257,21 +245,12 @@ async function resolveContentRefInSpace(spaceId: string, contentRef: ContentRef)
baseUrl += '/'; baseUrl += '/';
} }
const resolved = await resolveContentRef(contentRef, { return resolveContentRef(contentRef, {
space, space,
revisionId: space.revision, revisionId: space.revision,
pages, pages,
baseUrl, baseUrl,
}); });
if (!resolved) {
return null;
}
return {
...resolved,
subText: space.title,
};
} }
export function resolveContentRefWithFiles( export function resolveContentRefWithFiles(