Add message after publishing a product to pre-publish panel (#44864)

* Add message after publishing product

* add styles

* Fix animation styles

* Fix panel footer

* Remove console.logs

* Add changelog

* Fix product url

* Remove preview

* Add post-publish-section and fix animation

* Add hook useProductURL

* Add postpublish

* Refactor usePreview

* Use PostPublishSection and PostPublishTitle

* move style

* modify styles

* Fix footer

* Fix copy

* Add ts-ignore

* Add CopyButtonProps

* Fix productStatus

* Rename isPublished
This commit is contained in:
Fernando Marichal 2024-03-07 08:08:51 -04:00 committed by GitHub
parent 8ff89fa6db
commit 9865701cbb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 354 additions and 31 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add message after publishing a product to pre-publish panel #44864

View File

@ -14,6 +14,7 @@ import { MouseEvent } from 'react';
*/
import { useValidations } from '../../../../contexts/validation-context';
import { WPError } from '../../../../utils/get-product-error-message';
import { useProductURL } from '../../../../hooks/use-product-url';
import { PreviewButtonProps } from '../../preview-button';
export function usePreview( {
@ -36,11 +37,7 @@ export function usePreview( {
'id'
);
const [ permalink ] = useEntityProp< string >(
'postType',
productType,
'permalink'
);
const { getProductURL } = useProductURL( productType );
const { hasEdits, isDisabled } = useSelect(
( select ) => {
@ -72,12 +69,6 @@ export function usePreview( {
// @ts-expect-error There are no types for this.
const { editEntityRecord, saveEditedEntityRecord } = useDispatch( 'core' );
let previewLink: URL | undefined;
if ( typeof permalink === 'string' ) {
previewLink = new URL( permalink );
previewLink.searchParams.append( 'preview', 'true' );
}
/**
* Overrides the default anchor behaviour when the product has unsaved changes.
* Before redirecting to the preview page all changes are saved and then the
@ -157,7 +148,7 @@ export function usePreview( {
'aria-disabled': ariaDisabled,
// Note that the href is always passed for a11y support. So
// the final rendered element is always an anchor.
href: previewLink?.toString(),
href: getProductURL( true ),
variant: 'tertiary',
onClick: handleClick,
};

View File

@ -0,0 +1,2 @@
export * from './post-publish-section';
export * from './post-publish-title';

View File

@ -0,0 +1,98 @@
/**
* External dependencies
*/
import { createElement, useState, Fragment } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { Button, PanelBody, TextControl } from '@wordpress/components';
import { recordEvent } from '@woocommerce/tracks';
import { useCopyToClipboard } from '@wordpress/compose';
import { Ref } from 'react';
import { getNewPath } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
import { useProductScheduled } from '../../../hooks/use-product-scheduled';
import { CopyButtonProps, PostPublishSectionProps } from './types';
import { TRACKS_SOURCE } from '../../../constants';
import { useProductURL } from '../../../hooks/use-product-url';
export function PostPublishSection( { postType }: PostPublishSectionProps ) {
const { getProductURL } = useProductURL( postType );
const { isScheduled } = useProductScheduled( postType );
const [ showCopyConfirmation, setShowCopyConfirmation ] =
useState< boolean >( false );
const productURL = getProductURL( isScheduled );
const CopyButton = ( { text, onCopy, children }: CopyButtonProps ) => {
const ref = useCopyToClipboard(
text,
onCopy
) as Ref< HTMLButtonElement >;
return (
<Button variant="secondary" ref={ ref }>
{ children }
</Button>
);
};
const onCopyURL = () => {
recordEvent( 'product_prepublish_panel', {
source: TRACKS_SOURCE,
action: 'copy_product_url',
} );
setShowCopyConfirmation( true );
setTimeout( () => {
setShowCopyConfirmation( false );
}, 4000 );
};
const onSelectInput = ( event: React.FocusEvent< HTMLInputElement > ) => {
event.target.select();
};
return (
<PanelBody>
<p className="post-publish-section__postpublish-subheader">
<strong>{ __( 'Whats next?', 'woocommerce' ) }</strong>
</p>
<div className="post-publish-section__postpublish-post-address-container">
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
{ /* @ts-ignore TextControl requires an 'onChange' but it's not necessary here. */ }
<TextControl
className="post-publish-section__postpublish-post-address"
readOnly
label={ __( 'product address', 'woocommerce' ) }
value={ productURL }
onFocus={ onSelectInput }
/>
<div className="post-publish-section__copy-button-wrap">
<CopyButton text={ productURL } onCopy={ onCopyURL }>
<>
{ showCopyConfirmation
? __( 'Copied!', 'woocommerce' )
: __( 'Copy', 'woocommerce' ) }
</>
</CopyButton>
</div>
</div>
<div className="post-publish-section__postpublish-buttons">
{ ! isScheduled && (
<Button variant="primary" href={ productURL }>
{ __( 'View Product', 'woocommerce' ) }
</Button>
) }
<Button
variant={ isScheduled ? 'primary' : 'secondary' }
href={ getNewPath( {}, '/add-product', {} ) }
>
{ __( 'Add New Product', 'woocommerce' ) }
</Button>
</div>
</PanelBody>
);
}

View File

@ -0,0 +1,74 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { createElement, createInterpolateElement } from '@wordpress/element';
import { useEntityProp } from '@wordpress/core-data';
/**
* Internal dependencies
*/
import { PostPublishTitleProps } from './types';
import { useProductURL } from '../../../hooks/use-product-url';
import { useProductScheduled } from '../../../hooks/use-product-scheduled';
export function PostPublishTitle( {
productType = 'product',
}: PostPublishTitleProps ) {
const { getProductURL } = useProductURL( productType );
const { isScheduled, formattedDate } = useProductScheduled( productType );
const [ editedProductName ] = useEntityProp< string >(
'postType',
productType,
'name'
);
const productURLString = getProductURL( false );
const getPostPublishedTitle = () => {
if ( isScheduled ) {
return createInterpolateElement(
sprintf(
/* translators: %s is the date when the product will be published */
__(
'<productURL /> is now scheduled. It will go live on %s',
'woocommerce'
),
formattedDate
),
{
productURL: (
<a
className="woocommerce-product-list__product-name"
href={ productURLString }
target="_blank"
rel="noreferrer"
>
{ editedProductName }
</a>
),
}
);
}
return createInterpolateElement(
__( '<productURL /> is now live.', 'woocommerce' ),
{
productURL: (
<a
className="woocommerce-product-list__product-name"
href={ productURLString }
target="_blank"
rel="noreferrer"
>
{ editedProductName }
</a>
),
}
);
};
return (
<div className="woocommerce-product-publish-panel__published">
{ getPostPublishedTitle() }
</div>
);
}

View File

@ -0,0 +1,13 @@
export type PostPublishSectionProps = {
postType: string;
};
export type PostPublishTitleProps = {
productType: string;
};
export type CopyButtonProps = {
text: string;
onCopy: () => void;
children: JSX.Element;
};

View File

@ -2,11 +2,14 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
import { createElement } from '@wordpress/element';
import { createElement, Fragment } from '@wordpress/element';
import { Button } from '@wordpress/components';
import { useDispatch } from '@wordpress/data';
import { recordEvent } from '@woocommerce/tracks';
import { useEntityProp } from '@wordpress/core-data';
import { closeSmall } from '@wordpress/icons';
import classnames from 'classnames';
import type { Product } from '@woocommerce/data';
/**
* Internal dependencies
@ -18,6 +21,7 @@ import { TRACKS_SOURCE } from '../../constants';
import { VisibilitySection } from './visibility-section';
import { ScheduleSection } from './schedule-section';
import { ShowPrepublishChecksSection } from './show-prepublish-checks-section';
import { PostPublishSection, PostPublishTitle } from './post-publish';
export function PrepublishPanel( {
productType = 'product',
@ -33,8 +37,17 @@ export function PrepublishPanel( {
'date_created'
);
const [ productStatus, , prevStatus ] = useEntityProp<
Product[ 'status' ]
>( 'postType', productType, 'status' );
const { closePrepublishPanel } = useDispatch( productEditorUiStore );
const isPublishedOrScheduled =
productType === 'product' && prevStatus !== 'future'
? productStatus === 'publish'
: true;
if ( editedDate !== date ) {
title = __( 'Are you ready to schedule this product?', 'woocommerce' );
description = __(
@ -43,9 +56,25 @@ export function PrepublishPanel( {
);
}
return (
<div className="woocommerce-product-publish-panel">
<div className="woocommerce-product-publish-panel__header">
function getHeaderActions() {
if ( isPublishedOrScheduled ) {
return (
<Button
className="woocommerce-publish-panel-close"
icon={ closeSmall }
label={ __( 'Close panel', 'woocommerce' ) }
onClick={ () => {
recordEvent( 'product_prepublish_panel', {
source: TRACKS_SOURCE,
action: 'close',
} );
closePrepublishPanel();
} }
/>
);
}
return (
<>
<PublishButton productType={ productType } />
<Button
variant={ 'secondary' }
@ -59,14 +88,48 @@ export function PrepublishPanel( {
>
{ __( 'Cancel', 'woocommerce' ) }
</Button>
</div>
<div className="woocommerce-product-publish-panel__title">
</>
);
}
function getPanelTitle() {
if ( isPublishedOrScheduled ) {
return <PostPublishTitle productType={ productType } />;
}
return (
<>
<h4>{ title }</h4>
<span>{ description }</span>
</div>
<div className="woocommerce-product-publish-panel__content">
</>
);
}
function getPanelSections() {
if ( isPublishedOrScheduled ) {
return <PostPublishSection postType={ productType } />;
}
return (
<>
<VisibilitySection productType={ productType } />
<ScheduleSection postType={ productType } />
</>
);
}
return (
<div
className={ classnames( 'woocommerce-product-publish-panel', {
'is-published': isPublishedOrScheduled,
} ) }
>
<div className="woocommerce-product-publish-panel__header">
{ getHeaderActions() }
</div>
<div className="woocommerce-product-publish-panel__title">
{ getPanelTitle() }
</div>
<div className="woocommerce-product-publish-panel__content">
{ getPanelSections() }
</div>
<div className="woocommerce-product-publish-panel__footer">
<ShowPrepublishChecksSection />

View File

@ -7,23 +7,16 @@
overflow: auto;
position: fixed;
background: $white;
transform: translateX(+100%);
animation: product-publish-panel__slide-in-animation 0.1s forwards;
width: 100%;
@include break-medium() {
top: $admin-bar-height;
top: 0;
width: $sidebar-width - $border-width;
@include reduce-motion("animation");
body.is-fullscreen-mode & {
top: 0;
}
// Keep it open on focus to avoid conflict with navigate-regions animation.
[role="region"]:focus & {
transform: translateX(0%);
}
}
&__header {
@ -32,6 +25,7 @@
align-items: center;
gap: 8px;
padding: 0 $grid-unit-20;
justify-content: right;
> button {
flex: 1;
@ -41,6 +35,11 @@
width: 100%;
justify-content: center;
}
.woocommerce-publish-panel-close {
flex: unset;
width: unset;
}
}
&__title {
@ -53,22 +52,77 @@
}
&__content {
// Ensure the pre-publish panel accounts for the header and footer height.
min-height: calc( 100% - #{ $header-height + 200px } );
min-height: calc( 100% - #{ $header-height + 220px } );
}
&__footer {
padding: 0 $grid-unit-20;
padding: $gap $grid-unit-20 $gap-largest $grid-unit-20;
left: 0;
width: 100%;
min-height: $gap-largest + $gap-large;
display: flex;
align-items: center;
margin-bottom: 20px;
.components-base-control__field {
margin: 0;
}
}
.post-publish-section {
&__postpublish-subheader {
margin: 0 0 $gap-small;
}
&__postpublish-post-address-container {
align-items: flex-end;
display: flex;
margin-bottom: $gap-small;
gap: $gap;
}
&__postpublish-post-address {
flex: 1;
}
&__copy-button-wrap {
flex-shrink: 0;
margin-bottom: $gap-smaller;
}
&__postpublish-buttons {
display: flex;
justify-content: space-between;
gap: $gap-small;
.components-button {
padding: $gap-smaller;
flex: 1;
justify-content: center;
}
}
}
}
.is-published {
display: flex;
flex-direction: column;
.woocommerce-product-publish-panel {
&__header, &__title {
border-bottom: $border-width solid $gray-200;
font-weight: 500;
}
&__content {
min-height: 180px;
flex-grow: 1;
}
}
}
.interface-interface-skeleton__actions {
transform: translateX(+100%);
animation: product-publish-panel__slide-in-animation 0.1s forwards;
@include break-medium() {
// Keep it open on focus to avoid conflict with navigate-regions animation.
[role="region"]:focus & {
transform: translateX(0%);
}
}
}
@keyframes product-publish-panel__slide-in-animation {

View File

@ -0,0 +1,24 @@
/**
* External dependencies
*/
import { useEntityProp } from '@wordpress/core-data';
import { useCallback } from 'react';
export function useProductURL( productType: string ) {
const [ permalink ] = useEntityProp< string >(
'postType',
productType,
'permalink'
);
const getProductURL = useCallback(
( isPreview: boolean ) => {
const productURL = new URL( permalink ) as URL | undefined;
if ( isPreview ) {
productURL?.searchParams.append( 'preview', 'true' );
}
return productURL?.toString() || '';
},
[ permalink ]
);
return { getProductURL };
}