mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-10-14 17:19:42 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 30fa152f0f | |||
| 0540ebc1b9 | |||
| 0b515b0970 | |||
| 65acba0b61 |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix Mermaid block diagrams falling back to a code block on published sites.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix long Mermaid labels being clipped instead of wrapping at some browser zoom levels.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Stop page group breadcrumbs from linking to a URL that redirects to the group's first page.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Fix 404 on Markdown pages (View as Markdown) in site previews and when a theme is forced with `?theme=`.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Slack PR status
|
||||
|
||||
[GitbookIO/slack-pr-status](https://github.com/GitbookIO/slack-pr-status) adds review-status reactions to PR links posted in [#engineering](https://gitbook.slack.com/archives/C01NXGWJELS). It runs from `.github/workflows/slack-pr-status.yml` in `GitbookIO/gitbook`, `GitbookIO/gitbook-x` and `GitbookIO/integrations`. Emojis and statuses are configured in that workflow; see the action's README for the options.
|
||||
|
||||
Owner: Peter White.
|
||||
|
||||
| Configuration | Value |
|
||||
| --- | --- |
|
||||
| Slack app | **PR Status** (`A0C2AGLAD0T`), created from `.github/slack-pr-status-app-manifest.json` |
|
||||
| Bot token | Repository Actions secret `SLAPR_SLACK_BOT_TOKEN` |
|
||||
| GitHub token | The workflow's `GITHUB_TOKEN`, read-only |
|
||||
|
||||
Posts that link PRs from more than one repository are skipped, because each workflow's token can only read its own repository. PRs from forks get no reactions, because their workflows don't receive secrets.
|
||||
|
||||
To disable it in one repository, run `gh workflow disable slack-pr-status.yml --repo GitbookIO/<repository>`. To stop it everywhere, revoke the PR Status app's token.
|
||||
@@ -1,88 +0,0 @@
|
||||
# Slack PR reactions
|
||||
|
||||
[DataDog/slapr](https://github.com/DataDog/slapr) updates reactions on existing PR-link messages in [#engineering](https://gitbook.slack.com/archives/C01NXGWJELS). It runs in GitHub Actions. It does not post messages, approve or merge PRs, or change branch protection.
|
||||
|
||||
## Rollout and ownership
|
||||
|
||||
Owner: Peter White.
|
||||
|
||||
The rollout targets `GitbookIO/gitbook` (GBO), `GitbookIO/gitbook-x` (GBX), and `GitbookIO/integrations`. Each repository contains `.github/workflows/slapr.yml` and this runbook.
|
||||
|
||||
| Configuration | Value |
|
||||
| --- | --- |
|
||||
| Channel | `#engineering`, `C01NXGWJELS` |
|
||||
| App manifest | `.github/slapr-app-manifest.json` |
|
||||
| Slack app ID | `A0C2AGLAD0T` |
|
||||
| Bot user ID | `U0C2AGP16R1`, repository variable `SLAPR_BOT_USER_ID` |
|
||||
| Bot token | Repository Actions secret `SLAPR_SLACK_BOT_TOKEN` |
|
||||
| Action | `DataDog/slapr@1c074e1dfd433f91a679d5113f7f8b956b65d973` |
|
||||
| Approval threshold | Stock default, one approval |
|
||||
| Events | `pull_request_review: submitted`, `pull_request: closed` |
|
||||
|
||||
## Install and configure
|
||||
|
||||
1. In [Slack app management](https://api.slack.com/apps), create the **PR Status** app from `.github/slapr-app-manifest.json` in the GitBook workspace. Obtain workspace approval if required, then install it. The bot requests Slapr's documented `channels:history`, `reactions:read`, and `reactions:write` scopes. `#engineering` is public, so `groups:history` is unnecessary. No review map is configured, so `channels:read` is unnecessary.
|
||||
2. Invite **PR Status** only to `#engineering`. Keep it dedicated to Slapr: stock Slapr removes obsolete reactions belonging to this bot on a matched message. It leaves other users' reactions alone.
|
||||
3. Record the app ID and bot user ID above. The bot profile's member ID or Slack `auth.test` response's `user_id` is the required bot user ID, not the app ID or bot ID.
|
||||
4. Store the bot OAuth token as `SLAPR_SLACK_BOT_TOKEN` in each of the three repositories' Actions secrets. Use the GitHub secret UI or `gh secret set SLAPR_SLACK_BOT_TOKEN --repo GitbookIO/<repository>` and its hidden prompt. Never paste the token into a command argument, chat, workflow, or committed file. Alternatively, an organization secret must use selected-repository visibility limited to these three repositories.
|
||||
5. Set Actions variable `SLAPR_BOT_USER_ID` in each repository to that bot's user ID. The token and user ID must belong to the same installation. Use `gh variable set SLAPR_BOT_USER_ID --repo GitbookIO/<repository> --body '<user-id>'` or the GitHub UI.
|
||||
6. Merge the workflows through normal review, then run the live checks below. Repository maintainers handle reviews and merges; Slapr has only read access to GitHub contents and pull requests.
|
||||
|
||||
GBX runs this workflow on `blacksmith-2vcpu-ubuntu-2404`. GBO and integrations use `ubuntu-latest`, matching their existing workflows.
|
||||
|
||||
The workflow hardcodes the engineering channel and does not check out PR code. It uses the stock event triggers and leaves the approval threshold unset. There are no listeners, schedules, review maps, custom discovery, or additional services.
|
||||
|
||||
## Reactions
|
||||
|
||||
Slapr's status selection stays unchanged. Missing custom emoji names are mapped through the action's supported inputs to existing equivalents. Workspace lookup on 2026-09-17 confirmed `pr-approved` and `merged`; the other mappings are standard Slack emojis.
|
||||
|
||||
| Stock status | Reaction |
|
||||
| --- | --- |
|
||||
| Review started | `:eyes:` |
|
||||
| Partially approved | `:next_track_button:` |
|
||||
| Approved | `:pr-approved:` |
|
||||
| Changes requested | `:construction:` |
|
||||
| Merged | `:merged:` |
|
||||
| Closed without merging | `:no_entry_sign:` |
|
||||
| Comment-only review | `:speech_balloon:` |
|
||||
|
||||
The partially-approved reaction is configured but cannot occur with the default threshold of one approval. It becomes relevant only if that threshold is raised.
|
||||
|
||||
On approval, expect review-started and approved reactions. On merge, stock Slapr removes review-started and adds merged; it can retain approved alongside merged. Approval and merging have distinct reactions. Other stock review-state behavior is accepted.
|
||||
|
||||
## Live acceptance test
|
||||
|
||||
For each repository after credentials are configured and the workflow is on the default branch:
|
||||
|
||||
1. Open a fresh same-repository test PR through normal review. Post its full `https://github.com/GitbookIO/<repository>/pull/<number>` link as a new top-level message in `#engineering` before the review event.
|
||||
2. Have another contributor approve it. Open the **Slack PR reactions** Actions run and verify that the matched message has the bot's `pr-approved` reaction.
|
||||
3. Have an authorized maintainer merge it. Verify the closed-event run succeeds and the same message has the bot's `merged` reaction. No new Slack status message should appear.
|
||||
4. Record the PR URL, Slack message permalink, approval/merge workflow run URLs, date, and result below. A green workflow alone does not prove the message was found or reacted to.
|
||||
|
||||
| Repository | PR and Slack message | Approval run/result | Merge run/result |
|
||||
| --- | --- | --- | --- |
|
||||
| GitbookIO/gitbook | Pending | Not tested | Not tested |
|
||||
| GitbookIO/gitbook-x | Pending | Not tested | Not tested |
|
||||
| GitbookIO/integrations | Pending | Not tested | Not tested |
|
||||
|
||||
Only the first matching message in Slapr's recent-history response is updated. Older messages, thread replies, duplicate posts, fork PRs, and links posted after the event are outside this rollout's acceptance criteria. Slapr does not paginate history or reconcile missed events.
|
||||
|
||||
## Troubleshoot and disable
|
||||
|
||||
- No workflow run: confirm the workflow is merged, Actions permits the pinned action, and the event was a submitted review or PR closure. These are the only configured triggers.
|
||||
- `No message found requesting review`: confirm the full PR link was posted as a recent top-level message in `#engineering` before the event. This is a successful no-op in stock Slapr.
|
||||
- `not_in_channel` or `channel_not_found`: check the app installation, channel ID, and bot membership in `#engineering`.
|
||||
- `missing_scope`: compare the installed bot scopes with the manifest and reinstall after changes.
|
||||
- `invalid_auth` or `account_inactive`: replace the repository secrets with the current bot token. If the bot identity changed, update `SLAPR_BOT_USER_ID` too.
|
||||
- `invalid_name`: check the emoji mappings still exist. If a custom emoji was removed, map that input to another existing emoji, keeping approval and merged distinct.
|
||||
- Old reactions remain or removal fails: confirm `SLAPR_BOT_USER_ID` is the member ID for the token's bot.
|
||||
- GitHub permission errors: keep `contents: read` and `pull-requests: read`; do not introduce a PAT, write permissions, or `pull_request_target` to cover forks. Fork and Dependabot secret restrictions are accepted limitations.
|
||||
- Rate limits or transient API failures: inspect the Actions log and retry the failed run after the limit resets. There is no scheduled reconciliation.
|
||||
|
||||
Disable one repository with `gh workflow disable slapr.yml --repo GitbookIO/<repository>`, or disable **Slack PR reactions** in its Actions UI. Repeat for all three to stop the integration. Revoke the Slack app token to stop its access immediately, remove its repository secrets, and remove the bot from `#engineering` when retiring it. Existing reactions remain.
|
||||
|
||||
## Pin review
|
||||
|
||||
On 2026-09-17, the pinned action definition, Dockerfile, entry point, dependency declarations, GitHub reads, Slack history lookup and reaction writes were inspected. Without `review-map`, the action targets only the supplied channel. It reads PR/review state and adds/removes reactions; it does not post Slack messages or write to GitHub.
|
||||
|
||||
The action SHA pins Slapr's source. Its upstream Dockerfile still uses the mutable `python:3.14` image and installs dependencies, including `pyyaml>=5.0`, at build time. This rollout keeps stock Slapr unchanged. Review upstream source and dependency changes before updating the SHA in all three repositories.
|
||||
@@ -0,0 +1,33 @@
|
||||
name: Slack PR status
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted, dismissed]
|
||||
pull_request:
|
||||
types: [closed, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
slack-pr-status:
|
||||
# Secrets aren't available to workflows triggered from forks.
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: GitbookIO/slack-pr-status@main
|
||||
with:
|
||||
slack-token: ${{ secrets.SLAPR_SLACK_BOT_TOKEN }}
|
||||
config: |
|
||||
channels: [C01NXGWJELS]
|
||||
approvalsRequired: 1
|
||||
multiPr: aggregate
|
||||
emojis:
|
||||
commented: false
|
||||
partiallyApproved: next_track_button
|
||||
approved: pr-approved
|
||||
changesRequested: construction
|
||||
merged: merged
|
||||
closed: no_entry_sign
|
||||
partiallyReviewed: hourglass_flowing_sand
|
||||
@@ -1,30 +0,0 @@
|
||||
name: Slack PR reactions
|
||||
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
run_slapr:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Update PR reactions in engineering
|
||||
uses: DataDog/slapr@1c074e1dfd433f91a679d5113f7f8b956b65d973
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
slack-api-token: ${{ secrets.SLAPR_SLACK_BOT_TOKEN }}
|
||||
slack-channel-id: C01NXGWJELS
|
||||
bot-user-id: ${{ vars.SLAPR_BOT_USER_ID }}
|
||||
emoji-review-started: eyes
|
||||
emoji-partially-approved: next_track_button
|
||||
emoji-approved: pr-approved
|
||||
emoji-changes-requested: construction
|
||||
emoji-merged: merged
|
||||
emoji-closed: no_entry_sign
|
||||
emoji-commented: speech_balloon
|
||||
@@ -343,6 +343,7 @@
|
||||
"patchedDependencies": {
|
||||
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
|
||||
"next@16.3.8": "patches/next@16.3.8.patch",
|
||||
"mermaid@11.14.0": "patches/mermaid@11.14.0.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"@codemirror/state": "6.4.1",
|
||||
|
||||
+2
-1
@@ -67,6 +67,7 @@
|
||||
"patchedDependencies": {
|
||||
"decode-named-character-reference@1.0.2": "patches/decode-named-character-reference@1.0.2.patch",
|
||||
"@vercel/next@4.4.2": "patches/@vercel%2Fnext@4.4.2.patch",
|
||||
"next@16.3.8": "patches/next@16.3.8.patch"
|
||||
"next@16.3.8": "patches/next@16.3.8.patch",
|
||||
"mermaid@11.14.0": "patches/mermaid@11.14.0.patch"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1125,6 +1125,30 @@ const testCases: TestsCase[] = [
|
||||
).toBeVisible();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Markdown page with theme cookie',
|
||||
url: async () => {
|
||||
const data = await getSiteAPIToken(
|
||||
'https://gitbook.gitbook.io/test-gitbook-open/'
|
||||
);
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('token', data.apiToken);
|
||||
searchParams.set('theme', 'dark');
|
||||
|
||||
return `url/${getGitBookPreviewURL(`${data.site}/blocks?${searchParams.toString()}`)}`;
|
||||
},
|
||||
screenshot: false,
|
||||
run: async (page) => {
|
||||
// The app opens previews with `?theme=`, which the middleware persists in a cookie.
|
||||
const markdownURL = new URL(page.url());
|
||||
markdownURL.pathname = `${markdownURL.pathname.replace(/\/$/, '')}.md`;
|
||||
markdownURL.search = '';
|
||||
const response = await page.request.get(markdownURL.toString());
|
||||
expect(response.status()).toBe(200);
|
||||
expect(response.headers()['content-type']).toContain('text/markdown');
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Redirect to app for authentication when missing token',
|
||||
url: async () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { build } from 'bun';
|
||||
import { mkdir, rename, rm } from 'node:fs/promises';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdir, readFile, rename, rm } from 'node:fs/promises';
|
||||
import { createRequire } from 'node:module';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -12,14 +13,20 @@ const temporaryDir = join(outputDir, '.build');
|
||||
const require = createRequire(import.meta.url);
|
||||
const mermaidPackage = require('mermaid/package.json') as { version: string };
|
||||
const zenumlPackage = require('@mermaid-js/mermaid-zenuml/package.json') as { version: string };
|
||||
const mermaidPatch = await readFile(
|
||||
join(scriptDir, `../../../patches/mermaid@${mermaidPackage.version}.patch`)
|
||||
).catch(() => null);
|
||||
const mermaidPatchSuffix = mermaidPatch
|
||||
? `-patch.${createHash('sha256').update(mermaidPatch).digest('hex').slice(0, 8)}`
|
||||
: '';
|
||||
|
||||
// The runtime URL is served as immutable, it has to change whenever the bundled versions change.
|
||||
// The runtime URL is served as immutable, it has to change whenever the bundled versions or patch change.
|
||||
if (
|
||||
MERMAID_RUNTIME_PATH !==
|
||||
`mermaid/mermaid@${mermaidPackage.version}-zenuml@${zenumlPackage.version}.mjs`
|
||||
`mermaid/mermaid@${mermaidPackage.version}${mermaidPatchSuffix}-zenuml@${zenumlPackage.version}.mjs`
|
||||
) {
|
||||
throw new Error(
|
||||
'Update MERMAID_RUNTIME_PATH for the installed mermaid and @mermaid-js/mermaid-zenuml versions'
|
||||
'Update MERMAID_RUNTIME_PATH for the installed mermaid and @mermaid-js/mermaid-zenuml versions and the mermaid patch'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const MERMAID_RUNTIME_PATH = 'mermaid/mermaid@11.14.0-zenuml@0.2.2.mjs';
|
||||
export const MERMAID_RUNTIME_PATH = 'mermaid/mermaid@11.14.0-patch.caefc3e9-zenuml@0.2.2.mjs';
|
||||
|
||||
@@ -222,9 +222,14 @@ export async function PageHeader(props: {
|
||||
))}
|
||||
{ancestors.map((breadcrumb, index) => {
|
||||
const parentPages = ancestors[index - 1]?.pages ?? revision.pages;
|
||||
// A group's own URL redirects to its first document, so link straight to it.
|
||||
const target =
|
||||
breadcrumb.type === 'group'
|
||||
? (resolveFirstDocument([breadcrumb], [])?.page ?? breadcrumb)
|
||||
: breadcrumb;
|
||||
const href = linker.toPathForPage({
|
||||
pages: revision.pages,
|
||||
page: breadcrumb,
|
||||
page: target,
|
||||
});
|
||||
return (
|
||||
<li key={breadcrumb.id} className="inline">
|
||||
@@ -483,15 +488,16 @@ function getPageSiblings(
|
||||
if (page.hidden && page.id !== activeId) {
|
||||
continue;
|
||||
}
|
||||
// A group whose subtree has no document resolves to no page — the sidebar renders it as a
|
||||
// non-clickable label, so don't offer it as a navigable sibling (its path would 404).
|
||||
if (page.type === 'group' && page.id !== activeId && !resolveFirstDocument([page], [])) {
|
||||
// A group's own URL redirects to its first document, so link straight to it. A group whose
|
||||
// subtree has no document resolves to no page, so don't offer it (its path would 404).
|
||||
const target = page.type === 'group' ? resolveFirstDocument([page], [])?.page : page;
|
||||
if (!target) {
|
||||
continue;
|
||||
}
|
||||
result.push({
|
||||
id: page.id,
|
||||
title: page.linkTitle || page.title,
|
||||
href: context.linker.toPathForPage({ pages: context.revision.pages, page }),
|
||||
href: context.linker.toPathForPage({ pages: context.revision.pages, page: target }),
|
||||
emoji: page.emoji,
|
||||
icon: page.icon,
|
||||
isActive: page.id === activeId,
|
||||
|
||||
@@ -627,12 +627,11 @@ describe('resolveContentRef for direct space links', () => {
|
||||
{ label: 'API guides' },
|
||||
{ label: 'Authentification' },
|
||||
{ label: 'Référence', href: targetSiteSpace.urls.published },
|
||||
{ label: 'Getting started', icon: null, href: expect.any(String) },
|
||||
{ label: 'Getting started', icon: null, href: result?.href },
|
||||
]);
|
||||
expect(result?.ancestors?.[0]?.href).toBeUndefined();
|
||||
expect(result?.ancestors?.[1]?.href).toBeUndefined();
|
||||
expect(result?.ancestors?.[2]?.href).toBeUndefined();
|
||||
expect(result?.ancestors?.[4]?.href).toBeTruthy();
|
||||
});
|
||||
|
||||
it('uses the first document as the target for a page-group reference', async () => {
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
findGitPageURLTarget,
|
||||
findPageForGitPageURLTarget,
|
||||
} from './gitPageURL';
|
||||
import { resolvePageId } from './pages';
|
||||
import { resolveFirstDocument, resolvePageId } from './pages';
|
||||
import {
|
||||
findSiteSpaceBy,
|
||||
getFallbackSiteSpacePath,
|
||||
@@ -276,7 +276,14 @@ export async function resolveContentRef(
|
||||
style={iconStyle}
|
||||
/>
|
||||
) : null,
|
||||
href: linker.toPathForPage({ page: ancestor, pages: revision.pages }),
|
||||
// A group's own URL redirects to its first document, so link straight to it.
|
||||
href: linker.toPathForPage({
|
||||
page:
|
||||
ancestor.type === 'group'
|
||||
? (resolveFirstDocument([ancestor], [])?.page ?? ancestor)
|
||||
: ancestor,
|
||||
pages: revision.pages,
|
||||
}),
|
||||
})) ?? [];
|
||||
if (!page) {
|
||||
return null;
|
||||
|
||||
@@ -496,7 +496,9 @@ async function serveSiteRoutes(requestURL: URL, request: NextRequest) {
|
||||
themeMode === CustomizationThemeMode.Dark
|
||||
? CustomizationDefaultThemeMode.Dark
|
||||
: CustomizationDefaultThemeMode.Light;
|
||||
} else {
|
||||
} else if (routeTypeFromPathname !== 'static') {
|
||||
// Markdown, llms.txt, RSS and sitemap routes only exist under `sites/static`, so
|
||||
// forcing them dynamic 404s. They don't render a theme anyway. RND-12749.
|
||||
routeType = 'dynamic';
|
||||
requestHeaders.set(MiddlewareHeaders.Theme, themeMode);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
diff --git a/dist/chunks/mermaid.core/blockDiagram-DXYQGD6D.mjs b/dist/chunks/mermaid.core/blockDiagram-DXYQGD6D.mjs
|
||||
index f37722f2f65d3c49904ec704998a9a0ce6f01d21..0b97ddef583d120235530222990f08db3de83e5d 100644
|
||||
--- a/dist/chunks/mermaid.core/blockDiagram-DXYQGD6D.mjs
|
||||
+++ b/dist/chunks/mermaid.core/blockDiagram-DXYQGD6D.mjs
|
||||
@@ -3658,7 +3658,7 @@ async function calculateBlockSize(elem, block, db2) {
|
||||
const nodeEl = await insertNode(elem, node, { config: config2 });
|
||||
const boundingBox = nodeEl.node().getBBox();
|
||||
const obj = db2.getBlock(node.id);
|
||||
- obj.size = { width: boundingBox.width, height: boundingBox.height, x: 0, y: 0, node: nodeEl };
|
||||
+ obj.size = { width: boundingBox.width, height: boundingBox.height, x: 0, y: 0 };
|
||||
db2.setBlock(obj);
|
||||
nodeEl.remove();
|
||||
}
|
||||
diff --git a/dist/chunks/mermaid.core/chunk-U2HBQHQK.mjs b/dist/chunks/mermaid.core/chunk-U2HBQHQK.mjs
|
||||
index 6f5ff605824cec370f0ff1077fe3005823fa59f5..1468e9b37b3c1ef3449774b816202e3143174476 100644
|
||||
--- a/dist/chunks/mermaid.core/chunk-U2HBQHQK.mjs
|
||||
+++ b/dist/chunks/mermaid.core/chunk-U2HBQHQK.mjs
|
||||
@@ -308,7 +308,7 @@ async function addHtmlSpan(element, node, width, classes, addBackground = false,
|
||||
div.attr("class", "labelBkg");
|
||||
}
|
||||
let bbox = div.node().getBoundingClientRect();
|
||||
- if (bbox.width === width) {
|
||||
+ if (Math.round(bbox.width) === width) {
|
||||
div.style("display", "table");
|
||||
div.style("white-space", "break-spaces");
|
||||
div.style("width", width + "px");
|
||||
Reference in New Issue
Block a user