diff --git a/app/components/Sharing/Collection/PublicAccess.tsx b/app/components/Sharing/Collection/PublicAccess.tsx index 5314074f6c..05878ab83b 100644 --- a/app/components/Sharing/Collection/PublicAccess.tsx +++ b/app/components/Sharing/Collection/PublicAccess.tsx @@ -71,6 +71,19 @@ function InnerPublicAccess({ collection, share }: Props) { [share] ); + const handleShowTOCChanged = useCallback( + async (checked: boolean) => { + try { + await share?.save({ + showTOC: checked, + }); + } catch (err) { + toast.error(err.message); + } + }, + [share] + ); + const handlePublishedChange = useCallback( async (checked: boolean) => { try { @@ -204,6 +217,31 @@ function InnerPublicAccess({ collection, share }: Props) { /> } /> + + {t("Show table of contents")}  + + + + + + + } + actions={ + + } + /> { + try { + await share?.save({ + showTOC: checked, + }); + } catch (err) { + toast.error(err.message); + } + }, + [share] + ); + const handlePublishedChange = React.useCallback( async (checked: boolean) => { try { @@ -241,6 +254,31 @@ function PublicAccess({ document, share, sharedParent }: Props) { /> } /> + + {t("Show table of contents")}  + + + + + + + } + actions={ + + } + /> )} diff --git a/app/components/Sidebar/Shared.tsx b/app/components/Sidebar/Shared.tsx index 12c481f713..69d085f3d0 100644 --- a/app/components/Sidebar/Shared.tsx +++ b/app/components/Sidebar/Shared.tsx @@ -22,6 +22,7 @@ import { SharedCollectionLink } from "./components/SharedCollectionLink"; import { SharedDocumentLink } from "./components/SharedDocumentLink"; import SidebarButton from "./components/SidebarButton"; import ToggleButton from "./components/ToggleButton"; +import { useEffect } from "react"; type Props = { share: Share; @@ -37,6 +38,10 @@ function SharedSidebar({ share }: Props) { const rootNode = share.tree; const shareId = share.urlId || share.id; + useEffect(() => { + ui.tocVisible = share.showTOC; + }, []); + if (!rootNode?.children.length) { return null; } diff --git a/app/models/Share.ts b/app/models/Share.ts index eb66506ba9..3fa03146d6 100644 --- a/app/models/Share.ts +++ b/app/models/Share.ts @@ -75,6 +75,10 @@ class Share extends Model implements Searchable { @observable showLastUpdated: boolean; + @Field + @observable + showTOC: boolean; + @observable views: number; diff --git a/app/scenes/Document/components/Contents.tsx b/app/scenes/Document/components/Contents.tsx index 7b699b5aaa..8b5936ae99 100644 --- a/app/scenes/Document/components/Contents.tsx +++ b/app/scenes/Document/components/Contents.tsx @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"; import styled from "styled-components"; import breakpoint from "styled-components-breakpoint"; import { EditorStyleHelper } from "@shared/editor/styles/EditorStyleHelper"; -import { depths, s } from "@shared/styles"; +import { depths, hideScrollbars, s } from "@shared/styles"; import { useDocumentContext } from "~/components/DocumentContext"; import useWindowScrollPosition from "~/hooks/useWindowScrollPosition"; import { decodeURIComponentSafe } from "~/utils/urls"; @@ -78,16 +78,16 @@ function Contents() { const StickyWrapper = styled.div` display: none; - position: sticky; top: 90px; max-height: calc(100vh - 90px); width: ${EditorStyleHelper.tocWidth}px; + ${hideScrollbars()} + padding: 0 16px; overflow-y: auto; border-radius: 8px; - background: ${s("background")}; @supports (backdrop-filter: blur(20px)) { diff --git a/server/migrations/20251001133733-shares-show-toc.js b/server/migrations/20251001133733-shares-show-toc.js new file mode 100644 index 0000000000..2796b03aa1 --- /dev/null +++ b/server/migrations/20251001133733-shares-show-toc.js @@ -0,0 +1,15 @@ +"use strict"; + +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.addColumn("shares", "showTOC", { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false, + }); + }, + + async down(queryInterface) { + await queryInterface.removeColumn("shares", "showTOC"); + }, +}; diff --git a/server/models/Share.ts b/server/models/Share.ts index bcde7fd5d2..8b92f18b4c 100644 --- a/server/models/Share.ts +++ b/server/models/Share.ts @@ -150,6 +150,10 @@ class Share extends IdModel< @Column showLastUpdated: boolean; + @Default(false) + @Column + showTOC: boolean; + // hooks @BeforeUpdate diff --git a/server/presenters/share.ts b/server/presenters/share.ts index 62b40bf370..db62fb6b1b 100644 --- a/server/presenters/share.ts +++ b/server/presenters/share.ts @@ -17,6 +17,7 @@ export default function presentShare(share: Share, isAdmin = false) { includeChildDocuments: share.includeChildDocuments, allowIndexing: share.allowIndexing, showLastUpdated: share.showLastUpdated, + showTOC: share.showTOC, lastAccessedAt: share.lastAccessedAt || undefined, views: share.views || 0, domain: share.domain, diff --git a/server/routes/api/shares/schema.ts b/server/routes/api/shares/schema.ts index 0be16650d1..5371b3f506 100644 --- a/server/routes/api/shares/schema.ts +++ b/server/routes/api/shares/schema.ts @@ -54,6 +54,7 @@ export const SharesUpdateSchema = BaseSchema.extend({ published: z.boolean().optional(), allowIndexing: z.boolean().optional(), showLastUpdated: z.boolean().optional(), + showTOC: z.boolean().optional(), urlId: z .string() .regex(UrlHelper.SHARE_URL_SLUG_REGEX, { @@ -73,6 +74,7 @@ export const SharesCreateSchema = BaseSchema.extend({ published: z.boolean().default(false), allowIndexing: z.boolean().optional(), showLastUpdated: z.boolean().optional(), + showTOC: z.boolean().optional(), urlId: z .string() .regex(UrlHelper.SHARE_URL_SLUG_REGEX, { diff --git a/server/routes/api/shares/shares.ts b/server/routes/api/shares/shares.ts index 24f85cd27a..b17d9a454b 100644 --- a/server/routes/api/shares/shares.ts +++ b/server/routes/api/shares/shares.ts @@ -238,6 +238,7 @@ router.post( includeChildDocuments, allowIndexing, showLastUpdated, + showTOC, } = ctx.input.body; const { user } = ctx.state.auth; authorize(user, "createShare", user.team); @@ -274,6 +275,7 @@ router.post( includeChildDocuments, allowIndexing, showLastUpdated, + showTOC, urlId, }, }); @@ -303,6 +305,7 @@ router.post( urlId, allowIndexing, showLastUpdated, + showTOC, } = ctx.input.body; const { user } = ctx.state.auth; @@ -333,10 +336,12 @@ router.post( if (allowIndexing !== undefined) { share.allowIndexing = allowIndexing; } - if (showLastUpdated !== undefined) { share.showLastUpdated = showLastUpdated; } + if (showTOC !== undefined) { + share.showTOC = showTOC; + } await share.saveWithCtx(ctx); diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index 8c728feb50..718b170bde 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -365,6 +365,8 @@ "Disable this setting to discourage search engines from indexing the page": "Disable this setting to discourage search engines from indexing the page", "Show last modified": "Show last modified", "Display the last modified timestamp on the shared page": "Display the last modified timestamp on the shared page", + "Show table of contents": "Show table of contents", + "Display the table of contents on documents by default": "Display the table of contents on documents by default", "All documents in this collection will be shared on the web, including any new documents added later": "All documents in this collection will be shared on the web, including any new documents added later", "Invite": "Invite", "{{ userName }} was added to the collection": "{{ userName }} was added to the collection",