notebook: Add notepad CTA to empty notebook pages (#34471)

This commit is contained in:
Felix Kling 2022-04-29 17:05:17 +02:00 committed by GitHub
parent b90715a2e7
commit 3d4ce06605
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 140 additions and 14 deletions

View File

@ -390,7 +390,7 @@ const UnauthenticatedNotebooksSection: React.FunctionComponent<UnauthenticatedMy
)
}
const NOTEPAD_ENABLED_EVENT = 'SearchNotepadEnabled'
export const NOTEPAD_ENABLED_EVENT = 'SearchNotepadEnabled'
const NOTEPAD_DISABLED_EVENT = 'SearchNotepadDisabled'
const ToggleNotepadButton: React.FunctionComponent<TelemetryProps> = ({ telemetryService }) => {

View File

@ -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;
}
}
}

View File

@ -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<NotebookPageProps> = ({
const [notebookTitle, setNotebookTitle] = useState('')
const [updateQueue, setUpdateQueue] = useState<Partial<NotebookInput>[]>([])
const outlineContainerElement = useRef<HTMLDivElement | null>(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<NotebookPageProps> = ({
[notebookTitle, copyNotebook]
)
const showNotepadCTA = useMemo(
() =>
!notepadEnabled &&
!notepadCTASeen &&
isNotebookLoaded(latestNotebook) &&
latestNotebook.blocks.length === 0,
[latestNotebook, notepadCTASeen, notepadEnabled]
)
return (
<div className={classNames('w-100', styles.searchNotebookPage)}>
<PageTitle title={notebookTitle || 'Notebook'} />
@ -289,11 +316,68 @@ export const NotebookPage: React.FunctionComponent<NotebookPageProps> = ({
extensionsController={extensionsController}
outlineContainerElement={outlineContainerElement.current}
/>
<div className={styles.spacer} />
</>
)}
</div>
<div className={styles.spacer}>
{showNotepadCTA && (
<NotepadCTA
onEnable={() => {
telemetryService.log(NOTEPAD_ENABLED_EVENT)
setNotepadCTASeen(true)
setNotepadEnabled(true)
}}
onClose={() => setNotepadCTASeen(true)}
/>
)}
</div>
</div>
</div>
)
}
interface NotepadCTAProps {
onEnable: () => void
onClose: () => void
}
const NotepadCTA: React.FunctionComponent<NotepadCTAProps> = ({ onEnable, onClose }) => {
const assetsRoot = window.context?.assetsRoot || ''
const isLightTheme = useTheme().enhancedThemePreference === ThemePreference.Light
return (
<MarketingBlock wrapperClassName={styles.notepadCta}>
<aside className={styles.notepadCtaContent}>
<Button
arial-label="Hide"
variant="icon"
onClick={onClose}
size="sm"
className={styles.notepadCtaCloseButton}
>
<Icon as={CloseIcon} />
</Button>
<img
className="flex-shrink-0 mr-3"
src={`${assetsRoot}/img/notepad-illustration-${isLightTheme ? 'light' : 'dark'}.svg`}
alt=""
/>
<div>
<h3 className="d-inline-block">
<NotepadIcon /> Enable notepad
</h3>{' '}
<ProductStatusBadge status="beta" />
<p>
The notepad adds a toolbar to the bottom right of search results and file pages to help you
create notebooks from your code navigation activities.
</p>
<p>
<Button variant="primary" onClick={onEnable} size="sm">
Enable notepad
</Button>
</p>
</div>
</aside>
</MarketingBlock>
)
}