diff --git a/src/Integrations/Divi/ThemeBuilder/TbActivateAbility.php b/src/Integrations/Divi/ThemeBuilder/TbActivateAbility.php index b67dc62..218d11f 100644 --- a/src/Integrations/Divi/ThemeBuilder/TbActivateAbility.php +++ b/src/Integrations/Divi/ThemeBuilder/TbActivateAbility.php @@ -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')) diff --git a/src/Integrations/Divi/ThemeBuilder/TbAssignAbility.php b/src/Integrations/Divi/ThemeBuilder/TbAssignAbility.php index 5f3843b..d595d34 100644 --- a/src/Integrations/Divi/ThemeBuilder/TbAssignAbility.php +++ b/src/Integrations/Divi/ThemeBuilder/TbAssignAbility.php @@ -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')) diff --git a/src/Integrations/Divi/ThemeBuilder/TbListAbility.php b/src/Integrations/Divi/ThemeBuilder/TbListAbility.php index d678905..d011d84 100644 --- a/src/Integrations/Divi/ThemeBuilder/TbListAbility.php +++ b/src/Integrations/Divi/ThemeBuilder/TbListAbility.php @@ -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, diff --git a/src/Integrations/Divi/ThemeBuilder/Template.php b/src/Integrations/Divi/ThemeBuilder/Template.php index 2afd138..8816f94 100644 --- a/src/Integrations/Divi/ThemeBuilder/Template.php +++ b/src/Integrations/Divi/ThemeBuilder/Template.php @@ -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