Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions src/css/common/_modal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,10 @@
border-block-end-color: #ddd;
}

// The badge lives in the modal content because the minimum-supported
// WordPress Modal (5.5–6.3) has no headerActions prop; shift it up into
// the header area, clear of the close button.
.code-snippets-preview-modal__badge {
position: absolute;
inset-block-start: 18px;
inset-inline-end: 56px;
display: flex;
flex-shrink: 0;
justify-content: flex-end;
}

// Header and footer stay pinned; the CodeMirror editor is the only
Expand All @@ -85,9 +82,9 @@
overflow: hidden;
}

// The modal component wraps its children in an unstyled focus container;
// it must join the flex chain or the editor grows to its content height.
.components-modal__header + div {
// Newer Modal versions wrap children in an unstyled focus container; older
// supported versions render the badge directly after the header.
.components-modal__header + div:not(.code-snippets-preview-modal__badge) {
display: flex;
flex-flow: column;
flex: 1;
Expand Down
30 changes: 18 additions & 12 deletions src/js/components/ManageMenu/CommunityCloud/CloudSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { REST_BASES } from '../../../utils/restAPI'
import { isLicensed } from '../../../utils/screen'
import { isProSnippet } from '../../../utils/snippets/snippets'
import { TableNav } from '../../common/ListTable/TableNavigation'
import { LoadingStatusNotices } from '../../common/LoadingStatusNotices'
import { SnippetViewToggle } from '../../common/SnippetViewToggle'
import { CloudSnippetsTable } from './CloudSnippetsTable'
import { CloudSnippetAuthor, SearchResult } from './SearchResult'
Expand Down Expand Up @@ -191,18 +192,7 @@ const SearchResultsTable: React.FC<SearchResultsViewProps> = ({ snippetView, set
}

const SearchResults: React.FC<SearchResultsViewProps> = ({ snippetView, setSnippetView }) => {
const { searchResults, searchParams, isErrored } = useCloudSearch()

if (isErrored) {
return (
<div className="banner banner-error">
<p>{__(
'An error occurred while fetching search results. Please try again.',
'code-snippets'
)}</p>
</div>
)
}
const { searchResults, searchParams } = useCloudSearch()

if (!searchResults) {
return null
Expand All @@ -229,6 +219,21 @@ const SearchResults: React.FC<SearchResultsViewProps> = ({ snippetView, setSnipp
: null
}

const SearchStatus = () => {
const { isErrored, isLoading } = useCloudSearch()

return isErrored || isLoading
? <LoadingStatusNotices
isLoading={isLoading}
errorMessage={isErrored
? __('An error occurred while fetching search results. Please try again.', 'code-snippets')
: undefined}
loadingNotice={__('Loading community snippets…', 'code-snippets')}
noticeLabel={__('Community snippets status', 'code-snippets')}
/>
: null
}

export interface CloudSearchProps {
snippetView: SnippetView
setSnippetView: (view: SnippetView) => void
Expand All @@ -237,5 +242,6 @@ export interface CloudSearchProps {
export const CloudSearch: React.FC<CloudSearchProps> = ({ snippetView, setSnippetView }) =>
<div className="cloud-search">
<SearchBox />
<SearchStatus />
<SearchResults snippetView={snippetView} setSnippetView={setSnippetView} />
</div>
43 changes: 34 additions & 9 deletions src/js/components/ManageMenu/CommunityCloud/CloudSnippetsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Button } from '../../common/Button'
import { CloudSnippetDownloadButton } from '../../common/cloud/CloudSnippetDownloadButton'
import { CloudSnippetPreviewModal } from '../../common/cloud/CloudSnippetPreviewModal'
import { CloudStatusBadge } from '../../common/cloud/CloudStatusBadge'
import { useCloudSearch } from './WithCloudSearchContext'
import type { CloudSnippetSchema } from '../../../types/schema/CloudSnippetSchema'
import type { Dispatch, SetStateAction } from 'react'

Expand All @@ -27,6 +28,38 @@ interface CloudSnippetRowProps {
setSelected: SetSelected
}

interface CloudSnippetActionsProps extends Pick<CloudSnippetRowProps, 'snippet'> {
isPreviewOpen: boolean
setIsPreviewOpen: (isOpen: boolean) => void
}

const CloudSnippetActions: React.FC<CloudSnippetActionsProps> = ({
snippet,
isPreviewOpen,
setIsPreviewOpen
}) => {
const { doSearch } = useCloudSearch()

return (
<>
<div className="cloud-snippet-action-buttons">
<Button secondary onClick={() => setIsPreviewOpen(true)}>
{__('Preview', 'code-snippets')}
</Button>

<CloudSnippetDownloadButton snippet={snippet} onDownloaded={doSearch} />
</div>

<CloudSnippetPreviewModal
snippet={snippet}
isOpen={isPreviewOpen}
setIsOpen={setIsPreviewOpen}
onDownloaded={doSearch}
/>
</>
)
}

const CloudSnippetRow: React.FC<CloudSnippetRowProps> = ({ snippet, selected, setSelected }) => {
const [isPreviewOpen, setIsPreviewOpen] = useState(false)

Expand Down Expand Up @@ -69,15 +102,7 @@ const CloudSnippetRow: React.FC<CloudSnippetRowProps> = ({ snippet, selected, se
</td>

<td className="column-actions">
<div className="cloud-snippet-action-buttons">
<Button secondary onClick={() => setIsPreviewOpen(true)}>
{__('Preview', 'code-snippets')}
</Button>

<CloudSnippetDownloadButton snippet={snippet} />
</div>

<CloudSnippetPreviewModal snippet={snippet} isOpen={isPreviewOpen} setIsOpen={setIsPreviewOpen} />
<CloudSnippetActions {...{ snippet, isPreviewOpen, setIsPreviewOpen }} />
</td>
</tr>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useSnippetView } from '../../../hooks/useSnippetView'
import { fetchConstQueryParam, updateQueryParams } from '../../../utils/urls'
import { ScreenMetaSlot } from '../../common/ScreenMetaSlot'
import { SubnavTabs } from '../../common/SubnavTabs'
import { WithCloudSnippetDownloadsContext } from '../../common/cloud/WithCloudSnippetDownloadsContext'
import { WithCloudSearchContext, useCloudSearch } from './WithCloudSearchContext'
import { CloudSearch } from './CloudSearch'

Expand Down Expand Up @@ -75,7 +76,9 @@ const CommunityCloudInner = () => {

export const CommunityCloud = () =>
<WithRestAPIContext>
<WithCloudSearchContext>
<CommunityCloudInner />
</WithCloudSearchContext>
<WithCloudSnippetDownloadsContext>
<WithCloudSearchContext>
<CommunityCloudInner />
</WithCloudSearchContext>
</WithCloudSnippetDownloadsContext>
</WithRestAPIContext>
5 changes: 4 additions & 1 deletion src/js/components/ManageMenu/CommunityCloud/SearchResult.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CloudSnippetPreviewModal } from '../../common/cloud/CloudSnippetPreview
import { CloudStatusIndicator } from '../../common/cloud/CloudStatusBadge'
import { CloudUpdateIcon } from '../../common/icons/CloudIcons'
import { SnippetCard } from '../../common/SnippetCard'
import { useCloudSearch } from './WithCloudSearchContext'
import type { ReactNode } from 'react'
import type { CloudSnippetSchema } from '../../../types/schema/CloudSnippetSchema'

Expand Down Expand Up @@ -100,6 +101,7 @@ export const SearchResult: React.FC<SearchResultProps> = ({
isSelected = false,
onSelectedChange
}) => {
const { doSearch } = useCloudSearch()
const [isPreviewOpen, setIsPreviewOpen] = useState(false)

return (
Expand All @@ -126,7 +128,7 @@ export const SearchResult: React.FC<SearchResultProps> = ({
{__('Preview', 'code-snippets')}
</Button>

<CloudSnippetDownloadButton snippet={snippet} />
<CloudSnippetDownloadButton snippet={snippet} onDownloaded={doSearch} />
</>}
>
<CloudSnippetDetails snippet={snippet} author={author} setIsPreviewOpen={setIsPreviewOpen} />
Expand All @@ -135,6 +137,7 @@ export const SearchResult: React.FC<SearchResultProps> = ({
snippet={snippet}
isOpen={isPreviewOpen}
setIsOpen={setIsPreviewOpen}
onDownloaded={doSearch}
/>
</SnippetCard>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ const useRequestIds = () => {
export interface CloudSearchContext {
isErrored: boolean
doSearch: (paramsDelta?: Partial<CloudSearchParams>) => void
isLoading: boolean
isSearching: boolean
searchParams: CloudSearchParams
searchResults: CloudSearchResults | undefined
Expand All @@ -175,13 +176,15 @@ const useSearchApi = () => {
const { api } = useRestAPI()
const { isCurrentRequest, nextRequestId } = useRequestIds()
const [currentSearch, setCurrentSearch] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [searchResults, setSearchResults] = useState<CloudSearchResults | false | undefined>()
const [availableFilters, setAvailableFilters] = useState<AvailableCloudFilters>({})

const makeSearchRequest = useCallback((params: CloudSearchParams) => {
const requestId = nextRequestId()
const isFeaturedSearch = '' === params.query.trim()

setIsLoading(true)
setCurrentSearch(!isFeaturedSearch)
const baseUrl = isFeaturedSearch ? SEARCH_URLS.FEATURED : SEARCH_URLS.SEARCH_QUERY

Expand All @@ -197,10 +200,16 @@ const useSearchApi = () => {
setSearchResults(false)
}
})
.finally(() => setCurrentSearch(false))
.finally(() => {
if (isCurrentRequest(requestId)) {
setCurrentSearch(false)
setIsLoading(false)
}
})
}, [api, nextRequestId, isCurrentRequest])

return {
isLoading,
isSearching: currentSearch,
searchResults,
availableFilters,
Expand All @@ -211,7 +220,7 @@ const useSearchApi = () => {
const [Context, useCloudSearch] = createContextHook<CloudSearchContext>('useCloudSearch')

export const WithCloudSearchContext: React.FC<PropsWithChildren> = ({ children }) => {
const { isSearching, makeSearchRequest, availableFilters, searchResults } = useSearchApi()
const { isLoading, isSearching, makeSearchRequest, availableFilters, searchResults } = useSearchApi()
const [searchParams, setSearchParams] = useState<CloudSearchParams>(fetchSearchQueryParams)
const [madeInitialRequest, setMadeInitialRequest] = useState(false)

Expand All @@ -236,6 +245,7 @@ export const WithCloudSearchContext: React.FC<PropsWithChildren> = ({ children }

const value: CloudSearchContext = {
doSearch,
isLoading,
isSearching,
searchParams,
availableFilters,
Expand Down
41 changes: 41 additions & 0 deletions src/js/components/common/LoadingStatusNotices.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import { Notice } from './Notice'
import type { ReactNode } from 'react'

export interface LoadingStatusNoticesProps {
isLoading: boolean
errorMessage: string | undefined
loadingNotice: ReactNode
noticeLabel: string
}

export const LoadingStatusNotices: React.FC<LoadingStatusNoticesProps> = ({
isLoading,
errorMessage,
loadingNotice,
noticeLabel
}) => {
switch (true) {
case isLoading:
return (
<Notice aria-label={noticeLabel} className="inline">
<p>{loadingNotice}</p>
</Notice>
)

case errorMessage !== undefined:
return (
<Notice aria-label={noticeLabel} className="inline" type="error">
<p>{errorMessage}</p>
</Notice>
)

default:
return (
<Notice aria-label={noticeLabel} className="inline" type="warning">
<p>{__('An unknown error occurred. Please try again.', 'code-snippets')}</p>
</Notice>
)
}
}
40 changes: 28 additions & 12 deletions src/js/components/common/SnippetPreviewModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface SnippetPreviewModalProps {
setIsOpen: (isOpen: boolean) => void
snippet?: Snippet
extraActions?: PreviewExtraActions
footerActions?: ReactNode
}

const EDITOR_MODES: Record<string, string> = {
Expand Down Expand Up @@ -229,15 +230,32 @@ const SnippetPreviewActions: React.FC<SnippetPreviewActionsProps> = ({
)
}

/**
* The minimum-supported WordPress Modal (5.5–6.3) has no headerActions prop,
* so the badge renders in the content and CSS moves it into the header area.
*/
const PreviewTypeBadge: React.FC<{ type: SnippetType }> = ({ type }) =>
<div className="code-snippets-preview-modal__badge">
<Badge name={type} />
</div>

interface PreviewFooterProps {
snippet?: Snippet
extraActions?: PreviewExtraActions
footerActions?: ReactNode
closeModal: () => void
}

const PreviewFooter: React.FC<PreviewFooterProps> = ({
snippet,
extraActions,
footerActions,
closeModal
}) =>
snippet
? <SnippetPreviewActions {...{ snippet, extraActions, closeModal }} />
: footerActions
? <div className="code-snippets-preview-modal__footer">
<div className="code-snippets-preview-modal__buttons">{footerActions}</div>
</div>
: null

/**
* Modal for quickly viewing a snippet's code in a read-only CodeMirror editor,
* without navigating to the edit page. Shared between local snippets and cloud
Expand All @@ -251,7 +269,8 @@ export const SnippetPreviewModal: React.FC<SnippetPreviewModalProps> = ({
isOpen,
setIsOpen,
snippet,
extraActions
extraActions,
footerActions
}) => {
const textareaRef = useRef<HTMLTextAreaElement>(null)

Expand Down Expand Up @@ -291,13 +310,10 @@ export const SnippetPreviewModal: React.FC<SnippetPreviewModalProps> = ({
/>
</div>

{snippet
? <SnippetPreviewActions
snippet={snippet}
extraActions={extraActions}
closeModal={() => setIsOpen(false)}
/>
: null}
<PreviewFooter
{...{ snippet, extraActions, footerActions }}
closeModal={() => setIsOpen(false)}
/>
</Modal>
: null
}
Loading
Loading