Add Gutenberg dynamic-block wrappers for the front-end shortcodes
CI / No Debug Code (pull_request) Successful in 4s
CI / Tests (PHP 8.2) (pull_request) Successful in 52s
CI / Tests (PHP 8.1) (pull_request) Successful in 54s
CI / Tests (PHP 8.3) (pull_request) Successful in 1m29s
CI / Coding Standards (pull_request) Successful in 1m57s
CI / PHPStan (pull_request) Successful in 2m14s
CI / Build Plugin Zip (pull_request) Has been skipped
CI / No Debug Code (pull_request) Successful in 4s
CI / Tests (PHP 8.2) (pull_request) Successful in 52s
CI / Tests (PHP 8.1) (pull_request) Successful in 54s
CI / Tests (PHP 8.3) (pull_request) Successful in 1m29s
CI / Coding Standards (pull_request) Successful in 1m57s
CI / PHPStan (pull_request) Successful in 2m14s
CI / Build Plugin Zip (pull_request) Has been skipped
Wrap the four shortcodes (us_booking, us_student_login, us_student_register, us_group_classes) in dynamic blocks so pages can be previewed and styled in the block editor. Front-end rendering delegates to the same page objects the shortcodes use; in the editor's block-renderer REST preview a static, script-free BlockPreview is rendered instead (no live REST calls, redirects, or Stripe.js). The editor script (vanilla JS, no build step) registers each block with wp.serverSideRender previews and shortcode transforms; frontend.css is attached as the block style so previews pick up theme styling. Resolves #44 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,8 @@ src/ — All plugin PHP (PSR-4 namespace: Unsupervised\Schedula
|
|||||||
AdminMenu.php — Registers wp-admin menu pages
|
AdminMenu.php — Registers wp-admin menu pages
|
||||||
RestRegistrar.php — Registers all REST routes under us-scheduler/v1
|
RestRegistrar.php — Registers all REST routes under us-scheduler/v1
|
||||||
ShortcodeRegistrar.php — Registers [us_booking] and [us_student_login] shortcodes
|
ShortcodeRegistrar.php — Registers [us_booking] and [us_student_login] shortcodes
|
||||||
|
BlockRegistrar.php — Registers Gutenberg dynamic-block wrappers for the shortcodes
|
||||||
|
BlockPreview.php — Static editor-preview markup for the blocks
|
||||||
templates/ — PHP view files included by controllers/shortcodes
|
templates/ — PHP view files included by controllers/shortcodes
|
||||||
assets/ — CSS and JS (vanilla JS, no build step)
|
assets/ — CSS and JS (vanilla JS, no build step)
|
||||||
tests/Unit/ — PHPUnit unit tests (PSR-4: Unsupervised\Schedular\Tests\)
|
tests/Unit/ — PHPUnit unit tests (PSR-4: Unsupervised\Schedular\Tests\)
|
||||||
@@ -66,6 +68,8 @@ All database access goes through repository classes within their domain package.
|
|||||||
| `AdminMenu` | Registers wp-admin menu pages |
|
| `AdminMenu` | Registers wp-admin menu pages |
|
||||||
| `RestRegistrar` | Registers all REST routes under `us-scheduler/v1` |
|
| `RestRegistrar` | Registers all REST routes under `us-scheduler/v1` |
|
||||||
| `ShortcodeRegistrar` | Registers `[us_booking]` and `[us_student_login]` shortcodes |
|
| `ShortcodeRegistrar` | Registers `[us_booking]` and `[us_student_login]` shortcodes |
|
||||||
|
| `BlockRegistrar` | Registers Gutenberg dynamic-block wrappers for the shortcodes |
|
||||||
|
| `BlockPreview` | Static editor-preview markup for the blocks |
|
||||||
| `Auth\RoleManager` | Registers `us_instructor` and `us_student` roles with custom caps |
|
| `Auth\RoleManager` | Registers `us_instructor` and `us_student` roles with custom caps |
|
||||||
| `Auth\LoginPage` | Renders front-end student login form |
|
| `Auth\LoginPage` | Renders front-end student login form |
|
||||||
| `Availability\AvailabilitySlot` | Immutable value object for a slot row |
|
| `Availability\AvailabilitySlot` | Immutable value object for a slot row |
|
||||||
|
|||||||
@@ -33,3 +33,10 @@
|
|||||||
color: #c00;
|
color: #c00;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Shown only in block-editor previews (see BlockPreview). */
|
||||||
|
.us-editor-note {
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-style: italic;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/* global wp */
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { registerBlockType } = wp.blocks;
|
||||||
|
const { createElement: el } = wp.element;
|
||||||
|
const { useBlockProps } = wp.blockEditor;
|
||||||
|
const ServerSideRender = wp.serverSideRender;
|
||||||
|
const { __ } = wp.i18n;
|
||||||
|
|
||||||
|
const blocks = [
|
||||||
|
{
|
||||||
|
name: 'us-scheduler/booking',
|
||||||
|
title: __('Lesson Booking', 'unsupervised-schedular'),
|
||||||
|
description: __('Lets students browse availability and book lessons. Shows a styled preview in the editor.', 'unsupervised-schedular'),
|
||||||
|
icon: 'calendar-alt',
|
||||||
|
keywords: ['booking', 'lesson', 'schedule'],
|
||||||
|
shortcode: 'us_booking',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'us-scheduler/student-login',
|
||||||
|
title: __('Student Login', 'unsupervised-schedular'),
|
||||||
|
description: __('The front-end login form for students.', 'unsupervised-schedular'),
|
||||||
|
icon: 'admin-users',
|
||||||
|
keywords: ['login', 'student', 'sign in'],
|
||||||
|
shortcode: 'us_student_login',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'us-scheduler/student-register',
|
||||||
|
title: __('Student Registration', 'unsupervised-schedular'),
|
||||||
|
description: __('The invite-only student registration form.', 'unsupervised-schedular'),
|
||||||
|
icon: 'welcome-add-page',
|
||||||
|
keywords: ['register', 'student', 'invite'],
|
||||||
|
shortcode: 'us_student_register',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'us-scheduler/group-classes',
|
||||||
|
title: __('Group Classes', 'unsupervised-schedular'),
|
||||||
|
description: __('Lets students browse and enrol in group classes. Shows a styled preview in the editor.', 'unsupervised-schedular'),
|
||||||
|
icon: 'groups',
|
||||||
|
keywords: ['group', 'class', 'enrol'],
|
||||||
|
shortcode: 'us_group_classes',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
blocks.forEach((def) => {
|
||||||
|
registerBlockType(def.name, {
|
||||||
|
apiVersion: 3,
|
||||||
|
title: def.title,
|
||||||
|
description: def.description,
|
||||||
|
icon: def.icon,
|
||||||
|
category: 'widgets',
|
||||||
|
keywords: def.keywords,
|
||||||
|
supports: { html: false, multiple: false },
|
||||||
|
example: {},
|
||||||
|
edit: function Edit() {
|
||||||
|
return el('div', useBlockProps(), el(ServerSideRender, { block: def.name }));
|
||||||
|
},
|
||||||
|
save: () => null,
|
||||||
|
transforms: {
|
||||||
|
from: [{ type: 'shortcode', tag: def.shortcode }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}());
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Editor Blocks
|
||||||
|
|
||||||
|
Gutenberg dynamic-block wrappers for the plugin's four front-end shortcodes,
|
||||||
|
so the pages can be previewed and styled inside the block editor instead of
|
||||||
|
appearing as grey shortcode text.
|
||||||
|
|
||||||
|
## Blocks
|
||||||
|
|
||||||
|
| Block | Wraps shortcode | Front-end renderer |
|
||||||
|
|---|---|---|
|
||||||
|
| `us-scheduler/booking` | `[us_booking]` | `Booking\BookingPage::render()` |
|
||||||
|
| `us-scheduler/student-login` | `[us_student_login]` | `Auth\LoginPage::render()` |
|
||||||
|
| `us-scheduler/student-register` | `[us_student_register]` | `Auth\RegistrationPage::render()` |
|
||||||
|
| `us-scheduler/group-classes` | `[us_group_classes]` | `GroupClass\GroupClassPage::render()` |
|
||||||
|
|
||||||
|
The shortcodes remain registered for back-compat; blocks and shortcodes share
|
||||||
|
the same page objects (constructed once in `Plugin::boot()`), so front-end
|
||||||
|
output is identical either way. Pasting a shortcode into the block editor
|
||||||
|
auto-converts it to the matching block via a `transforms.from` shortcode
|
||||||
|
transform.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
- **`BlockRegistrar`** (`src/BlockRegistrar.php`) hooks `init` and registers
|
||||||
|
each block with `register_block_type()`: a `render_callback` per block, the
|
||||||
|
shared editor script (`assets/js/blocks.js`, handle
|
||||||
|
`us-scheduler-blocks`), and the front-end stylesheet
|
||||||
|
(`assets/css/frontend.css`, handle `us-scheduler`) as the block `style` so
|
||||||
|
it also loads inside the editor and previews pick up theme styling.
|
||||||
|
- **`assets/js/blocks.js`** (vanilla JS, no build step) registers the client
|
||||||
|
side of each block — title, icon, keywords, shortcode transform — and
|
||||||
|
renders the editor preview with `wp.serverSideRender`, which fetches the
|
||||||
|
server-rendered markup via the `/wp/v2/block-renderer` REST route.
|
||||||
|
- **`BlockPreview`** (`src/BlockPreview.php`) supplies static, script-free
|
||||||
|
markup for editor previews. `BlockRegistrar::isEditorPreview()` detects the
|
||||||
|
block-renderer context via the `REST_REQUEST` constant (front-end template
|
||||||
|
rendering never happens inside a REST request) and renders the preview
|
||||||
|
instead of the live page.
|
||||||
|
|
||||||
|
## Editor preview behaviour
|
||||||
|
|
||||||
|
Live pages cannot run in the editor: booking and group classes are populated
|
||||||
|
by JavaScript making authenticated REST calls (and may load Stripe.js),
|
||||||
|
registration requires a valid invite token, and login short-circuits for
|
||||||
|
logged-in users (the editing admin always is). Each preview therefore
|
||||||
|
reproduces the live wrapper elements and CSS classes with representative
|
||||||
|
placeholder content:
|
||||||
|
|
||||||
|
- **Booking** — `#us-booking-app` with sample `.us-day` / `.us-slot` rows and
|
||||||
|
disabled Book buttons.
|
||||||
|
- **Group classes** — `#us-group-app` with a sample `.us-class` card and a
|
||||||
|
disabled Enrol button.
|
||||||
|
- **Login** — the real `templates/frontend/login-page.php` template (it has
|
||||||
|
no request-state dependencies).
|
||||||
|
- **Registration** — a disabled sample of the `.us-register-form` fields.
|
||||||
|
|
||||||
|
Each preview starts with a `.us-editor-note` paragraph explaining what the
|
||||||
|
published page shows instead. The note class only appears in editor previews.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- `tests/Unit/BlockRegistrarTest.php` — hook registration, block/asset
|
||||||
|
registration, front-end delegation to the page objects, preview-mode
|
||||||
|
routing.
|
||||||
|
- `tests/Unit/BlockPreviewTest.php` — preview markup mirrors the live CSS
|
||||||
|
classes/ids and includes the editor note.
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Unsupervised\Schedular;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static, script-free markup for the editor previews of the front-end blocks.
|
||||||
|
*
|
||||||
|
* The booking and group-class pages are populated by JavaScript on the live
|
||||||
|
* site, and the registration page requires a valid invite token — none of
|
||||||
|
* which exist inside the block editor. These previews reproduce the same
|
||||||
|
* wrapper elements and CSS classes the live pages use, filled with
|
||||||
|
* representative placeholder content, so themes can be styled against
|
||||||
|
* realistic markup without firing REST calls, redirects, or Stripe.js.
|
||||||
|
*/
|
||||||
|
class BlockPreview {
|
||||||
|
|
||||||
|
public static function booking(): string {
|
||||||
|
$days = [
|
||||||
|
[
|
||||||
|
'label' => __( 'Monday', 'unsupervised-schedular' ),
|
||||||
|
'slots' => [
|
||||||
|
[ '16:00–16:30', 30 ],
|
||||||
|
[ '16:30–17:00', 30 ],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => __( 'Wednesday', 'unsupervised-schedular' ),
|
||||||
|
'slots' => [
|
||||||
|
[ '17:00–17:45', 45 ],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$dayHtml = '';
|
||||||
|
foreach ( $days as $day ) {
|
||||||
|
$slotHtml = '';
|
||||||
|
foreach ( $day['slots'] as $slot ) {
|
||||||
|
$slotHtml .= sprintf(
|
||||||
|
'<div class="us-slot"><span>%s (%d min)</span><button type="button" class="us-book-btn" disabled>%s</button></div>',
|
||||||
|
esc_html( $slot[0] ),
|
||||||
|
(int) $slot[1],
|
||||||
|
esc_html__( 'Book', 'unsupervised-schedular' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dayHtml .= sprintf(
|
||||||
|
'<div class="us-day"><h3 class="us-day-heading">%s</h3>%s</div>',
|
||||||
|
esc_html( $day['label'] ),
|
||||||
|
$slotHtml
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf(
|
||||||
|
'<div id="us-booking-app">%s<div id="us-slot-list">%s</div></div>',
|
||||||
|
self::note( __( 'Editor preview — students see live availability on the published page.', 'unsupervised-schedular' ) ),
|
||||||
|
$dayHtml
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function groupClasses(): string {
|
||||||
|
return sprintf(
|
||||||
|
'<div id="us-group-app">%s<div id="us-group-list"><div class="us-class"><h3>%s</h3><p>%s</p><p>%s</p><p>25.00 CAD</p><button type="button" class="us-enrol-btn" disabled>%s</button></div></div></div>',
|
||||||
|
self::note( __( 'Editor preview — students see live group classes on the published page.', 'unsupervised-schedular' ) ),
|
||||||
|
esc_html__( 'Beginner Group Class', 'unsupervised-schedular' ),
|
||||||
|
esc_html__( 'Saturdays 10:00–11:00', 'unsupervised-schedular' ),
|
||||||
|
esc_html__( 'A sample class shown so the page can be styled.', 'unsupervised-schedular' ),
|
||||||
|
esc_html__( 'Enrol', 'unsupervised-schedular' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The live login form renders fine without any request state, so the
|
||||||
|
* preview includes the real template (the editing user is logged in, which
|
||||||
|
* would otherwise short-circuit to an "already logged in" message).
|
||||||
|
*/
|
||||||
|
public static function login(): string {
|
||||||
|
$error = '';
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
include USC_PLUGIN_DIR . 'templates/frontend/login-page.php';
|
||||||
|
|
||||||
|
return self::note( __( 'Editor preview — logged-in visitors are offered a link to the booking page instead.', 'unsupervised-schedular' ) ) . (string) ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function registration(): string {
|
||||||
|
$fields = sprintf(
|
||||||
|
'<p><label for="us-reg-email">%s</label><input type="email" id="us-reg-email" value="student@example.com" readonly></p>',
|
||||||
|
esc_html__( 'Email', 'unsupervised-schedular' )
|
||||||
|
);
|
||||||
|
$fields .= sprintf(
|
||||||
|
'<p><label for="us-reg-name">%s</label><input type="text" id="us-reg-name"></p>',
|
||||||
|
esc_html__( 'Your name', 'unsupervised-schedular' )
|
||||||
|
);
|
||||||
|
$fields .= sprintf(
|
||||||
|
'<p><label for="us-reg-pass">%s</label><input type="password" id="us-reg-pass"></p>',
|
||||||
|
esc_html__( 'Password', 'unsupervised-schedular' )
|
||||||
|
);
|
||||||
|
$fields .= sprintf(
|
||||||
|
'<p><input type="submit" value="%s" disabled></p>',
|
||||||
|
esc_attr__( 'Create Account', 'unsupervised-schedular' )
|
||||||
|
);
|
||||||
|
|
||||||
|
return sprintf(
|
||||||
|
'<div class="us-register-form">%s<form>%s</form></div>',
|
||||||
|
self::note( __( 'Editor preview — the live form requires a valid invite link and lists signup policies.', 'unsupervised-schedular' ) ),
|
||||||
|
$fields
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function note( string $text ): string {
|
||||||
|
return '<p class="us-editor-note">' . esc_html( $text ) . '</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Unsupervised\Schedular;
|
||||||
|
|
||||||
|
use Unsupervised\Schedular\Auth\LoginPage;
|
||||||
|
use Unsupervised\Schedular\Auth\RegistrationPage;
|
||||||
|
use Unsupervised\Schedular\Booking\BookingPage;
|
||||||
|
use Unsupervised\Schedular\GroupClass\GroupClassPage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers Gutenberg dynamic-block wrappers for the front-end shortcodes so
|
||||||
|
* the pages can be previewed and styled inside the block editor.
|
||||||
|
*
|
||||||
|
* On the front end each block delegates to the same page object its shortcode
|
||||||
|
* uses, so output is identical either way. Inside the editor (the
|
||||||
|
* block-renderer REST preview used by wp.serverSideRender) a static preview
|
||||||
|
* from BlockPreview is rendered instead — same markup and CSS classes, no
|
||||||
|
* live REST calls, redirects, or Stripe.js.
|
||||||
|
*/
|
||||||
|
class BlockRegistrar {
|
||||||
|
|
||||||
|
public const SCRIPT_HANDLE = 'us-scheduler-blocks';
|
||||||
|
public const STYLE_HANDLE = 'us-scheduler';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private BookingPage $bookingPage,
|
||||||
|
private LoginPage $loginPage,
|
||||||
|
private RegistrationPage $registrationPage,
|
||||||
|
private GroupClassPage $groupClassPage,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function register(): void {
|
||||||
|
add_action( 'init', [ $this, 'registerBlocks' ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function registerBlocks(): void {
|
||||||
|
// The editor script registers the client side of each block (title,
|
||||||
|
// icon, shortcode transform) and previews it via wp.serverSideRender.
|
||||||
|
wp_register_script(
|
||||||
|
self::SCRIPT_HANDLE,
|
||||||
|
USC_PLUGIN_URL . 'assets/js/blocks.js',
|
||||||
|
[ 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-server-side-render', 'wp-i18n' ],
|
||||||
|
USC_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// The front-end stylesheet doubles as the block style so editor
|
||||||
|
// previews look like the published page. ShortcodeRegistrar registers
|
||||||
|
// the same handle on the front end, hence the guard.
|
||||||
|
if ( ! wp_style_is( self::STYLE_HANDLE, 'registered' ) ) {
|
||||||
|
wp_register_style( self::STYLE_HANDLE, USC_PLUGIN_URL . 'assets/css/frontend.css', [], USC_VERSION );
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $this->blocks() as $name => $renderCallback ) {
|
||||||
|
register_block_type(
|
||||||
|
$name,
|
||||||
|
[
|
||||||
|
'api_version' => '3',
|
||||||
|
'editor_script' => self::SCRIPT_HANDLE,
|
||||||
|
'style' => self::STYLE_HANDLE,
|
||||||
|
'render_callback' => $renderCallback,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block names mapped to their render callbacks.
|
||||||
|
*
|
||||||
|
* @return array<string, callable(array<string, mixed>): string>
|
||||||
|
*/
|
||||||
|
private function blocks(): array {
|
||||||
|
return [
|
||||||
|
'us-scheduler/booking' => [ $this, 'renderBooking' ],
|
||||||
|
'us-scheduler/student-login' => [ $this, 'renderLogin' ],
|
||||||
|
'us-scheduler/student-register' => [ $this, 'renderRegistration' ],
|
||||||
|
'us-scheduler/group-classes' => [ $this, 'renderGroupClasses' ],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the booking block.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $attributes Block attributes (unused — the blocks have none yet).
|
||||||
|
*/
|
||||||
|
public function renderBooking( array $attributes = [] ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
|
||||||
|
return $this->isEditorPreview() ? BlockPreview::booking() : $this->bookingPage->render( [] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the student-login block.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $attributes Block attributes (unused — the blocks have none yet).
|
||||||
|
*/
|
||||||
|
public function renderLogin( array $attributes = [] ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
|
||||||
|
return $this->isEditorPreview() ? BlockPreview::login() : $this->loginPage->render( [] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the student-registration block.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $attributes Block attributes (unused — the blocks have none yet).
|
||||||
|
*/
|
||||||
|
public function renderRegistration( array $attributes = [] ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
|
||||||
|
return $this->isEditorPreview() ? BlockPreview::registration() : $this->registrationPage->render( [] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the group-classes block.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $attributes Block attributes (unused — the blocks have none yet).
|
||||||
|
*/
|
||||||
|
public function renderGroupClasses( array $attributes = [] ): string { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
|
||||||
|
return $this->isEditorPreview() ? BlockPreview::groupClasses() : $this->groupClassPage->render( [] );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this render is the editor's block-renderer REST preview rather
|
||||||
|
* than a real front-end page render. Front-end template rendering never
|
||||||
|
* happens inside a REST request, so REST_REQUEST is a reliable signal.
|
||||||
|
*/
|
||||||
|
protected function isEditorPreview(): bool {
|
||||||
|
return defined( 'REST_REQUEST' ) && (bool) constant( 'REST_REQUEST' );
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-1
@@ -4,10 +4,14 @@ declare(strict_types=1);
|
|||||||
namespace Unsupervised\Schedular;
|
namespace Unsupervised\Schedular;
|
||||||
|
|
||||||
use Unsupervised\Schedular\Auth\InviteRepository;
|
use Unsupervised\Schedular\Auth\InviteRepository;
|
||||||
|
use Unsupervised\Schedular\Auth\LoginPage;
|
||||||
|
use Unsupervised\Schedular\Auth\RegistrationPage;
|
||||||
use Unsupervised\Schedular\Auth\RoleManager;
|
use Unsupervised\Schedular\Auth\RoleManager;
|
||||||
|
use Unsupervised\Schedular\Booking\BookingPage;
|
||||||
use Unsupervised\Schedular\Availability\AvailabilityRepository;
|
use Unsupervised\Schedular\Availability\AvailabilityRepository;
|
||||||
use Unsupervised\Schedular\Booking\BookingRepository;
|
use Unsupervised\Schedular\Booking\BookingRepository;
|
||||||
use Unsupervised\Schedular\GroupClass\EnrollmentRepository;
|
use Unsupervised\Schedular\GroupClass\EnrollmentRepository;
|
||||||
|
use Unsupervised\Schedular\GroupClass\GroupClassPage;
|
||||||
use Unsupervised\Schedular\Offering\OfferingRepository;
|
use Unsupervised\Schedular\Offering\OfferingRepository;
|
||||||
use Unsupervised\Schedular\Payment\BillingMethodResolver;
|
use Unsupervised\Schedular\Payment\BillingMethodResolver;
|
||||||
use Unsupervised\Schedular\Payment\PaymentRepository;
|
use Unsupervised\Schedular\Payment\PaymentRepository;
|
||||||
@@ -48,9 +52,17 @@ class Plugin {
|
|||||||
$stripe = new StripeGateway( $settings );
|
$stripe = new StripeGateway( $settings );
|
||||||
$paymentService = new PaymentService( $paymentRepo, $resolver, new ReceiptMailer(), $bookings, $enrollments, $settings, $stripe );
|
$paymentService = new PaymentService( $paymentRepo, $resolver, new ReceiptMailer(), $bookings, $enrollments, $settings, $stripe );
|
||||||
|
|
||||||
|
// The shortcode and block wrappers share the same page objects so
|
||||||
|
// front-end output is identical whichever way a page embeds them.
|
||||||
|
$bookingPage = new BookingPage();
|
||||||
|
$loginPage = new LoginPage();
|
||||||
|
$registrationPage = new RegistrationPage( $invites, $policies, $policyVersions, $acceptances );
|
||||||
|
$groupClassPage = new GroupClassPage();
|
||||||
|
|
||||||
( new RoleManager() )->register();
|
( new RoleManager() )->register();
|
||||||
( new AdminMenu( $availability, $bookings, $offerings, $questions, $policies, $policyVersions, $policyService, $invites, $enrollments, $settings, $paymentRepo, $paymentService, $resolver ) )->register();
|
( new AdminMenu( $availability, $bookings, $offerings, $questions, $policies, $policyVersions, $policyService, $invites, $enrollments, $settings, $paymentRepo, $paymentService, $resolver ) )->register();
|
||||||
( new RestRegistrar( $availability, $bookings, $offerings, $questions, $policies, $policyVersions, $policyService, $registrationGate, $enrollments, $paymentService ) )->register();
|
( new RestRegistrar( $availability, $bookings, $offerings, $questions, $policies, $policyVersions, $policyService, $registrationGate, $enrollments, $paymentService ) )->register();
|
||||||
( new ShortcodeRegistrar( $invites, $policies, $policyVersions, $acceptances ) )->register();
|
( new ShortcodeRegistrar( $bookingPage, $loginPage, $registrationPage, $groupClassPage ) )->register();
|
||||||
|
( new BlockRegistrar( $bookingPage, $loginPage, $registrationPage, $groupClassPage ) )->register();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,34 +3,20 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Unsupervised\Schedular;
|
namespace Unsupervised\Schedular;
|
||||||
|
|
||||||
use Unsupervised\Schedular\Auth\InviteRepository;
|
|
||||||
use Unsupervised\Schedular\Auth\LoginPage;
|
use Unsupervised\Schedular\Auth\LoginPage;
|
||||||
use Unsupervised\Schedular\Auth\RegistrationPage;
|
use Unsupervised\Schedular\Auth\RegistrationPage;
|
||||||
use Unsupervised\Schedular\Booking\BookingPage;
|
use Unsupervised\Schedular\Booking\BookingPage;
|
||||||
use Unsupervised\Schedular\GroupClass\GroupClassPage;
|
use Unsupervised\Schedular\GroupClass\GroupClassPage;
|
||||||
use Unsupervised\Schedular\Payment\StudioSettings;
|
use Unsupervised\Schedular\Payment\StudioSettings;
|
||||||
use Unsupervised\Schedular\Policy\AcceptanceRepository;
|
|
||||||
use Unsupervised\Schedular\Policy\PolicyRepository;
|
|
||||||
use Unsupervised\Schedular\Policy\PolicyVersionRepository;
|
|
||||||
|
|
||||||
class ShortcodeRegistrar {
|
class ShortcodeRegistrar {
|
||||||
|
|
||||||
private BookingPage $bookingPage;
|
|
||||||
private LoginPage $loginPage;
|
|
||||||
private RegistrationPage $registrationPage;
|
|
||||||
private GroupClassPage $groupClassPage;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
InviteRepository $invites,
|
private BookingPage $bookingPage,
|
||||||
PolicyRepository $policies,
|
private LoginPage $loginPage,
|
||||||
PolicyVersionRepository $policyVersions,
|
private RegistrationPage $registrationPage,
|
||||||
AcceptanceRepository $acceptances,
|
private GroupClassPage $groupClassPage,
|
||||||
) {
|
) {}
|
||||||
$this->bookingPage = new BookingPage();
|
|
||||||
$this->loginPage = new LoginPage();
|
|
||||||
$this->registrationPage = new RegistrationPage( $invites, $policies, $policyVersions, $acceptances );
|
|
||||||
$this->groupClassPage = new GroupClassPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function register(): void {
|
public function register(): void {
|
||||||
add_shortcode( 'us_booking', [ $this->bookingPage, 'render' ] );
|
add_shortcode( 'us_booking', [ $this->bookingPage, 'render' ] );
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Unsupervised\Schedular\Tests\Unit;
|
||||||
|
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use Unsupervised\Schedular\BlockPreview;
|
||||||
|
|
||||||
|
class BlockPreviewTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testBookingPreviewMirrorsTheLiveMarkup(): void
|
||||||
|
{
|
||||||
|
$html = BlockPreview::booking();
|
||||||
|
|
||||||
|
self::assertStringContainsString('id="us-booking-app"', $html);
|
||||||
|
self::assertStringContainsString('id="us-slot-list"', $html);
|
||||||
|
self::assertStringContainsString('class="us-day"', $html);
|
||||||
|
self::assertStringContainsString('class="us-slot"', $html);
|
||||||
|
self::assertStringContainsString('class="us-book-btn" disabled', $html);
|
||||||
|
self::assertStringContainsString('us-editor-note', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGroupClassesPreviewMirrorsTheLiveMarkup(): void
|
||||||
|
{
|
||||||
|
$html = BlockPreview::groupClasses();
|
||||||
|
|
||||||
|
self::assertStringContainsString('id="us-group-app"', $html);
|
||||||
|
self::assertStringContainsString('id="us-group-list"', $html);
|
||||||
|
self::assertStringContainsString('class="us-class"', $html);
|
||||||
|
self::assertStringContainsString('class="us-enrol-btn" disabled', $html);
|
||||||
|
self::assertStringContainsString('us-editor-note', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testLoginPreviewIncludesTheRealLoginTemplate(): void
|
||||||
|
{
|
||||||
|
Functions\when('wp_nonce_field')->justReturn('');
|
||||||
|
|
||||||
|
$html = BlockPreview::login();
|
||||||
|
|
||||||
|
self::assertStringContainsString('class="us-login-form"', $html);
|
||||||
|
self::assertStringContainsString('name="log"', $html);
|
||||||
|
self::assertStringContainsString('name="pwd"', $html);
|
||||||
|
self::assertStringContainsString('us-editor-note', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRegistrationPreviewShowsADisabledSampleForm(): void
|
||||||
|
{
|
||||||
|
$html = BlockPreview::registration();
|
||||||
|
|
||||||
|
self::assertStringContainsString('class="us-register-form"', $html);
|
||||||
|
self::assertStringContainsString('id="us-reg-email"', $html);
|
||||||
|
self::assertStringContainsString('id="us-reg-name"', $html);
|
||||||
|
self::assertStringContainsString('id="us-reg-pass"', $html);
|
||||||
|
self::assertStringContainsString('disabled', $html);
|
||||||
|
self::assertStringContainsString('us-editor-note', $html);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Unsupervised\Schedular\Tests\Unit;
|
||||||
|
|
||||||
|
use Brain\Monkey\Actions;
|
||||||
|
use Brain\Monkey\Functions;
|
||||||
|
use Mockery;
|
||||||
|
use Unsupervised\Schedular\Auth\LoginPage;
|
||||||
|
use Unsupervised\Schedular\Auth\RegistrationPage;
|
||||||
|
use Unsupervised\Schedular\BlockRegistrar;
|
||||||
|
use Unsupervised\Schedular\Booking\BookingPage;
|
||||||
|
use Unsupervised\Schedular\GroupClass\GroupClassPage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test double exposing editor-preview mode as a switch, since the real
|
||||||
|
* detection relies on the REST_REQUEST constant which cannot be toggled
|
||||||
|
* within a single PHP process.
|
||||||
|
*/
|
||||||
|
class TestableBlockRegistrar extends BlockRegistrar
|
||||||
|
{
|
||||||
|
public bool $preview = false;
|
||||||
|
|
||||||
|
protected function isEditorPreview(): bool
|
||||||
|
{
|
||||||
|
return $this->preview;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BlockRegistrarTest extends TestCase
|
||||||
|
{
|
||||||
|
private BookingPage&Mockery\MockInterface $bookingPage;
|
||||||
|
private LoginPage&Mockery\MockInterface $loginPage;
|
||||||
|
private RegistrationPage&Mockery\MockInterface $registrationPage;
|
||||||
|
private GroupClassPage&Mockery\MockInterface $groupClassPage;
|
||||||
|
private TestableBlockRegistrar $registrar;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
|
||||||
|
$this->bookingPage = Mockery::mock(BookingPage::class);
|
||||||
|
$this->loginPage = Mockery::mock(LoginPage::class);
|
||||||
|
$this->registrationPage = Mockery::mock(RegistrationPage::class);
|
||||||
|
$this->groupClassPage = Mockery::mock(GroupClassPage::class);
|
||||||
|
|
||||||
|
$this->registrar = new TestableBlockRegistrar(
|
||||||
|
$this->bookingPage,
|
||||||
|
$this->loginPage,
|
||||||
|
$this->registrationPage,
|
||||||
|
$this->groupClassPage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRegisterHooksBlockRegistrationOntoInit(): void
|
||||||
|
{
|
||||||
|
Actions\expectAdded('init')->once()->with([$this->registrar, 'registerBlocks']);
|
||||||
|
|
||||||
|
$this->registrar->register();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRegisterBlocksRegistersAllFourBlocksWithAssets(): void
|
||||||
|
{
|
||||||
|
Functions\expect('wp_register_script')
|
||||||
|
->once()
|
||||||
|
->with(
|
||||||
|
BlockRegistrar::SCRIPT_HANDLE,
|
||||||
|
Mockery::pattern('~assets/js/blocks\.js$~'),
|
||||||
|
Mockery::type('array'),
|
||||||
|
USC_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
Functions\when('wp_style_is')->justReturn(false);
|
||||||
|
Functions\expect('wp_register_style')
|
||||||
|
->once()
|
||||||
|
->with(
|
||||||
|
BlockRegistrar::STYLE_HANDLE,
|
||||||
|
Mockery::pattern('~assets/css/frontend\.css$~'),
|
||||||
|
[],
|
||||||
|
USC_VERSION
|
||||||
|
);
|
||||||
|
|
||||||
|
$registered = [];
|
||||||
|
Functions\when('register_block_type')->alias(
|
||||||
|
static function (string $name, array $args) use (&$registered): bool {
|
||||||
|
$registered[$name] = $args;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->registrar->registerBlocks();
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
[
|
||||||
|
'us-scheduler/booking',
|
||||||
|
'us-scheduler/student-login',
|
||||||
|
'us-scheduler/student-register',
|
||||||
|
'us-scheduler/group-classes',
|
||||||
|
],
|
||||||
|
array_keys($registered)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($registered as $args) {
|
||||||
|
self::assertSame(BlockRegistrar::SCRIPT_HANDLE, $args['editor_script']);
|
||||||
|
self::assertSame(BlockRegistrar::STYLE_HANDLE, $args['style']);
|
||||||
|
self::assertIsCallable($args['render_callback']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRegisterBlocksDoesNotReRegisterAnAlreadyRegisteredStyle(): void
|
||||||
|
{
|
||||||
|
Functions\when('wp_register_script')->justReturn(true);
|
||||||
|
Functions\when('wp_style_is')->justReturn(true);
|
||||||
|
Functions\expect('wp_register_style')->never();
|
||||||
|
Functions\when('register_block_type')->justReturn(true);
|
||||||
|
|
||||||
|
$this->registrar->registerBlocks();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFrontEndRenderDelegatesToThePageObjects(): void
|
||||||
|
{
|
||||||
|
$this->registrar->preview = false;
|
||||||
|
|
||||||
|
$this->bookingPage->shouldReceive('render')->once()->with([])->andReturn('booking-html');
|
||||||
|
$this->loginPage->shouldReceive('render')->once()->with([])->andReturn('login-html');
|
||||||
|
$this->registrationPage->shouldReceive('render')->once()->with([])->andReturn('register-html');
|
||||||
|
$this->groupClassPage->shouldReceive('render')->once()->with([])->andReturn('group-html');
|
||||||
|
|
||||||
|
self::assertSame('booking-html', $this->registrar->renderBooking());
|
||||||
|
self::assertSame('login-html', $this->registrar->renderLogin());
|
||||||
|
self::assertSame('register-html', $this->registrar->renderRegistration());
|
||||||
|
self::assertSame('group-html', $this->registrar->renderGroupClasses());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEditorPreviewRendersStaticMarkupWithoutTouchingThePages(): void
|
||||||
|
{
|
||||||
|
$this->registrar->preview = true;
|
||||||
|
|
||||||
|
Functions\when('wp_nonce_field')->justReturn('');
|
||||||
|
|
||||||
|
$this->bookingPage->shouldNotReceive('render');
|
||||||
|
$this->loginPage->shouldNotReceive('render');
|
||||||
|
$this->registrationPage->shouldNotReceive('render');
|
||||||
|
$this->groupClassPage->shouldNotReceive('render');
|
||||||
|
|
||||||
|
self::assertStringContainsString('us-booking-app', $this->registrar->renderBooking());
|
||||||
|
self::assertStringContainsString('us-login-form', $this->registrar->renderLogin());
|
||||||
|
self::assertStringContainsString('us-register-form', $this->registrar->renderRegistration());
|
||||||
|
self::assertStringContainsString('us-group-app', $this->registrar->renderGroupClasses());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIsEditorPreviewIsFalseOutsideRestRequests(): void
|
||||||
|
{
|
||||||
|
// REST_REQUEST is undefined in the test process, so the real
|
||||||
|
// registrar must take the front-end path.
|
||||||
|
$registrar = new BlockRegistrar(
|
||||||
|
$this->bookingPage,
|
||||||
|
$this->loginPage,
|
||||||
|
$this->registrationPage,
|
||||||
|
$this->groupClassPage,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->bookingPage->shouldReceive('render')->once()->with([])->andReturn('live');
|
||||||
|
|
||||||
|
self::assertSame('live', $registrar->renderBooking());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user