diff --git a/client/web/src/notebooks/listPage/NotebooksListPage.tsx b/client/web/src/notebooks/listPage/NotebooksListPage.tsx index 039c783d337..1cc3d4849a0 100644 --- a/client/web/src/notebooks/listPage/NotebooksListPage.tsx +++ b/client/web/src/notebooks/listPage/NotebooksListPage.tsx @@ -390,7 +390,7 @@ const UnauthenticatedNotebooksSection: React.FunctionComponent = ({ telemetryService }) => { diff --git a/client/web/src/notebooks/notebookPage/NotebookPage.module.scss b/client/web/src/notebooks/notebookPage/NotebookPage.module.scss index 27699783d49..13b457f505c 100644 --- a/client/web/src/notebooks/notebookPage/NotebookPage.module.scss +++ b/client/web/src/notebooks/notebookPage/NotebookPage.module.scss @@ -6,12 +6,6 @@ display: flex; } -.spacer { - // Allows scrolling past last blocks in the notebook - // for easier editing. - height: 80vh; -} - .auto-save-indicator { font-size: 1rem !important; width: 1rem !important; @@ -33,13 +27,61 @@ flex: 3; overflow: hidden auto; min-width: #{$viewport-lg}; - - .content { - max-width: #{$viewport-xl}; - padding: 0 1rem; - } + display: flex; + flex-direction: column; @media (--lg-breakpoint-down) { min-width: 0; } + + .content { + max-width: #{$viewport-xl}; + padding: 0 1rem; + // Content should never shrink, but the spacer should take up the + // remaining space on the page. + flex: none; + } + + .spacer { + max-width: #{$viewport-xl}; + // Allows scrolling past last blocks in the notebook + // for easier editing. It will also ensure that there is enough space + // between the notepad cta and the content + margin-top: 10rem; + // Spacer should never shrink. This makes sure that + // (1) there is always space between the bottom of the notebookpage and the screen and + // (2) the notepad CTA doesn't overlap with notebook content. + flex: 1 0 auto; + display: flex; + // Aligns notepad CTA at the bottom of the page + align-items: flex-end; + } +} + +.notepad-cta { + position: relative; + bottom: 0; + max-width: #{$viewport-md}; + margin: auto; + margin-bottom: 1rem; + + &--close-button { + position: absolute; + right: 0; + top: 0; + margin: 0.5rem; + } + + &--content { + display: flex; + align-items: center; + + @media (--xs-breakpoint-down) { + flex-direction: column; + } + + p:last-child { + margin-bottom: 0; + } + } } diff --git a/client/web/src/notebooks/notebookPage/NotebookPage.tsx b/client/web/src/notebooks/notebookPage/NotebookPage.tsx index e4c7331976a..6059e786140 100644 --- a/client/web/src/notebooks/notebookPage/NotebookPage.tsx +++ b/client/web/src/notebooks/notebookPage/NotebookPage.tsx @@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import classNames from 'classnames' import BookOutlineIcon from 'mdi-react/BookOutlineIcon' import CheckCircleIcon from 'mdi-react/CheckCircleIcon' +import CloseIcon from 'mdi-react/CloseIcon' import { RouteComponentProps } from 'react-router' import { Observable } from 'rxjs' import { catchError, delay, startWith, switchMap } from 'rxjs/operators' @@ -11,16 +12,30 @@ import { asError, isErrorLike } from '@sourcegraph/common' import { StreamingSearchResultsListProps } from '@sourcegraph/search-ui' import { ExtensionsControllerProps } from '@sourcegraph/shared/src/extensions/controller' import { PlatformContextProps } from '@sourcegraph/shared/src/platform/context' +import { useTemporarySetting } from '@sourcegraph/shared/src/settings/temporary/useTemporarySetting' import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService' import { ThemeProps } from '@sourcegraph/shared/src/theme' -import { LoadingSpinner, PageHeader, useEventObservable, useObservable, Alert } from '@sourcegraph/wildcard' +import { + LoadingSpinner, + PageHeader, + useEventObservable, + useObservable, + Alert, + ProductStatusBadge, + Button, + Icon, +} from '@sourcegraph/wildcard' import { Block } from '..' import { AuthenticatedUser } from '../../auth' +import { MarketingBlock } from '../../components/MarketingBlock' import { PageTitle } from '../../components/PageTitle' import { Timestamp } from '../../components/time/Timestamp' import { NotebookFields, NotebookInput, Scalars } from '../../graphql-operations' import { SearchStreamingProps } from '../../search' +import { NotepadIcon } from '../../search/Notepad' +import { ThemePreference } from '../../stores/themeState' +import { useTheme } from '../../theme' import { fetchNotebook as _fetchNotebook, updateNotebook as _updateNotebook, @@ -28,6 +43,7 @@ import { createNotebookStar as _createNotebookStar, deleteNotebookStar as _deleteNotebookStar, } from '../backend' +import { NOTEPAD_ENABLED_EVENT } from '../listPage/NotebooksListPage' import { copyNotebook as _copyNotebook, CopyNotebookProps } from '../notebook' import { blockToGQLInput, convertNotebookTitleToFileName, GQLBlockToGQLInput } from '../serialize' @@ -91,6 +107,8 @@ export const NotebookPage: React.FunctionComponent = ({ const [notebookTitle, setNotebookTitle] = useState('') const [updateQueue, setUpdateQueue] = useState[]>([]) const outlineContainerElement = useRef(null) + const [notepadCTASeen, setNotepadCTASeen] = useTemporarySetting('search.notepad.ctaSeen') + const [notepadEnabled, setNotepadEnabled] = useTemporarySetting('search.notepad.enabled') const exportedFileName = useMemo( () => `${notebookTitle ? convertNotebookTitleToFileName(notebookTitle) : 'notebook'}.snb.md`, @@ -172,6 +190,15 @@ export const NotebookPage: React.FunctionComponent = ({ [notebookTitle, copyNotebook] ) + const showNotepadCTA = useMemo( + () => + !notepadEnabled && + !notepadCTASeen && + isNotebookLoaded(latestNotebook) && + latestNotebook.blocks.length === 0, + [latestNotebook, notepadCTASeen, notepadEnabled] + ) + return (
@@ -289,11 +316,68 @@ export const NotebookPage: React.FunctionComponent = ({ extensionsController={extensionsController} outlineContainerElement={outlineContainerElement.current} /> -
)}
+
+ {showNotepadCTA && ( + { + telemetryService.log(NOTEPAD_ENABLED_EVENT) + setNotepadCTASeen(true) + setNotepadEnabled(true) + }} + onClose={() => setNotepadCTASeen(true)} + /> + )} +
) } + +interface NotepadCTAProps { + onEnable: () => void + onClose: () => void +} + +const NotepadCTA: React.FunctionComponent = ({ onEnable, onClose }) => { + const assetsRoot = window.context?.assetsRoot || '' + const isLightTheme = useTheme().enhancedThemePreference === ThemePreference.Light + + return ( + + + + ) +}