feat: lit/tb-activate – the explicit switch, always a dry run first (answer F1)

- only _et_enabled; dry run in both directions lists conditions, exclusions,
  layouts, overlaps and possible overlaps (B9); confirm: true switches
- enabling needs conditions and published layouts (B10); a detached
  template needs reattach first; the default template is refused
- the off-switch reaches an attached, enabled template outside publish,
  e.g. in the trash (ruling Q16); result names the fast way back
- every switch in the change log; failed switch rolled back

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-27 16:33:29 +02:00
co-authored by Claude Opus 5.5
parent 0975b2f69b
commit 64fc439cb2
6 changed files with 598 additions and 4 deletions
@@ -157,15 +157,27 @@ class ThemeBuilderAccess
/**
* A template the Theme Builder tools may change (lit/tb-assign, lit/tb-activate): rights and group gate
* (template(), a template the user may not edit reads as missing – ruling Q18), no Theme Builder draft (E7),
* never the default template (E8), the current template_rev.
* never the default template (E8), the current template_rev. $offSwitch (lit/tb-activate with enabled: false,
* ruling Q16): also a template visitors see although it is not published (e.g. in the trash – Divi renders
* every attached, enabled template, B8), so it can always be switched off; any other unpublished template
* still reads as missing.
*
* @param array<string, mixed> $input template_id, template_rev
*/
public function changeableTemplate(array $input): Template|WP_Error
public function changeableTemplate(array $input, bool $offSwitch = false): Template|WP_Error
{
$post = $this->template($input['template_id'] ?? null, true);
if ($post instanceof WP_Error) {
return $post;
$id = $offSwitch && Errors::codeOf($post) === 'not_found'
? $this->liveUnpublished($input['template_id'] ?? null)
: null;
if ($id === null) {
return $post;
}
$gate = $this->writableGate();
return $gate ?? $this->checkedTemplate($id, $input);
}
return $this->checkedTemplate((int) $post->ID, $input);
@@ -262,6 +274,27 @@ class ThemeBuilderAccess
);
}
/**
* ID of a template that is not published but live (listed by the live Theme Builder post and enabled) and the
* user may edit; null otherwise – the raw lookup of the off-switch (ruling Q16).
*/
private function liveUnpublished(mixed $templateId): ?int
{
$id = PostGuard::id($templateId);
$post = $id === null ? null : get_post($id);
if (
! $post instanceof WP_Post
|| $post->post_type !== DiviThemeBuilder::TEMPLATE
|| (string) $post->post_status === 'publish'
|| ! $this->canEditTemplate((int) $post->ID)
|| ! in_array((int) $post->ID, $this->store->templateIds($this->store->liveId()), true)
) {
return null;
}
return $this->store->template((int) $post->ID, true)?->isLive() === true ? (int) $post->ID : null;
}
/**
* The draft, default and template_rev checks of changeableTemplate() for template $id (rights already checked).
*