fix: TB dry runs hide uneditable live templates, off-switch despite draft (rulings Q20, Q21)
- ThemeBuilderAccess::editableLiveTemplates() shared by tb-list and the tb-assign/tb-activate dry runs; uneditable live templates only as a count (hidden_overlaps, hidden_live) - tb-activate enabled:false works while the Theme Builder has a draft, with a warning - possible_overlaps skips identical conditions; reattach records the previous enabled state; exclude_from must be a list; tb-activate live from the read-back Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,12 +24,18 @@ defined('ABSPATH') || exit;
|
||||
* in the change log. Refused for the default template, a template the live Theme Builder does not list (reattach
|
||||
* first) and, when enabling, one without conditions (it would apply nowhere) or with a layout that is no longer
|
||||
* published (B10). Switching off is the fast way back and fail-safe: it also reaches a live template that is not
|
||||
* published, e.g. in the trash (ruling Q16), and never needs the layouts.
|
||||
* published, e.g. in the trash (ruling Q16), never needs the layouts and works despite a Theme Builder draft (with
|
||||
* DRAFT_WARNING, ruling Q20). Overlaps name only templates the user may edit, the others as a count (ruling Q21).
|
||||
*/
|
||||
final class TbActivateAbility implements Ability
|
||||
{
|
||||
public const REASON = 'Die Vorlage konnte nicht umgeschaltet werden.';
|
||||
|
||||
/** Ruling Q20: the off-switch works despite a Theme Builder draft – which may restore its own state. */
|
||||
public const DRAFT_WARNING = 'Der Divi Theme Builder hat ungespeicherte Änderungen (Entwurf); wird ein dort '
|
||||
. 'geöffnetes Fenster gespeichert, stellt Divi evtl. seinen eigenen Stand wieder her und die Vorlage ist '
|
||||
. 'wieder aktiv – danach mit lit/tb-list prüfen oder die Änderungen im Theme Builder verwerfen.';
|
||||
|
||||
public function __construct(
|
||||
private ThemeBuilderAccess $access,
|
||||
private TemplateConditions $conditions,
|
||||
@@ -171,15 +177,26 @@ final class TbActivateAbility implements Ability
|
||||
$template->status === 'publish' ? '' : sprintf(' (im Papierkorb oder Status „%s“)', $template->status),
|
||||
implode('; ', $labels)
|
||||
);
|
||||
$live = $enabled ? $this->access->store()->liveTemplates() : [];
|
||||
$overlaps = Template::overlapDetails(
|
||||
$enabled ? $this->access->editableLiveTemplates() : ['templates' => [], 'hidden' => []],
|
||||
$template->id,
|
||||
$template->useOn
|
||||
);
|
||||
$message .= Template::hiddenOverlapsNote($overlaps['hidden_overlaps']);
|
||||
if (! $enabled && $this->draftPending()) {
|
||||
$message .= ' ' . self::DRAFT_WARNING;
|
||||
}
|
||||
|
||||
return Errors::confirmRequired($message, [
|
||||
'use_on' => $labels,
|
||||
'exclude_from' => array_map($this->conditions->label(...), $template->excludeFrom),
|
||||
'layouts' => $template->layouts,
|
||||
'overlaps' => Template::overlaps($live, $template->id, $template->useOn),
|
||||
'possible_overlaps' => Template::possibleOverlaps($live, $template->id, $template->useOn),
|
||||
]);
|
||||
] + $overlaps);
|
||||
}
|
||||
|
||||
private function draftPending(): bool
|
||||
{
|
||||
return $this->access->store()->draftId() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,6 +220,9 @@ final class TbActivateAbility implements Ability
|
||||
. 'erst nach „Statische CSS-Dateien leeren“ (Divi → Theme-Optionen → Builder).',
|
||||
$warnings
|
||||
);
|
||||
if (! $enabled && $this->draftPending()) {
|
||||
$warnings[] = self::DRAFT_WARNING;
|
||||
}
|
||||
$logId = $this->log->record(
|
||||
$this->name(),
|
||||
DiviThemeBuilder::TEMPLATE,
|
||||
@@ -214,7 +234,7 @@ final class TbActivateAbility implements Ability
|
||||
$template->useOn === [] ? 'keine' : implode(', ', $template->useOn)
|
||||
)
|
||||
);
|
||||
$after = $store->template($template->id, true) ?? $template;
|
||||
$after = $store->template($template->id, true);
|
||||
|
||||
$summary = $enabled
|
||||
? sprintf(
|
||||
@@ -224,7 +244,7 @@ final class TbActivateAbility implements Ability
|
||||
)
|
||||
: sprintf('Vorlage „%s“ deaktiviert – sie gilt nicht mehr für Besucher.', $template->title);
|
||||
|
||||
return $this->result($after, $summary, $logId, $enabled, $warnings);
|
||||
return $this->result($after ?? $template, $summary, $logId, $enabled, $after?->isLive() ?? $enabled, $warnings);
|
||||
}
|
||||
|
||||
/** @throws WriteFailed Always. */
|
||||
@@ -260,13 +280,14 @@ final class TbActivateAbility implements Ability
|
||||
string $summary,
|
||||
?int $logId,
|
||||
?bool $enabled = null,
|
||||
?bool $live = null,
|
||||
array $warnings = []
|
||||
): array {
|
||||
return ToolResult::ok($summary)
|
||||
->with('template_id', $template->id)
|
||||
->with('template_rev', $template->rev())
|
||||
->with('enabled', $enabled ?? $template->enabled)
|
||||
->with('live', $enabled ?? $template->isLive())
|
||||
->with('live', $live ?? $template->isLive())
|
||||
->warnings($warnings)
|
||||
->logId($logId)
|
||||
->urls(null, (string) admin_url('admin.php?page=et_theme_builder'))
|
||||
|
||||
@@ -118,9 +118,14 @@ final class TbAssignAbility implements Ability
|
||||
$useOn = is_array($input['use_on'] ?? null)
|
||||
? $this->conditions->check($input['use_on'], 'use_on')
|
||||
: ['ids' => [], 'errors' => ['use_on' => 'Liste von Bedingungs-IDs erwartet (leere Liste = keine)']];
|
||||
$excludeFrom = array_key_exists('exclude_from', $input)
|
||||
? $this->conditions->check($input['exclude_from'] ?? [], 'exclude_from')
|
||||
: ['ids' => $template->excludeFrom, 'errors' => []];
|
||||
$excludeFrom = match (true) {
|
||||
! array_key_exists('exclude_from', $input) => ['ids' => $template->excludeFrom, 'errors' => []],
|
||||
is_array($input['exclude_from']) => $this->conditions->check($input['exclude_from'], 'exclude_from'),
|
||||
default => [
|
||||
'ids' => [],
|
||||
'errors' => ['exclude_from' => 'Liste von Bedingungs-IDs erwartet ([] entfernt die Ausnahmen)'],
|
||||
],
|
||||
};
|
||||
$errors = array_merge($useOn['errors'], $excludeFrom['errors']);
|
||||
if ($errors !== []) {
|
||||
return Errors::invalidInput('Bedingungen sind ungültig; nichts wurde gespeichert.', $errors);
|
||||
@@ -181,20 +186,19 @@ final class TbAssignAbility implements Ability
|
||||
*/
|
||||
private function dryRun(Template $template, array $useOn, array $excludeFrom): WP_Error
|
||||
{
|
||||
$live = $this->access->store()->liveTemplates();
|
||||
$overlaps = Template::overlapDetails($this->access->editableLiveTemplates(), $template->id, $useOn);
|
||||
|
||||
return Errors::confirmRequired(
|
||||
sprintf(
|
||||
'Vorlage „%s“ ist aktiv; die neuen Bedingungen (use_on) und Ausnahmen (exclude_from) wirken sofort auf '
|
||||
. 'der Website – vorher/nachher unter before/after. Nichts wurde gespeichert (Probelauf).',
|
||||
$template->title
|
||||
. 'der Website – vorher/nachher unter before/after.%s Nichts wurde gespeichert (Probelauf).',
|
||||
$template->title,
|
||||
Template::hiddenOverlapsNote($overlaps['hidden_overlaps'])
|
||||
),
|
||||
[
|
||||
'before' => $this->labels($template->useOn, $template->excludeFrom),
|
||||
'after' => $this->labels($useOn, $excludeFrom),
|
||||
'overlaps' => Template::overlaps($live, $template->id, $useOn),
|
||||
'possible_overlaps' => Template::possibleOverlaps($live, $template->id, $useOn),
|
||||
]
|
||||
] + $overlaps
|
||||
);
|
||||
}
|
||||
|
||||
@@ -256,7 +260,12 @@ final class TbAssignAbility implements Ability
|
||||
sprintf(
|
||||
'Theme Builder: Bedingungen von „%s“ gesetzt%s (vorher: %s; Ausnahmen vorher: %s)',
|
||||
$template->title,
|
||||
$reattach ? ', wieder zugeordnet (deaktiviert)' : '',
|
||||
$reattach
|
||||
? sprintf(
|
||||
', wieder zugeordnet (deaktiviert, vorher %s)',
|
||||
$template->enabled ? 'aktiviert' : 'deaktiviert'
|
||||
)
|
||||
: '',
|
||||
self::listed($template->useOn),
|
||||
self::listed($template->excludeFrom)
|
||||
)
|
||||
@@ -276,7 +285,8 @@ final class TbAssignAbility implements Ability
|
||||
->with('template_id', $template->id)
|
||||
->with('template_rev', $after?->rev() ?? '')
|
||||
->with('live', $live)
|
||||
->with('previous', ['use_on' => $template->useOn, 'exclude_from' => $template->excludeFrom])
|
||||
->with('previous', ['use_on' => $template->useOn, 'exclude_from' => $template->excludeFrom]
|
||||
+ ($reattach ? ['enabled' => $template->enabled] : []))
|
||||
->warnings($warnings)
|
||||
->logId($logId)
|
||||
->urls(null, (string) admin_url('admin.php?page=et_theme_builder'))
|
||||
|
||||
@@ -19,8 +19,8 @@ defined('ABSPATH') || exit;
|
||||
* Divi's unused mark – Divi trashes marked posts 7 days later, F19, stage3b finding 10) so they can be
|
||||
* reattached; optionally Divi's condition catalog. Needs ThemeBuilderAccess::canUse(). Ruling Q8/B11: a
|
||||
* template the user may not edit does not appear at all (not listed, not in shared_with, asked for by ID it
|
||||
* reads like a missing one – P12); layout titles only for layouts the user may edit; condition labels name
|
||||
* objects by the stored ID only, never by title.
|
||||
* reads like a missing one – P12) – active ones only as a count (hidden_live, ruling Q21); layout titles only for
|
||||
* layouts the user may edit; condition labels name objects by the stored ID only, never by title.
|
||||
*/
|
||||
final class TbListAbility implements Ability
|
||||
{
|
||||
@@ -71,12 +71,16 @@ final class TbListAbility implements Ability
|
||||
'properties' => [
|
||||
'theme_builder_id' => ['type' => 'integer'],
|
||||
'draft_pending' => ['type' => 'boolean'],
|
||||
'hidden_live' => [
|
||||
'type' => 'integer',
|
||||
'description' => 'Aktive Vorlagen, die der Nutzer nicht bearbeiten darf (nur die Anzahl).',
|
||||
],
|
||||
'templates' => $objects,
|
||||
'detached' => $objects,
|
||||
'conditions' => $objects,
|
||||
'notes' => ['type' => 'array', 'items' => ['type' => 'string']],
|
||||
],
|
||||
'required' => ['theme_builder_id', 'draft_pending', 'templates', 'detached', 'notes'],
|
||||
'required' => ['theme_builder_id', 'draft_pending', 'hidden_live', 'templates', 'detached', 'notes'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -103,12 +107,13 @@ final class TbListAbility implements Ability
|
||||
}
|
||||
|
||||
$store = $this->access->store();
|
||||
$editable = fn (Template $template): bool => $this->access->canEditTemplate($template->id);
|
||||
$live = array_values(array_filter($store->liveTemplates(), $editable));
|
||||
$split = $this->access->editableLiveTemplates();
|
||||
$live = $split['templates'];
|
||||
$hiddenLive = count(array_filter($split['hidden'], static fn (Template $t): bool => $t->isLive()));
|
||||
$detached = [];
|
||||
foreach ($store->detachedIds(self::DETACHED_LIMIT) as $id) {
|
||||
$template = $store->template($id, false);
|
||||
if ($template !== null && $editable($template)) {
|
||||
if ($template !== null && $this->access->canEditTemplate($template->id)) {
|
||||
$detached[] = $template;
|
||||
}
|
||||
}
|
||||
@@ -134,7 +139,15 @@ final class TbListAbility implements Ability
|
||||
$notes = [];
|
||||
if ($draft) {
|
||||
$notes[] = 'Der Theme Builder hat ungespeicherte Änderungen (Entwurf); bis sie dort gespeichert oder '
|
||||
. 'verworfen sind, ändert LIT MCP keine Vorlagen.';
|
||||
. 'verworfen sind, ändert LIT MCP keine Vorlagen – nur ausblenden (lit/tb-activate mit enabled: '
|
||||
. 'false) geht.';
|
||||
}
|
||||
if ($hiddenLive > 0) {
|
||||
$notes[] = sprintf(
|
||||
'Weitere aktive Vorlagen ohne Berechtigung: %d (nicht aufgeführt; sie können sich mit den '
|
||||
. 'aufgeführten überschneiden).',
|
||||
$hiddenLive
|
||||
);
|
||||
}
|
||||
if ($detached !== []) {
|
||||
$notes[] = 'detached: Diese Vorlagen hat ein späteres Speichern im Theme Builder entfernt – oder jemand '
|
||||
@@ -146,6 +159,7 @@ final class TbListAbility implements Ability
|
||||
$result = [
|
||||
'theme_builder_id' => $store->liveId(),
|
||||
'draft_pending' => $draft,
|
||||
'hidden_live' => $hiddenLive,
|
||||
'templates' => array_map(fn (Template $t): array => $this->describe($t, $shared), $live),
|
||||
'detached' => array_map(fn (Template $t): array => $this->describe($t, $shared), $detached),
|
||||
'notes' => $notes,
|
||||
|
||||
@@ -164,8 +164,11 @@ final class Template
|
||||
}
|
||||
$conditions = [];
|
||||
foreach ($other->useOn as $theirs) {
|
||||
if (in_array($theirs, $useOn, true)) {
|
||||
continue; // identical: overlaps()
|
||||
}
|
||||
foreach ($useOn as $ours) {
|
||||
if ($theirs !== $ours && self::mayOverlap($theirs, $ours)) {
|
||||
if (self::mayOverlap($theirs, $ours)) {
|
||||
$conditions[] = $theirs;
|
||||
break;
|
||||
}
|
||||
@@ -179,6 +182,38 @@ final class Template
|
||||
return $overlaps;
|
||||
}
|
||||
|
||||
/**
|
||||
* The overlap details of a dry run (tb-assign, tb-activate) for template $exceptId with conditions $useOn:
|
||||
* identical and possible overlaps with the live templates the user may edit, and only the number of
|
||||
* overlapping live templates the user may not edit (ruling Q21, Q8/B11).
|
||||
*
|
||||
* @param array{templates: list<Template>, hidden: list<Template>} $live ThemeBuilderAccess::editableLiveTemplates()
|
||||
* @param list<string> $useOn
|
||||
* @return array{overlaps: list<array{id: int, title: string, conditions: list<string>}>,
|
||||
* possible_overlaps: list<array{id: int, title: string, conditions: list<string>}>, hidden_overlaps: int}
|
||||
*/
|
||||
public static function overlapDetails(array $live, int $exceptId, array $useOn): array
|
||||
{
|
||||
$hidden = array_merge(
|
||||
self::overlaps($live['hidden'], $exceptId, $useOn),
|
||||
self::possibleOverlaps($live['hidden'], $exceptId, $useOn)
|
||||
);
|
||||
|
||||
return [
|
||||
'overlaps' => self::overlaps($live['templates'], $exceptId, $useOn),
|
||||
'possible_overlaps' => self::possibleOverlaps($live['templates'], $exceptId, $useOn),
|
||||
'hidden_overlaps' => count(array_unique(array_column($hidden, 'id'))),
|
||||
];
|
||||
}
|
||||
|
||||
/** The sentence for hidden_overlaps > 0 (never names them). */
|
||||
public static function hiddenOverlapsNote(int $count): string
|
||||
{
|
||||
return $count > 0
|
||||
? sprintf(' Weitere aktive Vorlagen ohne Berechtigung, die sich überschneiden können: %d.', $count)
|
||||
: '';
|
||||
}
|
||||
|
||||
private static function mayOverlap(string $a, string $b): bool
|
||||
{
|
||||
foreach (self::scopesOf($a) as $x) {
|
||||
|
||||
@@ -160,7 +160,7 @@ class ThemeBuilderAccess
|
||||
* 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.
|
||||
* still reads as missing. The off-switch also works while the Theme Builder has unsaved changes (ruling Q20).
|
||||
*
|
||||
* @param array<string, mixed> $input template_id, template_rev
|
||||
*/
|
||||
@@ -177,10 +177,27 @@ class ThemeBuilderAccess
|
||||
|
||||
$gate = $this->writableGate();
|
||||
|
||||
return $gate ?? $this->checkedTemplate($id, $input);
|
||||
return $gate ?? $this->checkedTemplate($id, $input, true);
|
||||
}
|
||||
|
||||
return $this->checkedTemplate((int) $post->ID, $input);
|
||||
return $this->checkedTemplate((int) $post->ID, $input, $offSwitch);
|
||||
}
|
||||
|
||||
/**
|
||||
* The templates of the live Theme Builder post split by canEditTemplate() (ruling Q21 – one place for
|
||||
* tb-list and the dry runs of tb-assign/tb-activate): only "templates" may be shown; "hidden" serves counts
|
||||
* ("weitere aktive Vorlagen ohne Berechtigung"), never its IDs, titles or conditions (Q8/B11, P12).
|
||||
*
|
||||
* @return array{templates: list<Template>, hidden: list<Template>}
|
||||
*/
|
||||
public function editableLiveTemplates(): array
|
||||
{
|
||||
$split = ['templates' => [], 'hidden' => []];
|
||||
foreach ($this->store->liveTemplates() as $template) {
|
||||
$split[$this->canEditTemplate($template->id) ? 'templates' : 'hidden'][] = $template;
|
||||
}
|
||||
|
||||
return $split;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -297,12 +314,14 @@ class ThemeBuilderAccess
|
||||
|
||||
/**
|
||||
* The draft, default and template_rev checks of changeableTemplate() for template $id (rights already checked).
|
||||
* The off-switch skips the draft refusal (ruling Q20): hiding a template must always be possible; the caller
|
||||
* warns that saving an open Theme Builder editor may restore its own state.
|
||||
*
|
||||
* @param array<string, mixed> $input
|
||||
*/
|
||||
private function checkedTemplate(int $id, array $input): Template|WP_Error
|
||||
private function checkedTemplate(int $id, array $input, bool $offSwitch): Template|WP_Error
|
||||
{
|
||||
$refusal = $this->draftRefusal();
|
||||
$refusal = $offSwitch ? null : $this->draftRefusal();
|
||||
if ($refusal !== null) {
|
||||
return $refusal;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user