Merge branch 'trunk' into fix/wp-l2-retrieval
This commit is contained in:
commit
08a6eb6c17
|
@ -29,32 +29,19 @@ jobs:
|
|||
outputs:
|
||||
freeze: ${{ steps.check-freeze.outputs.freeze }}
|
||||
steps:
|
||||
- name: 'Install PHP'
|
||||
uses: shivammathur/setup-php@8e2ac35f639d3e794c1da1f28999385ab6fdf0fc
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
php-version: '7.4'
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install prerequisites
|
||||
run: |
|
||||
npm install -g pnpm@7
|
||||
pnpm install --filter monorepo-utils
|
||||
|
||||
- name: 'Check whether today is the code freeze day'
|
||||
id: check-freeze
|
||||
shell: php {0}
|
||||
run: |
|
||||
<?php
|
||||
$now = time();
|
||||
if ( getenv( 'TIME_OVERRIDE' ) ) {
|
||||
$now = strtotime( getenv( 'TIME_OVERRIDE' ) );
|
||||
}
|
||||
|
||||
// Code freeze comes 22 days prior to release day.
|
||||
$release_time = strtotime( '+22 days', $now );
|
||||
$release_day_of_week = date( 'l', $release_time );
|
||||
$release_day_of_month = (int) date( 'j', $release_time );
|
||||
|
||||
// If 22 days from now isn't the second Tuesday, then it's not code freeze day.
|
||||
if ( 'Tuesday' !== $release_day_of_week || $release_day_of_month < 8 || $release_day_of_month > 14 ) {
|
||||
file_put_contents( getenv( 'GITHUB_OUTPUT' ), "freeze=1\n", FILE_APPEND );
|
||||
} else {
|
||||
file_put_contents( getenv( 'GITHUB_OUTPUT' ), "freeze=0\n", FILE_APPEND );
|
||||
}
|
||||
run: pnpm utils code-freeze verify-day -g -o $TIME_OVERRIDE
|
||||
|
||||
maybe-create-next-milestone-and-release-branch:
|
||||
name: 'Maybe create next milestone and release branch'
|
||||
|
@ -63,12 +50,16 @@ jobs:
|
|||
contents: write
|
||||
issues: write
|
||||
needs: verify-code-freeze
|
||||
if: needs.verify-code-freeze.outputs.freeze == 0
|
||||
if: needs.verify-code-freeze.outputs.freeze == 'true'
|
||||
outputs:
|
||||
branch: ${{ steps.freeze.outputs.branch }}
|
||||
release_version: ${{ steps.freeze.outputs.release_version }}
|
||||
next_version: ${{ steps.freeze.outputs.next_version }}
|
||||
steps:
|
||||
- name: 'Install PHP'
|
||||
uses: shivammathur/setup-php@8e2ac35f639d3e794c1da1f28999385ab6fdf0fc
|
||||
with:
|
||||
php-version: '7.4'
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
|
|
|
@ -11,7 +11,7 @@ To get up and running within the WooCommerce Monorepo, you will need to make sur
|
|||
### Prerequisites
|
||||
|
||||
* [NVM](https://github.com/nvm-sh/nvm#installing-and-updating): While you can always install Node through other means, we recommend using NVM to ensure you're aligned with the version used by our development teams. Our repository contains [an `.nvmrc` file](.nvmrc) which helps ensure you are using the correct version of Node.
|
||||
* [PNPM](https://pnpm.io/installation): Our repository utilizes PNPM to manage project dependencies and run various scripts involved in building and testing projects.
|
||||
* [PNPM](https://pnpm.io/installation): Our repository utilizes PNPM to manage project dependencies and run various scripts involved in building and testing projects. Until the repository tooling has been updated to support PNPM 8, install PNPM 7 like so: `npm install pnpm@7 --global`
|
||||
* [PHP 7.2+](https://www.php.net/manual/en/install.php): WooCommerce Core currently features a minimum PHP version of 7.2. It is also needed to run Composer and various project build scripts.
|
||||
* [Composer](https://getcomposer.org/doc/00-intro.md): We use Composer to manage all of the dependencies for PHP packages and plugins.
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
"url": "https://github.com/woocommerce/woocommerce/issues"
|
||||
},
|
||||
"bin": {
|
||||
"utils": "./tools/monorepo-utils/dist/index.js"
|
||||
"utils": "./tools/monorepo-utils/bin/run"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm exec turbo run turbo:build",
|
||||
|
@ -30,7 +30,7 @@
|
|||
"create-extension": "node ./tools/create-extension/index.js",
|
||||
"cherry-pick": "node ./tools/cherry-pick/bin/run",
|
||||
"sync-dependencies": "pnpm exec syncpack -- fix-mismatches",
|
||||
"utils": "./tools/monorepo-utils/dist/index.js"
|
||||
"utils": "./tools/monorepo-utils/bin/run"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
|
|
|
@ -7,6 +7,11 @@ import { useSelect } from '@wordpress/data';
|
|||
import { getVisibleTasks, ONBOARDING_STORE_NAME } from '@woocommerce/data';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import sanitizeHTML from '../../lib/sanitize-html';
|
||||
|
||||
export type DefaultProgressTitleProps = {
|
||||
taskListId: string;
|
||||
};
|
||||
|
@ -64,6 +69,9 @@ export const DefaultProgressTitle: React.FC< DefaultProgressTitleProps > = ( {
|
|||
}
|
||||
|
||||
return (
|
||||
<h1 className="woocommerce-task-progress-header__title">{ title }</h1>
|
||||
<h1
|
||||
className="woocommerce-task-progress-header__title"
|
||||
dangerouslySetInnerHTML={ sanitizeHTML( title ) }
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,7 +7,10 @@ import { recordEvent } from '@woocommerce/tracks';
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { waitUntilElementIsPresent } from './utils';
|
||||
import {
|
||||
attachEventListenerToParentForChildren,
|
||||
waitUntilElementIsPresent,
|
||||
} from './utils';
|
||||
|
||||
/**
|
||||
* Get the product data.
|
||||
|
@ -244,9 +247,9 @@ const getDataForProductTabClickEvent = ( tabName: string ) => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Initializes the product tabs Tracks events.
|
||||
* Attaches the product tabs Tracks events.
|
||||
*/
|
||||
const initProductTabsTracks = () => {
|
||||
const attachProductTabsTracks = () => {
|
||||
const tabs = document.querySelectorAll( '.product_data_tabs > li' );
|
||||
|
||||
tabs.forEach( ( tab ) => {
|
||||
|
@ -262,9 +265,9 @@ const initProductTabsTracks = () => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Initializes the inventory tab Tracks events.
|
||||
* Attaches the inventory tab Tracks events.
|
||||
*/
|
||||
const initInventoryTabTracks = () => {
|
||||
const attachProductInventoryTabTracks = () => {
|
||||
document
|
||||
.querySelector( '#_manage_stock' )
|
||||
?.addEventListener( 'click', ( event ) => {
|
||||
|
@ -274,7 +277,7 @@ const initInventoryTabTracks = () => {
|
|||
} );
|
||||
|
||||
document
|
||||
.querySelector( '#_manage_stock_disabled' )
|
||||
.querySelector( '#_manage_stock_disabled > a' )
|
||||
?.addEventListener( 'click', () => {
|
||||
recordEvent(
|
||||
'product_manage_stock_disabled_store_settings_link_click'
|
||||
|
@ -291,10 +294,225 @@ const initInventoryTabTracks = () => {
|
|||
};
|
||||
|
||||
/**
|
||||
* Initialize all product screen tracks.
|
||||
* Attaches product tags tracks.
|
||||
*/
|
||||
const attachProductTagsTracks = () => {
|
||||
function deleteTagEventListener(/* event: Event */) {
|
||||
recordEvent( 'product_tags_delete', {
|
||||
page: 'product',
|
||||
tag_list_size:
|
||||
document.querySelector( '.tagchecklist' )?.children.length || 0,
|
||||
} );
|
||||
}
|
||||
|
||||
export const initProductScreenTracks = () => {
|
||||
function addTagsDeleteTracks() {
|
||||
const tagsDeleteButtons = document.querySelectorAll(
|
||||
'#product_tag .ntdelbutton'
|
||||
);
|
||||
tagsDeleteButtons.forEach( ( button ) => {
|
||||
button.removeEventListener( 'click', deleteTagEventListener );
|
||||
button.addEventListener( 'click', deleteTagEventListener );
|
||||
} );
|
||||
}
|
||||
waitUntilElementIsPresent(
|
||||
'#product_tag .tagchecklist',
|
||||
addTagsDeleteTracks
|
||||
);
|
||||
|
||||
document
|
||||
.querySelector( '.tagadd' )
|
||||
?.addEventListener( 'click', (/* event: Event */) => {
|
||||
const tagInput = document.querySelector< HTMLInputElement >(
|
||||
'#new-tag-product_tag'
|
||||
);
|
||||
if ( tagInput && tagInput.value && tagInput.value.length > 0 ) {
|
||||
recordEvent( 'product_tags_add', {
|
||||
page: 'product',
|
||||
tag_string_length: tagInput.value.length,
|
||||
tag_list_size:
|
||||
( document.querySelector( '.tagchecklist' )?.children
|
||||
.length || 0 ) + 1,
|
||||
most_used: false,
|
||||
} );
|
||||
setTimeout( () => {
|
||||
addTagsDeleteTracks();
|
||||
}, 500 );
|
||||
}
|
||||
} );
|
||||
|
||||
function addMostUsedTagEventListener( event: Event ) {
|
||||
recordEvent( 'product_tags_add', {
|
||||
page: 'product',
|
||||
tag_string_length: ( event.target as HTMLAnchorElement ).textContent
|
||||
?.length,
|
||||
tag_list_size:
|
||||
document.querySelector( '.tagchecklist' )?.children.length || 0,
|
||||
most_used: true,
|
||||
} );
|
||||
addTagsDeleteTracks();
|
||||
}
|
||||
|
||||
function addMostUsedTagsTracks() {
|
||||
const tagCloudLinks = document.querySelectorAll(
|
||||
'#tagcloud-product_tag .tag-cloud-link'
|
||||
);
|
||||
tagCloudLinks.forEach( ( button ) => {
|
||||
button.removeEventListener( 'click', addMostUsedTagEventListener );
|
||||
button.addEventListener( 'click', addMostUsedTagEventListener );
|
||||
} );
|
||||
}
|
||||
|
||||
document
|
||||
.querySelector( '.tagcloud-link' )
|
||||
?.addEventListener( 'click', () => {
|
||||
waitUntilElementIsPresent(
|
||||
'#tagcloud-product_tag',
|
||||
addMostUsedTagsTracks
|
||||
);
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches attributes tracks.
|
||||
*/
|
||||
const attachAttributesTracks = () => {
|
||||
function addNewTermEventHandler() {
|
||||
recordEvent( 'product_attributes_add_term', {
|
||||
page: 'product',
|
||||
} );
|
||||
}
|
||||
|
||||
function addNewAttributeTermTracks() {
|
||||
const addNewTermButtons = document.querySelectorAll(
|
||||
'.woocommerce_attribute .add_new_attribute'
|
||||
);
|
||||
addNewTermButtons.forEach( ( button ) => {
|
||||
button.removeEventListener( 'click', addNewTermEventHandler );
|
||||
button.addEventListener( 'click', addNewTermEventHandler );
|
||||
} );
|
||||
}
|
||||
addNewAttributeTermTracks();
|
||||
|
||||
document
|
||||
.querySelector( '.add_attribute' )
|
||||
?.addEventListener( 'click', () => {
|
||||
setTimeout( () => {
|
||||
addNewAttributeTermTracks();
|
||||
}, 1000 );
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches product attributes tracks.
|
||||
*/
|
||||
const attachProductAttributesTracks = () => {
|
||||
const attributesCount = document.querySelectorAll(
|
||||
'.woocommerce_attribute'
|
||||
).length;
|
||||
|
||||
document
|
||||
.querySelector( '.save_attributes' )
|
||||
?.addEventListener( 'click', ( event ) => {
|
||||
if (
|
||||
event.target instanceof Element &&
|
||||
event.target.classList.contains( 'disabled' )
|
||||
) {
|
||||
// skip in case the button is disabled
|
||||
return;
|
||||
}
|
||||
const newAttributesCount = document.querySelectorAll(
|
||||
'.woocommerce_attribute'
|
||||
).length;
|
||||
if ( newAttributesCount > attributesCount ) {
|
||||
const local_attributes = [
|
||||
...document.querySelectorAll(
|
||||
'.woocommerce_attribute:not(.pa_glbattr)'
|
||||
),
|
||||
].map( ( attr ) => {
|
||||
const terms =
|
||||
(
|
||||
attr.querySelector(
|
||||
"[name^='attribute_values']"
|
||||
) as HTMLTextAreaElement
|
||||
)?.value.split( '|' ).length ?? 0;
|
||||
return {
|
||||
name: (
|
||||
attr.querySelector(
|
||||
'[name^="attribute_names"]'
|
||||
) as HTMLInputElement
|
||||
)?.value,
|
||||
terms,
|
||||
};
|
||||
} );
|
||||
recordEvent( 'product_attributes_add', {
|
||||
page: 'product',
|
||||
enable_archive: '',
|
||||
default_sort_order: '',
|
||||
local_attributes,
|
||||
} );
|
||||
}
|
||||
} );
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches product variations tracks.
|
||||
*/
|
||||
const attachProductVariationsTracks = () => {
|
||||
document
|
||||
.querySelector(
|
||||
'#variable_product_options_inner .variations-add-attributes-link'
|
||||
)
|
||||
?.addEventListener( 'click', () => {
|
||||
recordEvent( 'product_variations_empty_state', {
|
||||
action: 'add_attribute_link',
|
||||
} );
|
||||
} );
|
||||
|
||||
document
|
||||
.querySelector(
|
||||
'#variable_product_options_inner .variations-learn-more-link'
|
||||
)
|
||||
?.addEventListener( 'click', () => {
|
||||
recordEvent( 'product_variations_empty_state', {
|
||||
action: 'learn_more_link',
|
||||
} );
|
||||
} );
|
||||
|
||||
const variationsSection = '#variable_product_options';
|
||||
|
||||
// We attach the events in this way because the buttons are added dynamically.
|
||||
attachEventListenerToParentForChildren( variationsSection, [
|
||||
{
|
||||
eventName: 'click',
|
||||
childQuery: '.add_variation_manually',
|
||||
callback: () => {
|
||||
recordEvent( 'product_variations_buttons', {
|
||||
action: 'add_variation_manually',
|
||||
} );
|
||||
},
|
||||
},
|
||||
{
|
||||
eventName: 'change',
|
||||
childQuery: '#field_to_edit',
|
||||
callback: () => {
|
||||
const selectElement = document.querySelector(
|
||||
'#field_to_edit'
|
||||
) as HTMLSelectElement;
|
||||
// Get the index of the selected option
|
||||
const selectedIndex = selectElement.selectedIndex;
|
||||
recordEvent( 'product_variations_buttons', {
|
||||
action: 'bulk_actions',
|
||||
selected: selectElement.options[ selectedIndex ]?.value,
|
||||
} );
|
||||
},
|
||||
},
|
||||
] );
|
||||
};
|
||||
|
||||
/**
|
||||
* Attaches general product screen tracks.
|
||||
*/
|
||||
const attachProductScreenTracks = () => {
|
||||
const initialPublishingData = getPublishingWidgetData();
|
||||
|
||||
document
|
||||
|
@ -395,156 +613,6 @@ export const initProductScreenTracks = () => {
|
|||
} );
|
||||
} );
|
||||
|
||||
// Product tags
|
||||
|
||||
function deleteTagEventListener(/* event: Event */) {
|
||||
recordEvent( 'product_tags_delete', {
|
||||
page: 'product',
|
||||
tag_list_size:
|
||||
document.querySelector( '.tagchecklist' )?.children.length || 0,
|
||||
} );
|
||||
}
|
||||
|
||||
function addTagsDeleteTracks() {
|
||||
const tagsDeleteButtons = document.querySelectorAll(
|
||||
'#product_tag .ntdelbutton'
|
||||
);
|
||||
tagsDeleteButtons.forEach( ( button ) => {
|
||||
button.removeEventListener( 'click', deleteTagEventListener );
|
||||
button.addEventListener( 'click', deleteTagEventListener );
|
||||
} );
|
||||
}
|
||||
waitUntilElementIsPresent(
|
||||
'#product_tag .tagchecklist',
|
||||
addTagsDeleteTracks
|
||||
);
|
||||
|
||||
document
|
||||
.querySelector( '.tagadd' )
|
||||
?.addEventListener( 'click', (/* event: Event */) => {
|
||||
const tagInput = document.querySelector< HTMLInputElement >(
|
||||
'#new-tag-product_tag'
|
||||
);
|
||||
if ( tagInput && tagInput.value && tagInput.value.length > 0 ) {
|
||||
recordEvent( 'product_tags_add', {
|
||||
page: 'product',
|
||||
tag_string_length: tagInput.value.length,
|
||||
tag_list_size:
|
||||
( document.querySelector( '.tagchecklist' )?.children
|
||||
.length || 0 ) + 1,
|
||||
most_used: false,
|
||||
} );
|
||||
setTimeout( () => {
|
||||
addTagsDeleteTracks();
|
||||
}, 500 );
|
||||
}
|
||||
} );
|
||||
|
||||
function addMostUsedTagEventListener( event: Event ) {
|
||||
recordEvent( 'product_tags_add', {
|
||||
page: 'product',
|
||||
tag_string_length: ( event.target as HTMLAnchorElement ).textContent
|
||||
?.length,
|
||||
tag_list_size:
|
||||
document.querySelector( '.tagchecklist' )?.children.length || 0,
|
||||
most_used: true,
|
||||
} );
|
||||
addTagsDeleteTracks();
|
||||
}
|
||||
|
||||
function addMostUsedTagsTracks() {
|
||||
const tagCloudLinks = document.querySelectorAll(
|
||||
'#tagcloud-product_tag .tag-cloud-link'
|
||||
);
|
||||
tagCloudLinks.forEach( ( button ) => {
|
||||
button.removeEventListener( 'click', addMostUsedTagEventListener );
|
||||
button.addEventListener( 'click', addMostUsedTagEventListener );
|
||||
} );
|
||||
}
|
||||
|
||||
document
|
||||
.querySelector( '.tagcloud-link' )
|
||||
?.addEventListener( 'click', () => {
|
||||
waitUntilElementIsPresent(
|
||||
'#tagcloud-product_tag',
|
||||
addMostUsedTagsTracks
|
||||
);
|
||||
} );
|
||||
|
||||
// Attribute tracks.
|
||||
|
||||
function addNewTermEventHandler() {
|
||||
recordEvent( 'product_attributes_add_term', {
|
||||
page: 'product',
|
||||
} );
|
||||
}
|
||||
|
||||
function addNewAttributeTermTracks() {
|
||||
const addNewTermButtons = document.querySelectorAll(
|
||||
'.woocommerce_attribute .add_new_attribute'
|
||||
);
|
||||
addNewTermButtons.forEach( ( button ) => {
|
||||
button.removeEventListener( 'click', addNewTermEventHandler );
|
||||
button.addEventListener( 'click', addNewTermEventHandler );
|
||||
} );
|
||||
}
|
||||
addNewAttributeTermTracks();
|
||||
|
||||
document
|
||||
.querySelector( '.add_attribute' )
|
||||
?.addEventListener( 'click', () => {
|
||||
setTimeout( () => {
|
||||
addNewAttributeTermTracks();
|
||||
}, 1000 );
|
||||
} );
|
||||
|
||||
const attributesCount = document.querySelectorAll(
|
||||
'.woocommerce_attribute'
|
||||
).length;
|
||||
|
||||
document
|
||||
.querySelector( '.save_attributes' )
|
||||
?.addEventListener( 'click', ( event ) => {
|
||||
if (
|
||||
event.target instanceof Element &&
|
||||
event.target.classList.contains( 'disabled' )
|
||||
) {
|
||||
// skip in case the button is disabled
|
||||
return;
|
||||
}
|
||||
const newAttributesCount = document.querySelectorAll(
|
||||
'.woocommerce_attribute'
|
||||
).length;
|
||||
if ( newAttributesCount > attributesCount ) {
|
||||
const local_attributes = [
|
||||
...document.querySelectorAll(
|
||||
'.woocommerce_attribute:not(.pa_glbattr)'
|
||||
),
|
||||
].map( ( attr ) => {
|
||||
const terms =
|
||||
(
|
||||
attr.querySelector(
|
||||
"[name^='attribute_values']"
|
||||
) as HTMLTextAreaElement
|
||||
)?.value.split( '|' ).length ?? 0;
|
||||
return {
|
||||
name: (
|
||||
attr.querySelector(
|
||||
'[name^="attribute_names"]'
|
||||
) as HTMLInputElement
|
||||
)?.value,
|
||||
terms,
|
||||
};
|
||||
} );
|
||||
recordEvent( 'product_attributes_add', {
|
||||
page: 'product',
|
||||
enable_archive: '',
|
||||
default_sort_order: '',
|
||||
local_attributes,
|
||||
} );
|
||||
}
|
||||
} );
|
||||
|
||||
document
|
||||
.querySelector(
|
||||
'#woocommerce-product-updated-message-view-product__link'
|
||||
|
@ -563,9 +631,19 @@ export const initProductScreenTracks = () => {
|
|||
recordEvent( 'product_view_product_dismiss', getProductData() );
|
||||
} );
|
||||
} );
|
||||
};
|
||||
|
||||
initProductTabsTracks();
|
||||
initInventoryTabTracks();
|
||||
/**
|
||||
* Initialize all product screen tracks.
|
||||
*/
|
||||
export const initProductScreenTracks = () => {
|
||||
attachAttributesTracks();
|
||||
attachProductScreenTracks();
|
||||
attachProductTagsTracks();
|
||||
attachProductAttributesTracks();
|
||||
attachProductVariationsTracks();
|
||||
attachProductTabsTracks();
|
||||
attachProductInventoryTabTracks();
|
||||
};
|
||||
|
||||
export function addExitPageListener( pageId: string ) {
|
||||
|
|
|
@ -1,3 +1,39 @@
|
|||
/**
|
||||
* Attaches a click event listener to a parent element and calls a callback when one of the child elements,
|
||||
* specified by the list of queries, is clicked. This allows handling events for child elements that may not
|
||||
* exist in the DOM when the event listener is added.
|
||||
*
|
||||
* @param {string} parentQuery query of the parent element.
|
||||
* @param {Array<Object>} children array of event, child query and callback pairs.
|
||||
*/
|
||||
export function attachEventListenerToParentForChildren(
|
||||
parentQuery: string,
|
||||
children: Array< {
|
||||
eventName: 'click' | 'change';
|
||||
childQuery: string;
|
||||
callback: () => void;
|
||||
} >
|
||||
) {
|
||||
const parent = document.querySelector( parentQuery );
|
||||
|
||||
if ( ! parent ) return;
|
||||
|
||||
const eventListener = ( event: Event ) => {
|
||||
children.forEach( ( { eventName, childQuery, callback } ) => {
|
||||
if (
|
||||
event.type === eventName &&
|
||||
( event.target as Element ).matches( childQuery )
|
||||
) {
|
||||
callback();
|
||||
}
|
||||
} );
|
||||
};
|
||||
|
||||
children.forEach( ( { eventName } ) => {
|
||||
parent.addEventListener( eventName, eventListener );
|
||||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive function that waits up to 3 seconds until an element is found, then calls the callback.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add tracks events to variations tab
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Item controls for attribute creation are always visible
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Dev - Allow to filter wc_help_tip
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Fixed the attributes table styling in TT2 tabs content area
|
|
@ -0,0 +1,4 @@
|
|||
Significance: patch
|
||||
Type: fix
|
||||
|
||||
Fix special characters not rendered in admin titles
|
|
@ -1,4 +0,0 @@
|
|||
Significance: minor
|
||||
Type: update
|
||||
|
||||
Support min_php_version and min_wp_version for the free extensions feed
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
Reset variable product tour after running e2e tests.
|
|
@ -0,0 +1,5 @@
|
|||
Significance: patch
|
||||
Type: tweak
|
||||
Comment: e2e test, not in release package
|
||||
|
||||
|
|
@ -1130,6 +1130,7 @@ $default-line-height: 18px;
|
|||
}
|
||||
.toolbar-top {
|
||||
.button,
|
||||
.attribute_taxonomy,
|
||||
.select2-container {
|
||||
margin: 1px;
|
||||
}
|
||||
|
@ -5620,6 +5621,7 @@ img.help_tip {
|
|||
a.delete,
|
||||
a.edit {
|
||||
float: right;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
a.delete {
|
||||
|
@ -5655,7 +5657,7 @@ img.help_tip {
|
|||
|
||||
.handlediv {
|
||||
background-position: 6px 5px !important;
|
||||
visibility: hidden;
|
||||
margin: 4px 0 -1px !important;
|
||||
height: 26px;
|
||||
}
|
||||
|
||||
|
@ -5671,7 +5673,6 @@ img.help_tip {
|
|||
|
||||
a.delete,
|
||||
a.edit,
|
||||
.handlediv,
|
||||
.sort {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
@ -5684,14 +5685,6 @@ img.help_tip {
|
|||
}
|
||||
}
|
||||
|
||||
h3:hover,
|
||||
&.ui-sortable-helper {
|
||||
a.delete,
|
||||
.handlediv {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
|
|
@ -594,9 +594,29 @@ ul.wc-tabs {
|
|||
font-size: var(--wp--preset--font-size--small);
|
||||
margin-left: 1em;
|
||||
|
||||
h2 {
|
||||
// Hide repeated heading.
|
||||
h2:first-of-type {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Attributes table styles.
|
||||
table.woocommerce-product-attributes {
|
||||
tbody {
|
||||
|
||||
td, th {
|
||||
padding: 0.2rem 0.2rem 0.2rem 0;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1083,6 +1083,10 @@ jQuery( function ( $ ) {
|
|||
} else {
|
||||
wc_meta_boxes_product_variations_ajax.unblock();
|
||||
}
|
||||
|
||||
window.wcTracks.recordEvent( 'product_variations_buttons', {
|
||||
action: 'remove_variation',
|
||||
} );
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -1150,6 +1154,10 @@ jQuery( function ( $ ) {
|
|||
}
|
||||
}
|
||||
);
|
||||
|
||||
window.wcTracks.recordEvent( 'product_variations_buttons', {
|
||||
action: 'generate_variations',
|
||||
} );
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
@ -5,9 +5,9 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
?>
|
||||
<div data-taxonomy="<?php echo esc_attr( $attribute->get_taxonomy() ); ?>" class="woocommerce_attribute wc-metabox postbox closed <?php echo esc_attr( implode( ' ', $metabox_class ) ); ?>" rel="<?php echo esc_attr( $attribute->get_position() ); ?>">
|
||||
<h3>
|
||||
<a href="#" class="remove_row delete"><?php esc_html_e( 'Remove', 'woocommerce' ); ?></a>
|
||||
<div class="handlediv" title="<?php esc_attr_e( 'Click to toggle', 'woocommerce' ); ?>"></div>
|
||||
<div class="tips sort" data-tip="<?php esc_attr_e( 'Drag and drop to set admin attribute order', 'woocommerce' ); ?>"></div>
|
||||
<a href="#" class="remove_row delete"><?php esc_html_e( 'Remove', 'woocommerce' ); ?></a>
|
||||
<strong class="attribute_name<?php echo esc_attr( $attribute->get_name() === '' ? ' placeholder' : '' ); ?>"><?php echo esc_html( $attribute->get_name() !== '' ? wc_attribute_label( $attribute->get_name() ) : __( 'Custom attribute', 'woocommerce' ) ); ?></strong>
|
||||
</h3>
|
||||
<div class="woocommerce_attribute_data wc-metabox-content hidden">
|
||||
|
|
|
@ -26,7 +26,7 @@ $arrow_img_url = WC_ADMIN_IMAGES_FOLDER_URL . '/product_data/no-variati
|
|||
echo wp_kses_post(
|
||||
sprintf(
|
||||
/* translators: %1$s: url for attributes tab, %2$s: url for variable product documentation */
|
||||
__( 'Add some attributes in the <a href="%1$s">Attributes</a> tab to generate variations. Make sure to check the <b>Used for variations</b> box. <a href="%2$s" target="_blank" rel="noreferrer">Learn more</a>', 'woocommerce' ),
|
||||
__( 'Add some attributes in the <a class="variations-add-attributes-link" href="%1$s">Attributes</a> tab to generate variations. Make sure to check the <b>Used for variations</b> box. <a class="variations-learn-more-link" href="%2$s" target="_blank" rel="noreferrer">Learn more</a>', 'woocommerce' ),
|
||||
esc_url( '#product_attributes' ),
|
||||
esc_url( 'https://woocommerce.com/document/variable-product/' )
|
||||
)
|
||||
|
|
|
@ -1593,12 +1593,24 @@ function wc_back_link( $label, $url ) {
|
|||
*/
|
||||
function wc_help_tip( $tip, $allow_html = false ) {
|
||||
if ( $allow_html ) {
|
||||
$tip = wc_sanitize_tooltip( $tip );
|
||||
$sanitized_tip = wc_sanitize_tooltip( $tip );
|
||||
} else {
|
||||
$tip = esc_attr( $tip );
|
||||
$sanitized_tip = esc_attr( $tip );
|
||||
}
|
||||
|
||||
return '<span class="woocommerce-help-tip" data-tip="' . $tip . '"></span>';
|
||||
/**
|
||||
* Filter the help tip.
|
||||
*
|
||||
* @since 7.7.0
|
||||
*
|
||||
* @param string $tip_html Help tip HTML.
|
||||
* @param string $sanitized_tip Sanitized help tip text.
|
||||
* @param string $tip Original help tip text.
|
||||
* @param bool $allow_html Allow sanitized HTML if true or escape.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
return apply_filters( 'wc_help_tip', '<span class="woocommerce-help-tip" data-tip="' . $sanitized_tip . '"></span>', $sanitized_tip, $tip, $allow_html );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -79,7 +79,6 @@ class DefaultFreeExtensions {
|
|||
public static function get_plugin( $slug ) {
|
||||
$plugins = array(
|
||||
'google-listings-and-ads' => [
|
||||
'min_php_version' => '7.4',
|
||||
'name' => __( 'Google Listings & Ads', 'woocommerce' ),
|
||||
'description' => sprintf(
|
||||
/* translators: 1: opening product link tag. 2: closing link tag */
|
||||
|
@ -131,7 +130,6 @@ class DefaultFreeExtensions {
|
|||
'image_url' => plugins_url( '/assets/images/onboarding/pinterest.png', WC_PLUGIN_FILE ),
|
||||
'manage_url' => 'admin.php?page=wc-admin&path=%2Fpinterest%2Flanding',
|
||||
'is_built_by_wc' => true,
|
||||
'min_php_version' => '7.3',
|
||||
],
|
||||
'pinterest-for-woocommerce:alt' => [
|
||||
'name' => __( 'Pinterest for WooCommerce', 'woocommerce' ),
|
||||
|
@ -351,7 +349,6 @@ class DefaultFreeExtensions {
|
|||
DefaultPaymentGateways::get_rules_for_cbd( false ),
|
||||
],
|
||||
'is_built_by_wc' => true,
|
||||
'min_wp_version' => '5.9',
|
||||
],
|
||||
'woocommerce-services:shipping' => [
|
||||
'description' => sprintf(
|
||||
|
@ -519,7 +516,6 @@ class DefaultFreeExtensions {
|
|||
],
|
||||
],
|
||||
'is_built_by_wc' => false,
|
||||
'min_wp_version' => '6.0',
|
||||
],
|
||||
'mailpoet' => [
|
||||
'name' => __( 'MailPoet', 'woocommerce' ),
|
||||
|
|
|
@ -21,7 +21,6 @@ class EvaluateExtension {
|
|||
* @return object The evaluated extension.
|
||||
*/
|
||||
public static function evaluate( $extension ) {
|
||||
global $wp_version;
|
||||
$rule_evaluator = new RuleEvaluator();
|
||||
|
||||
if ( isset( $extension->is_visible ) ) {
|
||||
|
@ -31,14 +30,6 @@ class EvaluateExtension {
|
|||
$extension->is_visible = true;
|
||||
}
|
||||
|
||||
if ( isset( $extension->min_php_version ) ) {
|
||||
$extension->is_visible = version_compare( PHP_VERSION, $extension->min_php_version, '>=' );
|
||||
}
|
||||
|
||||
if ( isset( $extension->min_wp_version ) ) {
|
||||
$extension->is_visible = version_compare( $wp_version, $extension->min_wp_version, '>=' );
|
||||
}
|
||||
|
||||
$installed_plugins = PluginsHelper::get_installed_plugin_slugs();
|
||||
$activated_plugins = PluginsHelper::get_active_plugin_slugs();
|
||||
$extension->is_installed = in_array( explode( ':', $extension->key )[0], $installed_plugins, true );
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
const { test, expect } = require( '@playwright/test' );
|
||||
const wcApi = require( '@woocommerce/woocommerce-rest-api' ).default;
|
||||
|
||||
const productPageURL = 'wp-admin/post-new.php?post_type=product';
|
||||
|
||||
const variableProductName = 'Variable Product with Three Variations';
|
||||
const manualVariableProduct = 'Manual Variable Product';
|
||||
const variationOnePrice = '9.99';
|
||||
|
@ -14,10 +16,7 @@ const defaultAttributes = [ 'val2', 'val1', 'val2' ];
|
|||
const stockAmount = '100';
|
||||
const lowStockAmount = '10';
|
||||
|
||||
test.describe( 'Add New Variable Product Page', () => {
|
||||
test.use( { storageState: process.env.ADMINSTATE } );
|
||||
|
||||
test.afterAll( async ( { baseURL } ) => {
|
||||
async function deleteProductsAddedByTests( baseURL ) {
|
||||
const api = new wcApi( {
|
||||
url: baseURL,
|
||||
consumerKey: process.env.CONSUMER_KEY,
|
||||
|
@ -38,11 +37,47 @@ test.describe( 'Add New Variable Product Page', () => {
|
|||
.concat( manualProducts.map( ( { id } ) => id ) );
|
||||
|
||||
await api.post( 'products/batch', { delete: ids } );
|
||||
}
|
||||
|
||||
async function resetVariableProductTour( baseURL, browser ) {
|
||||
// Go to the product page, so that the `window.wp.data` module is available
|
||||
const page = await browser.newPage( { baseURL: baseURL } );
|
||||
await page.goto( productPageURL );
|
||||
|
||||
// Get the current user's ID and user preferences
|
||||
const { id: userId, woocommerce_meta } = await page.evaluate( () => {
|
||||
return window.wp.data.select( 'core' ).getCurrentUser();
|
||||
} );
|
||||
|
||||
// Reset the variable product tour preference, so that it will be shown again
|
||||
const updatedWooCommerceMeta = {
|
||||
...woocommerce_meta,
|
||||
variable_product_tour_shown: '',
|
||||
};
|
||||
|
||||
// Save the updated user preferences
|
||||
await page.evaluate(
|
||||
async ( { userId, updatedWooCommerceMeta } ) => {
|
||||
await window.wp.data.dispatch( 'core' ).saveUser( {
|
||||
id: userId,
|
||||
woocommerce_meta: updatedWooCommerceMeta,
|
||||
} );
|
||||
},
|
||||
{ userId, updatedWooCommerceMeta }
|
||||
);
|
||||
}
|
||||
|
||||
test.describe( 'Add New Variable Product Page', () => {
|
||||
test.use( { storageState: process.env.ADMINSTATE } );
|
||||
|
||||
test.afterAll( async ( { baseURL, browser } ) => {
|
||||
await deleteProductsAddedByTests( baseURL );
|
||||
await resetVariableProductTour( baseURL, browser );
|
||||
} );
|
||||
|
||||
test( 'shows the variable product tour', async ( { page } ) => {
|
||||
await page.goto( 'wp-admin/post-new.php?post_type=product' );
|
||||
await page.selectOption( '#product-type', 'variable', { force: true } );
|
||||
await page.selectOption( '#product-type', 'variable' );
|
||||
|
||||
// because of the way that the tour is dynamically positioned,
|
||||
// Playwright can't automatically scroll the button into view,
|
||||
|
@ -53,10 +88,12 @@ test.describe( 'Add New Variable Product Page', () => {
|
|||
.getByRole( 'link', { name: 'Attributes' } )
|
||||
.scrollIntoViewIfNeeded();
|
||||
|
||||
// the tour only seems to display when not running headless, so just make sure
|
||||
if ( await page.locator( '.components-card-header' ).nth(1).isVisible() ) {
|
||||
// dismiss the variable product tour
|
||||
await page
|
||||
.getByRole( 'button', { name: 'Got it' } )
|
||||
.click( { force: true } );
|
||||
.getByRole( 'button', { name: 'Close Tour' } )
|
||||
.click();
|
||||
|
||||
// wait for the tour's dismissal to be saved
|
||||
await page.waitForResponse(
|
||||
|
@ -64,21 +101,25 @@ test.describe( 'Add New Variable Product Page', () => {
|
|||
response.url().includes( '/users/' ) &&
|
||||
response.status() === 200
|
||||
);
|
||||
|
||||
}
|
||||
} );
|
||||
|
||||
test( 'can create product, attributes and variations, edit variations and delete variations', async ( {
|
||||
page,
|
||||
} ) => {
|
||||
await page.goto( 'wp-admin/post-new.php?post_type=product' );
|
||||
await page.goto( productPageURL );
|
||||
await page.fill( '#title', variableProductName );
|
||||
await page.selectOption( '#product-type', 'variable', { force: true } );
|
||||
await page.selectOption( '#product-type', 'variable' );
|
||||
|
||||
await page.click( 'a[href="#product_attributes"]' );
|
||||
|
||||
// add 3 attributes
|
||||
for ( let i = 0; i < 3; i++ ) {
|
||||
if ( i > 0 ) {
|
||||
await page.click( 'button.add_attribute' );
|
||||
await page.getByRole( 'button', { name: 'Add' } )
|
||||
.nth(2)
|
||||
.click();
|
||||
}
|
||||
await page.waitForSelector(
|
||||
`input[name="attribute_names[${ i }]"]`
|
||||
|
@ -93,11 +134,13 @@ test.describe( 'Add New Variable Product Page', () => {
|
|||
.first()
|
||||
.type( 'val1 | val2' );
|
||||
}
|
||||
await page.click( 'text=Save attributes' );
|
||||
// wait for the attributes to be saved
|
||||
|
||||
await page.getByRole( 'button', { name: 'Save attributes'} ).click( { clickCount: 3 });
|
||||
|
||||
// wait for the tour's dismissal to be saved
|
||||
await page.waitForResponse(
|
||||
( response ) =>
|
||||
response.url().includes( '/post.php?post=' ) &&
|
||||
response.url().includes( '/post.php' ) &&
|
||||
response.status() === 200
|
||||
);
|
||||
|
||||
|
@ -107,14 +150,15 @@ test.describe( 'Add New Variable Product Page', () => {
|
|||
page.getByText( 'Product draft updated. ' )
|
||||
).toBeVisible();
|
||||
|
||||
page.on( 'dialog', ( dialog ) => dialog.accept() );
|
||||
|
||||
// manually create variations from all attributes
|
||||
await page.click( 'a[href="#variable_product_options"]' );
|
||||
|
||||
// event listener for handling the link_all_variations confirmation dialog
|
||||
page.on( 'dialog', ( dialog ) => dialog.accept() );
|
||||
|
||||
// generate variations from all attributes
|
||||
await page.click( 'button.generate_variations' );
|
||||
|
||||
// add variation attributes
|
||||
// verify variations have the correct attribute values
|
||||
for ( let i = 0; i < 8; i++ ) {
|
||||
const val1 = 'val1';
|
||||
const val2 = 'val2';
|
||||
|
@ -205,7 +249,7 @@ test.describe( 'Add New Variable Product Page', () => {
|
|||
test( 'can manually add a variation, manage stock levels, set variation defaults and remove a variation', async ( {
|
||||
page,
|
||||
} ) => {
|
||||
await page.goto( 'wp-admin/post-new.php?post_type=product' );
|
||||
await page.goto( productPageURL );
|
||||
await page.fill( '#title', manualVariableProduct );
|
||||
await page.selectOption( '#product-type', 'variable', { force: true } );
|
||||
await page.click( 'a[href="#product_attributes"]' );
|
||||
|
|
517
pnpm-lock.yaml
517
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
|||
dist/
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const { program } = require( '../dist/index' );
|
||||
|
||||
program.parse( process.argv );
|
|
@ -6,10 +6,12 @@
|
|||
"homepage": "https://github.com/woocommerce/woocommerce",
|
||||
"license": "GPLv2",
|
||||
"repository": "woocommerce/woocommerce",
|
||||
"main": "dist/index.js",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.0",
|
||||
"@commander-js/extra-typings": "^0.1.0",
|
||||
"chalk": "^4.1.2",
|
||||
"commander": "^9.4.0",
|
||||
"@commander-js/extra-typings": "^0.1.0",
|
||||
"dotenv": "^10.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -2,11 +2,18 @@
|
|||
* External dependencies
|
||||
*/
|
||||
import { Command } from '@commander-js/extra-typings';
|
||||
import chalk from 'chalk';
|
||||
import { setOutput } from '@actions/core';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { verifyDay } from '../utils/index';
|
||||
import {
|
||||
isTodayCodeFreezeDay,
|
||||
DAYS_BETWEEN_CODE_FREEZE_AND_RELEASE,
|
||||
getToday,
|
||||
getFutureDate,
|
||||
} from '../utils/index';
|
||||
|
||||
export const verifyDayCommand = new Command( 'verify-day' )
|
||||
.description( 'Verify if today is the code freeze day' )
|
||||
|
@ -14,8 +21,40 @@ export const verifyDayCommand = new Command( 'verify-day' )
|
|||
'-o, --override <override>',
|
||||
"Time Override: The time to use in checking whether the action should run (default: 'now')."
|
||||
)
|
||||
.action( () => {
|
||||
console.log( verifyDay() );
|
||||
.option(
|
||||
'-g --github',
|
||||
'CLI command is used in the Github Actions context.'
|
||||
)
|
||||
.action( ( { override, github } ) => {
|
||||
const today = getToday( override );
|
||||
const futureDate = getFutureDate( today );
|
||||
console.log(
|
||||
chalk.yellow( "Today's timestamp UTC is: " + today.toUTCString() )
|
||||
);
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`Checking to see if ${ DAYS_BETWEEN_CODE_FREEZE_AND_RELEASE } days from today is the second Tuesday of the month.`
|
||||
)
|
||||
);
|
||||
const isCodeFreezeDay = isTodayCodeFreezeDay( override );
|
||||
console.log(
|
||||
chalk.green(
|
||||
`${ futureDate.toUTCString() } ${
|
||||
isCodeFreezeDay ? 'is' : 'is not'
|
||||
} release day.`
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
chalk.green(
|
||||
`Today is ${
|
||||
isCodeFreezeDay ? 'indeed' : 'not'
|
||||
} code freeze day.`
|
||||
)
|
||||
);
|
||||
|
||||
if ( github ) {
|
||||
setOutput( 'freeze', isCodeFreezeDay.toString() );
|
||||
}
|
||||
|
||||
process.exit( 0 );
|
||||
} );
|
||||
|
|
|
@ -1,10 +1,36 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { verifyDay } from '../index';
|
||||
import { isTodayCodeFreezeDay } from '../index';
|
||||
|
||||
describe( 'verifyDay', () => {
|
||||
it( 'should return a string', () => {
|
||||
expect( verifyDay() ).toBe( 'Today is a good day to code freeze!' );
|
||||
describe( 'isTodayCodeFreezeDay', () => {
|
||||
it( 'should return false when given a day not 22 days before release', () => {
|
||||
const JUNE_5_2023 = '2023-06-05T00:00:00.000Z';
|
||||
const JUNE_12_2023 = '2023-06-12T00:00:00.000Z';
|
||||
const JUNE_26_2023 = '2023-06-26T00:00:00.000Z';
|
||||
const AUG_10_2023 = '2023-08-10T00:00:00.000Z';
|
||||
const AUG_17_2023 = '2023-08-17T00:00:00.000Z';
|
||||
const AUG_24_2023 = '2023-08-24T00:00:00.000Z';
|
||||
|
||||
expect( isTodayCodeFreezeDay( JUNE_5_2023 ) ).toBeFalsy();
|
||||
expect( isTodayCodeFreezeDay( JUNE_12_2023 ) ).toBeFalsy();
|
||||
expect( isTodayCodeFreezeDay( JUNE_26_2023 ) ).toBeFalsy();
|
||||
expect( isTodayCodeFreezeDay( AUG_10_2023 ) ).toBeFalsy();
|
||||
expect( isTodayCodeFreezeDay( AUG_17_2023 ) ).toBeFalsy();
|
||||
expect( isTodayCodeFreezeDay( AUG_24_2023 ) ).toBeFalsy();
|
||||
} );
|
||||
|
||||
it( 'should return true when given a day 22 days before release', () => {
|
||||
const JUNE_19_2023 = '2023-06-19T00:00:00.000Z';
|
||||
const JULY_17_2023 = '2023-07-17T00:00:00.000Z';
|
||||
const AUGUST_21_2023 = '2023-08-21T00:00:00.000Z';
|
||||
|
||||
expect( isTodayCodeFreezeDay( JUNE_19_2023 ) ).toBeTruthy();
|
||||
expect( isTodayCodeFreezeDay( JULY_17_2023 ) ).toBeTruthy();
|
||||
expect( isTodayCodeFreezeDay( AUGUST_21_2023 ) ).toBeTruthy();
|
||||
} );
|
||||
|
||||
it( 'should error out when passed an invalid date', () => {
|
||||
expect( () => isTodayCodeFreezeDay( 'invalid date' ) ).toThrow();
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -1,3 +1,46 @@
|
|||
export const verifyDay = () => {
|
||||
return 'Today is a good day to code freeze!';
|
||||
const MILLIS_IN_A_DAY = 24 * 60 * 60 * 1000;
|
||||
export const DAYS_BETWEEN_CODE_FREEZE_AND_RELEASE = 22;
|
||||
|
||||
/**
|
||||
* Get a Date object of now or the override time when specified.
|
||||
*
|
||||
* @param {string} now The time to use in checking if today is the day of the code freeze. Default to now.
|
||||
* @return {Date} The Date object of now or the override time when specified.
|
||||
*/
|
||||
export const getToday = ( now = 'now' ): Date => {
|
||||
const today = now === 'now' ? new Date() : new Date( now );
|
||||
if ( isNaN( today.getTime() ) ) {
|
||||
throw new Error(
|
||||
'Invalid date: Check the override parameter (-o, --override) is a correct Date string'
|
||||
);
|
||||
}
|
||||
return today;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a future date from today to see if its the release day.
|
||||
*
|
||||
* @param {string} today The time to use in checking if today is the day of the code freeze. Default to now.
|
||||
* @return {Date} The Date object of the future date.
|
||||
*/
|
||||
export const getFutureDate = ( today: Date ) => {
|
||||
return new Date(
|
||||
today.getTime() + DAYS_BETWEEN_CODE_FREEZE_AND_RELEASE * MILLIS_IN_A_DAY
|
||||
);
|
||||
};
|
||||
/**
|
||||
* Determines if today is the day of the code freeze.
|
||||
*
|
||||
* @param {string} now The time to use in checking if today is the day of the code freeze. Default to now.
|
||||
* @return {boolean} true if today is the day of the code freeze.
|
||||
*/
|
||||
export const isTodayCodeFreezeDay = ( now: string ) => {
|
||||
const today = getToday( now );
|
||||
const futureDate = getFutureDate( today );
|
||||
const month = futureDate.getUTCMonth();
|
||||
const year = futureDate.getUTCFullYear();
|
||||
const firstDayOfMonth = new Date( Date.UTC( year, month, 1 ) );
|
||||
const dayOfWeek = firstDayOfMonth.getUTCDay();
|
||||
const secondTuesday = dayOfWeek <= 2 ? 10 - dayOfWeek : 17 - dayOfWeek;
|
||||
return futureDate.getUTCDate() === secondTuesday;
|
||||
};
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
#! /usr/bin/env node
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
|
@ -9,9 +8,7 @@ import { Command } from '@commander-js/extra-typings';
|
|||
*/
|
||||
import CodeFreeze from './code-freeze/commands';
|
||||
|
||||
const program = new Command()
|
||||
export const program = new Command()
|
||||
.name( 'utils' )
|
||||
.description( 'Monorepo utilities' )
|
||||
.addCommand( CodeFreeze );
|
||||
|
||||
program.parse( process.argv );
|
||||
|
|
Loading…
Reference in New Issue