diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 212d42976088e..1d3791a85a945 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2928,6 +2928,9 @@ importers:
'@wordpress/date':
specifier: 4.38.0
version: 4.38.0
+ '@wordpress/dependency-extraction-webpack-plugin':
+ specifier: 4.17.0
+ version: 4.17.0(webpack@5.76.0)
'@wordpress/edit-post':
specifier: 7.15.0
version: 7.15.0(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0)
@@ -10498,6 +10501,17 @@ packages:
moment: 2.29.4
moment-timezone: 0.5.43
+ /@wordpress/dependency-extraction-webpack-plugin@4.17.0(webpack@5.76.0):
+ resolution: {integrity: sha512-vSWYcjHwdQrnvw6XeYfxRFLGZBEKR3IU82ahfiUEb0doomlR/ulZbnIZdC22Xo0KuQkvTSHEx71yhnvyKYolBg==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ webpack: ^4.8.3 || ^5.0.0
+ dependencies:
+ json2php: 0.0.7
+ webpack: 5.76.0(webpack-cli@4.9.1)
+ webpack-sources: 3.2.3
+ dev: false
+
/@wordpress/dependency-extraction-webpack-plugin@4.21.0(webpack@5.76.0):
resolution: {integrity: sha512-YLsopatpixhc0MlbH9Nt5Tutw8QA+zt8uAEy4MpHl6J9h4/xqiROh0QsA0qHrCD5Tn6gi3gLx9RdMTU/pLWQUQ==}
engines: {node: '>=14'}
diff --git a/projects/packages/blocks/changelog/update-extensions-build b/projects/packages/blocks/changelog/update-extensions-build
new file mode 100644
index 0000000000000..fa33a5d0df454
--- /dev/null
+++ b/projects/packages/blocks/changelog/update-extensions-build
@@ -0,0 +1,4 @@
+Significance: patch
+Type: added
+
+Build individual blocks
diff --git a/projects/packages/blocks/src/class-blocks.php b/projects/packages/blocks/src/class-blocks.php
index 9286bd4ebc36d..046f80c17d4bf 100644
--- a/projects/packages/blocks/src/class-blocks.php
+++ b/projects/packages/blocks/src/class-blocks.php
@@ -36,10 +36,11 @@ class Blocks {
* @type array $version_requirements Array containing required Gutenberg version and, if known, the WordPress version that was released with this minimum version.
* @type bool $plan_check Should we check for a specific plan before registering the block.
* }
+ * @param string $metadata_dir Directory holding the block.json metadata file.
*
* @return WP_Block_Type|false The registered block type on success, or false on failure.
*/
- public static function jetpack_register_block( $slug, $args = array() ) {
+ public static function jetpack_register_block( $slug, $args = array(), $metadata_dir = '' ) {
if ( 0 !== strpos( $slug, 'jetpack/' ) && ! strpos( $slug, '/' ) ) {
_doing_it_wrong( 'jetpack_register_block', 'Prefix the block with jetpack/ ', 'Jetpack 9.0.0' );
$slug = 'jetpack/' . $slug;
@@ -94,12 +95,12 @@ function () use ( $feature_name, $method_name ) {
// Ensure editor styles are registered so that the site editor knows about the
// editor style dependency when copying styles to the editor iframe.
- if ( ! isset( $args['editor_style'] ) ) {
+ if ( ! isset( $args['editor_style'] ) && ! $metadata_dir ) {
$args['editor_style'] = 'jetpack-blocks-editor';
}
}
- return register_block_type( $slug, $args );
+ return register_block_type( $metadata_dir ? $metadata_dir : $slug, $args );
}
/**
diff --git a/projects/plugins/jetpack/changelog/update-extensions-build b/projects/plugins/jetpack/changelog/update-extensions-build
new file mode 100644
index 0000000000000..05b55adf2a462
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/update-extensions-build
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+Build individual blocks
diff --git a/projects/plugins/jetpack/class.jetpack-gutenberg.php b/projects/plugins/jetpack/class.jetpack-gutenberg.php
index 0b2e3c09a461f..8c15a80b2a398 100644
--- a/projects/plugins/jetpack/class.jetpack-gutenberg.php
+++ b/projects/plugins/jetpack/class.jetpack-gutenberg.php
@@ -273,13 +273,15 @@ public static function preset_exists( $preset ) {
* Decodes JSON loaded from a preset file in the blocks folder
*
* @param string $preset The name of the .json file to load.
+ * @param object $associative TODO: define.
*
* @return mixed Returns an object if the file is present, or false if a valid .json file is not present.
*/
- public static function get_preset( $preset ) {
+ public static function get_preset( $preset, $associative = null ) {
return json_decode(
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
- file_get_contents( JETPACK__PLUGIN_DIR . self::get_blocks_directory() . $preset . '.json' )
+ file_get_contents( JETPACK__PLUGIN_DIR . self::get_blocks_directory() . $preset . '.json' ),
+ $associative
);
}
@@ -297,6 +299,47 @@ public static function get_jetpack_gutenberg_extensions_allowed_list() {
return self::get_extensions_preset_for_variation( $preset_extensions_manifest, $blocks_variation );
}
+ /**
+ * Returns a list of Jetpack Gutenberg extensions loading seaparately (blocks and plugins), based on index.json
+ *
+ * @return array A list of blocks: eg [ 'publicize', 'markdown' ]
+ */
+ public static function get_jetpack_gutenberg_single_extensions() {
+ $preset_extensions_manifest = self::preset_exists( 'index' )
+ ? self::get_preset( 'index' )
+ : (object) array();
+
+ return isset( $preset_extensions_manifest->single )
+ ? (array) $preset_extensions_manifest->single
+ : array();
+ }
+
+ /**
+ * Check for bundled extensions
+ *
+ * @return boolean true if has bundled extensions
+ */
+ public static function has_bundled_extension() {
+ $preset_extensions_manifest = self::preset_exists( 'index' )
+ ? self::get_preset( 'index', true )
+ : array();
+
+ $production = isset( $preset_extensions_manifest['production'] )
+ ? $preset_extensions_manifest['production']
+ : array();
+ $beta = isset( $preset_extensions_manifest['beta'] )
+ ? $preset_extensions_manifest['beta']
+ : array();
+ $experimental = isset( $preset_extensions_manifest['experimental'] )
+ ? $preset_extensions_manifest['experimental']
+ : array();
+ $no_post = isset( $preset_extensions_manifest['no-post-editor'] )
+ ? $preset_extensions_manifest['no-post-editor']
+ : array();
+
+ return count( $production ) > 0 || count( $beta ) > 0 || count( $experimental ) > 0 || count( $no_post ) > 0;
+ }
+
/**
* Returns a diff from a combined list of allowed extensions and extensions determined to be excluded
*
@@ -621,6 +664,9 @@ public static function enqueue_block_editor_assets() {
$blocks_dir = self::get_blocks_directory();
$blocks_variation = self::blocks_variation();
+ $has_bundle = self::has_bundled_extension();
+ $single_blocks = self::get_jetpack_gutenberg_single_extensions();
+ $has_single = count( $single_blocks ) > 0;
if ( 'production' !== $blocks_variation ) {
$blocks_env = '-' . esc_attr( $blocks_variation );
@@ -628,28 +674,49 @@ public static function enqueue_block_editor_assets() {
$blocks_env = '';
}
- Assets::register_script(
- 'jetpack-blocks-editor',
- "{$blocks_dir}editor{$blocks_env}.js",
- JETPACK__PLUGIN_FILE,
- array( 'textdomain' => 'jetpack' )
- );
+ if ( $has_single ) {
+ Assets::register_script(
+ 'editor-core',
+ "{$blocks_dir}editor-core.js",
+ JETPACK__PLUGIN_FILE,
+ array( 'textdomain' => 'jetpack' )
+ );
- // Hack around #20357 (specifically, that the editor bundle depends on
- // wp-edit-post but wp-edit-post's styles break the Widget Editor and
- // Site Editor) until a real fix gets unblocked.
- // @todo Remove this once #20357 is properly fixed.
- wp_styles()->query( 'jetpack-blocks-editor', 'registered' )->deps = array();
+ wp_enqueue_style( 'editor-core' );
- Assets::enqueue_script( 'jetpack-blocks-editor' );
+ wp_localize_script(
+ 'editor-core',
+ 'Jetpack_Block_Assets_Base_Url',
+ array(
+ 'url' => plugins_url( $blocks_dir . '/', JETPACK__PLUGIN_FILE ),
+ )
+ );
+ }
- wp_localize_script(
- 'jetpack-blocks-editor',
- 'Jetpack_Block_Assets_Base_Url',
- array(
- 'url' => plugins_url( $blocks_dir . '/', JETPACK__PLUGIN_FILE ),
- )
- );
+ if ( $has_bundle ) {
+ Assets::register_script(
+ 'jetpack-blocks-editor',
+ "{$blocks_dir}editor{$blocks_env}.js",
+ JETPACK__PLUGIN_FILE,
+ array( 'textdomain' => 'jetpack' )
+ );
+
+ // Hack around #20357 (specifically, that the editor bundle depends on
+ // wp-edit-post but wp-edit-post's styles break the Widget Editor and
+ // Site Editor) until a real fix gets unblocked.
+ // @todo Remove this once #20357 is properly fixed.
+ wp_styles()->query( 'jetpack-blocks-editor', 'registered' )->deps = array();
+
+ Assets::enqueue_script( 'jetpack-blocks-editor' );
+
+ wp_localize_script(
+ 'jetpack-blocks-editor',
+ 'Jetpack_Block_Assets_Base_Url',
+ array(
+ 'url' => plugins_url( $blocks_dir . '/', JETPACK__PLUGIN_FILE ),
+ )
+ );
+ }
if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
$user = wp_get_current_user();
@@ -736,14 +803,27 @@ public static function enqueue_block_editor_assets() {
);
}
- wp_localize_script(
- 'jetpack-blocks-editor',
- 'Jetpack_Editor_Initial_State',
- $initial_state
- );
+ if ( $has_bundle ) {
+ wp_localize_script(
+ 'jetpack-blocks-editor',
+ 'Jetpack_Editor_Initial_State',
+ $initial_state
+ );
- // Adds Connection package initial state.
- wp_add_inline_script( 'jetpack-blocks-editor', Connection_Initial_State::render(), 'before' );
+ // Adds Connection package initial state.
+ wp_add_inline_script( 'jetpack-blocks-editor', Connection_Initial_State::render(), 'before' );
+ }
+
+ if ( $has_single && ! $has_bundle ) {
+ wp_localize_script(
+ 'editor-core',
+ 'Jetpack_Editor_Initial_State',
+ $initial_state
+ );
+
+ // Adds Connection package initial state.
+ wp_add_inline_script( 'editor-core', Connection_Initial_State::render(), 'before' );
+ }
}
/**
@@ -1019,6 +1099,8 @@ public static function get_extensions_preset_for_variation( $preset_extensions_m
$preset_extensions = array_unique( array_merge( $preset_extensions, $production_extensions ) );
}
+ $preset_extensions = array_unique( array_merge( $preset_extensions, self::get_jetpack_gutenberg_single_extensions() ) );
+
return $preset_extensions;
}
diff --git a/projects/plugins/jetpack/extensions/blocks/business-hours-single/block.json b/projects/plugins/jetpack/extensions/blocks/business-hours-single/block.json
new file mode 100644
index 0000000000000..2363268afaf47
--- /dev/null
+++ b/projects/plugins/jetpack/extensions/blocks/business-hours-single/block.json
@@ -0,0 +1,28 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 3,
+ "name": "jetpack/business-hours-single",
+ "title": "Business Hours Single",
+ "description": "Display opening hours for your business.",
+ "keywords": [ "opening hours", "closing time", "schedule", "working day" ],
+ "version": "1.0.0",
+ "textdomain": "jetpack",
+ "category": "grow",
+ "supports": {
+ "html": true,
+ "color": {
+ "gradients": true
+ },
+ "spacing": {
+ "margin": true,
+ "padding": true
+ },
+ "typography": {
+ "fontSize": true,
+ "lineHeight": true
+ },
+ "align": [ "wide", "full" ]
+ },
+ "editorScript": "file:./editor.js",
+ "editorStyle": "file:./editor.css"
+}
diff --git a/projects/plugins/jetpack/extensions/blocks/business-hours-single/business-hours-single.php b/projects/plugins/jetpack/extensions/blocks/business-hours-single/business-hours-single.php
new file mode 100644
index 0000000000000..66f027dcc2671
--- /dev/null
+++ b/projects/plugins/jetpack/extensions/blocks/business-hours-single/business-hours-single.php
@@ -0,0 +1,176 @@
+ __NAMESPACE__ . '\render',
+ ),
+ $json_dir
+ );
+}
+add_action( 'init', __NAMESPACE__ . '\register_block' );
+
+/**
+ * Get's default days / hours to render a business hour block with no data provided.
+ *
+ * @return array
+ */
+function get_default_days() {
+ return array(
+ array(
+ 'name' => 'Sun',
+ 'hours' => array(),
+ ),
+ array(
+ 'name' => 'Mon',
+ 'hours' => array(
+ array(
+ 'opening' => '09:00',
+ 'closing' => '17:00',
+ ),
+ ),
+ ),
+ array(
+ 'name' => 'Tue',
+ 'hours' => array(
+ array(
+ 'opening' => '09:00',
+ 'closing' => '17:00',
+ ),
+ ),
+ ),
+ array(
+ 'name' => 'Wed',
+ 'hours' => array(
+ array(
+ 'opening' => '09:00',
+ 'closing' => '17:00',
+ ),
+ ),
+ ),
+ array(
+ 'name' => 'Thu',
+ 'hours' => array(
+ array(
+ 'opening' => '09:00',
+ 'closing' => '17:00',
+ ),
+ ),
+ ),
+ array(
+ 'name' => 'Fri',
+ 'hours' => array(
+ array(
+ 'opening' => '09:00',
+ 'closing' => '17:00',
+ ),
+ ),
+ ),
+ array(
+ 'name' => 'Sat',
+ 'hours' => array(),
+ ),
+ );
+}
+
+/**
+ * Dynamic rendering of the block.
+ *
+ * @param array $attributes Array containing the business hours block attributes.
+ *
+ * @return string
+ */
+function render( $attributes ) {
+ global $wp_locale;
+
+ if ( empty( $attributes['days'] ) || ! is_array( $attributes['days'] ) ) {
+ $attributes['days'] = get_default_days();
+ }
+
+ $wrapper_attributes = \WP_Block_Supports::get_instance()->apply_block_supports();
+
+ $start_of_week = (int) get_option( 'start_of_week', 0 );
+ $time_format = get_option( 'time_format' );
+ $content = sprintf(
+ '
',
+ ! empty( $attributes['className'] ) ? ' ' . esc_attr( $attributes['className'] ) : '',
+ ! empty( $wrapper_attributes['class'] ) ? ' ' . esc_attr( $wrapper_attributes['class'] ) : '',
+ ! empty( $wrapper_attributes['style'] ) ? ' style="' . esc_attr( $wrapper_attributes['style'] ) . '"' : ''
+ );
+
+ $days = array( 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' );
+
+ if ( $start_of_week ) {
+ $chunk1 = array_slice( $attributes['days'], 0, $start_of_week );
+ $chunk2 = array_slice( $attributes['days'], $start_of_week );
+ $attributes['days'] = array_merge( $chunk2, $chunk1 );
+ }
+
+ foreach ( $attributes['days'] as $day ) {
+ $content .= '- ' .
+ ucfirst( $wp_locale->get_weekday( array_search( $day['name'], $days, true ) ) ) .
+ '
';
+ $content .= '- ';
+ $days_hours = '';
+
+ foreach ( $day['hours'] as $key => $hour ) {
+ $opening = strtotime( $hour['opening'] );
+ $closing = strtotime( $hour['closing'] );
+ if ( ! $opening || ! $closing ) {
+ continue;
+ }
+ $days_hours .= sprintf(
+ '%1$s - %2$s',
+ gmdate( $time_format, $opening ),
+ gmdate( $time_format, $closing )
+ );
+ if ( $key + 1 < count( $day['hours'] ) ) {
+ $days_hours .= ', ';
+ }
+ }
+
+ if ( empty( $days_hours ) ) {
+ $days_hours = esc_html__( 'Closed', 'jetpack' );
+ }
+ $content .= $days_hours;
+ $content .= '
';
+ }
+
+ $content .= '
';
+
+ Jetpack_Gutenberg::load_assets_as_required( FEATURE_NAME );
+
+ /**
+ * Allows folks to filter the HTML content for the Business Hours block
+ *
+ * @since 7.1.0
+ *
+ * @param string $content The default HTML content set by `jetpack_business_hours_render`
+ * @param array $attributes Attributes generated in the block editor for the Business Hours block
+ */
+ return apply_filters( 'jetpack_business_hours_content', $content, $attributes );
+}
diff --git a/projects/plugins/jetpack/extensions/blocks/business-hours-single/components/day-edit.js b/projects/plugins/jetpack/extensions/blocks/business-hours-single/components/day-edit.js
new file mode 100644
index 0000000000000..3bbe9eae1f131
--- /dev/null
+++ b/projects/plugins/jetpack/extensions/blocks/business-hours-single/components/day-edit.js
@@ -0,0 +1,198 @@
+import { Button, TextControl, ToggleControl } from '@wordpress/components';
+import { Component, Fragment } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import classNames from 'classnames';
+import { isEmpty } from 'lodash';
+
+const defaultOpen = '09:00';
+const defaultClose = '17:00';
+
+class DayEdit extends Component {
+ renderInterval = ( interval, intervalIndex ) => {
+ const { day } = this.props;
+ const { opening, closing } = interval;
+ return (
+
+
+
+ { intervalIndex === 0 && this.renderDayToggle() }
+
+
+ {
+ this.setHour( value, 'opening', intervalIndex );
+ } }
+ />
+ {
+ this.setHour( value, 'closing', intervalIndex );
+ } }
+ />
+
+
+ { day.hours.length > 1 && (
+
+
+ { intervalIndex === day.hours.length - 1 && (
+
+
+
+
+
+
+
+ ) }
+
+ );
+ };
+
+ setHour = ( hourValue, hourType, hourIndex ) => {
+ const { day, attributes, setAttributes } = this.props;
+ const { days } = attributes;
+ setAttributes( {
+ days: days.map( value => {
+ if ( value.name === day.name ) {
+ return {
+ ...value,
+ hours: value.hours.map( ( hour, index ) => {
+ if ( index === hourIndex ) {
+ return {
+ ...hour,
+ [ hourType ]: hourValue,
+ };
+ }
+ return hour;
+ } ),
+ };
+ }
+ return value;
+ } ),
+ } );
+ };
+
+ toggleClosed = nextValue => {
+ const { day, attributes, setAttributes } = this.props;
+ const { days } = attributes;
+
+ setAttributes( {
+ days: days.map( value => {
+ if ( value.name === day.name ) {
+ const hours = nextValue
+ ? [
+ {
+ opening: defaultOpen,
+ closing: defaultClose,
+ },
+ ]
+ : [];
+ return {
+ ...value,
+ hours,
+ };
+ }
+ return value;
+ } ),
+ } );
+ };
+
+ addInterval = () => {
+ const { day, attributes, setAttributes } = this.props;
+ const { days } = attributes;
+ day.hours.push( { opening: '', closing: '' } );
+ setAttributes( {
+ days: days.map( value => {
+ if ( value.name === day.name ) {
+ return {
+ ...value,
+ hours: day.hours,
+ };
+ }
+ return value;
+ } ),
+ } );
+ };
+
+ removeInterval = hourIndex => {
+ const { day, attributes, setAttributes } = this.props;
+ const { days } = attributes;
+
+ setAttributes( {
+ days: days.map( value => {
+ if ( day.name === value.name ) {
+ return {
+ ...value,
+ hours: value.hours.filter( ( hour, index ) => {
+ return hourIndex !== index;
+ } ),
+ };
+ }
+ return value;
+ } ),
+ } );
+ };
+
+ isClosed() {
+ const { day } = this.props;
+ return isEmpty( day.hours );
+ }
+
+ renderDayToggle() {
+ const { day, localization } = this.props;
+ return (
+
+ { localization.days[ day.name ] }
+
+
+ );
+ }
+
+ renderClosed() {
+ const { day } = this.props;
+ return (
+
+
+ { this.renderDayToggle() }
+
+
+
+
+ );
+ }
+
+ render() {
+ const { day } = this.props;
+ return this.isClosed() ? this.renderClosed() : day.hours.map( this.renderInterval );
+ }
+}
+
+export default DayEdit;
diff --git a/projects/plugins/jetpack/extensions/blocks/business-hours-single/components/day-preview.js b/projects/plugins/jetpack/extensions/blocks/business-hours-single/components/day-preview.js
new file mode 100644
index 0000000000000..4c0c9c69ad9fe
--- /dev/null
+++ b/projects/plugins/jetpack/extensions/blocks/business-hours-single/components/day-preview.js
@@ -0,0 +1,54 @@
+import { date } from '@wordpress/date';
+import { Component } from '@wordpress/element';
+import { _x, sprintf } from '@wordpress/i18n';
+import { isEmpty } from 'lodash';
+
+class DayPreview extends Component {
+ formatTime( time ) {
+ const { timeFormat } = this.props;
+ const [ hours, minutes ] = time.split( ':' );
+ const _date = new Date();
+ if ( ! hours || ! minutes ) {
+ return false;
+ }
+ _date.setHours( hours );
+ _date.setMinutes( minutes );
+ return date( timeFormat, _date );
+ }
+
+ renderInterval = ( interval, key ) => {
+ const { day } = this.props;
+ const hours = day.hours;
+ return (
+
+ { sprintf(
+ '%1$s - %2$s',
+ this.formatTime( interval.opening ),
+ this.formatTime( interval.closing )
+ ) }
+ { hours.length > 1 + key && , }
+
+ );
+ };
+
+ render() {
+ const { day, localization } = this.props;
+ const hours = day.hours.filter(
+ // remove any malformed or empty intervals
+ interval => this.formatTime( interval.opening ) && this.formatTime( interval.closing )
+ );
+ return (
+
+
{ localization.days[ day.name ] }
+
+ { isEmpty( hours )
+ ? _x( 'Closed', 'business is closed on a full day', 'jetpack' )
+ : hours.map( this.renderInterval ) }
+
+
+
+ );
+ }
+}
+
+export default DayPreview;
diff --git a/projects/plugins/jetpack/extensions/blocks/business-hours-single/edit.js b/projects/plugins/jetpack/extensions/blocks/business-hours-single/edit.js
new file mode 100644
index 0000000000000..0c35aa9920378
--- /dev/null
+++ b/projects/plugins/jetpack/extensions/blocks/business-hours-single/edit.js
@@ -0,0 +1,91 @@
+import apiFetch from '@wordpress/api-fetch';
+import { Placeholder } from '@wordpress/components';
+import { getSettings } from '@wordpress/date';
+import { Component } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import classNames from 'classnames';
+import DayEdit from './components/day-edit';
+import DayPreview from './components/day-preview';
+import { icon } from '.';
+
+export const defaultLocalization = {
+ days: {
+ Sun: __( 'Sunday', 'jetpack' ),
+ Mon: __( 'Monday', 'jetpack' ),
+ Tue: __( 'Tuesday', 'jetpack' ),
+ Wed: __( 'Wednesday', 'jetpack' ),
+ Thu: __( 'Thursday', 'jetpack' ),
+ Fri: __( 'Friday', 'jetpack' ),
+ Sat: __( 'Saturday', 'jetpack' ),
+ },
+ startOfWeek: 0,
+};
+
+class BusinessHours extends Component {
+ state = {
+ localization: defaultLocalization,
+ hasFetched: false,
+ };
+
+ componentDidMount() {
+ this.apiFetch();
+ }
+
+ apiFetch() {
+ this.setState( { data: defaultLocalization }, () => {
+ apiFetch( { path: '/wpcom/v2/business-hours/localized-week' } ).then(
+ data => {
+ this.setState( { localization: data, hasFetched: true } );
+ },
+ () => {
+ this.setState( { localization: defaultLocalization, hasFetched: true } );
+ }
+ );
+ } );
+ }
+
+ render() {
+ const { attributes, className, isSelected } = this.props;
+ const { days } = attributes;
+ const { localization, hasFetched } = this.state;
+ const { startOfWeek } = localization;
+ const localizedWeek = days.concat( days.slice( 0, startOfWeek ) ).slice( startOfWeek );
+
+ if ( ! hasFetched ) {
+ return ;
+ }
+
+ if ( ! isSelected ) {
+ const settings = getSettings();
+ const {
+ formats: { time },
+ } = settings;
+ return (
+
+ { localizedWeek.map( ( day, key ) => {
+ return (
+
+ );
+ } ) }
+
+ );
+ }
+
+ return (
+
+ { localizedWeek.map( ( day, key ) => {
+ return (
+
+ );
+ } ) }
+
+ );
+ }
+}
+
+export default BusinessHours;
diff --git a/projects/plugins/jetpack/extensions/blocks/business-hours-single/editor.js b/projects/plugins/jetpack/extensions/blocks/business-hours-single/editor.js
new file mode 100644
index 0000000000000..03fa36550695a
--- /dev/null
+++ b/projects/plugins/jetpack/extensions/blocks/business-hours-single/editor.js
@@ -0,0 +1,8 @@
+import 'editor-core'; // editor-core is an external dependency
+import registerJetpackBlock from '../../shared/register-jetpack-block';
+import metadata from './block.json';
+import { settings } from '.';
+
+import './editor.scss';
+
+registerJetpackBlock( metadata.name.replace( 'jetpack/', '' ), settings );
diff --git a/projects/plugins/jetpack/extensions/blocks/business-hours-single/editor.scss b/projects/plugins/jetpack/extensions/blocks/business-hours-single/editor.scss
new file mode 100644
index 0000000000000..d596766155cf7
--- /dev/null
+++ b/projects/plugins/jetpack/extensions/blocks/business-hours-single/editor.scss
@@ -0,0 +1,170 @@
+@import '@automattic/jetpack-base-styles/gutenberg-base-styles';
+
+.wp-block-jetpack-business-hours {
+ overflow: hidden;
+
+ dd, dt {
+ @media (min-width: 480px ) {
+ display: inline-block;
+ }
+ }
+
+ dt {
+ min-width: 30%;
+ vertical-align: top;
+ }
+
+ dd {
+ margin: 0;
+ @media (min-width: 480px ) {
+ max-width: calc( 70% - 0.5em );
+ }
+ }
+
+ .components-toggle-control__label,
+ .components-base-control__label {
+ // Sets labels to 13px for consistency in the Site Editor.
+ // The Site Editor iframe doesn't include common styles,
+ // from which the font size of the labels are inherited.
+ // https://github.com/WordPress/gutenberg/blob/3da717b8d0ac7d7821fc6d0475695ccf3ae2829f/packages/base-styles/_variables.scss#L16
+ font-size: $default-font-size;
+ }
+
+ .components-base-control__field {
+ margin-bottom: 0;
+ }
+
+ .jetpack-business-hours__item {
+ margin-bottom: 0.5em;
+ }
+
+ .business-hours__row {
+ display: flex;
+ line-height: normal;
+ margin-bottom: 4px;
+
+ &.business-hours-row__add,
+ &.business-hours-row__closed {
+ margin-bottom: 20px;
+ }
+
+ .business-hours__day {
+ width: 44%;
+ display: flex;
+ align-items: flex-start;
+
+ .business-hours__day-name {
+ width: 60%;
+ font-weight: bold;
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .components-form-toggle {
+ margin-right: 4px;
+ margin-top: 4px;
+ }
+ }
+
+ .business-hours__hours {
+ width: 44%;
+ margin: 0;
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+
+ .components-button {
+ padding: 0;
+ }
+
+ .components-base-control {
+ display: inline-block;
+ margin-bottom: 0;
+ width: 48%;
+
+ &.business-hours__open {
+ margin-right: 4%;
+ }
+
+ .components-base-control__label {
+ margin-bottom: 0;
+ }
+ }
+ }
+ }
+
+ .business-hours__remove {
+ align-self: flex-end;
+ margin-bottom: 8px;
+ text-align: center;
+ width: 10%;
+ }
+
+ .business-hours-row__add button:hover {
+ box-shadow: none !important;
+ }
+
+ .business-hours__remove button {
+ display: block;
+ margin: 0 auto;
+ }
+
+ .business-hours-row__add .components-button.is-default:hover,
+ .business-hours__remove .components-button.is-default:hover,
+ .business-hours-row__add .components-button.is-default:focus,
+ .business-hours__remove .components-button.is-default:focus,
+ .business-hours-row__add .components-button.is-default:active,
+ .business-hours__remove .components-button.is-default:active {
+ background: none;
+ box-shadow: none;
+ }
+}
+
+/**
+ * We consider the editor area to be small when the business hours block is:
+ * - within a column block
+ * - in a screen < xlarge size with the sidebar open
+ * - in a screen < small size
+ * In these cases we'll apply small screen styles.
+ */
+@mixin editor-area-is-small {
+ @media ( max-width: $break-xlarge ) {
+ .is-sidebar-opened {
+ @content;
+ }
+ }
+ @media ( max-width: $break-small ) {
+ @content;
+ }
+
+ .wp-block-columns {
+ @content;
+ }
+}
+
+@include editor-area-is-small() {
+ .wp-block-jetpack-business-hours {
+ .business-hours__row {
+ flex-wrap: wrap;
+
+ &.business-hours-row__add {
+ .business-hours__day,
+ .business-hours__remove {
+ display: none;
+ }
+ }
+
+ .business-hours__day {
+ width: 100%;
+ }
+
+ .business-hours__hours {
+ width: 78%;
+ }
+ .business-hours__remove {
+ width: 18%;
+ }
+ }
+ }
+}
diff --git a/projects/plugins/jetpack/extensions/blocks/business-hours-single/index.js b/projects/plugins/jetpack/extensions/blocks/business-hours-single/index.js
new file mode 100644
index 0000000000000..1076c617d3dab
--- /dev/null
+++ b/projects/plugins/jetpack/extensions/blocks/business-hours-single/index.js
@@ -0,0 +1,87 @@
+import { Path } from '@wordpress/components';
+import { getIconColor } from '../../shared/block-icons';
+import renderMaterialIcon from '../../shared/render-material-icon';
+import BusinessHours from './edit';
+
+/**
+ * Block Registrations:
+ */
+
+const defaultDays = [
+ {
+ name: 'Sun',
+ hours: [], // Closed by default
+ },
+ {
+ name: 'Mon',
+ hours: [
+ {
+ opening: '09:00',
+ closing: '17:00',
+ },
+ ],
+ },
+ {
+ name: 'Tue',
+ hours: [
+ {
+ opening: '09:00',
+ closing: '17:00',
+ },
+ ],
+ },
+ {
+ name: 'Wed',
+ hours: [
+ {
+ opening: '09:00',
+ closing: '17:00',
+ },
+ ],
+ },
+ {
+ name: 'Thu',
+ hours: [
+ {
+ opening: '09:00',
+ closing: '17:00',
+ },
+ ],
+ },
+ {
+ name: 'Fri',
+ hours: [
+ {
+ opening: '09:00',
+ closing: '17:00',
+ },
+ ],
+ },
+ {
+ name: 'Sat',
+ hours: [], // Closed by default
+ },
+];
+export const icon = renderMaterialIcon(
+
+);
+
+export const settings = {
+ icon: {
+ src: icon,
+ foreground: getIconColor(),
+ },
+ attributes: {
+ days: {
+ type: 'array',
+ default: defaultDays,
+ },
+ },
+ example: {
+ attributes: {
+ days: defaultDays,
+ },
+ },
+ edit: props => ,
+ save: () => null,
+};
diff --git a/projects/plugins/jetpack/extensions/blocks/business-hours-single/style.scss b/projects/plugins/jetpack/extensions/blocks/business-hours-single/style.scss
new file mode 100644
index 0000000000000..1fc272a79e0d7
--- /dev/null
+++ b/projects/plugins/jetpack/extensions/blocks/business-hours-single/style.scss
@@ -0,0 +1,26 @@
+.jetpack-business-hours {
+ dt,
+ dd {
+ @media (min-width: 480px ) {
+ display: inline-block;
+ }
+ }
+
+ dt {
+ font-weight: bold;
+ margin-right: 0.5em;
+ min-width: 30%;
+ vertical-align: top;
+ }
+
+ dd {
+ margin: 0;
+ @media (min-width: 480px ) {
+ max-width: calc( 70% - 0.5em );
+ }
+ }
+}
+
+.jetpack-business-hours__item {
+ margin-bottom: 0.5em;
+}
diff --git a/projects/plugins/jetpack/extensions/index.json b/projects/plugins/jetpack/extensions/index.json
index 727694b981ac9..c06bd89c254e2 100644
--- a/projects/plugins/jetpack/extensions/index.json
+++ b/projects/plugins/jetpack/extensions/index.json
@@ -94,5 +94,6 @@
"tock",
"videopress",
"wordads"
- ]
+ ],
+ "single": [ "business-hours-single" ]
}
diff --git a/projects/plugins/jetpack/package.json b/projects/plugins/jetpack/package.json
index 92cbf4a02c1f2..2b6890e5c8cff 100644
--- a/projects/plugins/jetpack/package.json
+++ b/projects/plugins/jetpack/package.json
@@ -68,6 +68,7 @@
"@wordpress/compose": "6.15.0",
"@wordpress/data": "9.8.0",
"@wordpress/date": "4.38.0",
+ "@wordpress/dependency-extraction-webpack-plugin": "4.17.0",
"@wordpress/edit-post": "7.15.0",
"@wordpress/element": "5.15.0",
"@wordpress/hooks": "3.38.0",
diff --git a/projects/plugins/jetpack/tools/webpack.config.extensions.js b/projects/plugins/jetpack/tools/webpack.config.extensions.js
index 127621bbd07b7..7c68f86ef3eae 100644
--- a/projects/plugins/jetpack/tools/webpack.config.extensions.js
+++ b/projects/plugins/jetpack/tools/webpack.config.extensions.js
@@ -7,6 +7,9 @@ const path = require( 'path' );
const jetpackWebpackConfig = require( '@automattic/jetpack-webpack-config/webpack' );
const webpack = jetpackWebpackConfig.webpack;
const RemoveAssetWebpackPlugin = require( '@automattic/remove-asset-webpack-plugin' );
+const {
+ defaultRequestToExternal,
+} = require( '@wordpress/dependency-extraction-webpack-plugin/lib/util' );
const CopyWebpackPlugin = require( 'copy-webpack-plugin' );
const jsdom = require( 'jsdom' );
const CopyBlockEditorAssetsPlugin = require( './copy-block-editor-assets' );
@@ -40,6 +43,7 @@ const presetPath = path.join( __dirname, '../extensions', 'index.json' );
const presetIndex = require( presetPath );
const presetProductionBlocks = presetIndex.production || [];
const presetNoPostEditorBlocks = presetIndex[ 'no-post-editor' ] || [];
+const presetSingleBlocks = presetIndex.single || [];
const presetExperimentalBlocks = [
...presetProductionBlocks,
@@ -96,6 +100,22 @@ const editorNoPostEditorScript = [
),
];
+const editorSingleBlocksScripts = presetSingleBlocks.reduce( ( editorBlocks, block ) => {
+ const editorScriptPath = path.join( __dirname, '../extensions/blocks', block, 'editor.js' );
+ if ( fs.existsSync( editorScriptPath ) ) {
+ editorBlocks[ block + '/editor' ] = [ editorScriptPath ];
+ }
+ return editorBlocks;
+}, {} );
+
+const viewSingleBlocksScripts = presetSingleBlocks.reduce( ( viewBlocks, block ) => {
+ const viewScriptPath = path.join( __dirname, '../extensions/blocks', block, 'view.js' );
+ if ( fs.existsSync( viewScriptPath ) ) {
+ viewBlocks[ block + '/view' ] = [ viewSetup, ...[ viewScriptPath ] ];
+ }
+ return viewBlocks;
+}, {} );
+
const sharedWebpackConfig = {
mode: jetpackWebpackConfig.mode,
devtool: jetpackWebpackConfig.devtool,
@@ -268,4 +288,34 @@ module.exports = [
} ),
],
},
+ {
+ ...sharedWebpackConfig,
+ entry: {
+ ...editorSingleBlocksScripts,
+ ...viewSingleBlocksScripts,
+ 'editor-core': path.join( __dirname, '../extensions/editor.js' ),
+ },
+ plugins: [
+ new CopyWebpackPlugin( {
+ patterns: [
+ {
+ from: '**/block.json',
+ to: '[path][name][ext]',
+ context: path.join( __dirname, '../extensions/blocks' ),
+ },
+ ],
+ } ),
+ ...jetpackWebpackConfig.StandardPlugins( {
+ DependencyExtractionPlugin: {
+ injectPolyfill: true,
+ requestToExternal( request ) {
+ if ( request === 'editor-core' ) {
+ return 'editor-core';
+ }
+ return defaultRequestToExternal( request );
+ },
+ },
+ } ),
+ ],
+ },
];
diff --git a/tools/docker/jetpack-docker-config-default.yml b/tools/docker/jetpack-docker-config-default.yml
index 63ac16823faa9..10ef36b238d4d 100644
--- a/tools/docker/jetpack-docker-config-default.yml
+++ b/tools/docker/jetpack-docker-config-default.yml
@@ -7,6 +7,7 @@ default:
tools/docker/wordpress: /var/www/html
.: /usr/local/src/jetpack-monorepo
tools/docker/mu-plugins: /var/www/html/wp-content/mu-plugins
+ /Users/andrewlysenko/Automattic/gutenberg: /var/www/html/wp-content/plugins/gutenberg
# Extra configuration (none by default). Anything set under this key will be written out as
# an extra docker-compose configuration file.