mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-15 23:25:16 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba2a523722 | |||
| 57adb3e54a | |||
| 7377928c8a | |||
| 5f8a8fe9b1 | |||
| 29a0bf25c4 | |||
| 92bb016576 | |||
| c8810ea002 | |||
| aa8bbaddc4 |
@@ -0,0 +1,8 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$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": []
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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 }}
|
||||
+8
-3
@@ -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
|
||||
await page.waitForURL(
|
||||
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions/v/2.0/reference/api-reference/pets',
|
||||
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions/v/2.0/reference/api-reference/pets?fallback=true',
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -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
|
||||
await page.waitForURL(
|
||||
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-share-links/bRfQbzwsK8rbN1GRxx7K/v/2.0/reference/api-reference/pets',
|
||||
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-share-links/bRfQbzwsK8rbN1GRxx7K/v/2.0/reference/api-reference/pets?fallback=true',
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -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
|
||||
await page.waitForURL(
|
||||
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-va/v/2.0/reference/api-reference/pets',
|
||||
'https://gitbook-open-e2e-sites.gitbook.io/api-multi-versions-va/v/2.0/reference/api-reference/pets?fallback=true',
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -363,6 +363,11 @@ const testCases: TestsCase[] = [
|
||||
name: 'Page options',
|
||||
baseUrl: 'https://gitbook.gitbook.io/test-1-1/',
|
||||
tests: [
|
||||
{
|
||||
name: 'Hidden',
|
||||
url: 'page-options/page-hidden',
|
||||
run: waitForCookiesDialog,
|
||||
},
|
||||
{
|
||||
name: 'With cover',
|
||||
url: 'page-options/page-with-cover',
|
||||
|
||||
+4
-1
@@ -13,6 +13,8 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"unit": "bun test {src,packages}",
|
||||
"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"
|
||||
},
|
||||
"workspaces": [
|
||||
@@ -20,7 +22,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@geist-ui/icons": "^1.0.2",
|
||||
"@gitbook/api": "^0.53.0",
|
||||
"@gitbook/api": "^0.56.0",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@sentry/nextjs": "^7.94.1",
|
||||
@@ -65,6 +67,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@argos-ci/playwright": "^2.0.0",
|
||||
"@changesets/cli": "^2.27.7",
|
||||
"@cloudflare/next-on-pages": "^1.11.3",
|
||||
"@cloudflare/workers-types": "^4.20231218.0",
|
||||
"@playwright/test": "^1.42.1",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# @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,6 +1,7 @@
|
||||
{
|
||||
"name": "@gitbook/emoji-codepoints",
|
||||
"description": "Optimized mapping of codepoints to the fully qualified emoji codepoints",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"exports": "./index.ts",
|
||||
"dependencies": {},
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# @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,5 +1,6 @@
|
||||
{
|
||||
"name": "@gitbook/react-contentkit",
|
||||
"version": "0.2.0",
|
||||
"exports": "./src/index.ts",
|
||||
"dependencies": {
|
||||
"classnames": "^2.5.1",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# @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,6 +1,7 @@
|
||||
{
|
||||
"name": "@gitbook/react-math",
|
||||
"exports": "./src/index.ts",
|
||||
"version": "0.2.0",
|
||||
"dependencies": {
|
||||
"object-hash": "^3.0.0"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# @gitbook/react-openapi
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 57adb3e: Second release to fix publishing with changeset
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 5f8a8fe: Initial release
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "@gitbook/react-openapi",
|
||||
"exports": "./src/index.ts",
|
||||
"version": "0.2.0",
|
||||
"dependencies": {
|
||||
"@scalar/api-client-react": "^0.3.7",
|
||||
"@scalar/oas-utils": "0.1.6",
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
import React from 'react';
|
||||
|
||||
import { useScrollToHash } from '@/components/hooks';
|
||||
|
||||
/**
|
||||
@@ -9,5 +12,29 @@ export function PageClientLayout(props: {}) {
|
||||
// We use this hook in the page layout to ensure the elements for the blocks
|
||||
// are rendered before we scroll to the hash.
|
||||
useScrollToHash();
|
||||
|
||||
useStripFallbackQueryParam();
|
||||
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 { PageBody, PageCover } from '@/components/PageBody';
|
||||
import { PageHrefContext, absoluteHref, pageHref } from '@/lib/links';
|
||||
import { getPagePath } from '@/lib/pages';
|
||||
import { getPagePath, resolveFirstDocument } from '@/lib/pages';
|
||||
import { ContentRefContext } from '@/lib/references';
|
||||
import { tcls } from '@/lib/tailwind';
|
||||
import { getContentTitle } from '@/lib/utils';
|
||||
@@ -19,10 +19,12 @@ export const runtime = 'edge';
|
||||
/**
|
||||
* Fetch and render a page.
|
||||
*/
|
||||
export default async function Page(props: { params: PagePathParams }) {
|
||||
const { params } = props;
|
||||
export default async function Page(props: {
|
||||
params: PagePathParams;
|
||||
searchParams: { fallback?: string };
|
||||
}) {
|
||||
const { params, searchParams } = props;
|
||||
|
||||
const rawPathname = getPathnameParam(params);
|
||||
const {
|
||||
content: contentPointer,
|
||||
contentTarget,
|
||||
@@ -32,9 +34,14 @@ export default async function Page(props: { params: PagePathParams }) {
|
||||
pages,
|
||||
page,
|
||||
document,
|
||||
} = await fetchPageData(params);
|
||||
const linksContext: PageHrefContext = {};
|
||||
} = await getPageDataWithFallback({
|
||||
pagePathParams: params,
|
||||
searchParams,
|
||||
redirectOnFallback: true,
|
||||
});
|
||||
|
||||
const linksContext: PageHrefContext = {};
|
||||
const rawPathname = getPathnameParam(params);
|
||||
if (!page) {
|
||||
const pathname = normalizePathname(rawPathname);
|
||||
if (pathname !== rawPathname) {
|
||||
@@ -114,8 +121,18 @@ export async function generateViewport({ params }: { params: PagePathParams }):
|
||||
};
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: PagePathParams }): Promise<Metadata> {
|
||||
const { space, pages, page, customization, parent } = await fetchPageData(params);
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
searchParams,
|
||||
}: {
|
||||
params: PagePathParams;
|
||||
searchParams: { fallback?: string };
|
||||
}): Promise<Metadata> {
|
||||
const { space, pages, page, customization, parent } = await getPageDataWithFallback({
|
||||
pagePathParams: params,
|
||||
searchParams,
|
||||
});
|
||||
|
||||
if (!page) {
|
||||
notFound();
|
||||
}
|
||||
@@ -136,3 +153,34 @@ export async function generateMetadata({ params }: { params: PagePathParams }):
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Revision,
|
||||
RevisionPageDocument,
|
||||
RevisionPageGroup,
|
||||
RevisionPageLink,
|
||||
SiteCustomizationSettings,
|
||||
Space,
|
||||
} from '@gitbook/api';
|
||||
@@ -313,9 +314,17 @@ function selectPages(
|
||||
): FlatPageEntry[] => {
|
||||
return [
|
||||
{ page, depth },
|
||||
...page.pages.flatMap((child) =>
|
||||
child.type === 'link' ? [] : flattenPage(child, depth + 1),
|
||||
),
|
||||
...page.pages.flatMap((child) => {
|
||||
if (child.type === 'link') {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (child.hidden) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return flattenPage(child, depth + 1);
|
||||
}),
|
||||
];
|
||||
};
|
||||
|
||||
@@ -340,8 +349,16 @@ function selectPages(
|
||||
return limitTo(flattenPage(found.page, 0));
|
||||
}
|
||||
|
||||
const allPages = rootPages.flatMap((page) =>
|
||||
page.type === 'link' ? [] : flattenPage(page, 0),
|
||||
);
|
||||
const allPages = rootPages.flatMap((page) => {
|
||||
if (page.type === 'link') {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (page.hidden) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return flattenPage(page, 0);
|
||||
});
|
||||
return limitTo(allPages);
|
||||
}
|
||||
|
||||
@@ -18,8 +18,12 @@ export async function BlockContentRef(props: BlockProps<DocumentBlockContentRef>
|
||||
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;
|
||||
if (resolved.active && kind === 'space') {
|
||||
if ((resolved.active && kind === 'space') || isContentInOtherSpace) {
|
||||
return <SpaceRefCard {...props} resolved={resolved} />;
|
||||
}
|
||||
|
||||
@@ -61,6 +65,7 @@ async function SpaceRefCard(
|
||||
}
|
||||
href={resolved.href}
|
||||
title={resolved.text}
|
||||
postTitle={resolved.subText}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ function useVariantSpaceHref(variantSpaceUrl: string) {
|
||||
const targetUrl = new URL(variantSpaceUrl);
|
||||
targetUrl.pathname += `/${currentPathname}`;
|
||||
targetUrl.pathname = targetUrl.pathname.replace(/\/{2,}/g, '/').replace(/\/$/, '');
|
||||
targetUrl.searchParams.set('fallback', 'true');
|
||||
|
||||
return targetUrl.toString();
|
||||
}
|
||||
|
||||
@@ -63,17 +63,6 @@ async function sendSiteTrackPageViewRequest(args: {
|
||||
|
||||
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.
|
||||
* We don't use the API client to avoid shipping 80kb of JS to the client.
|
||||
@@ -86,11 +75,6 @@ async function trackPageView(args: {
|
||||
pageId: string | undefined;
|
||||
}) {
|
||||
const { apiHost, sitePointer, pageId, spaceId } = args;
|
||||
|
||||
if (isInIframe()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pageId === latestPageId) {
|
||||
// The hook can be called multiple times, we only want to track once.
|
||||
return;
|
||||
|
||||
@@ -96,6 +96,7 @@ export const SearchResults = React.forwardRef(function SearchResults(
|
||||
const fetchedResults = await (parent
|
||||
? searchParentContent(parent, query)
|
||||
: searchSpaceContent(spaceId, revisionId, query));
|
||||
|
||||
setResults(withAsk ? withQuestionResult(fetchedResults, query) : fetchedResults);
|
||||
}, 250);
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
SearchAIAnswer,
|
||||
SearchPageResult,
|
||||
Site,
|
||||
SiteSpace,
|
||||
Space,
|
||||
} from '@gitbook/api';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
import { getContentPointer } from '@/app/(space)/fetch';
|
||||
import { streamResponse } from '@/lib/actions';
|
||||
@@ -52,6 +52,61 @@ export interface AskAnswerResult {
|
||||
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
|
||||
*/
|
||||
@@ -60,6 +115,16 @@ export async function searchSpaceContent(
|
||||
revisionId: string,
|
||||
query: string,
|
||||
): 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);
|
||||
return data.items.map((item) => transformPageResult(item, undefined)).flat();
|
||||
}
|
||||
@@ -72,36 +137,18 @@ export async function searchParentContent(
|
||||
query: string,
|
||||
): Promise<OrderedComputedResult[]> {
|
||||
const pointer = getContentPointer();
|
||||
const isSite = 'siteId' in pointer;
|
||||
|
||||
const [data, collectionSpaces, siteSpaces] = await Promise.all([
|
||||
if (isSite) {
|
||||
return searchSiteContent({ query });
|
||||
}
|
||||
|
||||
const [data, collectionSpaces] = await Promise.all([
|
||||
api.searchParentContent(parent.id, query),
|
||||
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[] = [];
|
||||
|
||||
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>,
|
||||
),
|
||||
);
|
||||
}
|
||||
let spaces: Space[] = collectionSpaces ? collectionSpaces : [];
|
||||
|
||||
return data.items
|
||||
.map((spaceItem) => {
|
||||
@@ -183,19 +230,24 @@ function transformAnswer(
|
||||
};
|
||||
}
|
||||
|
||||
function transformPageResult(item: SearchPageResult, space?: Space) {
|
||||
function transformSectionsAndPage(args: {
|
||||
item: SearchPageResult;
|
||||
space?: Space;
|
||||
spaceURL?: string;
|
||||
}): [ComputedPageResult, ComputedSectionResult[]] {
|
||||
const { item, space, spaceURL } = args;
|
||||
|
||||
// Resolve a relative path to an absolute URL
|
||||
// if the search result is relative to another space, we use the space URL
|
||||
const getURL = (path: string) => {
|
||||
if (space) {
|
||||
let url = space.urls.published ?? space.urls.app;
|
||||
if (!url.endsWith('/')) {
|
||||
url += '/';
|
||||
const getURL = (path: string, spaceURL?: string) => {
|
||||
if (spaceURL) {
|
||||
if (!spaceURL.endsWith('/')) {
|
||||
spaceURL += '/';
|
||||
}
|
||||
if (path.startsWith('/')) {
|
||||
path = path.slice(1);
|
||||
}
|
||||
return url + path;
|
||||
return spaceURL + path;
|
||||
} else {
|
||||
return absoluteHref(path);
|
||||
}
|
||||
@@ -206,7 +258,7 @@ function transformPageResult(item: SearchPageResult, space?: Space) {
|
||||
type: 'section',
|
||||
id: item.id + '/' + section.id,
|
||||
title: section.title,
|
||||
href: getURL(section.path),
|
||||
href: getURL(section.path, spaceURL),
|
||||
body: section.body,
|
||||
})) ?? [];
|
||||
|
||||
@@ -218,5 +270,25 @@ function transformPageResult(item: SearchPageResult, space?: Space) {
|
||||
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];
|
||||
}
|
||||
|
||||
@@ -19,6 +19,14 @@ export function PagesList(props: {
|
||||
return (
|
||||
<ul className={tcls('flex', 'flex-1', 'flex-col', 'gap-y-0.5', style)}>
|
||||
{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') {
|
||||
return (
|
||||
<PageGroupItem
|
||||
@@ -29,8 +37,6 @@ export function PagesList(props: {
|
||||
context={context}
|
||||
/>
|
||||
);
|
||||
} else if (page.type === 'link') {
|
||||
return <PageLinkItem key={page.id} page={page} context={context} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1046,6 +1046,41 @@ 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.
|
||||
*/
|
||||
|
||||
+14
-3
@@ -75,12 +75,13 @@ export function resolvePageId(
|
||||
|
||||
/**
|
||||
* Resolve the next/previous page before another one.
|
||||
* It ignores hidden pages as this is used for navigation purpose.
|
||||
*/
|
||||
export function resolvePrevNextPages(
|
||||
rootPages: Revision['pages'],
|
||||
page: RevisionPageDocument,
|
||||
): { previous?: RevisionPageDocument; next?: RevisionPageDocument } {
|
||||
const flat = flattenPages(rootPages);
|
||||
const flat = flattenPages(rootPages, (page) => !page.hidden);
|
||||
|
||||
const currentIndex = flat.findIndex((p) => p.id === page.id);
|
||||
if (currentIndex === -1) {
|
||||
@@ -152,17 +153,27 @@ function resolvePageDocument(
|
||||
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[] = [];
|
||||
for (const page of pages) {
|
||||
if (page.type === 'link') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (filter && !filter(page)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (page.type === 'document') {
|
||||
result.push(page);
|
||||
}
|
||||
result.push(...flattenPages(page.pages));
|
||||
result.push(...flattenPages(page.pages, filter));
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
+25
-4
@@ -18,6 +18,8 @@ import { getPagePath, resolvePageId } from './pages';
|
||||
export interface ResolvedContentRef {
|
||||
/** Text to render in the content ref */
|
||||
text: string;
|
||||
/** Additional sub text to render in the content ref */
|
||||
subText?: string;
|
||||
/** Emoji associated with the reference */
|
||||
emoji?: string;
|
||||
/** URL to open for the content ref */
|
||||
@@ -103,10 +105,14 @@ export async function resolveContentRef(
|
||||
return resolveContentRefInSpace(contentRef.space, contentRef);
|
||||
}
|
||||
|
||||
const page =
|
||||
const resolvePageResult =
|
||||
!contentRef.page || contentRef.page === activePage?.id
|
||||
? activePage
|
||||
: resolvePageId(pages, contentRef.page)?.page;
|
||||
? { page: activePage, ancestors: [] }
|
||||
: undefined
|
||||
: resolvePageId(pages, contentRef.page);
|
||||
|
||||
const page = resolvePageResult?.page;
|
||||
if (!page) {
|
||||
return null;
|
||||
}
|
||||
@@ -134,7 +140,13 @@ export async function resolveContentRef(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
text = page.title;
|
||||
const parentPage = (resolvePageResult?.ancestors || []).slice(-1).pop();
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -245,12 +257,21 @@ async function resolveContentRefInSpace(spaceId: string, contentRef: ContentRef)
|
||||
baseUrl += '/';
|
||||
}
|
||||
|
||||
return resolveContentRef(contentRef, {
|
||||
const resolved = await resolveContentRef(contentRef, {
|
||||
space,
|
||||
revisionId: space.revision,
|
||||
pages,
|
||||
baseUrl,
|
||||
});
|
||||
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...resolved,
|
||||
subText: space.title,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveContentRefWithFiles(
|
||||
|
||||
Reference in New Issue
Block a user