Pewarta Proxy

📋 Server Request & Proxy Logs

Loading active proxy settings...
Developed by Khairul Adha (adhexa.id)
Mengambil log server...

⚙️ Pengaturan Proxy Target

Skip to content
2 changes: 2 additions & 0 deletions packages/core-data/src/private-apis.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import {
useResolvedSelection,
} from './hooks/use-post-editor-awareness-state';
import { lock } from './lock-unlock';
import { retrySyncConnection } from './sync';

export const privateApis = {};
lock( privateApis, {
useEntityRecordsWithPermissions,
RECEIVE_INTERMEDIATE_RESULTS,
retrySyncConnection,
useActiveCollaborators,
useResolvedSelection,
} );
2 changes: 2 additions & 0 deletions packages/core-data/src/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ const {
CRDT_DOC_META_PERSISTENCE_KEY,
CRDT_RECORD_MAP_KEY,
LOCAL_EDITOR_ORIGIN,
retrySyncConnection,
} = unlock( syncPrivateApis );

export {
Delta,
CRDT_DOC_META_PERSISTENCE_KEY,
CRDT_RECORD_MAP_KEY,
LOCAL_EDITOR_ORIGIN,
retrySyncConnection,
};

let syncManager: SyncManager;
Expand Down
134 changes: 97 additions & 37 deletions packages/editor/src/components/sync-connection-modal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,37 @@
import { useSelect, select } from '@wordpress/data';
import { useCopyToClipboard } from '@wordpress/compose';
import { serialize } from '@wordpress/blocks';
import { store as coreDataStore } from '@wordpress/core-data';
import {
store as coreDataStore,
privateApis as coreDataPrivateApis,
} from '@wordpress/core-data';
import {
privateApis,
store as blockEditorStore,
} from '@wordpress/block-editor';
import {
Button,
Modal,
Icon,
__experimentalHStack as HStack,
__experimentalVStack as VStack,
} from '@wordpress/components';
import { useState, useEffect, useRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { error as errorIcon } from '@wordpress/icons';
import { __, sprintf, _n } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import { getSyncErrorMessages } from '../../utils/sync-error-messages';
import { store as editorStore } from '../../store';
import { unlock } from '../../lock-unlock';
import { useRetryCountdown } from './use-retry-countdown';

const { BlockCanvasCover } = unlock( privateApis );
const { retrySyncConnection } = unlock( coreDataPrivateApis );

// Debounce time for initial disconnected status to allow connection to establish.
const INITIAL_DISCONNECTED_DEBOUNCE_MS = 5000;
const noop = () => {};

/**
* Sync connection modal that displays when any entity reports a disconnection.
Expand All @@ -38,10 +43,22 @@ const INITIAL_DISCONNECTED_DEBOUNCE_MS = 5000;
* @return {Element|null} The modal component or null if not disconnected.
*/
export function SyncConnectionModal() {
const connectionState = useSelect( ( selectFn ) => {
return selectFn( coreDataStore ).getSyncConnectionStatus() || null;
const { connectionState, postType } = useSelect( ( selectFn ) => {
const currentPostType = selectFn( editorStore ).getCurrentPostType();
return {
connectionState:
selectFn( coreDataStore ).getSyncConnectionStatus() || null,
postType: currentPostType
? selectFn( coreDataStore ).getPostType( currentPostType )
: null,
};
}, [] );

const { secondsRemaining, markRetrying } = useRetryCountdown(
connectionState?.retryInMs,
connectionState?.status
);

const copyButtonRef = useCopyToClipboard( () => {
const blocks = select( blockEditorStore ).getBlocks();
return serialize( blocks );
Expand All @@ -53,23 +70,24 @@ export function SyncConnectionModal() {
// Once true, disconnected status will show immediately without debounce.
const hasInitializedRef = useRef( false );

useEffect( () => {
const status = connectionState?.status;
const connectionStatus = connectionState?.status;
const connectionErrorCode = connectionState?.error?.code;

useEffect( () => {
// Clear any pending debounce timer when status changes.
if ( debounceTimerRef.current ) {
clearTimeout( debounceTimerRef.current );
debounceTimerRef.current = null;
}

if ( status === 'connected' ) {
if ( connectionStatus === 'connected' ) {
hasInitializedRef.current = true;
setSyncConnectionMessage( null );
} else if ( status === 'disconnected' ) {
} else if ( connectionStatus === 'disconnected' ) {
const showModal = () => {
hasInitializedRef.current = true;
setSyncConnectionMessage(
getSyncErrorMessages( connectionState.error ?? {} )
getSyncErrorMessages( { code: connectionErrorCode } )
);
};

Expand All @@ -89,51 +107,93 @@ export function SyncConnectionModal() {
clearTimeout( debounceTimerRef.current );
}
};
}, [ connectionState ] );
}, [ connectionStatus, connectionErrorCode ] );

if ( ! syncConnectionMessage ) {
return null;
}

const { title, description } = syncConnectionMessage;
const { title, description, canRetry } = syncConnectionMessage;

let retryCountdownText;
if ( secondsRemaining > 0 ) {
retryCountdownText = sprintf(
/* translators: %d: number of seconds until retry */
_n(
'Retrying connection in %d second\u2026',
'Retrying connection in %d seconds\u2026',
secondsRemaining
),
secondsRemaining
);
} else if ( secondsRemaining === 0 ) {
retryCountdownText = __( 'Retrying\u2026' );
}

let editPostHref = 'edit.php';
if ( postType?.slug ) {
editPostHref = `edit.php?post_type=${ postType.slug }`;
}

const isRetrying = secondsRemaining === 0;

return (
<BlockCanvasCover.Fill>
<Modal
__experimentalHideHeader
icon={ errorIcon }
className="editor-sync-connection-modal"
isDismissible={ false }
isFullScreen={ false }
onRequestClose={ () => {} }
onRequestClose={ noop }
shouldCloseOnClickOutside={ false }
shouldCloseOnEsc={ false }
size="medium"
title={ title }
>
<div className="editor-sync-connection-modal__container">
<VStack alignment="center" justify="center" spacing={ 2 }>
<Icon fill="#ccc" icon={ errorIcon } size={ 64 } />
<h1>{ title }</h1>
<p className="editor-sync-connection-modal__description">
{ description }
<VStack spacing={ 6 }>
<p>{ description }</p>
{ retryCountdownText && (
<p className="editor-sync-connection-modal__retry-countdown">
{ retryCountdownText }
</p>
<HStack spacing={ 2 } justify="center">
) }
<HStack justify="right">
<Button
__next40pxDefaultSize
href={ editPostHref }
isDestructive
variant="tertiary"
>
{ sprintf(
/* translators: %s: Post type name (e.g., "Posts", "Pages"). */
__( 'Back to %s' ),
postType?.labels?.name ?? __( 'Posts' )
) }
</Button>
<Button
__next40pxDefaultSize
ref={ copyButtonRef }
variant={ canRetry ? 'secondary' : 'primary' }
>
{ __( 'Copy Post Content' ) }
</Button>
{ canRetry && (
<Button
__next40pxDefaultSize
ref={ copyButtonRef }
aria-disabled={ isRetrying }
isBusy={ isRetrying }
variant="primary"
onClick={ () => {
if ( isRetrying ) {
return;
}
markRetrying();
retrySyncConnection();
} }
>
{ __( 'Copy post content' ) }
</Button>
<Button
__next40pxDefaultSize
href="edit.php"
isDestructive
variant="secondary"
>
{ __( 'Edit another post' ) }
{ __( 'Retry' ) }
</Button>
</HStack>
</VStack>
</div>
) }
</HStack>
</VStack>
</Modal>
</BlockCanvasCover.Fill>
);
Expand Down
14 changes: 6 additions & 8 deletions packages/editor/src/components/sync-connection-modal/style.scss
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
.editor-sync-connection-modal__container {
align-items: center;
display: flex;
margin: auto;
max-width: 30rem;
@use "@wordpress/base-styles/colors" as *;

.editor-sync-connection-modal p {
margin: 0;
}

.editor-sync-connection-modal__description {
font-size: 1.3rem;
text-align: center;
.editor-sync-connection-modal__retry-countdown {
color: $gray-700;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* WordPress dependencies
*/
import { useState, useEffect, useRef } from '@wordpress/element';

const MIN_RETRYING_DISPLAY_MS = 600;

/**
* Hook that computes a countdown in seconds from a retryInMs value.
*
* @param {number|undefined} retryInMs Milliseconds until next retry.
* @param {string|undefined} status Current connection status.
* @return {Object} Object with `secondsRemaining` (number|null) and `markRetrying` callback.
*/
export function useRetryCountdown( retryInMs, status ) {
const [ secondsRemaining, setSecondsRemaining ] = useState( null );
const [ isRetrying, setIsRetrying ] = useState( false );
const retryAtRef = useRef( null );

// Show "Retrying…" for a minimum duration when manually triggered.
const markRetrying = () => setIsRetrying( true );

useEffect( () => {
if ( ! isRetrying ) {
return;
}
const id = setTimeout(
() => setIsRetrying( false ),
MIN_RETRYING_DISPLAY_MS
);
return () => clearTimeout( id );
}, [ isRetrying ] );

useEffect( () => {
// Only clear countdown when explicitly connected.
if ( status === 'connected' ) {
setSecondsRemaining( null );
retryAtRef.current = null;
return;
}

// For transient states (e.g. 'connecting' during a retry attempt)
// or when retryInMs is not yet available, keep the previous
// countdown value to avoid a brief flash.
if ( status !== 'disconnected' || ! retryInMs ) {
return;
}

const retryAt = Date.now() + retryInMs;
retryAtRef.current = retryAt;
setSecondsRemaining( Math.ceil( retryInMs / 1000 ) );

const intervalId = setInterval( () => {
const remaining = Math.ceil(
( retryAtRef.current - Date.now() ) / 1000
);
setSecondsRemaining( Math.max( 0, remaining ) );
if ( remaining <= 0 ) {
clearInterval( intervalId );
setIsRetrying( true );
}
}, 1000 );

return () => clearInterval( intervalId );
}, [ retryInMs, status ] );

const displaySeconds = isRetrying ? 0 : secondsRemaining;

return { secondsRemaining: displaySeconds, markRetrying };
}
29 changes: 17 additions & 12 deletions packages/editor/src/utils/sync-error-messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,36 @@ import { __ } from '@wordpress/i18n';
*/
const ERROR_MESSAGES = {
'authentication-failed': {
title: __( 'Authentication Failed' ),
title: __( 'Unable to connect' ),
description: __(
'Authentication with the collaborative editing server failed. ' +
'Please verify that you have the necessary permissions.'
"Real-time collaboration couldn't verify your permissions. " +

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Love that this change was made.

'Check that you have access to edit this post, or contact your site administrator.'
),
canRetry: false,
},
'connection-expired': {
title: __( 'Connection Expired' ),
title: __( 'Connection expired' ),
description: __(
'The connection to the collaborative editing server has expired.'
'Your connection to real-time collaboration has timed out. ' +
'Editing is paused to prevent conflicts with other editors.'
),
canRetry: true,
},
'connection-limit-exceeded': {
title: __( 'Connection Limit Exceeded' ),
title: __( 'Too many editors connected' ),
description: __(
'The collaborative editing server has reached its maximum connection capacity. ' +
'Please try again later or contact your site administrator.'
'Real-time collaboration has reached its connection limit. ' +
'Try again later or contact your site administrator.'
),
canRetry: true,
},
'unknown-error': {
title: __( 'Disconnected' ),
title: __( 'Connection lost' ),
description: __(
'You are currently disconnected from the collaborative editing server. ' +
'Editing is temporarily disabled to prevent conflicts.'
'The connection to real-time collaboration was interrupted. ' +
'Editing is paused to prevent conflicts with other editors.'
),
canRetry: true,
},
};

Expand All @@ -42,7 +47,7 @@ const ERROR_MESSAGES = {
* Provides default messages based on error.code.
*
* @param {Object} error - Connection error object.
* @return {Object} Object with title and description strings.
* @return {Object} Object with title, description, and canRetry flag.
*/
export function getSyncErrorMessages( error ) {
if ( ERROR_MESSAGES[ error?.code ] ) {
Expand Down
Loading
Loading