diff --git a/apps/ui/src/components/connect-site-picker/index.tsx b/apps/ui/src/components/connect-site-picker/index.tsx new file mode 100644 index 0000000000..a0c1d42573 --- /dev/null +++ b/apps/ui/src/components/connect-site-picker/index.tsx @@ -0,0 +1,336 @@ +import { Spinner, VisuallyHidden } from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { external, search } from '@wordpress/icons'; +import { Badge, Button, Icon } from '@wordpress/ui'; +import { clsx } from 'clsx'; +import { useMemo, useState } from 'react'; +import { useConnector } from '@/data/core'; +import { useUserLocale } from '@/data/queries/use-user-locale'; +import { useOffline } from '@/hooks/use-offline'; +import { getLocalizedLink } from '@/lib/docs-links'; +import { presentRemoteSites, searchRemoteSites, type ConnectSiteGroup } from './site-presentation'; +import styles from './style.module.css'; +import type { SyncSite } from '@/data/core'; + +const createWpcomSiteUrl = new URL( 'https://wordpress.com/setup/new-hosted-site' ); +createWpcomSiteUrl.searchParams.set( 'ref', 'studio' ); +createWpcomSiteUrl.searchParams.set( 'section', 'studio-sync' ); +createWpcomSiteUrl.searchParams.set( 'showDomainStep', 'true' ); + +function getEnvironmentLabel( site: SyncSite ): string { + if ( site.isPressable && site.environmentType === 'development' ) return __( 'Development' ); + if ( site.isPressable && site.environmentType === 'staging' ) return __( 'Staging' ); + if ( site.isStaging ) return __( 'Staging' ); + return __( 'Production' ); +} + +function getEnvironmentIntent( site: SyncSite ) { + if ( site.isPressable && site.environmentType === 'development' ) return 'informational'; + if ( site.isStaging || ( site.isPressable && site.environmentType === 'staging' ) ) + return 'medium'; + return 'stable'; +} + +function getSiteStatus( site: SyncSite, group: ConnectSiteGroup ): string { + if ( group === 'needs-transfer' ) { + return __( 'Enable hosting features on WordPress.com before connecting this site.' ); + } + if ( group === 'needs-upgrade' ) { + return __( 'Upgrade this site to a supported plan before connecting it.' ); + } + if ( site.syncSupport === 'missing-permissions' ) { + return __( "Your account doesn't have permission to manage this site." ); + } + if ( site.syncSupport === 'deleted' ) return __( 'This site has been deleted.' ); + return __( 'This site does not support pulling into Studio.' ); +} + +export function getSiteName( site: SyncSite ): string { + if ( site.name.trim() ) return site.name.trim(); + try { + return new URL( site.url ).hostname; + } catch { + return __( 'WordPress site' ); + } +} + +function RemoteSiteCard( { + site, + group, + isSelected, + onSelect, +}: ReturnType< typeof presentRemoteSites >[ number ] & { + isSelected: boolean; + onSelect: ( id: number ) => void; +} ) { + const connector = useConnector(); + const isAvailable = group === 'available'; + const siteName = getSiteName( site ); + const providerLabel = site.isPressable ? __( 'Pressable' ) : __( 'WP.com' ); + const environmentLabel = getEnvironmentLabel( site ); + const siteStatus = isAvailable ? '' : getSiteStatus( site, group ); + const className = clsx( + styles.siteCard, + isSelected && styles.siteCardSelected, + ! isAvailable && styles.siteCardUnavailable + ); + + return ( +
  • + + { group === 'needs-transfer' && ( + + ) } + { group === 'needs-upgrade' && ( + + ) } +
  • + ); +} + +export type ConnectSitePickerProps = { + sites: SyncSite[] | undefined; + isLoading: boolean; + isFetching: boolean; + error: unknown; + onRefresh: () => void; + selectedId: number | null; + onSelect: ( id: number ) => void; + // Shown when the account has no sites this flow can use. + emptyTitle?: string; + emptyDescription?: string; +}; + +/** + * The list of WordPress.com and Pressable sites a Studio site can be wired to, + * with its search, its grouping into what can and can't be connected, and the + * states around loading them. Shared by onboarding, which uses it to bring a + * live site down into Studio, and by publishing, which uses it to send one up — + * the choice is the same either way, so it should look the same. + */ +export function ConnectSitePicker( { + sites, + isLoading, + isFetching, + error, + onRefresh, + selectedId, + onSelect, + emptyTitle = __( 'No sites found' ), + emptyDescription = __( 'This account has no WordPress.com or Pressable sites to show.' ), +}: ConnectSitePickerProps ) { + const connector = useConnector(); + const locale = useUserLocale(); + const isOffline = useOffline(); + const [ searchQuery, setSearchQuery ] = useState( '' ); + + const presentedSites = useMemo( () => presentRemoteSites( sites ?? [] ), [ sites ] ); + const filteredSites = useMemo( + () => searchRemoteSites( presentedSites, searchQuery ), + [ presentedSites, searchQuery ] + ); + const isSingleSite = presentedSites.length === 1 && searchQuery.trim() === ''; + const isSingleAvailableSite = isSingleSite && presentedSites[ 0 ].group === 'available'; + + if ( isOffline ) { + return ( +
    +

    { __( "You're offline" ) }

    +

    { __( 'Reconnect to load your WordPress.com and Pressable sites.' ) }

    +
    + ); + } + + if ( isLoading ) { + return ( +
    + +

    { __( 'Loading your sites…' ) }

    +
    + ); + } + + if ( error ) { + return ( +
    +

    { __( "We couldn't load your sites" ) }

    +

    { __( 'Check your connection and try again.' ) }

    + +
    + ); + } + + if ( presentedSites.length === 0 ) { + return ( +
    +

    { emptyTitle }

    +

    { emptyDescription }

    + +
    + ); + } + + const sections = [ + { + key: 'available', + title: __( 'Available to connect' ), + description: __( 'Select a site to create its local copy.' ), + sites: filteredSites.filter( ( entry ) => entry.group === 'available' ), + }, + { + key: 'unavailable', + title: __( 'Unavailable' ), + description: __( 'These sites cannot currently be connected to Studio.' ), + sites: filteredSites.filter( ( entry ) => entry.group !== 'available' ), + }, + ]; + + return ( + <> +
    + { ! isSingleSite && ( + + ) } +

    + + + +

    +
    + + { filteredSites.length === 0 ? ( +
    +

    + { sprintf( + // translators: %s is the site search query. + __( 'No sites match “%s”.' ), + searchQuery + ) } +

    +
    + ) : isSingleAvailableSite ? ( + + ) : ( +
    + { sections.map( + ( section ) => + section.sites.length > 0 && ( +
    +
    +

    { section.title }

    +

    { section.description }

    +
    + +
    + ) + ) } +
    + ) } + + ); +} diff --git a/apps/ui/src/ui-classic/router/route-onboarding-connect/site-presentation.test.ts b/apps/ui/src/components/connect-site-picker/site-presentation.test.ts similarity index 100% rename from apps/ui/src/ui-classic/router/route-onboarding-connect/site-presentation.test.ts rename to apps/ui/src/components/connect-site-picker/site-presentation.test.ts diff --git a/apps/ui/src/ui-classic/router/route-onboarding-connect/site-presentation.ts b/apps/ui/src/components/connect-site-picker/site-presentation.ts similarity index 100% rename from apps/ui/src/ui-classic/router/route-onboarding-connect/site-presentation.ts rename to apps/ui/src/components/connect-site-picker/site-presentation.ts diff --git a/apps/ui/src/components/connect-site-picker/style.module.css b/apps/ui/src/components/connect-site-picker/style.module.css new file mode 100644 index 0000000000..39e8fbe5df --- /dev/null +++ b/apps/ui/src/components/connect-site-picker/style.module.css @@ -0,0 +1,226 @@ +/* The site cards, search, and list states shared by onboarding and the + publish flow. */ + +.state p { + margin: 0; + color: var(--wpds-color-fg-content-neutral-weak); +} + +.state { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + max-width: 520px; + margin-inline: auto; + padding: 0 20px 32px; +} + +.state h2 { + margin: 0; + font-size: var(--wpds-typography-font-size-lg); + font-weight: 500; +} + +.siteControls { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + margin-bottom: 32px; +} + +.search { + display: flex; + align-items: center; + gap: 8px; + width: min(100%, 520px); + padding: 0 12px; + border: var(--wpds-border-width-xs) solid var(--wpds-color-stroke-surface-neutral); + border-radius: 6px; + background: var(--wpds-color-bg-surface-neutral); + color: var(--wpds-color-fg-content-neutral-weak); +} + +.search:focus-within { + border-color: var(--wpds-color-stroke-focus-brand); + box-shadow: 0 0 0 1px var(--wpds-color-stroke-focus-brand); +} + +.search input { + width: 100%; + height: 40px; + padding: 0; + border: 0; + outline: 0; + background: transparent; + color: var(--wpds-color-fg-content-neutral); + font: inherit; +} + +.search input::placeholder { + color: var(--wpds-color-fg-content-neutral-weak); +} + +.helperLinks { + display: flex; + align-items: center; + gap: 2px; + margin: 0; + color: var(--wpds-color-fg-content-neutral-weak); +} + +.helperLinks button { + padding-inline: 2px; +} + +.sections { + display: flex; + flex-direction: column; + gap: 40px; + text-align: start; +} + +.section { + display: flex; + flex-direction: column; + gap: 16px; +} + +.sectionHeader { + text-align: center; +} + +.sectionHeader h2 { + margin: 0 0 4px; + font-size: var(--wpds-typography-font-size-lg); + font-weight: 500; +} + +.sectionHeader p { + margin: 0; + color: var(--wpds-color-fg-content-neutral-weak); + font-size: var(--wpds-typography-font-size-sm); +} + +.siteGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr)); + gap: 20px; + margin: 0; + padding: 0; + list-style: none; +} + +.singleSiteGrid { + grid-template-columns: minmax(0, 440px); + justify-content: center; +} + +.siteCardWrapper { + position: relative; + display: flex; + flex-direction: column; + min-width: 0; +} + +.siteCard { + display: flex; + flex: 1; + flex-direction: column; + width: 100%; + padding: 6px; + border: 0; + border-radius: 12px; + background: transparent; + color: var(--wpds-color-fg-content-neutral); + cursor: pointer; + text-align: start; +} + +.siteCardUnavailable, +.siteCardUnavailable:hover { + cursor: default; +} + +.siteCardSelected .siteThumb { + box-shadow: 0 0 0 1px var(--wpds-color-stroke-interactive-brand); +} + +.siteCard:focus-visible { + outline: 2px solid var(--wpds-color-stroke-focus-brand); + outline-offset: 2px; +} + +.siteThumb { + position: relative; + display: block; + width: 100%; + aspect-ratio: 3 / 2; + overflow: hidden; + border-radius: 8px; + background: var(--wpds-color-bg-surface-neutral-strong); + box-shadow: 0 0 0 var(--wpds-border-width-xs) var(--wpds-color-stroke-surface-neutral); + transition: box-shadow 0.15s ease; +} + +.siteThumb img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.siteCardUnavailable .siteThumb { + opacity: 0.65; +} + +.siteText { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; + padding: 10px 8px 8px; +} + +.siteName { + overflow: hidden; + font-weight: 500; + text-overflow: ellipsis; + white-space: nowrap; +} + +.siteUrl { + overflow: hidden; + color: var(--wpds-color-fg-content-neutral-weak); + font-size: var(--wpds-typography-font-size-sm); + text-overflow: ellipsis; + white-space: nowrap; +} + +.siteStatus { + margin-top: 4px; + color: var(--wpds-color-fg-content-neutral-weak); + font-size: var(--wpds-typography-font-size-sm); + line-height: 1.4; +} + +.badges { + position: absolute; + inset-inline-end: 8px; + inset-block-end: 8px; + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.siteAction { + align-self: center; + margin-top: 4px; +} + + +@media (max-width: 600px) { + .siteGrid { + grid-template-columns: minmax(0, 1fr); + } +} diff --git a/apps/ui/src/components/site-toolbar/index.tsx b/apps/ui/src/components/site-toolbar/index.tsx index 0fa1d81994..7d94af0bfe 100644 --- a/apps/ui/src/components/site-toolbar/index.tsx +++ b/apps/ui/src/components/site-toolbar/index.tsx @@ -1,7 +1,7 @@ import { useIsMutating } from '@tanstack/react-query'; import { __ } from '@wordpress/i18n'; import { external, Icon, moreVertical } from '@wordpress/icons'; -import { Button, Dialog, IconButton, Tooltip } from '@wordpress/ui'; +import { Button, IconButton, Tooltip } from '@wordpress/ui'; import { clsx } from 'clsx'; import { useEffect, useMemo, useRef, useState } from 'react'; import * as Menu from '@/components/menu'; @@ -21,7 +21,7 @@ import { import { useSidebarCollapsed } from '@/hooks/use-sidebar-collapsed'; import { getSiteDisplayUrl, getSiteUrl } from '@/lib/get-site-url'; import { DisconnectSiteDialog } from './disconnect-site-dialog'; -import { PublishPickerView } from './publish-picker-view'; +import { PublishSiteDialog } from './publish-site-dialog'; import { ShareDialog } from './share-dialog'; import styles from './style.module.css'; import { SyncDialog, type SyncDirection } from './sync-dialog'; @@ -294,11 +294,7 @@ export function SiteToolbar( { site, className, openPullOnLoad = false }: SiteTo { /* Mounted only while open: it loads the account's sites on mount. */ } { publishOpen ? ( - - - setPublishOpen( false ) } /> - - + ) : null } { shareOpen ? : null } diff --git a/apps/ui/src/components/site-toolbar/publish-picker-view.module.css b/apps/ui/src/components/site-toolbar/publish-picker-view.module.css deleted file mode 100644 index 29f93bc641..0000000000 --- a/apps/ui/src/components/site-toolbar/publish-picker-view.module.css +++ /dev/null @@ -1,94 +0,0 @@ -.picker { - display: flex; - flex-direction: column; -} - -.header { - display: flex; - align-items: center; - gap: var(--wpds-dimension-padding-sm); - padding: var(--wpds-dimension-padding-sm) var(--wpds-dimension-padding-md); - border-bottom: 1px solid var(--wpds-color-stroke-surface-neutral-weak); -} - -.title { - font-size: var(--wpds-typography-font-size-sm); - font-weight: 500; - color: var(--wpds-color-fg-content-neutral); -} - -.body { - max-height: 240px; - overflow-y: auto; -} - -.status { - padding: var(--wpds-dimension-padding-lg); - color: var(--wpds-color-fg-content-neutral-weak); - font-size: var(--wpds-typography-font-size-sm); - text-align: center; -} - -.list { - list-style: none; - margin: 0; - padding: var(--wpds-dimension-padding-xs) 0; -} - -.item { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 2px; - width: 100%; - background: transparent; - border: none; - padding: var(--wpds-dimension-padding-sm) var(--wpds-dimension-padding-lg); - cursor: pointer; - font: inherit; - color: inherit; - text-align: left; -} - -.item:hover, -.item:focus-visible { - background-color: var(--wpds-color-bg-interactive-neutral-weak-active); - outline: none; -} - -.itemName { - font-size: var(--wpds-typography-font-size-sm); - font-weight: 500; - color: var(--wpds-color-fg-content-neutral); -} - -.itemUrl { - font-size: var(--wpds-typography-font-size-sm); - color: var(--wpds-color-fg-content-neutral-weak); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 100%; -} - -.create { - display: flex; - align-items: center; - gap: var(--wpds-dimension-padding-sm); - width: 100%; - background: transparent; - border: none; - border-top: 1px solid var(--wpds-color-stroke-surface-neutral-weak); - padding: var(--wpds-dimension-padding-sm) var(--wpds-dimension-padding-lg); - cursor: pointer; - font: inherit; - font-size: var(--wpds-typography-font-size-sm); - color: var(--wpds-color-fg-interactive-brand); - text-align: left; -} - -.create:hover, -.create:focus-visible { - background-color: var(--wpds-color-bg-interactive-neutral-weak-active); - outline: none; -} diff --git a/apps/ui/src/components/site-toolbar/publish-picker-view.tsx b/apps/ui/src/components/site-toolbar/publish-picker-view.tsx deleted file mode 100644 index 4f8df43205..0000000000 --- a/apps/ui/src/components/site-toolbar/publish-picker-view.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { useQueryClient } from '@tanstack/react-query'; -import { __ } from '@wordpress/i18n'; -import { chevronLeft, plus } from '@wordpress/icons'; -import { Icon, IconButton } from '@wordpress/ui'; -import { useConnector } from '@/data/core'; -import { useAuthUser } from '@/data/queries/use-auth-user'; -import { connectedWpcomSitesQueryKey } from '@/data/queries/use-connected-wpcom-sites'; -import { usePickableWpcomSites } from '@/data/queries/use-wpcom-sites'; -import styles from './publish-picker-view.module.css'; -import { stripProtocol } from './utils'; -import type { SiteDetails, SyncSite } from '@/data/core'; - -type Props = { - site: SiteDetails; - // Fires after any action that ends the picker flow (site picked, checkout - // link opened, or the back button pressed). The parent uses this to swap - // back to the main dropdown view. - onClose: () => void; -}; - -export function PublishPickerView( { site, onClose }: Props ) { - const connector = useConnector(); - const queryClient = useQueryClient(); - const { data: authUser } = useAuthUser(); - const pickableSites = usePickableWpcomSites(); - - const openExternal = ( url: string ) => { - void connector.openExternalUrl( url ); - }; - - const handlePickSite = async ( pickedSite: SyncSite ) => { - try { - await connector.connectWpcomSite( site.id, { - ...pickedSite, - localSiteId: site.id, - syncSupport: 'already-connected', - } ); - await queryClient.invalidateQueries( { - queryKey: connectedWpcomSitesQueryKey( site.id ), - } ); - onClose(); - } catch ( error ) { - console.error( 'Failed to connect WordPress.com site:', error ); - } - }; - - const handleCreateNew = () => { - const checkoutUrl = connector.getPublishCheckoutUrl( site ); - if ( checkoutUrl ) { - // Desktop receives the new site via the wp-studio:// deep link; surfaces - // that can't (the local web server) opt into a server-side watch instead. - void connector.watchForPublishedSite?.( site.id ); - openExternal( checkoutUrl ); - } - // The connect listener (deep link on desktop, sync-connect SSE on the local - // server) handles the follow-up connection, so we just close the picker. - onClose(); - }; - - return ( -
    -
    - - { __( 'Publish this site' ) } -
    - { authUser ? ( -
    - { pickableSites.isLoading ? ( -
    { __( 'Loading sites…' ) }
    - ) : pickableSites.data && pickableSites.data.length > 0 ? ( -
      - { pickableSites.data.map( ( candidate ) => ( -
    • - -
    • - ) ) } -
    - ) : ( -
    - { __( 'No WordPress.com sites available to publish to.' ) } -
    - ) } -
    - ) : null } - -
    - ); -} diff --git a/apps/ui/src/components/site-toolbar/publish-site-dialog.module.css b/apps/ui/src/components/site-toolbar/publish-site-dialog.module.css new file mode 100644 index 0000000000..3201fae9ce --- /dev/null +++ b/apps/ui/src/components/site-toolbar/publish-site-dialog.module.css @@ -0,0 +1,25 @@ +.intro { + margin: 0 0 var(--wpds-dimension-padding-lg); + font-size: var(--wpds-typography-font-size-sm); + color: var(--wpds-color-fg-content-neutral-weak); +} + +.error { + margin: 0 0 var(--wpds-dimension-padding-md); + padding: var(--wpds-dimension-padding-sm) var(--wpds-dimension-padding-md); + border: var(--wpds-border-width-xs) solid var(--wpds-color-stroke-surface-error); + border-radius: var(--wpds-border-radius-md); + background: var(--wpds-color-bg-surface-error-weak); + color: var(--wpds-color-fg-content-error); + font-size: var(--wpds-typography-font-size-sm); +} + +/* Sits opposite Cancel and Connect: making a new site is a way out of this + list, not a step in it. */ +.createButton { + margin-inline-end: auto; +} + +.createButton svg { + fill: currentColor; +} diff --git a/apps/ui/src/components/site-toolbar/publish-site-dialog.tsx b/apps/ui/src/components/site-toolbar/publish-site-dialog.tsx new file mode 100644 index 0000000000..5638ecda79 --- /dev/null +++ b/apps/ui/src/components/site-toolbar/publish-site-dialog.tsx @@ -0,0 +1,143 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { __ } from '@wordpress/i18n'; +import { external, Icon } from '@wordpress/icons'; +import { Button, Dialog } from '@wordpress/ui'; +import { useState } from 'react'; +import { ConnectSitePicker } from '@/components/connect-site-picker'; +import { useConnector } from '@/data/core'; +import { connectedWpcomSitesQueryKey } from '@/data/queries/use-connected-wpcom-sites'; +import { usePickableWpcomSites } from '@/data/queries/use-wpcom-sites'; +import styles from './publish-site-dialog.module.css'; +import type { SiteDetails } from '@/data/core'; + +type Props = { + site: SiteDetails; + open: boolean; + onOpenChange: ( open: boolean ) => void; +}; + +/** + * Choosing where a Studio site goes live. The same picker onboarding uses to + * bring a site down into Studio, pointed the other way — one list of the + * WordPress.com and Pressable sites this account can reach, with room to see + * them rather than a popover to squint at. + */ +export function PublishSiteDialog( { site, open, onOpenChange }: Props ) { + const connector = useConnector(); + const queryClient = useQueryClient(); + const pickableSites = usePickableWpcomSites(); + const [ selectedId, setSelectedId ] = useState< number | null >( null ); + const [ isConnecting, setIsConnecting ] = useState( false ); + const [ error, setError ] = useState( '' ); + + const selectedSite = pickableSites.data?.find( ( candidate ) => candidate.id === selectedId ); + + const close = ( next: boolean ) => { + if ( isConnecting ) { + return; + } + onOpenChange( next ); + if ( ! next ) { + setSelectedId( null ); + setError( '' ); + } + }; + + const handleConnect = async () => { + if ( ! selectedSite || isConnecting ) { + return; + } + setIsConnecting( true ); + setError( '' ); + try { + await connector.connectWpcomSite( site.id, { + ...selectedSite, + localSiteId: site.id, + syncSupport: 'already-connected', + } ); + await queryClient.invalidateQueries( { queryKey: connectedWpcomSitesQueryKey( site.id ) } ); + close( false ); + } catch ( caught ) { + setError( + caught instanceof Error + ? caught.message + : __( 'Failed to connect the site. Please try again.' ) + ); + } finally { + setIsConnecting( false ); + } + }; + + const handleCreateNew = () => { + const checkoutUrl = connector.getPublishCheckoutUrl( site ); + if ( checkoutUrl ) { + // Desktop receives the new site via the wp-studio:// deep link; surfaces + // that can't (the local web server) opt into a server-side watch instead. + void connector.watchForPublishedSite?.( site.id ); + void connector.openExternalUrl( checkoutUrl ); + } + // The connect listener (deep link on desktop, sync-connect SSE on the local + // server) handles the follow-up connection, so we just get out of the way. + close( false ); + }; + + return ( + + + + { __( 'Publish this site' ) } + + +

    + { __( + 'Choose the WordPress.com or Pressable site to publish to. Pushing sends this Studio site’s files and database there.' + ) } +

    + { error ? ( +

    + { error } +

    + ) : null } + void pickableSites.refetch() } + selectedId={ selectedId } + onSelect={ setSelectedId } + emptyTitle={ __( 'No sites available' ) } + emptyDescription={ __( + 'Every site on this account is already connected to a Studio site, or cannot be published to.' + ) } + /> +
    + + + + { __( 'Cancel' ) } + + + +
    +
    + ); +} diff --git a/apps/ui/src/ui-classic/router/route-onboarding-connect/index.tsx b/apps/ui/src/ui-classic/router/route-onboarding-connect/index.tsx index d3f3f9c165..df75f85274 100644 --- a/apps/ui/src/ui-classic/router/route-onboarding-connect/index.tsx +++ b/apps/ui/src/ui-classic/router/route-onboarding-connect/index.tsx @@ -5,6 +5,11 @@ import { check, chevronLeft, external, search } from '@wordpress/icons'; import { Badge, Button, Icon } from '@wordpress/ui'; import { clsx } from 'clsx'; import { useCallback, useEffect, useMemo, useState } from 'react'; +import { + presentRemoteSites, + searchRemoteSites, + type ConnectSiteGroup, +} from '@/components/connect-site-picker/site-presentation'; import { OnboardingFooter } from '@/components/onboarding-footer'; import { toast } from '@/data/app-messages'; import { useConnector } from '@/data/core'; @@ -18,7 +23,6 @@ import { getLocalizedLink } from '@/lib/docs-links'; import { onboardingLayoutRoute, useOnboardingProgress } from '../layout-onboarding'; import sharedStyles from '../layout-onboarding/style.module.css'; import { ConnectSiteLifecycleError, runConnectSiteLifecycle } from './connect-site'; -import { presentRemoteSites, searchRemoteSites, type ConnectSiteGroup } from './site-presentation'; import styles from './style.module.css'; import type { SyncSite } from '@/data/core';