Omit the class description when the group block shows a single class #113

Merged
thatguygriff merged 1 commits from feature/group-class-single-embed-description into main 2026-07-28 13:48:07 +00:00
9 changed files with 62 additions and 10 deletions
Showing only changes of commit c4acdb7ca4 - Show all commits
+3
View File
@@ -13,6 +13,9 @@ each change under the current top section as you work.
## [1.2.2] ## [1.2.2]
### Added
- The **Group Classes** block can now be pinned to a single class, under **Classes shown → Class** in the block sidebar (shortcode: `[us_group_classes offering="…"]`). Pick a class and the block shows only that one, so it can be embedded on a page that describes the class. In this mode the class's own description is left out to avoid repeating the page copy — the card shows the schedule, instructor, price, enrolment deadline and the enrol/withdraw controls. Leaving it on **All classes** keeps the full browsable catalog with descriptions.
## [1.2.1] ## [1.2.1]
### Fixed ### Fixed
+1 -1
View File
@@ -192,7 +192,7 @@
{ title: __('Classes shown', 'unsupervised-schedular') }, { title: __('Classes shown', 'unsupervised-schedular') },
el(GroupClassSelect, { el(GroupClassSelect, {
label: __('Class', 'unsupervised-schedular'), label: __('Class', 'unsupervised-schedular'),
help: __('Show only one group class, for embedding on a page dedicated to it.', 'unsupervised-schedular'), help: __('Show only one group class, for embedding on a page dedicated to it. That classs description is left out — the card shows just the schedule, price and enrolment controls.', 'unsupervised-schedular'),
value: attributes.offeringId, value: attributes.offeringId,
onChange: (offeringId) => setAttributes({ offeringId }), onChange: (offeringId) => setAttributes({ offeringId }),
}) })
+4 -1
View File
@@ -12,6 +12,9 @@
// When the shortcode/block pins a single offering, only that class is // When the shortcode/block pins a single offering, only that class is
// shown, so the page can be embedded alongside a full class description. // shown, so the page can be embedded alongside a full class description.
// The class's own description is then omitted from the card — the page it
// sits on already describes the class — leaving the schedule, price and
// enrolment controls.
const singleOfferingId = Number(app.dataset.offering || 0); const singleOfferingId = Number(app.dataset.offering || 0);
function apiFetch(path, options = {}) { function apiFetch(path, options = {}) {
@@ -148,7 +151,7 @@
${whenLabel(o) ? `<p class="us-class-when">${escHtml(whenLabel(o))}</p>` : ''} ${whenLabel(o) ? `<p class="us-class-when">${escHtml(whenLabel(o))}</p>` : ''}
${o.instructor_name ? `<p class="us-class-instructor">With ${escHtml(o.instructor_name)}</p>` : ''} ${o.instructor_name ? `<p class="us-class-instructor">With ${escHtml(o.instructor_name)}</p>` : ''}
${o.schedule_note ? `<p>${escHtml(o.schedule_note)}</p>` : ''} ${o.schedule_note ? `<p>${escHtml(o.schedule_note)}</p>` : ''}
${o.description ? `<p>${escHtml(o.description)}</p>` : ''} ${!singleOfferingId && o.description ? `<p>${escHtml(o.description)}</p>` : ''}
<p>${escHtml(Number(o.price).toFixed(2))} ${escHtml(o.currency)}</p> <p>${escHtml(Number(o.price).toFixed(2))} ${escHtml(o.currency)}</p>
${!enrolledMap.has(Number(o.id)) && isEnrollmentOpen(o) && enrolmentDeadline(o) ${!enrolledMap.has(Number(o.id)) && isEnrollmentOpen(o) && enrolmentDeadline(o)
? `<p class="us-enrol-deadline">Enrol by ${escHtml(formatDate(enrolmentDeadline(o)))}</p>` ? `<p class="us-enrol-deadline">Enrol by ${escHtml(formatDate(enrolmentDeadline(o)))}</p>`
+4 -2
View File
@@ -30,7 +30,7 @@ Four blocks have sidebar (inspector) options:
| `us-scheduler/student-login` | `bookingPageId` (number) | `0` | Page the "View available lessons" link points to for logged-in visitors, and the post-login redirect target. `0` = the current page. | | `us-scheduler/student-login` | `bookingPageId` (number) | `0` | Page the "View available lessons" link points to for logged-in visitors, and the post-login redirect target. `0` = the current page. |
| `us-scheduler/student-login` | `autoRedirect` (boolean) | `false` | Send logged-in visitors straight to the booking page instead of showing the link. Does nothing until a booking page is chosen. | | `us-scheduler/student-login` | `autoRedirect` (boolean) | `false` | Send logged-in visitors straight to the booking page instead of showing the link. Does nothing until a booking page is chosen. |
| `us-scheduler/student-register` | `loginPageId` (number) | `0` | Page the "Sign in to your account" link points to after a student confirms their email. `0` = the WordPress login screen. Shortcode equivalent: `[us_student_register login_page_id="…"]`. | | `us-scheduler/student-register` | `loginPageId` (number) | `0` | Page the "Sign in to your account" link points to after a student confirms their email. `0` = the WordPress login screen. Shortcode equivalent: `[us_student_register login_page_id="…"]`. |
| `us-scheduler/group-classes` | `offeringId` (number) | `0` | Restrict the page to a single group class, for embedding on a page dedicated to that class. `0` = browse all classes. Shortcode equivalent: `[us_group_classes offering="…"]`. | | `us-scheduler/group-classes` | `offeringId` (number) | `0` | Restrict the page to a single group class, for embedding on a page dedicated to that class. The class description is then omitted — only the schedule, instructor, price and enrolment controls are shown, so the surrounding page's own copy is not repeated. `0` = browse all classes, descriptions included. Shortcode equivalent: `[us_group_classes offering="…"]`. |
The page selects list all published pages; if a chosen page is later deleted, The page selects list all published pages; if a chosen page is later deleted,
the blocks fall back to their defaults. The group-classes block's class the blocks fall back to their defaults. The group-classes block's class
@@ -78,7 +78,9 @@ placeholder content:
- **Booking** — `#us-booking-app` with sample `.us-day` / `.us-slot` rows and - **Booking** — `#us-booking-app` with sample `.us-day` / `.us-slot` rows and
disabled Book buttons. disabled Book buttons.
- **Group classes** — `#us-group-app` with a sample `.us-class` card and a - **Group classes** — `#us-group-app` with a sample `.us-class` card and a
disabled Enrol button. disabled Enrol button. When `offeringId` pins a single class the preview
drops the sample description, matching what the live page renders in that
mode.
- **Login** — the real `templates/frontend/login-page.php` template (it has - **Login** — the real `templates/frontend/login-page.php` template (it has
no request-state dependencies). no request-state dependencies).
- **Registration** — a disabled sample of the `.us-register-form` fields. - **Registration** — a disabled sample of the `.us-register-form` fields.
+1 -1
View File
@@ -176,7 +176,7 @@ class becomes enrollable for them — they choose whether to enrol.
- Model: `Unsupervised\Schedular\GroupClass\Enrollment` - Model: `Unsupervised\Schedular\GroupClass\Enrollment`
- Admin controller: `Unsupervised\Schedular\GroupClass\GroupClassController``renderPage` (studio admin per-class summary, `view_all_lessons`) and `renderInstructorPage` (instructor summary + `?class_id` roster detail, `view_own_lessons`) - Admin controller: `Unsupervised\Schedular\GroupClass\GroupClassController``renderPage` (studio admin per-class summary, `view_all_lessons`) and `renderInstructorPage` (instructor summary + `?class_id` roster detail, `view_own_lessons`)
- REST endpoint: `Unsupervised\Schedular\GroupClass\EnrollmentEndpoint` - REST endpoint: `Unsupervised\Schedular\GroupClass\EnrollmentEndpoint`
- Frontend: `Unsupervised\Schedular\GroupClass\GroupClassPage` (`[us_group_classes]` shortcode; `offering="…"` restricts it to a single class for embedding on a dedicated page — the block equivalent is the `offeringId` attribute) - Frontend: `Unsupervised\Schedular\GroupClass\GroupClassPage` (`[us_group_classes]` shortcode; `offering="…"` restricts it to a single class for embedding on a dedicated page — the block equivalent is the `offeringId` attribute). In single-class mode `assets/js/group-classes.js` leaves the class description out of the card, since the page it is embedded on already describes the class; the schedule, instructor, schedule note, price and enrolment controls are still shown.
- Reuses `Registration\RegistrationGate` (intake answers + booking-scoped policy acceptance, type `enrollment`) - Reuses `Registration\RegistrationGate` (intake answers + booking-scoped policy acceptance, type `enrollment`)
> **Payment:** a priced enrolment creates a payment via `Payment\PaymentService` > **Payment:** a priced enrolment creates a payment via `Payment\PaymentService`
+20 -4
View File
@@ -58,13 +58,29 @@ class BlockPreview {
); );
} }
public static function groupClasses(): string { /**
* Sample group-class card.
*
* @param bool $singleClass Whether the block is pinned to one class, in
* which case the live page omits the class
* description and the preview does too.
*/
public static function groupClasses( bool $singleClass = false ): string {
$note = $singleClass
? __( 'Editor preview — the published page shows the chosen class with its live schedule and enrolment status.', 'unsupervised-schedular' )
: __( 'Editor preview — students see live group classes on the published page.', 'unsupervised-schedular' );
$description = $singleClass
? ''
: '<p>' . esc_html__( 'A sample class shown so the page can be styled.', 'unsupervised-schedular' ) . '</p>';
return sprintf( 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>', '<div id="us-group-app">%s<div id="us-group-list"><div class="us-class"><h3>%s</h3><p class="us-class-when">%s</p>%s<p>25.00 CAD</p><p class="us-enrol-deadline">%s</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' ) ), self::note( $note ),
esc_html__( 'Beginner Group Class', 'unsupervised-schedular' ), esc_html__( 'Beginner Group Class', 'unsupervised-schedular' ),
esc_html__( 'Saturdays 10:00 AM11:00 AM', 'unsupervised-schedular' ), esc_html__( 'Saturdays 10:00 AM11:00 AM', 'unsupervised-schedular' ),
esc_html__( 'A sample class shown so the page can be styled.', 'unsupervised-schedular' ), $description,
esc_html__( 'Enrol by Sep 6, 2026', 'unsupervised-schedular' ),
esc_html__( 'Enrol', 'unsupervised-schedular' ) esc_html__( 'Enrol', 'unsupervised-schedular' )
); );
} }
+5 -1
View File
@@ -160,7 +160,11 @@ class BlockRegistrar {
* @param array<string, mixed> $attributes Block attributes. * @param array<string, mixed> $attributes Block attributes.
*/ */
public function renderGroupClasses( array $attributes = [] ): string { public function renderGroupClasses( array $attributes = [] ): string {
return $this->isEditorPreview() ? BlockPreview::groupClasses() : $this->groupClassPage->render( $attributes ); if ( ! $this->isEditorPreview() ) {
return $this->groupClassPage->render( $attributes );
}
return BlockPreview::groupClasses( Val::int( $attributes['offeringId'] ?? 0 ) > 0 );
} }
/** /**
+11
View File
@@ -29,6 +29,17 @@ class BlockPreviewTest extends TestCase
self::assertStringContainsString('class="us-class"', $html); self::assertStringContainsString('class="us-class"', $html);
self::assertStringContainsString('class="us-enrol-btn" disabled', $html); self::assertStringContainsString('class="us-enrol-btn" disabled', $html);
self::assertStringContainsString('us-editor-note', $html); self::assertStringContainsString('us-editor-note', $html);
self::assertStringContainsString('A sample class shown so the page can be styled.', $html);
}
public function testSingleClassGroupPreviewDropsTheDescriptionButKeepsScheduleAndEnrolment(): void
{
$html = BlockPreview::groupClasses(true);
self::assertStringNotContainsString('A sample class shown so the page can be styled.', $html);
self::assertStringContainsString('class="us-class-when"', $html);
self::assertStringContainsString('class="us-enrol-deadline"', $html);
self::assertStringContainsString('class="us-enrol-btn" disabled', $html);
} }
public function testLoginPreviewIncludesTheRealLoginTemplate(): void public function testLoginPreviewIncludesTheRealLoginTemplate(): void
+13
View File
@@ -179,6 +179,19 @@ class BlockRegistrarTest extends TestCase
self::assertStringContainsString('us-group-app', $this->registrar->renderGroupClasses()); self::assertStringContainsString('us-group-app', $this->registrar->renderGroupClasses());
} }
public function testEditorPreviewOfAPinnedGroupClassOmitsTheDescription(): void
{
$this->registrar->preview = true;
$this->groupClassPage->shouldNotReceive('render');
$all = $this->registrar->renderGroupClasses();
$single = $this->registrar->renderGroupClasses(['offeringId' => 12]);
self::assertStringContainsString('A sample class shown so the page can be styled.', $all);
self::assertStringNotContainsString('A sample class shown so the page can be styled.', $single);
self::assertStringContainsString('us-enrol-deadline', $single);
}
public function testIsEditorPreviewIsFalseOutsideRestRequests(): void public function testIsEditorPreviewIsFalseOutsideRestRequests(): void
{ {
// REST_REQUEST is undefined in the test process, so the real // REST_REQUEST is undefined in the test process, so the real