Merge branch 'trunk' into feature/34906-marketing-channels-card
This commit is contained in:
commit
449da4e91c
|
@ -27,26 +27,26 @@ runs:
|
|||
echo "BUILD_FILTERS=$(node ./.github/actions/setup-woocommerce-monorepo/scripts/parse-input-filter.js '${{ inputs.build-filters }}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup PNPM
|
||||
uses: pnpm/action-setup@10693b3829bf86eb2572aef5f3571dcf5ca9287d
|
||||
uses: pnpm/action-setup@c3b53f6a16e57305370b4ae5a540c2077a1d50dd
|
||||
with:
|
||||
version: '^7.13.3'
|
||||
version: '^7.22.0'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: pnpm
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@e04e1d97f0c0481c6e1ba40f8a538454fe5d7709
|
||||
uses: shivammathur/setup-php@8e2ac35f639d3e794c1da1f28999385ab6fdf0fc
|
||||
with:
|
||||
php-version: ${{ inputs.php-version }}
|
||||
coverage: none
|
||||
tools: phpcs, sirbrillig/phpcs-changed
|
||||
|
||||
- name: Cache Composer Dependencies
|
||||
uses: actions/cache@fd5de65bc895cf536527842281bea11763fefd77
|
||||
uses: actions/cache@58c146cc91c5b9e778e71775dfe9bf1442ad9a12
|
||||
with:
|
||||
path: ~/.cache/composer/files
|
||||
key: ${{ runner.os }}-php-${{ inputs.php-version }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
|
@ -59,7 +59,7 @@ runs:
|
|||
pnpm install ${{ steps.parse-input.outputs.INSTALL_FILTERS }}
|
||||
|
||||
- name: Cache Build Output
|
||||
uses: actions/cache@fd5de65bc895cf536527842281bea11763fefd77
|
||||
uses: actions/cache@58c146cc91c5b9e778e71775dfe9bf1442ad9a12
|
||||
with:
|
||||
path: node_modules/.cache/turbo
|
||||
key: ${{ runner.os }}-build-output-${{ hashFiles('node_modules/.cache/turbo/*-meta.json') }}
|
||||
|
|
|
@ -24,7 +24,7 @@ jobs:
|
|||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@2fddd8803e2f5c9604345a0b591c3020ee971a93
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c
|
||||
|
||||
- name: Install Octokit
|
||||
run: npm --prefix .github/workflows/scripts install @octokit/action
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
name: "Pull request post-merge processing"
|
||||
name: 'Pull request post-merge processing'
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
|
@ -13,7 +13,7 @@ jobs:
|
|||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: "Get the action scripts"
|
||||
- name: 'Get the action scripts'
|
||||
run: |
|
||||
scripts="assign-milestone-to-merged-pr.php add-post-merge-comment.php post-request-shared.php"
|
||||
for script in $scripts
|
||||
|
@ -29,11 +29,11 @@ jobs:
|
|||
done
|
||||
env:
|
||||
GITHUB_API_URL: ${{ env.GITHUB_API_URL }}
|
||||
- name: "Install PHP"
|
||||
uses: shivammathur/setup-php@v2
|
||||
- name: 'Install PHP'
|
||||
uses: shivammathur/setup-php@8e2ac35f639d3e794c1da1f28999385ab6fdf0fc
|
||||
with:
|
||||
php-version: '7.4'
|
||||
- name: "Run the script to assign a milestone"
|
||||
- name: 'Run the script to assign a milestone'
|
||||
if: |
|
||||
!github.event.pull_request.milestone &&
|
||||
github.event.pull_request.base.ref == 'trunk'
|
||||
|
|
|
@ -30,7 +30,7 @@ jobs:
|
|||
freeze: ${{ steps.check-freeze.outputs.freeze }}
|
||||
steps:
|
||||
- name: 'Install PHP'
|
||||
uses: shivammathur/setup-php@v2
|
||||
uses: shivammathur/setup-php@8e2ac35f639d3e794c1da1f28999385ab6fdf0fc
|
||||
with:
|
||||
php-version: '7.4'
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
uses: actions/checkout@v3
|
||||
|
||||
- name: 'Setup node'
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@64ed1c7eab4cce3362f8c340dee64e5eaeef8f7c
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Altering styles to correctly target fields within slot fills on product editor.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Include CSS for experimental tree control so it renders properly in Storybook.
|
|
@ -0,0 +1,5 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
Comment: Move registerFill call to inside an useEffect since it was updating a component while rendering another component
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add new WooProductTabItem component for slot filling tab items.
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Updating the product editor fill components to support multiple targets.
|
|
@ -44,6 +44,7 @@
|
|||
"@woocommerce/navigation": "workspace:*",
|
||||
"@wordpress/a11y": "3.5.0",
|
||||
"@wordpress/api-fetch": "^6.0.1",
|
||||
"@wordpress/base-styles": "^4.3.0",
|
||||
"@wordpress/block-editor": "^9.8.0",
|
||||
"@wordpress/block-library": "^7.16.0",
|
||||
"@wordpress/blocks": "^11.18.0",
|
||||
|
@ -164,5 +165,11 @@
|
|||
"pnpm lint:fix",
|
||||
"pnpm test-staged"
|
||||
]
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -9,7 +10,6 @@ import { TreeItemProps } from '../types';
|
|||
|
||||
export function useTreeItem( { item, level, ...props }: TreeItemProps ) {
|
||||
const nextLevel = level + 1;
|
||||
const nextHeadingPaddingLeft = ( level - 1 ) * 28 + 12;
|
||||
|
||||
return {
|
||||
item,
|
||||
|
@ -19,8 +19,8 @@ export function useTreeItem( { item, level, ...props }: TreeItemProps ) {
|
|||
},
|
||||
headingProps: {
|
||||
style: {
|
||||
paddingLeft: nextHeadingPaddingLeft,
|
||||
},
|
||||
'--level': level,
|
||||
} as React.CSSProperties,
|
||||
},
|
||||
treeProps: {
|
||||
items: item.children,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
flex-grow: 1;
|
||||
gap: $gap-smaller;
|
||||
min-height: $gap-largest;
|
||||
padding: 0 $gap-small;
|
||||
padding: 0 $gap-small 0 calc( ( var( --level ) - 1 ) * ( $gap + $gap-small ) + $gap-small );
|
||||
border-radius: 2px;
|
||||
|
||||
&:hover,
|
||||
|
|
|
@ -87,6 +87,7 @@ export { CollapsibleContent } from './collapsible-content';
|
|||
export { createOrderedChildren, sortFillsByOrder } from './utils';
|
||||
export { WooProductFieldItem as __experimentalWooProductFieldItem } from './woo-product-field-item';
|
||||
export { WooProductSectionItem as __experimentalWooProductSectionItem } from './woo-product-section-item';
|
||||
export { WooProductTabItem as __experimentalWooProductTabItem } from './woo-product-tab-item';
|
||||
export {
|
||||
ProductSectionLayout as __experimentalProductSectionLayout,
|
||||
ProductFieldSection as __experimentalProductFieldSection,
|
||||
|
|
|
@ -11,9 +11,9 @@
|
|||
&__body {
|
||||
padding: $gap-large;
|
||||
|
||||
> .components-base-control,
|
||||
> .components-dropdown,
|
||||
> .woocommerce-rich-text-editor {
|
||||
.components-base-control,
|
||||
.components-dropdown,
|
||||
.woocommerce-rich-text-editor {
|
||||
&:not(:first-child):not(.components-radio-control) {
|
||||
margin-top: $gap-large - $gap-smaller;
|
||||
margin-bottom: 0;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* External Dependencies
|
||||
*/
|
||||
@import 'node_modules/@wordpress/base-styles/colors.native';
|
||||
@import '@automattic/tour-kit/dist/esm/styles.scss';
|
||||
|
||||
/**
|
||||
|
@ -56,3 +57,4 @@
|
|||
@import 'collapsible-content/style.scss';
|
||||
@import 'form/style.scss';
|
||||
@import 'product-section-layout/style.scss';
|
||||
@import 'experimental-tree-control/tree.scss';
|
||||
|
|
|
@ -20,7 +20,10 @@ function createOrderedChildren< T = Fill.Props, S = Record< string, unknown > >(
|
|||
injectProps?: S
|
||||
) {
|
||||
if ( typeof children === 'function' ) {
|
||||
return cloneElement( children( props ), { order, ...injectProps } );
|
||||
return cloneElement( children( { ...props, order, ...injectProps } ), {
|
||||
order,
|
||||
...injectProps,
|
||||
} );
|
||||
} else if ( isValidElement( children ) ) {
|
||||
return cloneElement( children, { ...props, order, ...injectProps } );
|
||||
}
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Slot, Fill } from '@wordpress/components';
|
||||
import { createElement, Children } from '@wordpress/element';
|
||||
import { createElement, Children, Fragment } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { createOrderedChildren, sortFillsByOrder } from '../utils';
|
||||
import { useSlotContext, SlotContextHelpersType } from '../slot-context';
|
||||
import { ProductFillLocationType } from '../woo-product-tab-item';
|
||||
|
||||
type WooProductFieldItemProps = {
|
||||
id: string;
|
||||
section: string;
|
||||
sections: ProductFillLocationType[];
|
||||
pluginId: string;
|
||||
order?: number;
|
||||
};
|
||||
|
||||
type WooProductFieldSlotProps = {
|
||||
|
@ -24,21 +24,32 @@ type WooProductFieldSlotProps = {
|
|||
|
||||
export const WooProductFieldItem: React.FC< WooProductFieldItemProps > & {
|
||||
Slot: React.FC< Slot.Props & WooProductFieldSlotProps >;
|
||||
} = ( { children, order = 20, section, id } ) => {
|
||||
} = ( { children, sections, id } ) => {
|
||||
const { registerFill, getFillHelpers } = useSlotContext();
|
||||
|
||||
useEffect( () => {
|
||||
registerFill( id );
|
||||
}, [] );
|
||||
|
||||
return (
|
||||
<Fill name={ `woocommerce_product_field_${ section }` }>
|
||||
<>
|
||||
{ sections.map( ( { name: sectionName, order: sectionOrder } ) => (
|
||||
<Fill
|
||||
name={ `woocommerce_product_field_${ sectionName }` }
|
||||
key={ sectionName }
|
||||
>
|
||||
{ ( fillProps: Fill.Props ) => {
|
||||
return createOrderedChildren<
|
||||
Fill.Props & SlotContextHelpersType,
|
||||
Fill.Props &
|
||||
SlotContextHelpersType & {
|
||||
sectionName: string;
|
||||
},
|
||||
{ _id: string }
|
||||
>(
|
||||
children,
|
||||
order,
|
||||
sectionOrder || 20,
|
||||
{
|
||||
sectionName,
|
||||
...fillProps,
|
||||
...getFillHelpers(),
|
||||
},
|
||||
|
@ -46,6 +57,8 @@ export const WooProductFieldItem: React.FC< WooProductFieldItemProps > & {
|
|||
);
|
||||
} }
|
||||
</Fill>
|
||||
) ) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -3,41 +3,51 @@
|
|||
*/
|
||||
import React from 'react';
|
||||
import { Slot, Fill } from '@wordpress/components';
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { createElement, Fragment } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { createOrderedChildren, sortFillsByOrder } from '../utils';
|
||||
import { ProductFillLocationType } from '../woo-product-tab-item';
|
||||
|
||||
type WooProductSectionItemProps = {
|
||||
id: string;
|
||||
location: string;
|
||||
tabs: ProductFillLocationType[];
|
||||
pluginId: string;
|
||||
order?: number;
|
||||
};
|
||||
|
||||
type WooProductFieldSlotProps = {
|
||||
location: string;
|
||||
type WooProductSectionSlotProps = {
|
||||
tab: string;
|
||||
};
|
||||
|
||||
export const WooProductSectionItem: React.FC< WooProductSectionItemProps > & {
|
||||
Slot: React.FC< Slot.Props & WooProductFieldSlotProps >;
|
||||
} = ( { children, order = 20, location } ) => (
|
||||
<Fill name={ `woocommerce_product_section_${ location }` }>
|
||||
Slot: React.FC< Slot.Props & WooProductSectionSlotProps >;
|
||||
} = ( { children, tabs } ) => {
|
||||
return (
|
||||
<>
|
||||
{ tabs.map( ( { name: tabName, order: tabOrder } ) => (
|
||||
<Fill
|
||||
name={ `woocommerce_product_section_${ tabName }` }
|
||||
key={ tabName }
|
||||
>
|
||||
{ ( fillProps: Fill.Props ) => {
|
||||
return createOrderedChildren< Fill.Props >(
|
||||
children,
|
||||
order,
|
||||
fillProps
|
||||
);
|
||||
return createOrderedChildren<
|
||||
Fill.Props & { tabName: string }
|
||||
>( children, tabOrder || 20, {
|
||||
tabName,
|
||||
...fillProps,
|
||||
} );
|
||||
} }
|
||||
</Fill>
|
||||
) ) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
WooProductSectionItem.Slot = ( { fillProps, location } ) => (
|
||||
WooProductSectionItem.Slot = ( { fillProps, tab } ) => (
|
||||
<Slot
|
||||
name={ `woocommerce_product_section_${ location }` }
|
||||
name={ `woocommerce_product_section_${ tab }` }
|
||||
fillProps={ fillProps }
|
||||
>
|
||||
{ ( fills ) => {
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
# WooProductTabItem Slot & Fill
|
||||
|
||||
A Slotfill component that will allow you to add a new tab to the product editor.
|
||||
|
||||
## Usage
|
||||
|
||||
```jsx
|
||||
<WooProductTabItem id={ key } location="tab/general" order={ 2 } pluginId="test-plugin" tabProps={ { title: 'New tab', name: 'new-tab' } } >
|
||||
<Card>
|
||||
<CardBody>{ /* Tab content */ }</CardBody>
|
||||
</Card>
|
||||
</WooProductTabItem>
|
||||
|
||||
<WooProductTabItem.Slot location="tab/general" />
|
||||
```
|
||||
|
||||
### WooProductTabItem (fill)
|
||||
|
||||
This is the fill component. You must provide the `id` prop to identify your section fill with a unique string. This component will accept a series of props:
|
||||
|
||||
| Prop | Type | Description |
|
||||
| ---------- | ------ | -------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | String | A unique string to identify your fill. Used for configuiration management. |
|
||||
| `location` | String | The string used to identify the particular location that you want to render your section. |
|
||||
| `pluginId` | String | A unique plugin ID to identify the plugin/extension that this fill is associated with. |
|
||||
| `tabProps` | Object | An object containing tab props: name, title, className, disabled (see TabPanel.Tab from @wordpress/components) |
|
||||
| `order` | Number | (optional) This number will dictate the order that the sections rendered by a Slot will be appear. |
|
||||
|
||||
### WooProductTabItem.Slot (slot)
|
||||
|
||||
This is the slot component, and will not be used as frequently. It must also receive the required `location` prop that will be identical to the fill `location`.
|
||||
|
||||
| Name | Type | Description |
|
||||
| ---------- | ------ | ---------------------------------------------------------------------------------------------------- |
|
||||
| `location` | String | Unique to the location that the Slot appears, and must be the same as the one provided to any fills. |
|
|
@ -0,0 +1 @@
|
|||
export * from './woo-product-tab-item';
|
|
@ -0,0 +1,107 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import React, { ReactElement, ReactNode } from 'react';
|
||||
import { Slot, Fill, TabPanel } from '@wordpress/components';
|
||||
import { createElement, Fragment } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { createOrderedChildren } from '../utils';
|
||||
|
||||
export type ProductFillLocationType = { name: string; order?: number };
|
||||
|
||||
type WooProductTabItemProps = {
|
||||
id: string;
|
||||
pluginId: string;
|
||||
tabProps:
|
||||
| TabPanel.Tab
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
| ( ( fillProps: Record< string, any > | undefined ) => TabPanel.Tab );
|
||||
templates?: Array< ProductFillLocationType >;
|
||||
};
|
||||
|
||||
type WooProductFieldSlotProps = {
|
||||
template: string;
|
||||
children: (
|
||||
tabs: TabPanel.Tab[],
|
||||
tabChildren: Record< string, ReactNode >
|
||||
) => ReactElement | null;
|
||||
};
|
||||
|
||||
export const WooProductTabItem: React.FC< WooProductTabItemProps > & {
|
||||
Slot: React.VFC<
|
||||
Omit< Slot.Props, 'children' > & WooProductFieldSlotProps
|
||||
>;
|
||||
} = ( { children, tabProps, templates } ) => {
|
||||
if ( ! templates ) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn( 'WooProductTabItem fill is missing templates property.' );
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{ templates.map( ( templateData ) => (
|
||||
<Fill
|
||||
name={ `woocommerce_product_tab_${ templateData.name }` }
|
||||
key={ templateData.name }
|
||||
>
|
||||
{ ( fillProps: Fill.Props ) => {
|
||||
return createOrderedChildren< Fill.Props >(
|
||||
children,
|
||||
templateData.order || 20,
|
||||
{},
|
||||
{
|
||||
tabProps,
|
||||
templateName: templateData.name,
|
||||
order: templateData.order || 20,
|
||||
...fillProps,
|
||||
}
|
||||
);
|
||||
} }
|
||||
</Fill>
|
||||
) ) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
WooProductTabItem.Slot = ( { fillProps, template, children } ) => (
|
||||
<Slot
|
||||
name={ `woocommerce_product_tab_${ template }` }
|
||||
fillProps={ fillProps }
|
||||
>
|
||||
{ ( fills ) => {
|
||||
const tabData = fills.reduce(
|
||||
( { childrenMap, tabs }, fill ) => {
|
||||
const props: WooProductTabItemProps & { order: number } =
|
||||
fill[ 0 ].props;
|
||||
if ( props && props.tabProps ) {
|
||||
childrenMap[ props.tabProps.name ] = fill[ 0 ];
|
||||
const tabProps =
|
||||
typeof props.tabProps === 'function'
|
||||
? props.tabProps( fillProps )
|
||||
: props.tabProps;
|
||||
tabs.push( {
|
||||
...tabProps,
|
||||
order: props.order ?? 20,
|
||||
} );
|
||||
}
|
||||
return {
|
||||
childrenMap,
|
||||
tabs,
|
||||
};
|
||||
},
|
||||
{ childrenMap: {}, tabs: [] } as {
|
||||
childrenMap: Record< string, ReactElement >;
|
||||
tabs: Array< TabPanel.Tab & { order: number } >;
|
||||
}
|
||||
);
|
||||
const orderedTabs = tabData.tabs.sort( ( a, b ) => {
|
||||
return a.order - b.order;
|
||||
} );
|
||||
|
||||
return children( orderedTabs, tabData.childrenMap );
|
||||
} }
|
||||
</Slot>
|
||||
);
|
|
@ -2,7 +2,7 @@
|
|||
"phpVersion": null,
|
||||
"core": null,
|
||||
"plugins": [
|
||||
"https://downloads.wordpress.org/plugin/woocommerce.7.1.0.zip",
|
||||
"https://downloads.wordpress.org/plugin/woocommerce.7.3.0.zip",
|
||||
"."
|
||||
],
|
||||
"config": {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Significance: patch
|
||||
Type: update
|
||||
|
||||
Disable TikTok in the OBW
|
||||
bump WooCommerce version
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add FeedbackModal and ProductMVPFeedbackModal components
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add a function to help decide if comments section should be shown
|
|
@ -29,6 +29,10 @@ type CustomerEffortScoreProps = {
|
|||
onModalShownCallback?: () => void;
|
||||
onModalDismissedCallback?: () => void;
|
||||
icon?: React.ReactElement | null;
|
||||
shouldShowComments?: (
|
||||
firstQuestionScore: number,
|
||||
secondQuestionScore: number
|
||||
) => boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -48,6 +52,7 @@ type CustomerEffortScoreProps = {
|
|||
* @param {Function} props.onNoticeDismissedCallback Function to call when the notice is dismissed.
|
||||
* @param {Function} props.onModalShownCallback Function to call when the modal is shown.
|
||||
* @param {Function} props.onModalDismissedCallback Function to call when modal is dismissed.
|
||||
* @param {Function} props.shouldShowComments Callback to determine if comments section should be shown.
|
||||
* @param {Object} props.icon Icon (React component) to be shown on the notice.
|
||||
*/
|
||||
const CustomerEffortScore: React.VFC< CustomerEffortScoreProps > = ( {
|
||||
|
@ -62,6 +67,10 @@ const CustomerEffortScore: React.VFC< CustomerEffortScoreProps > = ( {
|
|||
onModalShownCallback = noop,
|
||||
onModalDismissedCallback = noop,
|
||||
icon,
|
||||
shouldShowComments = ( firstQuestionScore, secondQuestionScore ) =>
|
||||
[ firstQuestionScore, secondQuestionScore ].some(
|
||||
( score ) => score === 1 || score === 2
|
||||
),
|
||||
} ) => {
|
||||
const [ shouldCreateNotice, setShouldCreateNotice ] = useState( true );
|
||||
const [ visible, setVisible ] = useState( false );
|
||||
|
@ -108,6 +117,7 @@ const CustomerEffortScore: React.VFC< CustomerEffortScoreProps > = ( {
|
|||
secondQuestion={ secondQuestion }
|
||||
recordScoreCallback={ recordScoreCallback }
|
||||
onCloseModal={ onModalDismissedCallback }
|
||||
shouldShowComments={ shouldShowComments }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -145,6 +155,10 @@ CustomerEffortScore.propTypes = {
|
|||
* The second survey question.
|
||||
*/
|
||||
secondQuestion: PropTypes.string,
|
||||
/**
|
||||
* A function to determine whether or not the comments field shown be shown.
|
||||
*/
|
||||
shouldShowComments: PropTypes.func,
|
||||
};
|
||||
|
||||
export { CustomerEffortScore };
|
||||
|
|
|
@ -32,6 +32,7 @@ import { __ } from '@wordpress/i18n';
|
|||
* @param {string} props.defaultScore Default score.
|
||||
* @param {Function} props.onCloseModal Callback for when user closes modal by clicking cancel.
|
||||
* @param {Function} props.customOptions List of custom score options, contains label and value.
|
||||
* @param {Function} props.shouldShowComments A function to determine whether or not the comments field shown be shown.
|
||||
*/
|
||||
function CustomerFeedbackModal( {
|
||||
recordScoreCallback,
|
||||
|
@ -42,6 +43,10 @@ function CustomerFeedbackModal( {
|
|||
defaultScore = NaN,
|
||||
onCloseModal,
|
||||
customOptions,
|
||||
shouldShowComments = ( firstQuestionScore, secondQuestionScore ) =>
|
||||
[ firstQuestionScore, secondQuestionScore ].some(
|
||||
( score ) => score === 1 || score === 2
|
||||
),
|
||||
}: {
|
||||
recordScoreCallback: (
|
||||
score: number,
|
||||
|
@ -55,6 +60,10 @@ function CustomerFeedbackModal( {
|
|||
defaultScore?: number;
|
||||
onCloseModal?: () => void;
|
||||
customOptions?: { label: string; value: string }[];
|
||||
shouldShowComments?: (
|
||||
firstQuestionScore: number,
|
||||
secondQuestionScore: number
|
||||
) => boolean;
|
||||
} ): JSX.Element | null {
|
||||
const options =
|
||||
customOptions && customOptions.length > 0
|
||||
|
@ -200,8 +209,10 @@ function CustomerFeedbackModal( {
|
|||
</div>
|
||||
) }
|
||||
|
||||
{ [ firstQuestionScore, secondQuestionScore ].some(
|
||||
( score ) => score === 1 || score === 2
|
||||
{ typeof shouldShowComments === 'function' &&
|
||||
shouldShowComments(
|
||||
firstQuestionScore,
|
||||
secondQuestionScore
|
||||
) && (
|
||||
<div className="woocommerce-customer-effort-score__comments">
|
||||
<TextareaControl
|
||||
|
@ -218,7 +229,9 @@ function CustomerFeedbackModal( {
|
|||
'Optional, but much apprecated. We love reading your feedback!',
|
||||
'woocommerce'
|
||||
) }
|
||||
onChange={ ( value: string ) => setComments( value ) }
|
||||
onChange={ ( value: string ) =>
|
||||
setComments( value )
|
||||
}
|
||||
rows={ 5 }
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
.woocommerce-feedback-modal__buttons {
|
||||
text-align: right;
|
||||
|
||||
.components-button {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-feedback-modal .woocommerce-feedback-modal__description {
|
||||
max-width: 550px;
|
||||
margin: 0 0 1.5em 0;
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createElement, useState } from '@wordpress/element';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, Modal } from '@wordpress/components';
|
||||
import { Text } from '@woocommerce/experimental';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Provides a modal requesting customer feedback.
|
||||
*
|
||||
* Answers and comments are sent to a callback function.
|
||||
*
|
||||
* @param {Object} props Component props.
|
||||
* @param {Function} props.onSubmit Function to call when the results are sent.
|
||||
* @param {string} props.title Title displayed in the modal.
|
||||
* @param {string} props.description Description displayed in the modal.
|
||||
* @param {string} props.isSubmitButtonDisabled Boolean to enable/disable the send button.
|
||||
* @param {string} props.submitButtonLabel Label for the send button.
|
||||
* @param {string} props.cancelButtonLabel Label for the cancel button.
|
||||
* @param {Function} props.onModalClose Callback for when user closes modal by clicking cancel.
|
||||
* @param {Function} props.children Children to be rendered.
|
||||
*/
|
||||
function FeedbackModal( {
|
||||
onSubmit,
|
||||
title,
|
||||
description,
|
||||
onModalClose,
|
||||
children,
|
||||
isSubmitButtonDisabled,
|
||||
submitButtonLabel,
|
||||
cancelButtonLabel,
|
||||
}: {
|
||||
onSubmit: () => void;
|
||||
title: string;
|
||||
description?: string;
|
||||
onModalClose?: () => void;
|
||||
children?: JSX.Element;
|
||||
isSubmitButtonDisabled?: boolean;
|
||||
submitButtonLabel?: string;
|
||||
cancelButtonLabel?: string;
|
||||
} ): JSX.Element | null {
|
||||
const [ isOpen, setOpen ] = useState( true );
|
||||
|
||||
const closeModal = () => {
|
||||
setOpen( false );
|
||||
if ( onModalClose ) {
|
||||
onModalClose();
|
||||
}
|
||||
};
|
||||
|
||||
if ( ! isOpen ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className="woocommerce-feedback-modal"
|
||||
title={ title }
|
||||
onRequestClose={ closeModal }
|
||||
shouldCloseOnClickOutside={ false }
|
||||
>
|
||||
<Text
|
||||
variant="body"
|
||||
as="p"
|
||||
className="woocommerce-feedback-modal__description"
|
||||
size={ 14 }
|
||||
lineHeight="20px"
|
||||
marginBottom="1.5em"
|
||||
>
|
||||
{ description }
|
||||
</Text>
|
||||
{ children }
|
||||
<div className="woocommerce-feedback-modal__buttons">
|
||||
<Button isTertiary onClick={ closeModal } name="cancel">
|
||||
{ cancelButtonLabel }
|
||||
</Button>
|
||||
<Button
|
||||
isPrimary={ ! isSubmitButtonDisabled }
|
||||
isSecondary={ isSubmitButtonDisabled }
|
||||
onClick={ () => {
|
||||
onSubmit();
|
||||
setOpen( false );
|
||||
} }
|
||||
name="send"
|
||||
disabled={ isSubmitButtonDisabled }
|
||||
>
|
||||
{ submitButtonLabel }
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
FeedbackModal.propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.string,
|
||||
onModalClose: PropTypes.func,
|
||||
isSubmitButtonDisabled: PropTypes.bool,
|
||||
submitButtonLabel: PropTypes.string,
|
||||
cancelButtonLabel: PropTypes.string,
|
||||
};
|
||||
|
||||
export { FeedbackModal };
|
|
@ -0,0 +1 @@
|
|||
export * from './feedback-modal';
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
||||
import { createElement } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { FeedbackModal } from '../index';
|
||||
|
||||
const mockRecordScoreCallback = jest.fn();
|
||||
|
||||
describe( 'FeedbackModal', () => {
|
||||
it( 'should render a modal', async () => {
|
||||
render(
|
||||
<FeedbackModal
|
||||
onSubmit={ mockRecordScoreCallback }
|
||||
title="Testing"
|
||||
submitButtonLabel="Send"
|
||||
cancelButtonLabel="Cancel"
|
||||
/>
|
||||
);
|
||||
|
||||
// Wait for the modal to render.
|
||||
await screen.findByRole( 'dialog' );
|
||||
|
||||
expect(
|
||||
screen.getByRole( 'button', { name: /Send/i } )
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole( 'button', { name: /Cancel/i } )
|
||||
).toBeInTheDocument();
|
||||
} );
|
||||
|
||||
it( 'should close modal when cancel button pressed', async () => {
|
||||
render(
|
||||
<FeedbackModal
|
||||
onSubmit={ mockRecordScoreCallback }
|
||||
title="Testing"
|
||||
submitButtonLabel="Send"
|
||||
cancelButtonLabel="Cancel"
|
||||
/>
|
||||
);
|
||||
|
||||
// Wait for the modal to render.
|
||||
await screen.findByRole( 'dialog' );
|
||||
|
||||
// Press cancel button.
|
||||
fireEvent.click( screen.getByRole( 'button', { name: /Cancel/i } ) );
|
||||
|
||||
expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument();
|
||||
} );
|
||||
} );
|
|
@ -1,3 +1,5 @@
|
|||
export * from './customer-effort-score';
|
||||
export * from './customer-feedback-simple';
|
||||
export * from './customer-feedback-modal';
|
||||
export * from './product-mvp-feedback-modal';
|
||||
export * from './feedback-modal';
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './product-mvp-feedback-modal';
|
|
@ -0,0 +1,23 @@
|
|||
.woocommerce-product-mvp-feedback-modal {
|
||||
&__subtitle {
|
||||
margin-top: $gap-smaller !important;
|
||||
}
|
||||
&__checkboxes {
|
||||
margin: $gap-small 0;
|
||||
}
|
||||
&__comments {
|
||||
margin-top: 2em;
|
||||
margin-bottom: 1.5em;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
text-transform: none;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createElement, Fragment, useState } from '@wordpress/element';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CheckboxControl, TextareaControl } from '@wordpress/components';
|
||||
import { Text } from '@woocommerce/experimental';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { FeedbackModal } from '../feedback-modal';
|
||||
|
||||
/**
|
||||
* Provides a modal requesting customer feedback.
|
||||
*
|
||||
*
|
||||
* @param {Object} props Component props.
|
||||
* @param {Function} props.recordScoreCallback Function to call when the results are sent.
|
||||
* @param {Function} props.onCloseModal Callback for when user closes modal by clicking cancel.
|
||||
*/
|
||||
function ProductMVPFeedbackModal( {
|
||||
recordScoreCallback,
|
||||
onCloseModal,
|
||||
}: {
|
||||
recordScoreCallback: ( checked: string[], comments: string ) => void;
|
||||
onCloseModal?: () => void;
|
||||
} ): JSX.Element | null {
|
||||
const [ missingFeatures, setMissingFeatures ] = useState( false );
|
||||
const [ missingPlugins, setMissingPlugins ] = useState( false );
|
||||
const [ difficultToUse, setDifficultToUse ] = useState( false );
|
||||
const [ slowBuggyOrBroken, setSlowBuggyOrBroken ] = useState( false );
|
||||
const [ other, setOther ] = useState( false );
|
||||
const checkboxes = [
|
||||
{
|
||||
key: 'missing-features',
|
||||
label: __( 'Missing features', 'woocommerce' ),
|
||||
checked: missingFeatures,
|
||||
onChange: setMissingFeatures,
|
||||
},
|
||||
{
|
||||
key: 'missing-plugins',
|
||||
label: __( 'Missing plugins', 'woocommerce' ),
|
||||
checked: missingPlugins,
|
||||
onChange: setMissingPlugins,
|
||||
},
|
||||
{
|
||||
key: 'difficult-to-use',
|
||||
label: __( 'It is difficult to use', 'woocommerce' ),
|
||||
checked: difficultToUse,
|
||||
onChange: setDifficultToUse,
|
||||
},
|
||||
{
|
||||
key: 'slow-buggy-or-broken',
|
||||
label: __( 'It is slow, buggy, or broken', 'woocommerce' ),
|
||||
checked: slowBuggyOrBroken,
|
||||
onChange: setSlowBuggyOrBroken,
|
||||
},
|
||||
{
|
||||
key: 'other',
|
||||
label: __( 'Other (describe below)', 'woocommerce' ),
|
||||
checked: other,
|
||||
onChange: setOther,
|
||||
},
|
||||
];
|
||||
const [ comments, setComments ] = useState( '' );
|
||||
|
||||
const onSendFeedback = () => {
|
||||
const checked = checkboxes
|
||||
.filter( ( checkbox ) => checkbox.checked )
|
||||
.map( ( checkbox ) => checkbox.key );
|
||||
recordScoreCallback( checked, comments );
|
||||
};
|
||||
|
||||
const isSendButtonDisabled =
|
||||
! comments &&
|
||||
! missingFeatures &&
|
||||
! missingPlugins &&
|
||||
! difficultToUse &&
|
||||
! slowBuggyOrBroken &&
|
||||
! other;
|
||||
|
||||
return (
|
||||
<FeedbackModal
|
||||
title={ __(
|
||||
'Thanks for trying out the new product editor!',
|
||||
'woocommerce'
|
||||
) }
|
||||
description={ __(
|
||||
'We’re working on making it better, and your feedback will help improve the experience for thousands of merchants like you.',
|
||||
'woocommerce'
|
||||
) }
|
||||
onSubmit={ onSendFeedback }
|
||||
onModalClose={ onCloseModal }
|
||||
isSubmitButtonDisabled={ isSendButtonDisabled }
|
||||
submitButtonLabel={ __( 'Send feedback', 'woocommerce' ) }
|
||||
cancelButtonLabel={ __( 'Skip', 'woocommerce' ) }
|
||||
>
|
||||
<>
|
||||
<Text
|
||||
variant="subtitle.small"
|
||||
as="p"
|
||||
weight="600"
|
||||
size="14"
|
||||
lineHeight="20px"
|
||||
>
|
||||
{ __(
|
||||
'What made you switch back to the classic product editor?',
|
||||
'woocommerce'
|
||||
) }
|
||||
</Text>
|
||||
<Text
|
||||
weight="400"
|
||||
size="12"
|
||||
as="p"
|
||||
lineHeight="16px"
|
||||
color="#757575"
|
||||
className="woocommerce-product-mvp-feedback-modal__subtitle"
|
||||
>
|
||||
{ __( '(Check all that apply)', 'woocommerce' ) }
|
||||
</Text>
|
||||
<div className="woocommerce-product-mvp-feedback-modal__checkboxes">
|
||||
{ checkboxes.map( ( checkbox, index ) => (
|
||||
<CheckboxControl
|
||||
key={ index }
|
||||
label={ checkbox.label }
|
||||
name={ checkbox.key }
|
||||
checked={ checkbox.checked }
|
||||
onChange={ checkbox.onChange }
|
||||
/>
|
||||
) ) }
|
||||
</div>
|
||||
<div className="woocommerce-product-mvp-feedback-modal__comments">
|
||||
<TextareaControl
|
||||
label={ __( 'Additional comments', 'woocommerce' ) }
|
||||
value={ comments }
|
||||
placeholder={ __(
|
||||
'Optional, but much apprecated. We love reading your feedback!',
|
||||
'woocommerce'
|
||||
) }
|
||||
onChange={ ( value: string ) => setComments( value ) }
|
||||
rows={ 5 }
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</FeedbackModal>
|
||||
);
|
||||
}
|
||||
|
||||
ProductMVPFeedbackModal.propTypes = {
|
||||
recordScoreCallback: PropTypes.func.isRequired,
|
||||
onCloseModal: PropTypes.func,
|
||||
};
|
||||
|
||||
export { ProductMVPFeedbackModal };
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
||||
import { createElement } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ProductMVPFeedbackModal } from '../index';
|
||||
|
||||
const mockRecordScoreCallback = jest.fn();
|
||||
|
||||
describe( 'ProductMVPFeedbackModal', () => {
|
||||
it( 'should close the ProductMVPFeedback modal when skip button pressed', async () => {
|
||||
render(
|
||||
<ProductMVPFeedbackModal
|
||||
recordScoreCallback={ mockRecordScoreCallback }
|
||||
/>
|
||||
);
|
||||
// Wait for the modal to render.
|
||||
await screen.findByRole( 'dialog' );
|
||||
// Press cancel button.
|
||||
fireEvent.click( screen.getByRole( 'button', { name: /Skip/i } ) );
|
||||
expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument();
|
||||
} );
|
||||
it( 'should enable Send button when an option is checked', async () => {
|
||||
render(
|
||||
<ProductMVPFeedbackModal
|
||||
recordScoreCallback={ mockRecordScoreCallback }
|
||||
/>
|
||||
);
|
||||
// Wait for the modal to render.
|
||||
await screen.findByRole( 'dialog' );
|
||||
fireEvent.click( screen.getByRole( 'checkbox', { name: /other/i } ) );
|
||||
fireEvent.click(
|
||||
screen.getByRole( 'button', { name: /Send feedback/i } )
|
||||
);
|
||||
} );
|
||||
it( 'should call the function sent as recordScoreCallback with the checked options', async () => {
|
||||
render(
|
||||
<ProductMVPFeedbackModal
|
||||
recordScoreCallback={ mockRecordScoreCallback }
|
||||
/>
|
||||
);
|
||||
// Wait for the modal to render.
|
||||
await screen.findByRole( 'dialog' );
|
||||
fireEvent.click( screen.getByRole( 'checkbox', { name: /other/i } ) );
|
||||
expect( mockRecordScoreCallback ).toHaveBeenCalledWith(
|
||||
[ 'other' ],
|
||||
''
|
||||
);
|
||||
} );
|
||||
} );
|
|
@ -1,4 +1,6 @@
|
|||
@import 'customer-feedback-simple/customer-feedback-simple.scss';
|
||||
@import 'product-mvp-feedback-modal/product-mvp-feedback-modal.scss';
|
||||
@import 'feedback-modal/feedback-modal.scss';
|
||||
|
||||
.woocommerce-customer-effort-score__selection {
|
||||
margin: 1em 0 1.5em 0;
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Update type definition for ProductForm
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Removing unused code from ProductForm data store.
|
|
@ -24,6 +24,7 @@ export function getProductFormSuccess( productForm: ProductForm ) {
|
|||
fields: productForm.fields,
|
||||
sections: productForm.sections,
|
||||
subsections: productForm.subsections,
|
||||
tabs: productForm.tabs,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ const reducer: Reducer< ProductFormState, Action > = (
|
|||
fields: [],
|
||||
sections: [],
|
||||
subsections: [],
|
||||
tabs: [],
|
||||
},
|
||||
action
|
||||
) => {
|
||||
|
@ -42,6 +43,7 @@ const reducer: Reducer< ProductFormState, Action > = (
|
|||
fields: action.fields,
|
||||
sections: action.sections,
|
||||
subsections: action.subsections,
|
||||
tabs: action.tabs,
|
||||
};
|
||||
break;
|
||||
case TYPES.GET_PRODUCT_FORM_ERROR:
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { apiFetch, select } from '@wordpress/data-controls';
|
||||
import { controls } from '@wordpress/data';
|
||||
import { apiFetch } from '@wordpress/data-controls';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -15,10 +14,6 @@ import {
|
|||
} from './actions';
|
||||
import { WC_ADMIN_NAMESPACE } from '../constants';
|
||||
import { ProductFormField, ProductForm } from './types';
|
||||
import { STORE_NAME } from './constants';
|
||||
|
||||
const resolveSelect =
|
||||
controls && controls.resolveSelect ? controls.resolveSelect : select;
|
||||
|
||||
export function* getFields() {
|
||||
try {
|
||||
|
@ -34,10 +29,6 @@ export function* getFields() {
|
|||
}
|
||||
}
|
||||
|
||||
export function* getCountry() {
|
||||
yield resolveSelect( STORE_NAME, 'getProductForm' );
|
||||
}
|
||||
|
||||
export function* getProductForm() {
|
||||
try {
|
||||
const url = WC_ADMIN_NAMESPACE + '/product-form';
|
||||
|
|
|
@ -23,10 +23,16 @@ export type ProductFormSection = BaseComponent & {
|
|||
|
||||
export type Subsection = BaseComponent;
|
||||
|
||||
export type Tabs = BaseComponent & {
|
||||
name: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export type ProductForm = {
|
||||
fields: ProductFormField[];
|
||||
sections: ProductFormSection[];
|
||||
subsections: Subsection[];
|
||||
tabs: Tabs[];
|
||||
};
|
||||
|
||||
export type ProductFormState = ProductForm & {
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add WooOnboardingTaskListHeader component
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { createElement } from '@wordpress/element';
|
||||
import { Slot, Fill } from '@wordpress/components';
|
||||
|
||||
type WooOnboardingTaskListHeaderProps = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* A Fill for adding Onboarding Task List headers.
|
||||
*
|
||||
* @slotFill WooOnboardingTaskListHeader
|
||||
* @scope woocommerce-tasks
|
||||
* @param {Object} props React props.
|
||||
* @param {string} props.id Task id.
|
||||
*/
|
||||
export const WooOnboardingTaskListHeader = ( {
|
||||
id,
|
||||
...props
|
||||
}: WooOnboardingTaskListHeaderProps & Slot.Props ) => (
|
||||
<Fill
|
||||
name={ 'woocommerce_onboarding_task_list_header_' + id }
|
||||
{ ...props }
|
||||
/>
|
||||
);
|
||||
|
||||
WooOnboardingTaskListHeader.Slot = ( {
|
||||
id,
|
||||
fillProps,
|
||||
}: WooOnboardingTaskListHeaderProps & Slot.Props ) => (
|
||||
<Slot
|
||||
name={ 'woocommerce_onboarding_task_list_header_' + id }
|
||||
fillProps={ fillProps }
|
||||
/>
|
||||
);
|
|
@ -0,0 +1 @@
|
|||
export * from './WooOnboardingTaskListHeader';
|
|
@ -13,4 +13,5 @@ export { default as WCPayLogo } from './images/wcpay-logo';
|
|||
export { WooPaymentGatewaySetup } from './components/WooPaymentGatewaySetup';
|
||||
export { WooPaymentGatewayConfigure } from './components/WooPaymentGatewayConfigure';
|
||||
export { WooOnboardingTaskListItem } from './components/WooOnboardingTaskListItem';
|
||||
export { WooOnboardingTaskListHeader } from './components/WooOnboardingTaskListHeader';
|
||||
export { WooOnboardingTask } from './components/WooOnboardingTask';
|
||||
|
|
|
@ -14,12 +14,11 @@ import { ALLOW_TRACKING_OPTION_NAME } from './constants';
|
|||
const CUSTOMER_EFFORT_SCORE_EXIT_PAGE_KEY = 'customer-effort-score-exit-page';
|
||||
|
||||
let allowTracking = false;
|
||||
resolveSelect( OPTIONS_STORE_NAME )
|
||||
const trackingPromise = resolveSelect( OPTIONS_STORE_NAME )
|
||||
.getOption( ALLOW_TRACKING_OPTION_NAME )
|
||||
.then( ( trackingOption ) => {
|
||||
allowTracking = trackingOption === 'yes';
|
||||
} );
|
||||
|
||||
/**
|
||||
* Gets the list of exited pages from Localstorage.
|
||||
*/
|
||||
|
@ -42,7 +41,8 @@ export const getExitPageData = () => {
|
|||
*
|
||||
* @param {string} pageId of page exited early.
|
||||
*/
|
||||
export const addExitPage = ( pageId: string ) => {
|
||||
export const addExitPage = async ( pageId: string ) => {
|
||||
await trackingPromise;
|
||||
if ( ! ( window.localStorage && allowTracking ) ) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -86,6 +86,7 @@ export const CustomerEffortScoreModalContainer: React.FC = () => {
|
|||
visibleCESModalData.props?.onCloseModal?.();
|
||||
hideCesModal();
|
||||
} }
|
||||
shouldShowComments={ visibleCESModalData.props?.shouldShowComments }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useEffect } from 'react';
|
||||
import { compose } from '@wordpress/compose';
|
||||
import { withDispatch, withSelect } from '@wordpress/data';
|
||||
import { OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||
|
@ -29,19 +30,20 @@ function CustomerEffortScoreTracksContainer( {
|
|||
resolving,
|
||||
clearQueue,
|
||||
} ) {
|
||||
if ( resolving ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const queueForPage = queue.filter(
|
||||
( item ) =>
|
||||
item.pagenow === window.pagenow &&
|
||||
item.adminpage === window.adminpage
|
||||
);
|
||||
|
||||
useEffect( () => {
|
||||
if ( queueForPage.length ) {
|
||||
clearQueue();
|
||||
}
|
||||
}, [ queueForPage ] );
|
||||
|
||||
if ( resolving ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -3,6 +3,8 @@ const TYPES = {
|
|||
ADD_CES_SURVEY: 'ADD_CES_SURVEY',
|
||||
SHOW_CES_MODAL: 'SHOW_CES_MODAL',
|
||||
HIDE_CES_MODAL: 'HIDE_CES_MODAL',
|
||||
SHOW_PRODUCT_MVP_FEEDBACK_MODAL: 'SHOW_PRODUCT_MVP_FEEDBACK_MODAL',
|
||||
HIDE_PRODUCT_MVP_FEEDBACK_MODAL: 'HIDE_PRODUCT_MVP_FEEDBACK_MODAL',
|
||||
};
|
||||
|
||||
export default TYPES;
|
||||
|
|
|
@ -141,3 +141,21 @@ export function addCesSurveyForCustomerSearch() {
|
|||
},
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add show product MVP Feedback modal.
|
||||
*/
|
||||
export function showProductMVPFeedbackModal() {
|
||||
return {
|
||||
type: TYPES.SHOW_PRODUCT_MVP_FEEDBACK_MODAL,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide product MVP Feedback modal.
|
||||
*/
|
||||
export function hideProductMVPFeedbackModal() {
|
||||
return {
|
||||
type: TYPES.HIDE_PRODUCT_MVP_FEEDBACK_MODAL,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ const DEFAULT_STATE = {
|
|||
queue: [],
|
||||
cesModalData: undefined,
|
||||
showCESModal: false,
|
||||
showProductMVPFeedbackModal: false,
|
||||
};
|
||||
|
||||
const reducer = ( state = DEFAULT_STATE, action ) => {
|
||||
|
@ -62,6 +63,16 @@ const reducer = ( state = DEFAULT_STATE, action ) => {
|
|||
...state,
|
||||
queue: [ ...state.queue, newTrack ],
|
||||
};
|
||||
case TYPES.SHOW_PRODUCT_MVP_FEEDBACK_MODAL:
|
||||
return {
|
||||
...state,
|
||||
showProductMVPFeedbackModal: true,
|
||||
};
|
||||
case TYPES.HIDE_PRODUCT_MVP_FEEDBACK_MODAL:
|
||||
return {
|
||||
...state,
|
||||
showProductMVPFeedbackModal: false,
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
|
|
|
@ -5,3 +5,7 @@ export function getCesSurveyQueue( state ) {
|
|||
export function getVisibleCESModalData( state ) {
|
||||
return state.showCESModal ? state.cesModalData : undefined;
|
||||
}
|
||||
|
||||
export function isProductMVPFeedbackModalVisible( state ) {
|
||||
return state.showProductMVPFeedbackModal;
|
||||
}
|
||||
|
|
|
@ -21,9 +21,12 @@ import { STORE_KEY } from './data/constants';
|
|||
|
||||
export const PRODUCT_MVP_CES_ACTION_OPTION_NAME =
|
||||
'woocommerce_ces_product_mvp_ces_action';
|
||||
export const NEW_PRODUCT_MANAGEMENT =
|
||||
'woocommerce_new_product_management_enabled';
|
||||
|
||||
export const ProductMVPCESFooter: React.FC = () => {
|
||||
const { showCesModal } = useDispatch( STORE_KEY );
|
||||
const { showCesModal, showProductMVPFeedbackModal } =
|
||||
useDispatch( STORE_KEY );
|
||||
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
|
||||
const {
|
||||
cesAction,
|
||||
|
@ -83,6 +86,7 @@ export const ProductMVPCESFooter: React.FC = () => {
|
|||
"Thanks for the feedback. We'll put it to good use!",
|
||||
'woocommerce'
|
||||
),
|
||||
shouldShowComments: () => true,
|
||||
},
|
||||
{},
|
||||
{
|
||||
|
@ -98,6 +102,16 @@ export const ProductMVPCESFooter: React.FC = () => {
|
|||
} );
|
||||
};
|
||||
|
||||
const onDisablingNewProductExperience = () => {
|
||||
updateOptions( {
|
||||
[ PRODUCT_MVP_CES_ACTION_OPTION_NAME ]: 'hide',
|
||||
} );
|
||||
updateOptions( {
|
||||
[ NEW_PRODUCT_MANAGEMENT ]: 'no',
|
||||
} );
|
||||
showProductMVPFeedbackModal();
|
||||
};
|
||||
|
||||
const onDisablingCES = () => {
|
||||
updateOptions( {
|
||||
[ PRODUCT_MVP_CES_ACTION_OPTION_NAME ]: 'hide',
|
||||
|
@ -125,7 +139,7 @@ export const ProductMVPCESFooter: React.FC = () => {
|
|||
{ __( 'Share feedback', 'woocommerce' ) }
|
||||
</Button>
|
||||
<Button
|
||||
onClick={ onDisablingCES }
|
||||
onClick={ onDisablingNewProductExperience }
|
||||
variant="tertiary"
|
||||
>
|
||||
{ __( 'Turn it off', 'woocommerce' ) }
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useDispatch, useSelect } from '@wordpress/data';
|
||||
import { ProductMVPFeedbackModal } from '@woocommerce/customer-effort-score';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { getAdminLink } from '@woocommerce/settings';
|
||||
import { useFormContext } from '@woocommerce/components';
|
||||
import { Product } from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { STORE_KEY } from './data/constants';
|
||||
|
||||
export const ProductMVPFeedbackModalContainer: React.FC = () => {
|
||||
const { values } = useFormContext< Product >();
|
||||
const { hideProductMVPFeedbackModal } = useDispatch( STORE_KEY );
|
||||
const { isProductMVPModalVisible } = useSelect( ( select ) => {
|
||||
const { isProductMVPFeedbackModalVisible } = select( STORE_KEY );
|
||||
return {
|
||||
isProductMVPModalVisible: isProductMVPFeedbackModalVisible(),
|
||||
};
|
||||
} );
|
||||
|
||||
const classicEditorUrl = values.id
|
||||
? getAdminLink( `post.php?post=${ values.id }&action=edit` )
|
||||
: getAdminLink( 'post-new.php?post_type=product' );
|
||||
|
||||
const recordScore = ( checked: string[], comments: string ) => {
|
||||
recordEvent( 'product_mvp_feedback', {
|
||||
action: 'disable',
|
||||
checked,
|
||||
comments: comments || '',
|
||||
} );
|
||||
hideProductMVPFeedbackModal();
|
||||
window.location.href = `${ classicEditorUrl }&new-product-experience-disabled=true`;
|
||||
};
|
||||
|
||||
const onCloseModal = () => {
|
||||
recordEvent( 'product_mvp_feedback', {
|
||||
action: 'disable',
|
||||
checked: '',
|
||||
comments: '',
|
||||
} );
|
||||
hideProductMVPFeedbackModal();
|
||||
window.location.href = classicEditorUrl;
|
||||
};
|
||||
|
||||
if ( ! isProductMVPModalVisible ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ProductMVPFeedbackModal
|
||||
recordScoreCallback={ recordScore }
|
||||
onCloseModal={ onCloseModal }
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -19,21 +19,23 @@ async function isProductMVPCESHidden(): Promise< boolean > {
|
|||
export const useProductMVPCESFooter = () => {
|
||||
const { updateOptions } = useDispatch( OPTIONS_STORE_NAME );
|
||||
|
||||
const showCesFooter = ( actionName = 'show' ) => {
|
||||
updateOptions( {
|
||||
[ PRODUCT_MVP_CES_ACTION_OPTION_NAME ]: actionName,
|
||||
} );
|
||||
};
|
||||
|
||||
const onSaveDraft = async () => {
|
||||
if ( ( await isProductMVPCESHidden() ) === false ) {
|
||||
updateOptions( {
|
||||
[ PRODUCT_MVP_CES_ACTION_OPTION_NAME ]: 'new_product',
|
||||
} );
|
||||
showCesFooter( 'new_product' );
|
||||
}
|
||||
};
|
||||
|
||||
const onPublish = async () => {
|
||||
if ( ( await isProductMVPCESHidden() ) === false ) {
|
||||
updateOptions( {
|
||||
[ PRODUCT_MVP_CES_ACTION_OPTION_NAME ]: 'new_product',
|
||||
} );
|
||||
showCesFooter( 'new_product' );
|
||||
}
|
||||
};
|
||||
|
||||
return { onSaveDraft, onPublish };
|
||||
return { onSaveDraft, onPublish, showCesFooter };
|
||||
};
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useEffect, useLayoutEffect, useState } from '@wordpress/element';
|
||||
|
||||
export const useHeaderHeight = () => {
|
||||
const [ headerHeight, setHeaderHeight ] = useState( 60 );
|
||||
const [ adminBarHeight, setAdminBarHeight ] = useState( 32 );
|
||||
|
||||
useEffect( () => {
|
||||
const wpbody = document.querySelector( '#wpbody' ) as Node;
|
||||
const observer = new MutationObserver( () => {
|
||||
setHeaderHeight(
|
||||
parseInt( ( wpbody as HTMLElement ).style.marginTop, 10 )
|
||||
);
|
||||
} );
|
||||
observer.observe( wpbody, {
|
||||
attributes: true,
|
||||
} );
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [] );
|
||||
|
||||
useLayoutEffect( () => {
|
||||
const handleResize = () => {
|
||||
const adminBar = document.querySelector(
|
||||
'#wpadminbar'
|
||||
) as HTMLElement;
|
||||
setAdminBarHeight( adminBar.clientHeight );
|
||||
};
|
||||
window.addEventListener( 'resize', handleResize );
|
||||
return () => {
|
||||
window.removeEventListener( 'resize', handleResize );
|
||||
};
|
||||
}, [] );
|
||||
|
||||
return {
|
||||
adminBarHeight,
|
||||
headerHeight,
|
||||
};
|
||||
};
|
|
@ -1,6 +1,4 @@
|
|||
.woocommerce-marketing-card-header-title {
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
line-height: 28px;
|
||||
letter-spacing: 0;
|
||||
@include font-size( 20 );
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
|
|
@ -126,7 +126,7 @@ const EditProductPage: React.FC = () => {
|
|||
product.status === 'trash' &&
|
||||
! isPendingAction &&
|
||||
! wasDeletedUsingAction && (
|
||||
<ProductFormLayout>
|
||||
<ProductFormLayout id="error">
|
||||
<div className="woocommerce-edit-product__error">
|
||||
{ __(
|
||||
'You cannot edit this item because it is in the Trash. Please restore it and try again.',
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useFormContext } from '@woocommerce/components';
|
||||
|
||||
import { Product } from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { Attributes } from '../../fields/attributes';
|
||||
|
||||
export const AttributesField = () => {
|
||||
const {
|
||||
getInputProps,
|
||||
values: { id: productId },
|
||||
} = useFormContext< Product >();
|
||||
return (
|
||||
<Attributes
|
||||
{ ...getInputProps( 'attributes', {
|
||||
productId,
|
||||
} ) }
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
__experimentalWooProductSectionItem as WooProductSectionItem,
|
||||
__experimentalWooProductFieldItem as WooProductFieldItem,
|
||||
__experimentalProductSectionLayout as ProductSectionLayout,
|
||||
Link,
|
||||
} from '@woocommerce/components';
|
||||
import { registerPlugin } from '@wordpress/plugins';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { AttributesField } from './index';
|
||||
import { ATTRIBUTES_SECTION_ID, TAB_GENERAL_ID, PLUGIN_ID } from '../constants';
|
||||
|
||||
import './attributes-section.scss';
|
||||
|
||||
const AttributesSection = () => (
|
||||
<>
|
||||
<WooProductSectionItem
|
||||
id={ ATTRIBUTES_SECTION_ID }
|
||||
tabs={ [ { name: TAB_GENERAL_ID, order: 5 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
>
|
||||
<ProductSectionLayout
|
||||
title={ __( 'Attributes', 'woocommerce' ) }
|
||||
className="woocommerce-product-attributes-section"
|
||||
description={
|
||||
<>
|
||||
<span>
|
||||
{ __(
|
||||
'Add descriptive pieces of information that customers can use to filter and search for this product.',
|
||||
'woocommerce'
|
||||
) }
|
||||
</span>
|
||||
<Link
|
||||
className="woocommerce-form-section__header-link"
|
||||
href="https://woocommerce.com/document/managing-product-taxonomies/#product-attributes"
|
||||
target="_blank"
|
||||
type="external"
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'learn_more_about_attributes_help'
|
||||
);
|
||||
} }
|
||||
>
|
||||
{ __(
|
||||
'Learn more about attributes',
|
||||
'woocommerce'
|
||||
) }
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<WooProductFieldItem.Slot section={ ATTRIBUTES_SECTION_ID } />
|
||||
</ProductSectionLayout>
|
||||
</WooProductSectionItem>
|
||||
<WooProductFieldItem
|
||||
id="attributes/add"
|
||||
sections={ [ { name: ATTRIBUTES_SECTION_ID, order: 1 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
>
|
||||
<AttributesField />
|
||||
</WooProductFieldItem>
|
||||
</>
|
||||
);
|
||||
|
||||
registerPlugin( 'wc-admin-product-editor-attributes-section', {
|
||||
// @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated.
|
||||
scope: 'woocommerce-product-editor',
|
||||
render: () => <AttributesSection />,
|
||||
} );
|
|
@ -0,0 +1 @@
|
|||
export * from './attributes-field';
|
|
@ -1,7 +1,17 @@
|
|||
export const PRODUCT_DETAILS_SLUG = 'product-details';
|
||||
|
||||
export const PRICING_SECTION_BASIC_ID = 'pricing/basic';
|
||||
export const PRICING_SECTION_TAXES_ID = 'pricing/taxes';
|
||||
export const DETAILS_SECTION_ID = 'general/details';
|
||||
export const INVENTORY_SECTION_ID = 'inventory/inventory';
|
||||
export const INVENTORY_SECTION_ADVANCED_ID = 'inventory/advanced';
|
||||
export const IMAGES_SECTION_ID = 'general/images';
|
||||
export const ATTRIBUTES_SECTION_ID = 'general/attributes';
|
||||
export const SHIPPING_SECTION_BASIC_ID = 'shipping/shipping';
|
||||
export const SHIPPING_SECTION_DIMENSIONS_ID = 'shipping/dimensions';
|
||||
|
||||
export const TAB_INVENTORY_ID = 'tab/inventory';
|
||||
export const TAB_GENERAL_ID = 'tab/general';
|
||||
export const TAB_SHIPPING_ID = 'tab/shipping';
|
||||
export const TAB_PRICING_ID = 'tab/pricing';
|
||||
|
||||
export const PLUGIN_ID = 'woocommerce';
|
||||
export const PRODUCT_DETAILS_SLUG = 'product-details';
|
||||
|
|
|
@ -28,9 +28,8 @@ const DetailsSection = () => (
|
|||
<>
|
||||
<WooProductSectionItem
|
||||
id={ DETAILS_SECTION_ID }
|
||||
location={ TAB_GENERAL_ID }
|
||||
tabs={ [ { name: TAB_GENERAL_ID, order: 1 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
order={ 1 }
|
||||
>
|
||||
<ProductFieldSection
|
||||
id={ DETAILS_SECTION_ID }
|
||||
|
@ -43,41 +42,36 @@ const DetailsSection = () => (
|
|||
</WooProductSectionItem>
|
||||
<WooProductFieldItem
|
||||
id="details/name"
|
||||
section={ DETAILS_SECTION_ID }
|
||||
sections={ [ { name: DETAILS_SECTION_ID, order: 1 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
order={ 1 }
|
||||
>
|
||||
<DetailsNameField />
|
||||
</WooProductFieldItem>
|
||||
<WooProductFieldItem
|
||||
id="details/categories"
|
||||
section={ DETAILS_SECTION_ID }
|
||||
sections={ [ { name: DETAILS_SECTION_ID, order: 3 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
order={ 3 }
|
||||
>
|
||||
<DetailsCategoriesField />
|
||||
</WooProductFieldItem>
|
||||
<WooProductFieldItem
|
||||
id="details/feature"
|
||||
section={ DETAILS_SECTION_ID }
|
||||
sections={ [ { name: DETAILS_SECTION_ID, order: 5 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
order={ 5 }
|
||||
>
|
||||
<DetailsFeatureField />
|
||||
</WooProductFieldItem>
|
||||
<WooProductFieldItem
|
||||
id="details/summary"
|
||||
section={ DETAILS_SECTION_ID }
|
||||
sections={ [ { name: DETAILS_SECTION_ID, order: 7 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
order={ 7 }
|
||||
>
|
||||
<DetailsSummaryField />
|
||||
</WooProductFieldItem>
|
||||
<WooProductFieldItem
|
||||
id="details/description"
|
||||
section={ DETAILS_SECTION_ID }
|
||||
sections={ [ { name: DETAILS_SECTION_ID, order: 9 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
order={ 9 }
|
||||
>
|
||||
<DetailsDescriptionField />
|
||||
</WooProductFieldItem>
|
||||
|
|
|
@ -23,9 +23,8 @@ const ImagesSection = () => (
|
|||
<>
|
||||
<WooProductSectionItem
|
||||
id={ IMAGES_SECTION_ID }
|
||||
location={ TAB_GENERAL_ID }
|
||||
tabs={ [ { name: TAB_GENERAL_ID, order: 3 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
order={ 3 }
|
||||
>
|
||||
<ProductFieldSection
|
||||
id={ IMAGES_SECTION_ID }
|
||||
|
@ -58,9 +57,8 @@ const ImagesSection = () => (
|
|||
</WooProductSectionItem>
|
||||
<WooProductFieldItem
|
||||
id="images/gallery"
|
||||
section={ IMAGES_SECTION_ID }
|
||||
sections={ [ { name: IMAGES_SECTION_ID, order: 1 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
order={ 1 }
|
||||
>
|
||||
<ImagesGalleryField />
|
||||
</WooProductFieldItem>
|
||||
|
|
|
@ -2,6 +2,12 @@
|
|||
* Internal dependencies
|
||||
*/
|
||||
import './product-form-fills';
|
||||
import './product-form-tab-fills';
|
||||
import './product-form-variation-tab-fills';
|
||||
|
||||
export * from './shipping-section/shipping-section-fills';
|
||||
export * from './details-section/details-section-fills';
|
||||
export * from './images-section/images-section-fills';
|
||||
export * from './attributes-section/attributes-section-fills';
|
||||
export * from './inventory-section/inventory-section-fills';
|
||||
export * from './pricing-section/pricing-section-fills';
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
export * from './inventory-field-sku';
|
||||
export * from './inventory-field-track-quantity';
|
||||
export * from './inventory-field-stock-manual';
|
||||
export * from './inventory-field-stock-manage';
|
||||
export * from './inventory-field-stock-out';
|
||||
export * from './inventory-field-stock-limit';
|
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useFormContext } from '@woocommerce/components';
|
||||
import { TextControl } from '@wordpress/components';
|
||||
import { Product } from '@woocommerce/data';
|
||||
|
||||
export const InventorySkuField = () => {
|
||||
const { getInputProps } = useFormContext< Product >();
|
||||
|
||||
return (
|
||||
<TextControl
|
||||
label={ __( 'SKU (Stock Keeping Unit)', 'woocommerce' ) }
|
||||
{ ...getInputProps( 'sku', {
|
||||
className: 'half-width-field',
|
||||
} ) }
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useFormContext } from '@woocommerce/components';
|
||||
import { CheckboxControl } from '@wordpress/components';
|
||||
import { Product } from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getCheckboxTracks } from '../../sections/utils';
|
||||
|
||||
export const InventoryStockLimitField = () => {
|
||||
const { getCheckboxControlProps } = useFormContext< Product >();
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4>{ __( 'Restrictions', 'woocommerce' ) }</h4>
|
||||
<CheckboxControl
|
||||
label={ __(
|
||||
'Limit purchases to 1 item per order',
|
||||
'woocommerce'
|
||||
) }
|
||||
{ ...getCheckboxControlProps(
|
||||
'sold_individually',
|
||||
getCheckboxTracks( 'sold_individually' )
|
||||
) }
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -2,11 +2,11 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import { useFormContext, Link } from '@woocommerce/components';
|
||||
import { TextControl } from '@wordpress/components';
|
||||
import { getAdminLink } from '@woocommerce/settings';
|
||||
import interpolateComponents from '@automattic/interpolate-components';
|
||||
import { Link, useFormContext } from '@woocommerce/components';
|
||||
import { Product } from '@woocommerce/data';
|
||||
import interpolateComponents from '@automattic/interpolate-components';
|
||||
import { getAdminLink } from '@woocommerce/settings';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
|
||||
/**
|
||||
|
@ -14,7 +14,7 @@ import { recordEvent } from '@woocommerce/tracks';
|
|||
*/
|
||||
import { getAdminSetting } from '~/utils/admin-settings';
|
||||
|
||||
export const ManageStockSection: React.FC = () => {
|
||||
export const InventoryStockManageField = () => {
|
||||
const { getInputProps } = useFormContext< Product >();
|
||||
const notifyLowStockAmount = getAdminSetting( 'notifyLowStockAmount', 2 );
|
||||
|
|
@ -2,11 +2,11 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { RadioControl } from '@wordpress/components';
|
||||
import { useFormContext } from '@woocommerce/components';
|
||||
import { RadioControl } from '@wordpress/components';
|
||||
import { Product } from '@woocommerce/data';
|
||||
|
||||
export const ManualStockSection: React.FC = () => {
|
||||
export const InventoryStockManualField = () => {
|
||||
const { getInputProps } = useFormContext< Product >();
|
||||
const inputProps = getInputProps( 'stock_status' );
|
||||
// These properties cause issues with the RadioControl component.
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useFormContext } from '@woocommerce/components';
|
||||
import { RadioControl } from '@wordpress/components';
|
||||
import { Product } from '@woocommerce/data';
|
||||
|
||||
export const InventoryStockOutField = () => {
|
||||
const { getInputProps } = useFormContext< Product >();
|
||||
|
||||
const backordersProp = getInputProps( 'backorders' );
|
||||
// These properties cause issues with the RadioControl component.
|
||||
// A fix to form upstream would help if we can identify what type of input is used.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
delete backordersProp.checked;
|
||||
delete backordersProp.value;
|
||||
|
||||
return (
|
||||
<RadioControl
|
||||
label={ __( 'When out of stock', 'woocommerce' ) }
|
||||
options={ [
|
||||
{
|
||||
label: __( 'Allow purchases', 'woocommerce' ),
|
||||
value: 'yes',
|
||||
},
|
||||
{
|
||||
label: __(
|
||||
'Allow purchases, but notify customers',
|
||||
'woocommerce'
|
||||
),
|
||||
value: 'notify',
|
||||
},
|
||||
{
|
||||
label: __( "Don't allow purchases", 'woocommerce' ),
|
||||
value: 'no',
|
||||
},
|
||||
] }
|
||||
{ ...backordersProp }
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
useFormContext,
|
||||
__experimentalConditionalWrapper as ConditionalWrapper,
|
||||
} from '@woocommerce/components';
|
||||
import { Tooltip, ToggleControl } from '@wordpress/components';
|
||||
import { Product } from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { getAdminSetting } from '~/utils/admin-settings';
|
||||
import { getCheckboxTracks } from '../../sections/utils';
|
||||
|
||||
export const InventoryTrackQuantityField = () => {
|
||||
const { getCheckboxControlProps } = useFormContext< Product >();
|
||||
|
||||
const canManageStock = getAdminSetting( 'manageStock', 'yes' ) === 'yes';
|
||||
|
||||
return (
|
||||
<ConditionalWrapper
|
||||
condition={ ! canManageStock }
|
||||
wrapper={ ( children: JSX.Element ) => (
|
||||
<Tooltip
|
||||
text={ __(
|
||||
'Quantity tracking is disabled for all products. Go to global store settings to change it.',
|
||||
'woocommerce'
|
||||
) }
|
||||
position="top center"
|
||||
>
|
||||
<div className="woocommerce-product-form__tooltip-disabled-overlay">
|
||||
{ children }
|
||||
</div>
|
||||
</Tooltip>
|
||||
) }
|
||||
>
|
||||
<ToggleControl
|
||||
label={ __( 'Track quantity for this product', 'woocommerce' ) }
|
||||
{ ...getCheckboxControlProps(
|
||||
'manage_stock',
|
||||
getCheckboxTracks( 'manage_stock' )
|
||||
) }
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore This prop does exist, but is not typed in @wordpress/components.
|
||||
disabled={ ! canManageStock }
|
||||
/>
|
||||
</ConditionalWrapper>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,154 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
__experimentalWooProductSectionItem as WooProductSectionItem,
|
||||
__experimentalWooProductFieldItem as WooProductFieldItem,
|
||||
__experimentalProductSectionLayout as ProductSectionLayout,
|
||||
Link,
|
||||
useFormContext,
|
||||
CollapsibleContent,
|
||||
} from '@woocommerce/components';
|
||||
import { Card, CardBody } from '@wordpress/components';
|
||||
import { registerPlugin } from '@wordpress/plugins';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { getAdminLink } from '@woocommerce/settings';
|
||||
import { Product } from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
InventorySkuField,
|
||||
InventoryTrackQuantityField,
|
||||
InventoryStockManualField,
|
||||
InventoryStockManageField,
|
||||
InventoryStockLimitField,
|
||||
InventoryStockOutField,
|
||||
} from './index';
|
||||
import {
|
||||
INVENTORY_SECTION_ID,
|
||||
INVENTORY_SECTION_ADVANCED_ID,
|
||||
TAB_INVENTORY_ID,
|
||||
PLUGIN_ID,
|
||||
} from '../constants';
|
||||
|
||||
const InventorySection = () => {
|
||||
const { values } = useFormContext< Product >();
|
||||
|
||||
return (
|
||||
<>
|
||||
<WooProductSectionItem
|
||||
id={ INVENTORY_SECTION_ID }
|
||||
tabs={ [ { name: TAB_INVENTORY_ID, order: 1 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
>
|
||||
<ProductSectionLayout
|
||||
title={ __( 'Inventory', 'woocommerce' ) }
|
||||
description={
|
||||
<>
|
||||
<span>
|
||||
{ __(
|
||||
'Set up and manage inventory for this product, including status and available quantity.',
|
||||
'woocommerce'
|
||||
) }
|
||||
</span>
|
||||
<Link
|
||||
href={ getAdminLink(
|
||||
'admin.php?page=wc-settings&tab=products§ion=inventory'
|
||||
) }
|
||||
target="_blank"
|
||||
type="wp-admin"
|
||||
onClick={ () => {
|
||||
recordEvent( 'add_product_inventory_help' );
|
||||
} }
|
||||
className="woocommerce-form-section__header-link"
|
||||
>
|
||||
{ __(
|
||||
'Manage global inventory settings',
|
||||
'woocommerce'
|
||||
) }
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Card>
|
||||
<CardBody>
|
||||
<WooProductFieldItem.Slot
|
||||
section={ INVENTORY_SECTION_ID }
|
||||
/>
|
||||
<CollapsibleContent
|
||||
toggleText={ __( 'Advanced', 'woocommerce' ) }
|
||||
>
|
||||
<WooProductFieldItem.Slot
|
||||
section={ INVENTORY_SECTION_ADVANCED_ID }
|
||||
/>
|
||||
</CollapsibleContent>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</ProductSectionLayout>
|
||||
</WooProductSectionItem>
|
||||
<WooProductFieldItem
|
||||
id="inventory/sku"
|
||||
sections={ [ { name: INVENTORY_SECTION_ID, order: 1 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
>
|
||||
<InventorySkuField />
|
||||
</WooProductFieldItem>
|
||||
<WooProductFieldItem
|
||||
id="inventory/track-quantity"
|
||||
sections={ [ { name: INVENTORY_SECTION_ID, order: 3 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
>
|
||||
<InventoryTrackQuantityField />
|
||||
</WooProductFieldItem>
|
||||
|
||||
{ values.manage_stock ? (
|
||||
<WooProductFieldItem
|
||||
id="inventory/stock-manage"
|
||||
sections={ [ { name: INVENTORY_SECTION_ID, order: 5 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
>
|
||||
<InventoryStockManageField />
|
||||
</WooProductFieldItem>
|
||||
) : (
|
||||
<WooProductFieldItem
|
||||
id="inventory/stock-manual"
|
||||
sections={ [ { name: INVENTORY_SECTION_ID, order: 5 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
>
|
||||
<InventoryStockManualField />
|
||||
</WooProductFieldItem>
|
||||
) }
|
||||
|
||||
{ values.manage_stock && (
|
||||
<WooProductFieldItem
|
||||
id="inventory/advanced/stock-out"
|
||||
sections={ [
|
||||
{ name: INVENTORY_SECTION_ADVANCED_ID, order: 1 },
|
||||
] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
>
|
||||
<InventoryStockOutField />
|
||||
</WooProductFieldItem>
|
||||
) }
|
||||
|
||||
<WooProductFieldItem
|
||||
id="inventory/advanced/stock-limit"
|
||||
sections={ [
|
||||
{ name: INVENTORY_SECTION_ADVANCED_ID, order: 3 },
|
||||
] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
>
|
||||
<InventoryStockLimitField />
|
||||
</WooProductFieldItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
registerPlugin( 'wc-admin-product-editor-inventory-section', {
|
||||
// @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated.
|
||||
scope: 'woocommerce-product-editor',
|
||||
render: () => <InventorySection />,
|
||||
} );
|
|
@ -0,0 +1,4 @@
|
|||
export * from './pricing-field-list';
|
||||
export * from './pricing-field-sale';
|
||||
export * from './pricing-field-taxes-charge';
|
||||
export * from './pricing-field-taxes-class';
|
|
@ -0,0 +1,116 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useFormContext, Link } from '@woocommerce/components';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { useContext } from '@wordpress/element';
|
||||
import { Product, SETTINGS_STORE_NAME } from '@woocommerce/data';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import interpolateComponents from '@automattic/interpolate-components';
|
||||
import {
|
||||
BaseControl,
|
||||
// @ts-expect-error `__experimentalInputControl` does exist.
|
||||
__experimentalInputControl as InputControl,
|
||||
} from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CurrencyInputProps } from './pricing-section-fills';
|
||||
import { formatCurrencyDisplayValue } from '../../sections/utils';
|
||||
import { CurrencyContext } from '../../../lib/currency-context';
|
||||
import { ADMIN_URL } from '~/utils/admin-settings';
|
||||
|
||||
type PricingListFieldProps = {
|
||||
currencyInputProps: CurrencyInputProps;
|
||||
};
|
||||
|
||||
export const PricingListField: React.FC< PricingListFieldProps > = ( {
|
||||
currencyInputProps,
|
||||
} ) => {
|
||||
const { getInputProps } = useFormContext< Product >();
|
||||
const context = useContext( CurrencyContext );
|
||||
const { getCurrencyConfig, formatAmount } = context;
|
||||
const currencyConfig = getCurrencyConfig();
|
||||
|
||||
const { isResolving: isTaxSettingsResolving, taxSettings } = useSelect(
|
||||
( select ) => {
|
||||
const { getSettings, hasFinishedResolution } =
|
||||
select( SETTINGS_STORE_NAME );
|
||||
return {
|
||||
isResolving: ! hasFinishedResolution( 'getSettings', [
|
||||
'tax',
|
||||
] ),
|
||||
taxSettings: getSettings( 'tax' ).tax || {},
|
||||
taxesEnabled:
|
||||
getSettings( 'general' )?.general
|
||||
?.woocommerce_calc_taxes === 'yes',
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const regularPriceProps = getInputProps(
|
||||
'regular_price',
|
||||
currencyInputProps
|
||||
);
|
||||
|
||||
const taxIncludedInPriceText = __(
|
||||
'Per your {{link}}store settings{{/link}}, tax is {{strong}}included{{/strong}} in the price.',
|
||||
'woocommerce'
|
||||
);
|
||||
const taxNotIncludedInPriceText = __(
|
||||
'Per your {{link}}store settings{{/link}}, tax is {{strong}}not included{{/strong}} in the price.',
|
||||
'woocommerce'
|
||||
);
|
||||
const pricesIncludeTax =
|
||||
taxSettings.woocommerce_prices_include_tax === 'yes';
|
||||
|
||||
const taxSettingsElement = interpolateComponents( {
|
||||
mixedString: pricesIncludeTax
|
||||
? taxIncludedInPriceText
|
||||
: taxNotIncludedInPriceText,
|
||||
components: {
|
||||
link: (
|
||||
<Link
|
||||
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=tax` }
|
||||
target="_blank"
|
||||
type="external"
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'product_pricing_list_price_help_tax_settings_click'
|
||||
);
|
||||
} }
|
||||
>
|
||||
<></>
|
||||
</Link>
|
||||
),
|
||||
strong: <strong />,
|
||||
},
|
||||
} );
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseControl
|
||||
id="product_pricing_regular_price"
|
||||
help={ regularPriceProps?.help ?? '' }
|
||||
>
|
||||
<InputControl
|
||||
{ ...regularPriceProps }
|
||||
name="regular_price"
|
||||
label={ __( 'List price', 'woocommerce' ) }
|
||||
value={ formatCurrencyDisplayValue(
|
||||
String( regularPriceProps?.value ),
|
||||
currencyConfig,
|
||||
formatAmount
|
||||
) }
|
||||
/>
|
||||
</BaseControl>
|
||||
{ ! isTaxSettingsResolving && (
|
||||
<span className="woocommerce-product-form__secondary-text">
|
||||
{ taxSettingsElement }
|
||||
</span>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,213 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
useFormContext,
|
||||
Link,
|
||||
__experimentalTooltip as Tooltip,
|
||||
DateTimePickerControl,
|
||||
} from '@woocommerce/components';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { useContext, useState, useEffect } from '@wordpress/element';
|
||||
import { Product, OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import interpolateComponents from '@automattic/interpolate-components';
|
||||
import { format as formatDate } from '@wordpress/date';
|
||||
import moment from 'moment';
|
||||
import {
|
||||
BaseControl,
|
||||
// @ts-expect-error `__experimentalInputControl` does exist.
|
||||
__experimentalInputControl as InputControl,
|
||||
ToggleControl,
|
||||
} from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { CurrencyInputProps } from './pricing-section-fills';
|
||||
import { formatCurrencyDisplayValue } from '../../sections/utils';
|
||||
import { CurrencyContext } from '../../../lib/currency-context';
|
||||
|
||||
type PricingListFieldProps = {
|
||||
currencyInputProps: CurrencyInputProps;
|
||||
};
|
||||
|
||||
const PRODUCT_SCHEDULED_SALE_SLUG = 'product-scheduled-sale';
|
||||
|
||||
export const PricingSaleField: React.FC< PricingListFieldProps > = ( {
|
||||
currencyInputProps,
|
||||
} ) => {
|
||||
const { getInputProps, values, setValues } = useFormContext< Product >();
|
||||
|
||||
const { dateFormat, timeFormat } = useSelect( ( select ) => {
|
||||
const { getOption } = select( OPTIONS_STORE_NAME );
|
||||
return {
|
||||
dateFormat: ( getOption( 'date_format' ) as string ) || 'F j, Y',
|
||||
timeFormat: ( getOption( 'time_format' ) as string ) || 'H:i',
|
||||
};
|
||||
} );
|
||||
|
||||
const context = useContext( CurrencyContext );
|
||||
const { getCurrencyConfig, formatAmount } = context;
|
||||
const currencyConfig = getCurrencyConfig();
|
||||
|
||||
const [ showSaleSchedule, setShowSaleSchedule ] = useState( false );
|
||||
const [ userToggledSaleSchedule, setUserToggledSaleSchedule ] =
|
||||
useState( false );
|
||||
const [ autoToggledSaleSchedule, setAutoToggledSaleSchedule ] =
|
||||
useState( false );
|
||||
|
||||
useEffect( () => {
|
||||
if ( userToggledSaleSchedule || autoToggledSaleSchedule ) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasDateOnSaleFrom =
|
||||
typeof values.date_on_sale_from_gmt === 'string' &&
|
||||
values.date_on_sale_from_gmt.length > 0;
|
||||
const hasDateOnSaleTo =
|
||||
typeof values.date_on_sale_to_gmt === 'string' &&
|
||||
values.date_on_sale_to_gmt.length > 0;
|
||||
|
||||
const hasSaleSchedule = hasDateOnSaleFrom || hasDateOnSaleTo;
|
||||
|
||||
if ( hasSaleSchedule ) {
|
||||
setAutoToggledSaleSchedule( true );
|
||||
setShowSaleSchedule( true );
|
||||
}
|
||||
}, [ userToggledSaleSchedule, autoToggledSaleSchedule, values ] );
|
||||
|
||||
const salePriceProps = getInputProps( 'sale_price', currencyInputProps );
|
||||
|
||||
const dateTimePickerProps = {
|
||||
className: 'woocommerce-product__date-time-picker',
|
||||
isDateOnlyPicker: true,
|
||||
dateTimeFormat: dateFormat,
|
||||
};
|
||||
|
||||
const onSaleScheduleToggleChange = ( value: boolean ) => {
|
||||
recordEvent( 'product_pricing_schedule_sale_toggle_click', {
|
||||
enabled: value,
|
||||
} );
|
||||
|
||||
setUserToggledSaleSchedule( true );
|
||||
setShowSaleSchedule( value );
|
||||
|
||||
if ( value ) {
|
||||
setValues( {
|
||||
date_on_sale_from_gmt: moment().startOf( 'day' ).toISOString(),
|
||||
date_on_sale_to_gmt: null,
|
||||
} as Product );
|
||||
} else {
|
||||
setValues( {
|
||||
date_on_sale_from_gmt: null,
|
||||
date_on_sale_to_gmt: null,
|
||||
} as Product );
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseControl
|
||||
id="product_pricing_sale_price"
|
||||
help={ salePriceProps?.help ?? '' }
|
||||
>
|
||||
<InputControl
|
||||
{ ...salePriceProps }
|
||||
name="sale_price"
|
||||
label={ __( 'Sale price', 'woocommerce' ) }
|
||||
value={ formatCurrencyDisplayValue(
|
||||
String( salePriceProps?.value ),
|
||||
currencyConfig,
|
||||
formatAmount
|
||||
) }
|
||||
/>
|
||||
</BaseControl>
|
||||
|
||||
<ToggleControl
|
||||
label={
|
||||
<>
|
||||
{ __( 'Schedule sale', 'woocommerce' ) }
|
||||
<Tooltip
|
||||
text={ interpolateComponents( {
|
||||
mixedString: __(
|
||||
'The sale will start at the beginning of the "From" date ({{startTime/}}) and expire at the end of the "To" date ({{endTime/}}). {{moreLink/}}',
|
||||
'woocommerce'
|
||||
),
|
||||
components: {
|
||||
startTime: (
|
||||
<span>
|
||||
{ formatDate(
|
||||
timeFormat,
|
||||
moment().startOf( 'day' )
|
||||
) }
|
||||
</span>
|
||||
),
|
||||
endTime: (
|
||||
<span>
|
||||
{ formatDate(
|
||||
timeFormat,
|
||||
moment().endOf( 'day' )
|
||||
) }
|
||||
</span>
|
||||
),
|
||||
moreLink: (
|
||||
<Link
|
||||
href="https://woocommerce.com/document/managing-products/#product-data"
|
||||
target="_blank"
|
||||
type="external"
|
||||
onClick={ () =>
|
||||
recordEvent(
|
||||
'add_product_learn_more',
|
||||
{
|
||||
category:
|
||||
PRODUCT_SCHEDULED_SALE_SLUG,
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
{ __(
|
||||
'Learn more',
|
||||
'woocommerce'
|
||||
) }
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
} ) }
|
||||
/>
|
||||
</>
|
||||
}
|
||||
checked={ showSaleSchedule }
|
||||
onChange={ onSaleScheduleToggleChange }
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore disabled prop exists
|
||||
disabled={ ! ( values.sale_price?.length > 0 ) }
|
||||
/>
|
||||
|
||||
{ showSaleSchedule && (
|
||||
<>
|
||||
<DateTimePickerControl
|
||||
label={ __( 'From', 'woocommerce' ) }
|
||||
placeholder={ __( 'Now', 'woocommerce' ) }
|
||||
timeForDateOnly={ 'start-of-day' }
|
||||
currentDate={ values.date_on_sale_from_gmt }
|
||||
{ ...getInputProps( 'date_on_sale_from_gmt', {
|
||||
...dateTimePickerProps,
|
||||
} ) }
|
||||
/>
|
||||
|
||||
<DateTimePickerControl
|
||||
label={ __( 'To', 'woocommerce' ) }
|
||||
placeholder={ __( 'No end date', 'woocommerce' ) }
|
||||
timeForDateOnly={ 'end-of-day' }
|
||||
currentDate={ values.date_on_sale_to_gmt }
|
||||
{ ...getInputProps( 'date_on_sale_to_gmt', {
|
||||
...dateTimePickerProps,
|
||||
} ) }
|
||||
/>
|
||||
</>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useFormContext } from '@woocommerce/components';
|
||||
import { Product } from '@woocommerce/data';
|
||||
import { RadioControl } from '@wordpress/components';
|
||||
|
||||
export const PricingTaxesChargeField = () => {
|
||||
const { getInputProps } = useFormContext< Product >();
|
||||
|
||||
const taxStatusProps = getInputProps( 'tax_status' );
|
||||
// These properties cause issues with the RadioControl component.
|
||||
// A fix to form upstream would help if we can identify what type of input is used.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
delete taxStatusProps.checked;
|
||||
delete taxStatusProps.value;
|
||||
|
||||
return (
|
||||
<RadioControl
|
||||
{ ...taxStatusProps }
|
||||
label={ __( 'Charge sales tax on', 'woocommerce' ) }
|
||||
options={ [
|
||||
{
|
||||
label: __( 'Product and shipping', 'woocommerce' ),
|
||||
value: 'taxable',
|
||||
},
|
||||
{
|
||||
label: __( 'Only shipping', 'woocommerce' ),
|
||||
value: 'shipping',
|
||||
},
|
||||
{
|
||||
label: __( `Don't charge tax`, 'woocommerce' ),
|
||||
value: 'none',
|
||||
},
|
||||
] }
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,87 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
useFormContext,
|
||||
CollapsibleContent,
|
||||
Link,
|
||||
} from '@woocommerce/components';
|
||||
import {
|
||||
Product,
|
||||
EXPERIMENTAL_TAX_CLASSES_STORE_NAME,
|
||||
TaxClass,
|
||||
} from '@woocommerce/data';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { RadioControl } from '@wordpress/components';
|
||||
import interpolateComponents from '@automattic/interpolate-components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { STANDARD_RATE_TAX_CLASS_SLUG } from '../../constants';
|
||||
|
||||
export const PricingTaxesClassField = () => {
|
||||
const { getInputProps } = useFormContext< Product >();
|
||||
|
||||
const { isResolving: isTaxClassesResolving, taxClasses } = useSelect(
|
||||
( select ) => {
|
||||
const { hasFinishedResolution, getTaxClasses } = select(
|
||||
EXPERIMENTAL_TAX_CLASSES_STORE_NAME
|
||||
);
|
||||
return {
|
||||
isResolving: ! hasFinishedResolution( 'getTaxClasses' ),
|
||||
taxClasses: getTaxClasses< TaxClass[] >(),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const taxClassProps = getInputProps( 'tax_class' );
|
||||
// These properties cause issues with the RadioControl component.
|
||||
// A fix to form upstream would help if we can identify what type of input is used.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
delete taxClassProps.checked;
|
||||
delete taxClassProps.value;
|
||||
|
||||
return (
|
||||
<CollapsibleContent toggleText={ __( 'Advanced', 'woocommerce' ) }>
|
||||
{ ! isTaxClassesResolving && taxClasses.length > 0 && (
|
||||
<RadioControl
|
||||
{ ...taxClassProps }
|
||||
label={
|
||||
<>
|
||||
<span>{ __( 'Tax class', 'woocommerce' ) }</span>
|
||||
<span className="woocommerce-product-form__secondary-text">
|
||||
{ interpolateComponents( {
|
||||
mixedString: __(
|
||||
'Apply a tax rate if this product qualifies for tax reduction or exemption. {{link}}Learn more{{/link}}',
|
||||
'woocommerce'
|
||||
),
|
||||
components: {
|
||||
link: (
|
||||
<Link
|
||||
href="https://woocommerce.com/document/setting-up-taxes-in-woocommerce/#shipping-tax-class"
|
||||
target="_blank"
|
||||
type="external"
|
||||
>
|
||||
<></>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
} ) }
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
options={ taxClasses.map( ( taxClass ) => ( {
|
||||
label: taxClass.name,
|
||||
value:
|
||||
taxClass.slug === STANDARD_RATE_TAX_CLASS_SLUG
|
||||
? ''
|
||||
: taxClass.slug,
|
||||
} ) ) }
|
||||
/>
|
||||
) }
|
||||
</CollapsibleContent>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,182 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
__experimentalWooProductSectionItem as WooProductSectionItem,
|
||||
__experimentalWooProductFieldItem as WooProductFieldItem,
|
||||
__experimentalProductSectionLayout as ProductSectionLayout,
|
||||
Link,
|
||||
useFormContext,
|
||||
} from '@woocommerce/components';
|
||||
import { registerPlugin } from '@wordpress/plugins';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { Product } from '@woocommerce/data';
|
||||
import { useContext } from '@wordpress/element';
|
||||
import { Card, CardBody } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
PricingListField,
|
||||
PricingSaleField,
|
||||
PricingTaxesClassField,
|
||||
PricingTaxesChargeField,
|
||||
} from './index';
|
||||
import { useProductHelper } from '../../use-product-helper';
|
||||
import {
|
||||
PRICING_SECTION_BASIC_ID,
|
||||
PRICING_SECTION_TAXES_ID,
|
||||
TAB_PRICING_ID,
|
||||
PLUGIN_ID,
|
||||
} from '../constants';
|
||||
import { CurrencyContext } from '../../../lib/currency-context';
|
||||
|
||||
import './pricing-section.scss';
|
||||
|
||||
export type CurrencyInputProps = {
|
||||
prefix: string;
|
||||
className: string;
|
||||
sanitize: ( value: Product[ keyof Product ] ) => string;
|
||||
onFocus: ( event: React.FocusEvent< HTMLInputElement > ) => void;
|
||||
onKeyUp: ( event: React.KeyboardEvent< HTMLInputElement > ) => void;
|
||||
};
|
||||
|
||||
const PricingSection = () => {
|
||||
const { setValues, values } = useFormContext< Product >();
|
||||
const { sanitizePrice } = useProductHelper();
|
||||
|
||||
const context = useContext( CurrencyContext );
|
||||
const { getCurrencyConfig } = context;
|
||||
const currencyConfig = getCurrencyConfig();
|
||||
|
||||
const currencyInputProps: CurrencyInputProps = {
|
||||
prefix: currencyConfig.symbol,
|
||||
className: 'half-width-field components-currency-control',
|
||||
sanitize: ( value: Product[ keyof Product ] ) => {
|
||||
return sanitizePrice( String( value ) );
|
||||
},
|
||||
onFocus( event: React.FocusEvent< HTMLInputElement > ) {
|
||||
// In some browsers like safari .select() function inside
|
||||
// the onFocus event doesn't work as expected because it
|
||||
// conflicts with onClick the first time user click the
|
||||
// input. Using setTimeout defers the text selection and
|
||||
// avoid the unexpected behaviour.
|
||||
setTimeout(
|
||||
function deferSelection( element: HTMLInputElement ) {
|
||||
element.select();
|
||||
},
|
||||
0,
|
||||
event.currentTarget
|
||||
);
|
||||
},
|
||||
onKeyUp( event: React.KeyboardEvent< HTMLInputElement > ) {
|
||||
const name = event.currentTarget.name as keyof Pick<
|
||||
Product,
|
||||
'regular_price' | 'sale_price'
|
||||
>;
|
||||
const amount = Number.parseFloat(
|
||||
sanitizePrice( values[ name ] || '0' )
|
||||
);
|
||||
const step = Number( event.currentTarget.step || '1' );
|
||||
if ( event.code === 'ArrowUp' ) {
|
||||
setValues( {
|
||||
[ name ]: String( amount + step ),
|
||||
} as unknown as Product );
|
||||
}
|
||||
if ( event.code === 'ArrowDown' ) {
|
||||
setValues( {
|
||||
[ name ]: String( amount - step ),
|
||||
} as unknown as Product );
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<WooProductSectionItem
|
||||
id={ PRICING_SECTION_BASIC_ID }
|
||||
tabs={ [ { name: TAB_PRICING_ID, order: 1 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
>
|
||||
<ProductSectionLayout
|
||||
title={ __( 'Pricing', 'woocommerce' ) }
|
||||
description={
|
||||
<>
|
||||
<span>
|
||||
{ __(
|
||||
'Set a competitive price, put the product on sale, and manage tax calculations.',
|
||||
'woocommerce'
|
||||
) }
|
||||
</span>
|
||||
<Link
|
||||
className="woocommerce-form-section__header-link"
|
||||
href="https://woocommerce.com/posts/how-to-price-products-strategies-expert-tips/"
|
||||
target="_blank"
|
||||
type="external"
|
||||
onClick={ () => {
|
||||
recordEvent( 'add_product_pricing_help' );
|
||||
} }
|
||||
>
|
||||
{ __(
|
||||
'How to price your product: expert tips',
|
||||
'woocommerce'
|
||||
) }
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Card>
|
||||
<CardBody>
|
||||
<WooProductFieldItem.Slot
|
||||
section={ PRICING_SECTION_BASIC_ID }
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardBody>
|
||||
<WooProductFieldItem.Slot
|
||||
section={ PRICING_SECTION_TAXES_ID }
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</ProductSectionLayout>
|
||||
</WooProductSectionItem>
|
||||
<WooProductFieldItem
|
||||
id="pricing/list"
|
||||
sections={ [ { name: PRICING_SECTION_BASIC_ID, order: 1 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
>
|
||||
<PricingListField currencyInputProps={ currencyInputProps } />
|
||||
</WooProductFieldItem>
|
||||
<WooProductFieldItem
|
||||
id="pricing/sale"
|
||||
sections={ [ { name: PRICING_SECTION_BASIC_ID, order: 3 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
>
|
||||
<PricingSaleField currencyInputProps={ currencyInputProps } />
|
||||
</WooProductFieldItem>
|
||||
<WooProductFieldItem
|
||||
id="pricing/taxes/charge"
|
||||
sections={ [ { name: PRICING_SECTION_TAXES_ID, order: 1 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
>
|
||||
<PricingTaxesChargeField />
|
||||
</WooProductFieldItem>
|
||||
<WooProductFieldItem
|
||||
id="pricing/taxes/class"
|
||||
sections={ [ { name: PRICING_SECTION_TAXES_ID, order: 3 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
>
|
||||
<PricingTaxesClassField />
|
||||
</WooProductFieldItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
registerPlugin( 'wc-admin-product-editor-pricing-section', {
|
||||
// @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated.
|
||||
scope: 'woocommerce-product-editor',
|
||||
render: () => <PricingSection />,
|
||||
} );
|
|
@ -1,7 +1,6 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
__experimentalWooProductFieldItem as WooProductFieldItem,
|
||||
renderField,
|
||||
|
@ -20,9 +19,8 @@ export const Fields: React.FC< { fields: ProductFormField[] } > = ( {
|
|||
<WooProductFieldItem
|
||||
key={ field.properties.name }
|
||||
id={ field.id }
|
||||
section={ field.section }
|
||||
sections={ [ { name: field.section, order: field.order } ] }
|
||||
pluginId={ field.plugin_id }
|
||||
order={ field.order }
|
||||
>
|
||||
<>
|
||||
{ renderField( field.type, {
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Card, CardBody } from '@wordpress/components';
|
||||
import {
|
||||
__experimentalWooProductSectionItem as WooProductSectionItem,
|
||||
__experimentalProductFieldSection as ProductFieldSection,
|
||||
|
@ -18,9 +16,10 @@ export const Sections: React.FC< { sections: ProductFormSection[] } > = ( {
|
|||
<WooProductSectionItem
|
||||
key={ section.id }
|
||||
id={ section.id }
|
||||
location={ section.location }
|
||||
tabs={ [
|
||||
{ name: section.location, order: section.order },
|
||||
] }
|
||||
pluginId={ section.plugin_id }
|
||||
order={ section.order }
|
||||
>
|
||||
<ProductFieldSection
|
||||
id={ section.id }
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { registerPlugin } from '@wordpress/plugins';
|
||||
import {
|
||||
__experimentalWooProductTabItem as WooProductTabItem,
|
||||
__experimentalWooProductSectionItem as WooProductSectionItem,
|
||||
useFormContext,
|
||||
} from '@woocommerce/components';
|
||||
import { Product } from '@woocommerce/data';
|
||||
import { useMemo } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { OptionsSection } from '../sections/options-section';
|
||||
import { ProductVariationsSection } from '../sections/product-variations-section';
|
||||
import {
|
||||
TAB_GENERAL_ID,
|
||||
TAB_SHIPPING_ID,
|
||||
TAB_INVENTORY_ID,
|
||||
TAB_PRICING_ID,
|
||||
} from './constants';
|
||||
|
||||
const Tabs = () => {
|
||||
const { values: product } = useFormContext< Product >();
|
||||
const tabPropData = useMemo(
|
||||
() => ( {
|
||||
general: {
|
||||
name: 'general',
|
||||
title: __( 'General', 'woocommerce' ),
|
||||
},
|
||||
pricing: {
|
||||
name: 'pricing',
|
||||
title: __( 'Pricing', 'woocommerce' ),
|
||||
disabled: !! product?.variations?.length,
|
||||
},
|
||||
inventory: {
|
||||
name: 'inventory',
|
||||
title: __( 'Inventory', 'woocommerce' ),
|
||||
disabled: !! product?.variations?.length,
|
||||
},
|
||||
shipping: {
|
||||
name: 'shipping',
|
||||
title: __( 'Shipping', 'woocommerce' ),
|
||||
disabled: !! product?.variations?.length,
|
||||
},
|
||||
options: {
|
||||
name: 'options',
|
||||
title: __( 'Options', 'woocommerce' ),
|
||||
},
|
||||
} ),
|
||||
[ product.variations ]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<WooProductTabItem
|
||||
id="tab/general"
|
||||
templates={ [ { name: 'tab/general', order: 1 } ] }
|
||||
pluginId="core"
|
||||
tabProps={ tabPropData.general }
|
||||
>
|
||||
<WooProductSectionItem.Slot tab={ TAB_GENERAL_ID } />
|
||||
</WooProductTabItem>
|
||||
<WooProductTabItem
|
||||
id="tab/pricing"
|
||||
templates={ [ { name: 'tab/general', order: 3 } ] }
|
||||
pluginId="core"
|
||||
tabProps={ tabPropData.pricing }
|
||||
>
|
||||
<WooProductSectionItem.Slot tab={ TAB_PRICING_ID } />
|
||||
</WooProductTabItem>
|
||||
<WooProductTabItem
|
||||
id="tab/inventory"
|
||||
templates={ [ { name: 'tab/general', order: 5 } ] }
|
||||
pluginId="core"
|
||||
tabProps={ tabPropData.inventory }
|
||||
>
|
||||
<WooProductSectionItem.Slot tab={ TAB_INVENTORY_ID } />
|
||||
</WooProductTabItem>
|
||||
<WooProductTabItem
|
||||
id="tab/shipping"
|
||||
templates={ [ { name: 'tab/general', order: 7 } ] }
|
||||
pluginId="core"
|
||||
tabProps={ tabPropData.shipping }
|
||||
>
|
||||
<WooProductSectionItem.Slot
|
||||
tab={ TAB_SHIPPING_ID }
|
||||
fillProps={ { product } }
|
||||
/>
|
||||
</WooProductTabItem>
|
||||
{ window.wcAdminFeatures[ 'product-variation-management' ] ? (
|
||||
<WooProductTabItem
|
||||
id="tab/options"
|
||||
templates={ [ { name: 'tab/general', order: 9 } ] }
|
||||
pluginId="core"
|
||||
tabProps={ tabPropData.options }
|
||||
>
|
||||
<>
|
||||
<OptionsSection />
|
||||
<ProductVariationsSection />
|
||||
</>
|
||||
</WooProductTabItem>
|
||||
) : null }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
registerPlugin( 'wc-admin-product-editor-form-tab-fills', {
|
||||
// @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated.
|
||||
scope: 'woocommerce-product-editor',
|
||||
render: () => {
|
||||
return <Tabs />;
|
||||
},
|
||||
} );
|
|
@ -0,0 +1,95 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { registerPlugin } from '@wordpress/plugins';
|
||||
import {
|
||||
__experimentalWooProductTabItem as WooProductTabItem,
|
||||
__experimentalWooProductSectionItem as WooProductSectionItem,
|
||||
} from '@woocommerce/components';
|
||||
import { PartialProduct } from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ProductVariationDetailsSection } from '../sections/product-variation-details-section';
|
||||
import { TAB_INVENTORY_ID, TAB_SHIPPING_ID, TAB_PRICING_ID } from './constants';
|
||||
|
||||
const tabPropData = {
|
||||
general: {
|
||||
name: 'general',
|
||||
title: __( 'General', 'woocommerce' ),
|
||||
},
|
||||
pricing: {
|
||||
name: 'pricing',
|
||||
title: __( 'Pricing', 'woocommerce' ),
|
||||
},
|
||||
inventory: {
|
||||
name: 'inventory',
|
||||
title: __( 'Inventory', 'woocommerce' ),
|
||||
},
|
||||
shipping: {
|
||||
name: 'shipping',
|
||||
title: __( 'Shipping', 'woocommerce' ),
|
||||
},
|
||||
options: {
|
||||
name: 'options',
|
||||
title: __( 'Options', 'woocommerce' ),
|
||||
},
|
||||
};
|
||||
|
||||
const Tabs = () => {
|
||||
return (
|
||||
<>
|
||||
<WooProductTabItem
|
||||
id="tab/general/variation"
|
||||
templates={ [ { name: 'tab/variation', order: 1 } ] }
|
||||
pluginId="core"
|
||||
tabProps={ tabPropData.general }
|
||||
>
|
||||
<ProductVariationDetailsSection />
|
||||
</WooProductTabItem>
|
||||
<WooProductTabItem
|
||||
id="tab/pricing"
|
||||
templates={ [ { name: 'tab/variation', order: 3 } ] }
|
||||
pluginId="core"
|
||||
tabProps={ tabPropData.pricing }
|
||||
>
|
||||
<WooProductSectionItem.Slot tab={ TAB_PRICING_ID } />
|
||||
</WooProductTabItem>
|
||||
<WooProductTabItem
|
||||
id="tab/inventory"
|
||||
templates={ [ { name: 'tab/variation', order: 5 } ] }
|
||||
pluginId="core"
|
||||
tabProps={ tabPropData.inventory }
|
||||
>
|
||||
<WooProductSectionItem.Slot tab={ TAB_INVENTORY_ID } />
|
||||
</WooProductTabItem>
|
||||
<WooProductTabItem
|
||||
id="tab/shipping"
|
||||
templates={ [ { name: 'tab/variation', order: 7 } ] }
|
||||
pluginId="core"
|
||||
tabProps={ tabPropData.shipping }
|
||||
>
|
||||
{ ( { product }: { product: PartialProduct } ) => (
|
||||
<WooProductSectionItem.Slot
|
||||
tab={ TAB_SHIPPING_ID }
|
||||
fillProps={ { product } }
|
||||
/>
|
||||
) }
|
||||
</WooProductTabItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Preloading product form data, as product pages are waiting on this to be resolved.
|
||||
* The above Form component won't get rendered until the getProductForm is resolved.
|
||||
*/
|
||||
registerPlugin( 'wc-admin-product-editor-form-variation-tab-fills', {
|
||||
// @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated.
|
||||
scope: 'woocommerce-product-editor',
|
||||
render: () => {
|
||||
return <Tabs />;
|
||||
},
|
||||
} );
|
|
@ -0,0 +1,6 @@
|
|||
export * from './shipping-field-class';
|
||||
export * from './shipping-field-dimensions-width';
|
||||
export * from './shipping-field-dimensions-length';
|
||||
export * from './shipping-field-dimensions-height';
|
||||
export * from './shipping-field-dimensions-weight';
|
||||
export * from './types';
|
|
@ -0,0 +1,213 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useState } from '@wordpress/element';
|
||||
import { useSelect, useDispatch } from '@wordpress/data';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { Link, useFormContext, Spinner } from '@woocommerce/components';
|
||||
import {
|
||||
EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME,
|
||||
PartialProduct,
|
||||
ProductShippingClass,
|
||||
} from '@woocommerce/data';
|
||||
import interpolateComponents from '@automattic/interpolate-components';
|
||||
import { recordEvent } from '@woocommerce/tracks';
|
||||
import { SelectControl } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
ADD_NEW_SHIPPING_CLASS_OPTION_VALUE,
|
||||
UNCATEGORIZED_CATEGORY_SLUG,
|
||||
} from '../../constants';
|
||||
import { ADMIN_URL } from '~/utils/admin-settings';
|
||||
import { AddNewShippingClassModal } from '../../shared/add-new-shipping-class-modal';
|
||||
import { ProductShippingSectionPropsType } from './index';
|
||||
|
||||
export const DEFAULT_SHIPPING_CLASS_OPTIONS: SelectControl.Option[] = [
|
||||
{ value: '', label: __( 'No shipping class', 'woocommerce' ) },
|
||||
{
|
||||
value: ADD_NEW_SHIPPING_CLASS_OPTION_VALUE,
|
||||
label: __( 'Add new shipping class', 'woocommerce' ),
|
||||
},
|
||||
];
|
||||
|
||||
function mapShippingClassToSelectOption(
|
||||
shippingClasses: ProductShippingClass[]
|
||||
): SelectControl.Option[] {
|
||||
return shippingClasses.map( ( { slug, name } ) => ( {
|
||||
value: slug,
|
||||
label: name,
|
||||
} ) );
|
||||
}
|
||||
|
||||
type ServerErrorResponse = {
|
||||
code: string;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line jsdoc/check-line-alignment
|
||||
/**
|
||||
* This extracts a shipping class from the product categories. Using
|
||||
* the first category different to `Uncategorized` and check if the
|
||||
* category was not added to the shipping class list
|
||||
*
|
||||
* @see https://github.com/woocommerce/woocommerce/issues/34657
|
||||
* @see https://github.com/woocommerce/woocommerce/issues/35037
|
||||
* @param product The product
|
||||
* @param shippingClasses The shipping classes
|
||||
* @return The default shipping class
|
||||
*/
|
||||
function extractDefaultShippingClassFromProduct(
|
||||
product?: PartialProduct,
|
||||
shippingClasses?: ProductShippingClass[]
|
||||
): Partial< ProductShippingClass > | undefined {
|
||||
const category = product?.categories?.find(
|
||||
( { slug } ) => slug !== UNCATEGORIZED_CATEGORY_SLUG
|
||||
);
|
||||
if (
|
||||
category &&
|
||||
! shippingClasses?.some( ( { slug } ) => slug === category.slug )
|
||||
) {
|
||||
return {
|
||||
name: category.name,
|
||||
slug: category.slug,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const ShippingClassField: React.FC<
|
||||
ProductShippingSectionPropsType
|
||||
> = ( { product } ) => {
|
||||
const { getInputProps, getSelectControlProps, setValue } =
|
||||
useFormContext< PartialProduct >();
|
||||
const [ showShippingClassModal, setShowShippingClassModal ] =
|
||||
useState( false );
|
||||
const shippingClassProps = getInputProps( 'shipping_class' );
|
||||
|
||||
const { shippingClasses, hasResolvedShippingClasses } = useSelect(
|
||||
( select ) => {
|
||||
const { getProductShippingClasses, hasFinishedResolution } = select(
|
||||
EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME
|
||||
);
|
||||
return {
|
||||
hasResolvedShippingClasses: hasFinishedResolution(
|
||||
'getProductShippingClasses'
|
||||
),
|
||||
shippingClasses:
|
||||
getProductShippingClasses< ProductShippingClass[] >(),
|
||||
};
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const { createProductShippingClass, invalidateResolution } = useDispatch(
|
||||
EXPERIMENTAL_PRODUCT_SHIPPING_CLASSES_STORE_NAME
|
||||
);
|
||||
const { createErrorNotice } = useDispatch( 'core/notices' );
|
||||
|
||||
function handleShippingClassServerError(
|
||||
error: ServerErrorResponse
|
||||
): Promise< ProductShippingClass > {
|
||||
let message = __(
|
||||
'We couldn’t add this shipping class. Try again in a few seconds.',
|
||||
'woocommerce'
|
||||
);
|
||||
|
||||
if ( error.code === 'term_exists' ) {
|
||||
message = __(
|
||||
'A shipping class with that slug already exists.',
|
||||
'woocommerce'
|
||||
);
|
||||
}
|
||||
|
||||
createErrorNotice( message, {
|
||||
explicitDismiss: true,
|
||||
} );
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{ hasResolvedShippingClasses ? (
|
||||
<>
|
||||
<SelectControl
|
||||
label={ __( 'Shipping class', 'woocommerce' ) }
|
||||
{ ...getSelectControlProps( 'shipping_class', {
|
||||
className: 'half-width-field',
|
||||
} ) }
|
||||
onChange={ ( value: string ) => {
|
||||
if (
|
||||
value === ADD_NEW_SHIPPING_CLASS_OPTION_VALUE
|
||||
) {
|
||||
setShowShippingClassModal( true );
|
||||
return;
|
||||
}
|
||||
shippingClassProps.onChange( value );
|
||||
} }
|
||||
options={ [
|
||||
...DEFAULT_SHIPPING_CLASS_OPTIONS,
|
||||
...mapShippingClassToSelectOption(
|
||||
shippingClasses ?? []
|
||||
),
|
||||
] }
|
||||
/>
|
||||
<span className="woocommerce-product-form__secondary-text">
|
||||
{ interpolateComponents( {
|
||||
mixedString: __(
|
||||
'Manage shipping classes and rates in {{link}}global settings{{/link}}.',
|
||||
'woocommerce'
|
||||
),
|
||||
components: {
|
||||
link: (
|
||||
<Link
|
||||
href={ `${ ADMIN_URL }admin.php?page=wc-settings&tab=shipping§ion=classes` }
|
||||
target="_blank"
|
||||
type="external"
|
||||
onClick={ () => {
|
||||
recordEvent(
|
||||
'product_shipping_global_settings_link_click'
|
||||
);
|
||||
} }
|
||||
>
|
||||
<></>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
} ) }
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<div className="product-shipping-section__spinner-wrapper">
|
||||
<Spinner />
|
||||
</div>
|
||||
) }
|
||||
{ showShippingClassModal && (
|
||||
<AddNewShippingClassModal
|
||||
shippingClass={ extractDefaultShippingClassFromProduct(
|
||||
product,
|
||||
shippingClasses
|
||||
) }
|
||||
onAdd={ ( shippingClassValues ) =>
|
||||
createProductShippingClass<
|
||||
Promise< ProductShippingClass >
|
||||
>( shippingClassValues )
|
||||
.then( ( value ) => {
|
||||
recordEvent(
|
||||
'product_new_shipping_class_modal_add_button_click'
|
||||
);
|
||||
invalidateResolution(
|
||||
'getProductShippingClasses'
|
||||
);
|
||||
setValue( 'shipping_class', value.slug );
|
||||
return value;
|
||||
} )
|
||||
.catch( handleShippingClassServerError )
|
||||
}
|
||||
onCancel={ () => setShowShippingClassModal( false ) }
|
||||
/>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useFormContext } from '@woocommerce/components';
|
||||
import { PartialProduct } from '@woocommerce/data';
|
||||
import {
|
||||
BaseControl,
|
||||
// @ts-expect-error `__experimentalInputControl` does exist.
|
||||
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||
__experimentalInputControl as InputControl,
|
||||
} from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useProductHelper } from '../../use-product-helper';
|
||||
import { getInterpolatedSizeLabel } from './utils';
|
||||
import { ShippingDimensionsPropsType } from './index';
|
||||
|
||||
export const ShippingDimensionsHeightField = ( {
|
||||
dimensionProps,
|
||||
setHighlightSide,
|
||||
}: ShippingDimensionsPropsType ) => {
|
||||
const { getInputProps } = useFormContext< PartialProduct >();
|
||||
const { formatNumber } = useProductHelper();
|
||||
|
||||
const inputHeightProps = getInputProps(
|
||||
'dimensions.height',
|
||||
dimensionProps
|
||||
);
|
||||
return (
|
||||
<BaseControl
|
||||
id="product_shipping_dimensions_height"
|
||||
className={ inputHeightProps.className }
|
||||
help={ inputHeightProps.help }
|
||||
>
|
||||
<InputControl
|
||||
{ ...inputHeightProps }
|
||||
value={ formatNumber( String( inputHeightProps.value ) ) }
|
||||
label={ getInterpolatedSizeLabel(
|
||||
__( 'Height {{span}}C{{/span}}', 'woocommerce' )
|
||||
) }
|
||||
onFocus={ () => {
|
||||
setHighlightSide( 'C' );
|
||||
} }
|
||||
/>
|
||||
</BaseControl>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useFormContext } from '@woocommerce/components';
|
||||
import { PartialProduct } from '@woocommerce/data';
|
||||
import {
|
||||
BaseControl,
|
||||
// @ts-expect-error `__experimentalInputControl` does exist.
|
||||
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||
__experimentalInputControl as InputControl,
|
||||
} from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useProductHelper } from '../../use-product-helper';
|
||||
import { getInterpolatedSizeLabel } from './utils';
|
||||
import { ShippingDimensionsPropsType } from './index';
|
||||
|
||||
export const ShippingDimensionsLengthField = ( {
|
||||
dimensionProps,
|
||||
setHighlightSide,
|
||||
}: ShippingDimensionsPropsType ) => {
|
||||
const { getInputProps } = useFormContext< PartialProduct >();
|
||||
const { formatNumber } = useProductHelper();
|
||||
|
||||
const inputLengthProps = getInputProps(
|
||||
'dimensions.length',
|
||||
dimensionProps
|
||||
);
|
||||
return (
|
||||
<BaseControl
|
||||
id="product_shipping_dimensions_length"
|
||||
className={ inputLengthProps.className }
|
||||
help={ inputLengthProps.help }
|
||||
>
|
||||
<InputControl
|
||||
{ ...inputLengthProps }
|
||||
value={ formatNumber( String( inputLengthProps.value ) ) }
|
||||
label={ getInterpolatedSizeLabel(
|
||||
__( 'Length {{span}}B{{/span}}', 'woocommerce' )
|
||||
) }
|
||||
onFocus={ () => {
|
||||
setHighlightSide( 'B' );
|
||||
} }
|
||||
/>
|
||||
</BaseControl>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useFormContext } from '@woocommerce/components';
|
||||
import { OPTIONS_STORE_NAME, PartialProduct } from '@woocommerce/data';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import {
|
||||
BaseControl,
|
||||
// @ts-expect-error `__experimentalInputControl` does exist.
|
||||
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||
__experimentalInputControl as InputControl,
|
||||
} from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useProductHelper } from '../../use-product-helper';
|
||||
|
||||
export const ShippingDimensionsWeightField = () => {
|
||||
const { getInputProps } = useFormContext< PartialProduct >();
|
||||
const { formatNumber, parseNumber } = useProductHelper();
|
||||
|
||||
const { weightUnit, hasResolvedUnits } = useSelect( ( select ) => {
|
||||
const { getOption, hasFinishedResolution } =
|
||||
select( OPTIONS_STORE_NAME );
|
||||
return {
|
||||
weightUnit: getOption( 'woocommerce_weight_unit' ),
|
||||
hasResolvedUnits: hasFinishedResolution( 'getOption', [
|
||||
'woocommerce_weight_unit',
|
||||
] ),
|
||||
};
|
||||
}, [] );
|
||||
|
||||
if ( ! hasResolvedUnits ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const inputWeightProps = getInputProps( 'weight', {
|
||||
sanitize: ( value: PartialProduct[ keyof PartialProduct ] ) =>
|
||||
parseNumber( String( value ) ),
|
||||
} );
|
||||
|
||||
return (
|
||||
<BaseControl
|
||||
id="product_shipping_weight"
|
||||
className={ inputWeightProps.className }
|
||||
help={ inputWeightProps.help }
|
||||
>
|
||||
<InputControl
|
||||
{ ...inputWeightProps }
|
||||
value={ formatNumber( String( inputWeightProps.value ) ) }
|
||||
label={ __( 'Weight', 'woocommerce' ) }
|
||||
suffix={ weightUnit }
|
||||
/>
|
||||
</BaseControl>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useFormContext } from '@woocommerce/components';
|
||||
import { PartialProduct } from '@woocommerce/data';
|
||||
import {
|
||||
BaseControl,
|
||||
// @ts-expect-error `__experimentalInputControl` does exist.
|
||||
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
|
||||
__experimentalInputControl as InputControl,
|
||||
} from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useProductHelper } from '../../use-product-helper';
|
||||
import { getInterpolatedSizeLabel } from './utils';
|
||||
import { ShippingDimensionsPropsType } from './index';
|
||||
|
||||
export const ShippingDimensionsWidthField = ( {
|
||||
dimensionProps,
|
||||
setHighlightSide,
|
||||
}: ShippingDimensionsPropsType ) => {
|
||||
const { getInputProps } = useFormContext< PartialProduct >();
|
||||
const { formatNumber } = useProductHelper();
|
||||
|
||||
const inputWidthProps = getInputProps( 'dimensions.width', dimensionProps );
|
||||
|
||||
return (
|
||||
<BaseControl
|
||||
id="product_shipping_dimensions_width"
|
||||
className={ inputWidthProps.className }
|
||||
help={ inputWidthProps.help }
|
||||
>
|
||||
<InputControl
|
||||
{ ...inputWidthProps }
|
||||
value={ formatNumber( String( inputWidthProps.value ) ) }
|
||||
label={ getInterpolatedSizeLabel(
|
||||
__( 'Width {{span}}A{{/span}}', 'woocommerce' )
|
||||
) }
|
||||
onFocus={ () => {
|
||||
setHighlightSide( 'A' );
|
||||
} }
|
||||
/>
|
||||
</BaseControl>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,189 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import {
|
||||
__experimentalWooProductSectionItem as WooProductSectionItem,
|
||||
__experimentalWooProductFieldItem as WooProductFieldItem,
|
||||
__experimentalProductSectionLayout as ProductSectionLayout,
|
||||
} from '@woocommerce/components';
|
||||
import { registerPlugin } from '@wordpress/plugins';
|
||||
import { PartialProduct, OPTIONS_STORE_NAME } from '@woocommerce/data';
|
||||
import { useSelect } from '@wordpress/data';
|
||||
import { useState } from '@wordpress/element';
|
||||
import { Card, CardBody } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
ShippingClassField,
|
||||
ShippingDimensionsWidthField,
|
||||
ShippingDimensionsLengthField,
|
||||
ShippingDimensionsHeightField,
|
||||
ShippingDimensionsWeightField,
|
||||
ProductShippingSectionPropsType,
|
||||
DimensionPropsType,
|
||||
ShippingDimensionsPropsType,
|
||||
} from './index';
|
||||
import {
|
||||
PLUGIN_ID,
|
||||
SHIPPING_SECTION_BASIC_ID,
|
||||
SHIPPING_SECTION_DIMENSIONS_ID,
|
||||
TAB_SHIPPING_ID,
|
||||
} from '../constants';
|
||||
import {
|
||||
ShippingDimensionsImage,
|
||||
ShippingDimensionsImageProps,
|
||||
} from '../../fields/shipping-dimensions-image';
|
||||
import { useProductHelper } from '../../use-product-helper';
|
||||
|
||||
import './shipping-section.scss';
|
||||
|
||||
const ShippingSection = () => {
|
||||
const [ highlightSide, setHighlightSide ] =
|
||||
useState< ShippingDimensionsImageProps[ 'highlight' ] >();
|
||||
const { parseNumber } = useProductHelper();
|
||||
|
||||
const { dimensionUnit, hasResolvedUnits } = useSelect( ( select ) => {
|
||||
const { getOption, hasFinishedResolution } =
|
||||
select( OPTIONS_STORE_NAME );
|
||||
return {
|
||||
dimensionUnit: getOption( 'woocommerce_dimension_unit' ),
|
||||
weightUnit: getOption( 'woocommerce_weight_unit' ),
|
||||
hasResolvedUnits:
|
||||
hasFinishedResolution( 'getOption', [
|
||||
'woocommerce_dimension_unit',
|
||||
] ) &&
|
||||
hasFinishedResolution( 'getOption', [
|
||||
'woocommerce_weight_unit',
|
||||
] ),
|
||||
};
|
||||
}, [] );
|
||||
|
||||
const dimensionProps: DimensionPropsType = {
|
||||
onBlur: () => {
|
||||
setHighlightSide( undefined );
|
||||
},
|
||||
sanitize: ( value: PartialProduct[ keyof PartialProduct ] ) =>
|
||||
parseNumber( String( value ) ),
|
||||
suffix: dimensionUnit,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<WooProductSectionItem
|
||||
id={ SHIPPING_SECTION_BASIC_ID }
|
||||
tabs={ [ { name: TAB_SHIPPING_ID, order: 1 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
>
|
||||
<ProductSectionLayout
|
||||
title={ __( 'Shipping', 'woocommerce' ) }
|
||||
description={ __(
|
||||
'Set up shipping costs and enter dimensions used for accurate rate calculations.',
|
||||
'woocommerce'
|
||||
) }
|
||||
>
|
||||
<Card>
|
||||
<CardBody className="product-shipping-section__classes">
|
||||
<WooProductFieldItem.Slot
|
||||
section={ SHIPPING_SECTION_BASIC_ID }
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardBody className="product-shipping-section__dimensions">
|
||||
<h4>{ __( 'Dimensions', 'woocommerce' ) }</h4>
|
||||
<p className="woocommerce-product-form__secondary-text">
|
||||
{ __(
|
||||
`Enter the size of the product as you'd put it in a shipping box, including packaging like bubble wrap.`,
|
||||
'woocommerce'
|
||||
) }
|
||||
</p>
|
||||
<div className="product-shipping-section__dimensions-body">
|
||||
<div className="product-shipping-section__dimensions-body-col">
|
||||
{ hasResolvedUnits && (
|
||||
<WooProductFieldItem.Slot
|
||||
section={
|
||||
SHIPPING_SECTION_DIMENSIONS_ID
|
||||
}
|
||||
fillProps={
|
||||
{
|
||||
setHighlightSide,
|
||||
dimensionProps,
|
||||
} as ShippingDimensionsPropsType
|
||||
}
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
<div className="product-shipping-section__dimensions-body-col">
|
||||
<ShippingDimensionsImage
|
||||
highlight={ highlightSide }
|
||||
className="product-shipping-section__dimensions-image"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</ProductSectionLayout>
|
||||
</WooProductSectionItem>
|
||||
<WooProductFieldItem
|
||||
id="shipping/class"
|
||||
sections={ [ { name: SHIPPING_SECTION_BASIC_ID, order: 1 } ] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
>
|
||||
{ ( { product }: ProductShippingSectionPropsType ) => (
|
||||
<ShippingClassField product={ product } />
|
||||
) }
|
||||
</WooProductFieldItem>
|
||||
<WooProductFieldItem
|
||||
id="shipping/dimensions/width"
|
||||
sections={ [
|
||||
{ name: SHIPPING_SECTION_DIMENSIONS_ID, order: 1 },
|
||||
] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
>
|
||||
{ ( { ...props }: ShippingDimensionsPropsType ) => (
|
||||
<ShippingDimensionsWidthField { ...props } />
|
||||
) }
|
||||
</WooProductFieldItem>
|
||||
<WooProductFieldItem
|
||||
id="shipping/dimensions/length"
|
||||
sections={ [
|
||||
{ name: SHIPPING_SECTION_DIMENSIONS_ID, order: 3 },
|
||||
] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
>
|
||||
{ ( { ...props }: ShippingDimensionsPropsType ) => (
|
||||
<ShippingDimensionsLengthField { ...props } />
|
||||
) }
|
||||
</WooProductFieldItem>
|
||||
<WooProductFieldItem
|
||||
id="shipping/dimensions/height"
|
||||
sections={ [
|
||||
{ name: SHIPPING_SECTION_DIMENSIONS_ID, order: 5 },
|
||||
] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
>
|
||||
{ ( { ...props }: ShippingDimensionsPropsType ) => (
|
||||
<ShippingDimensionsHeightField { ...props } />
|
||||
) }
|
||||
</WooProductFieldItem>
|
||||
<WooProductFieldItem
|
||||
id="shipping/dimensions/weight"
|
||||
sections={ [
|
||||
{ name: SHIPPING_SECTION_DIMENSIONS_ID, order: 7 },
|
||||
] }
|
||||
pluginId={ PLUGIN_ID }
|
||||
>
|
||||
<ShippingDimensionsWeightField />
|
||||
</WooProductFieldItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
registerPlugin( 'wc-admin-product-editor-shipping-section', {
|
||||
// @ts-expect-error 'scope' does exist. @types/wordpress__plugins is outdated.
|
||||
scope: 'woocommerce-product-editor',
|
||||
render: () => <ShippingSection />,
|
||||
} );
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { PartialProduct } from '@woocommerce/data';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ShippingDimensionsImageProps } from '../../fields/shipping-dimensions-image';
|
||||
|
||||
export type ProductShippingSectionPropsType = {
|
||||
product?: PartialProduct;
|
||||
};
|
||||
|
||||
export type DimensionPropsType = {
|
||||
onBlur: () => void;
|
||||
sanitize: ( value: PartialProduct[ keyof PartialProduct ] ) => string;
|
||||
suffix: unknown;
|
||||
};
|
||||
|
||||
export type ShippingDimensionsPropsType = {
|
||||
dimensionProps: DimensionPropsType;
|
||||
setHighlightSide: (
|
||||
side: ShippingDimensionsImageProps[ 'highlight' ]
|
||||
) => void;
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import interpolateComponents from '@automattic/interpolate-components';
|
||||
|
||||
export const getInterpolatedSizeLabel = ( mixedString: string ) => {
|
||||
return interpolateComponents( {
|
||||
mixedString,
|
||||
components: {
|
||||
span: <span className="woocommerce-product-form__secondary-text" />,
|
||||
},
|
||||
} );
|
||||
};
|
|
@ -2,11 +2,13 @@
|
|||
* Internal dependencies
|
||||
*/
|
||||
import { ProductMVPCESFooter } from '~/customer-effort-score-tracks/product-mvp-ces-footer';
|
||||
import { ProductMVPFeedbackModalContainer } from '~/customer-effort-score-tracks/product-mvp-feedback-modal-container';
|
||||
|
||||
export const ProductFormFooter: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<ProductMVPCESFooter />
|
||||
<ProductMVPFeedbackModalContainer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -30,7 +30,8 @@ $product-form-tabs-height: 56px;
|
|||
left: $admin-menu-width;
|
||||
width: calc(100% - $admin-menu-width);
|
||||
background: $white;
|
||||
z-index: 1001;
|
||||
// This z-index is directly below the header of 1001.
|
||||
z-index: 1000;
|
||||
border-bottom: 1px solid $gray-400;
|
||||
border-top: 1px solid $gray-400;
|
||||
padding: 0 var(--large-gap) 0 var(--large-gap);
|
||||
|
@ -81,6 +82,16 @@ $product-form-tabs-height: 56px;
|
|||
}
|
||||
}
|
||||
|
||||
.woocommerce-admin-product-layout .woocommerce-layout__header {
|
||||
min-height: $header-height + $product-form-tabs-height;
|
||||
.woocommerce-admin-product-layout {
|
||||
.woocommerce-layout__primary {
|
||||
padding-top: $product-form-tabs-height;
|
||||
}
|
||||
|
||||
.woocommerce-layout__header.is-scrolled {
|
||||
box-shadow: none;
|
||||
|
||||
~.woocommerce-layout__primary .product-form-layout .components-tab-panel__tabs {
|
||||
box-shadow: $header-scroll-shadow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue