feat: lit/divi-dynamic-sources mit post_id + node – Schleife des Knotens und passende loop_*-Quellen (X6)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
5b52e18a31
commit
02e7c3e8ec
@@ -276,7 +276,12 @@ final class DiviIntegration implements Integration, Bootable
|
|||||||
new DiviDuplicateNodeAbility($writer),
|
new DiviDuplicateNodeAbility($writer),
|
||||||
new DiviReplaceTextAbility($this->schema, $loader, $writer),
|
new DiviReplaceTextAbility($this->schema, $loader, $writer),
|
||||||
new DiviNodeCopyAbility($writer, $loader, $this->diviWritable, $this->libraryInsertable),
|
new DiviNodeCopyAbility($writer, $loader, $this->diviWritable, $this->libraryInsertable),
|
||||||
new DiviDynamicSourcesAbility($this->schema, new DynamicContentMap(), $this->access->roles()),
|
new DiviDynamicSourcesAbility(
|
||||||
|
$this->schema,
|
||||||
|
new DynamicContentMap(),
|
||||||
|
$this->access->roles(),
|
||||||
|
loader: $loader
|
||||||
|
),
|
||||||
new DiviPresetGetAbility($this->designWriter, $this->globalUsages()),
|
new DiviPresetGetAbility($this->designWriter, $this->globalUsages()),
|
||||||
new DiviPresetsSetAbility(
|
new DiviPresetsSetAbility(
|
||||||
$this->designWriter,
|
$this->designWriter,
|
||||||
|
|||||||
@@ -7,11 +7,15 @@ namespace LIT\MCP\Integrations\Divi\Pages;
|
|||||||
use Closure;
|
use Closure;
|
||||||
use LIT\MCP\Abilities\Ability;
|
use LIT\MCP\Abilities\Ability;
|
||||||
use LIT\MCP\Abilities\Annotations;
|
use LIT\MCP\Abilities\Annotations;
|
||||||
|
use LIT\MCP\Abilities\Errors;
|
||||||
use LIT\MCP\Abilities\JsonMap;
|
use LIT\MCP\Abilities\JsonMap;
|
||||||
|
use LIT\MCP\Integrations\Divi\BlockTree\NodeIndex;
|
||||||
use LIT\MCP\Integrations\Divi\Compose\DynamicContentRules;
|
use LIT\MCP\Integrations\Divi\Compose\DynamicContentRules;
|
||||||
|
use LIT\MCP\Integrations\Divi\Compose\LoopContext;
|
||||||
use LIT\MCP\Integrations\Divi\Compose\Placeholders;
|
use LIT\MCP\Integrations\Divi\Compose\Placeholders;
|
||||||
use LIT\MCP\Integrations\Divi\Library\DiviRoles;
|
use LIT\MCP\Integrations\Divi\Library\DiviRoles;
|
||||||
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
use LIT\MCP\Integrations\Divi\Release\DiviFeature;
|
||||||
|
use LIT\MCP\Integrations\Divi\Release\FeatureSets;
|
||||||
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
use LIT\MCP\Integrations\Divi\Release\RequiresDiviFeatures;
|
||||||
use LIT\MCP\Integrations\WordPress\PostGuard;
|
use LIT\MCP\Integrations\WordPress\PostGuard;
|
||||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||||
@@ -22,8 +26,12 @@ defined('ABSPATH') || exit;
|
|||||||
/**
|
/**
|
||||||
* lit/divi-dynamic-sources (spec §5.7, E12): Divi's dynamic content options for a post type
|
* lit/divi-dynamic-sources (spec §5.7, E12): Divi's dynamic content options for a post type
|
||||||
* (DynamicContentOptions::get_options with a sample entry of the type the user may edit, else 0;
|
* (DynamicContentOptions::get_options with a sample entry of the type the user may edit, else 0;
|
||||||
* no loop_* – they need a node in a Divi loop, ruling X6; via DynamicContentMap::options()) with the
|
* no loop_* there; via DynamicContentMap::options()) with the placeholder that writes each, and
|
||||||
* placeholder that writes each, and the fields of the type usable as {{field:<key>}}.
|
* the fields of the type usable as {{field:<key>}}.
|
||||||
|
*
|
||||||
|
* Ruling X6: with post_id and node (path ID) the entry itself is asked instead of a sample – loaded with the
|
||||||
|
* rights of lit-divi-node-get (DiviPageLoader) – and loop_sources lists the loop_* options of the node's loop
|
||||||
|
* kind (LoopContext, DynamicContentMap::loopFields()); a node outside any loop gets a note instead.
|
||||||
*
|
*
|
||||||
* Answer F4: only text-like fields are offered; relation/file fields show IDs in Divi (F26), Pods
|
* Answer F4: only text-like fields are offered; relation/file fields show IDs in Divi (F26), Pods
|
||||||
* table storage is not post meta, protected keys never (S12), unknown types not (ruling P9) – each
|
* table storage is not post meta, protected keys never (S12), unknown types not (ruling P9) – each
|
||||||
@@ -44,13 +52,15 @@ final class DiviDynamicSourcesAbility implements Ability, RequiresDiviFeatures
|
|||||||
* @param (Closure(string): int)|null $samplePost Post type → an entry to ask Divi with (0 = none).
|
* @param (Closure(string): int)|null $samplePost Post type → an entry to ask Divi with (0 = none).
|
||||||
* @param (Closure(string): ?string)|null $podStorage Post type → Pods storage ("meta", "table");
|
* @param (Closure(string): ?string)|null $podStorage Post type → Pods storage ("meta", "table");
|
||||||
* null without a pod.
|
* null without a pod.
|
||||||
|
* @param DiviPageLoader|null $loader Loads the entry of post_id; null: post_id/node unsupported.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ContentSchema $schema,
|
private ContentSchema $schema,
|
||||||
private DynamicContentMap $map,
|
private DynamicContentMap $map,
|
||||||
private DiviRoles $roles,
|
private DiviRoles $roles,
|
||||||
?Closure $samplePost = null,
|
?Closure $samplePost = null,
|
||||||
?Closure $podStorage = null
|
?Closure $podStorage = null,
|
||||||
|
private ?DiviPageLoader $loader = null
|
||||||
) {
|
) {
|
||||||
$this->samplePost = $samplePost ?? self::defaultSample(...);
|
$this->samplePost = $samplePost ?? self::defaultSample(...);
|
||||||
$this->fields = new DynamicFields($schema, $podStorage);
|
$this->fields = new DynamicFields($schema, $podStorage);
|
||||||
@@ -71,14 +81,26 @@ final class DiviDynamicSourcesAbility implements Ability, RequiresDiviFeatures
|
|||||||
return 'Listet die Dynamic-Content-Quellen, die Divi für einen Inhaltstyp anbietet (Beitragsdaten, Links, '
|
return 'Listet die Dynamic-Content-Quellen, die Divi für einen Inhaltstyp anbietet (Beitragsdaten, Links, '
|
||||||
. 'Website-Daten …) mit dem Platzhalter dazu ({{post:…}}, {{link:<ID>}}) bzw. dem Namen für die Vollform, '
|
. 'Website-Daten …) mit dem Platzhalter dazu ({{post:…}}, {{link:<ID>}}) bzw. dem Namen für die Vollform, '
|
||||||
. 'und die Felder des Typs, die sich als {{field:<Schlüssel>}} eignen. Nur textartige Felder; Beziehungs-, '
|
. 'und die Felder des Typs, die sich als {{field:<Schlüssel>}} eignen. Nur textartige Felder; Beziehungs-, '
|
||||||
. 'Datei-, Tabellen- und unbekannte Felder stehen mit Grund unter omitted.';
|
. 'Datei-, Tabellen- und unbekannte Felder stehen mit Grund unter omitted. Mit post_id und node (Pfad-ID '
|
||||||
|
. 'aus lit-divi-outline) zusätzlich die Schleifen-Quellen ({{loop:…}}), die an diesem Knoten passen.';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function inputSchema(): array
|
public function inputSchema(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'type' => 'object',
|
'type' => 'object',
|
||||||
'properties' => ['post_type' => ['type' => 'string', 'description' => 'z. B. "page" oder "person".']],
|
'properties' => [
|
||||||
|
'post_type' => ['type' => 'string', 'description' => 'z. B. "page" oder "person".'],
|
||||||
|
'post_id' => [
|
||||||
|
'type' => 'integer',
|
||||||
|
'minimum' => 1,
|
||||||
|
'description' => 'Eintrag dieses Typs; nur zusammen mit node.',
|
||||||
|
],
|
||||||
|
'node' => [
|
||||||
|
'type' => 'string',
|
||||||
|
'description' => 'Pfad-ID aus lit-divi-outline, z. B. "s0.r0.c1.m0"; nur zusammen mit post_id.',
|
||||||
|
],
|
||||||
|
],
|
||||||
'required' => ['post_type'],
|
'required' => ['post_type'],
|
||||||
'additionalProperties' => false,
|
'additionalProperties' => false,
|
||||||
];
|
];
|
||||||
@@ -117,6 +139,28 @@ final class DiviDynamicSourcesAbility implements Ability, RequiresDiviFeatures
|
|||||||
'fields' => $entry(['key', 'label', 'type', 'source', 'placeholder']),
|
'fields' => $entry(['key', 'label', 'type', 'source', 'placeholder']),
|
||||||
'omitted' => $entry(['key', 'label', 'reason']),
|
'omitted' => $entry(['key', 'label', 'reason']),
|
||||||
'notes' => ['type' => 'array', 'items' => ['type' => 'string']],
|
'notes' => ['type' => 'array', 'items' => ['type' => 'string']],
|
||||||
|
'loop' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'description' => 'Nur mit node: die Divi-Schleife des Knotens (query_type null = keine).',
|
||||||
|
'properties' => [
|
||||||
|
'node' => ['type' => 'string'],
|
||||||
|
'query_type' => ['type' => ['string', 'null']],
|
||||||
|
'label' => ['type' => ['string', 'null']],
|
||||||
|
],
|
||||||
|
'required' => ['node', 'query_type', 'label'],
|
||||||
|
],
|
||||||
|
'loop_sources' => [
|
||||||
|
'type' => 'array',
|
||||||
|
'items' => [
|
||||||
|
'type' => 'object',
|
||||||
|
'properties' => [
|
||||||
|
'name' => ['type' => 'string'],
|
||||||
|
'placeholder' => ['type' => ['string', 'null']],
|
||||||
|
'settings' => ['type' => 'object'],
|
||||||
|
],
|
||||||
|
'required' => ['name', 'placeholder', 'settings'],
|
||||||
|
],
|
||||||
|
],
|
||||||
],
|
],
|
||||||
'required' => ['post_type', 'sample_post_id', 'sources', 'fields', 'omitted', 'notes'],
|
'required' => ['post_type', 'sample_post_id', 'sources', 'fields', 'omitted', 'notes'],
|
||||||
];
|
];
|
||||||
@@ -135,17 +179,28 @@ final class DiviDynamicSourcesAbility implements Ability, RequiresDiviFeatures
|
|||||||
/**
|
/**
|
||||||
* Divi's dynamic content registry (DynamicSources); listing custom fields asks the role option
|
* Divi's dynamic content registry (DynamicSources); listing custom fields asks the role option
|
||||||
* read_dynamic_content_custom_fields (Roles) and omits them without it, so a broken roles check must not pass.
|
* read_dynamic_content_custom_fields (Roles) and omits them without it, so a broken roles check must not pass.
|
||||||
|
* post_id loads that entry (PageRead, plus what the entry adds like in lit-divi-node-get).
|
||||||
*/
|
*/
|
||||||
public function diviFeatures(array $input): array
|
public function diviFeatures(array $input): array
|
||||||
{
|
{
|
||||||
return [DiviFeature::DynamicSources, DiviFeature::Roles];
|
$base = [DiviFeature::DynamicSources, DiviFeature::Roles];
|
||||||
|
if (! array_key_exists('post_id', $input)) {
|
||||||
|
return $base;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FeatureSets::merge($base, [DiviFeature::PageRead], FeatureSets::ofEntry($input['post_id'], false));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function permission(array $input): bool|WP_Error
|
public function permission(array $input): bool|WP_Error
|
||||||
{
|
{
|
||||||
$type = PostGuard::postType($input['post_type'] ?? null, $this->schema, 'edit_posts');
|
$type = PostGuard::postType($input['post_type'] ?? null, $this->schema, 'edit_posts');
|
||||||
|
if ($type instanceof WP_Error) {
|
||||||
|
return $type;
|
||||||
|
}
|
||||||
|
|
||||||
return $type instanceof WP_Error ? $type : true;
|
$page = $this->page($input);
|
||||||
|
|
||||||
|
return $page instanceof WP_Error ? $page : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function execute(array $input): mixed
|
public function execute(array $input): mixed
|
||||||
@@ -156,7 +211,17 @@ final class DiviDynamicSourcesAbility implements Ability, RequiresDiviFeatures
|
|||||||
}
|
}
|
||||||
|
|
||||||
$postType = (string) $input['post_type'];
|
$postType = (string) $input['post_type'];
|
||||||
$sample = ($this->samplePost)($postType);
|
$page = $this->page($input);
|
||||||
|
if ($page instanceof WP_Error) {
|
||||||
|
return $page;
|
||||||
|
}
|
||||||
|
|
||||||
|
$node = is_string($input['node'] ?? null) ? $input['node'] : '';
|
||||||
|
if ($page !== null && $page->index->find($node) === null) {
|
||||||
|
return DiviErrors::unknownNode($node, $page->post);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sample = $page === null ? ($this->samplePost)($postType) : (int) $page->post->ID;
|
||||||
$notes = [
|
$notes = [
|
||||||
'Quellen ohne Platzhalter in der Vollform als $variable({"type":"content","value":{"name":"<name>",'
|
'Quellen ohne Platzhalter in der Vollform als $variable({"type":"content","value":{"name":"<name>",'
|
||||||
. '"settings":{…}}})$ mit den genannten Einstellungen schreiben.',
|
. '"settings":{…}}})$ mit den genannten Einstellungen schreiben.',
|
||||||
@@ -191,7 +256,7 @@ final class DiviDynamicSourcesAbility implements Ability, RequiresDiviFeatures
|
|||||||
. 'Felder lesen“ nicht (Divi → Rollen-Editor).';
|
. 'Felder lesen“ nicht (Divi → Rollen-Editor).';
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
$result = [
|
||||||
'post_type' => $postType,
|
'post_type' => $postType,
|
||||||
'sample_post_id' => $sample,
|
'sample_post_id' => $sample,
|
||||||
'sources' => $sources,
|
'sources' => $sources,
|
||||||
@@ -199,6 +264,93 @@ final class DiviDynamicSourcesAbility implements Ability, RequiresDiviFeatures
|
|||||||
'omitted' => $omitted,
|
'omitted' => $omitted,
|
||||||
'notes' => $notes,
|
'notes' => $notes,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
return $page === null ? $result : $this->withLoop($result, $page, $node);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The entry of post_id with node checked as a path ID; null when neither is given. Both or none (X6); the entry
|
||||||
|
* is loaded with read rights (DiviPageLoader::load(…, false), like lit-divi-node-get) and must be of post_type.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $input
|
||||||
|
*/
|
||||||
|
private function page(array $input): DiviPage|WP_Error|null
|
||||||
|
{
|
||||||
|
$hasPost = array_key_exists('post_id', $input);
|
||||||
|
if (! $hasPost && ! array_key_exists('node', $input)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $hasPost || ! is_string($input['node'] ?? null)) {
|
||||||
|
return Errors::invalidInput('post_id und node nur zusammen angeben.', [
|
||||||
|
$hasPost ? 'node' : 'post_id' => 'fehlt (post_id und node gehören zusammen)',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match(NodeIndex::ID_PATTERN, $input['node']) !== 1) {
|
||||||
|
return Errors::invalidInput('node ist keine Pfad-ID.', ['node' => 'z. B. "s0.r1.c0.m2"']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->loader === null) {
|
||||||
|
return Errors::unsupported(
|
||||||
|
'Schleifen-Quellen je Knoten sind hier nicht verfügbar.',
|
||||||
|
'lit/divi-dynamic-sources ohne post_id und node aufrufen.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$page = $this->loader->load($input['post_id'], false);
|
||||||
|
if ($page instanceof WP_Error || $page->post->post_type === $input['post_type']) {
|
||||||
|
return $page;
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = (string) $page->post->post_type;
|
||||||
|
|
||||||
|
return Errors::invalidInput(
|
||||||
|
sprintf('Eintrag %d ist vom Typ "%s", nicht "%s".', $page->post->ID, $type, $input['post_type']),
|
||||||
|
['post_type' => sprintf('"%s" (Typ von Eintrag %d)', $type, $page->post->ID)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the loop of $node and the loop_* sources of its kind (X6), or the note that there is no loop context.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $result
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function withLoop(array $result, DiviPage $page, string $node): array
|
||||||
|
{
|
||||||
|
$scope = LoopContext::of($page->index, $node);
|
||||||
|
$kind = $scope->kind();
|
||||||
|
$result['loop'] = [
|
||||||
|
'node' => $node,
|
||||||
|
'query_type' => $scope->queryType(),
|
||||||
|
'label' => $kind === null ? null : $scope->label(),
|
||||||
|
];
|
||||||
|
$result['loop_sources'] = [];
|
||||||
|
if ($kind === null) {
|
||||||
|
$result['notes'][] = sprintf(
|
||||||
|
'Knoten %s liegt in keiner Divi-Schleife (kein Schleifen-Kontext): Schleifen-Inhalte (loop_…, '
|
||||||
|
. '{{loop:…}}) gehen dort nicht. Eine Schleife wird im Divi-Builder (Loop Builder) eingerichtet.',
|
||||||
|
$node
|
||||||
|
);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->map->loopFields($kind, (int) $page->post->ID) as $name => $settings) {
|
||||||
|
$result['loop_sources'][] = [
|
||||||
|
'name' => (string) $name,
|
||||||
|
'placeholder' => self::placeholder((string) $name),
|
||||||
|
'settings' => JsonMap::of($settings),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$result['notes'][] = sprintf(
|
||||||
|
'Schleifen-Quellen gelten nur in dieser Schleife (Typ: %s) – in diesem Knoten und darunter, bis eine '
|
||||||
|
. 'innere Schleife beginnt.',
|
||||||
|
$scope->label()
|
||||||
|
);
|
||||||
|
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function placeholder(string $name): ?string
|
private static function placeholder(string $name): ?string
|
||||||
@@ -208,6 +360,11 @@ final class DiviDynamicSourcesAbility implements Ability, RequiresDiviFeatures
|
|||||||
return '{{post:' . $post . '}}';
|
return '{{post:' . $post . '}}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$loop = array_search($name, Placeholders::LOOP, true);
|
||||||
|
if ($loop !== false) {
|
||||||
|
return '{{loop:' . $loop . '}}';
|
||||||
|
}
|
||||||
|
|
||||||
if (! str_starts_with($name, DynamicContentRules::LINK_PREFIX)) {
|
if (! str_starts_with($name, DynamicContentRules::LINK_PREFIX)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace LIT\MCP\Tests\Unit\Integrations\Divi\Pages;
|
|||||||
|
|
||||||
use Brain\Monkey\Functions;
|
use Brain\Monkey\Functions;
|
||||||
use LIT\MCP\Integrations\Divi\Pages\DiviDynamicSourcesAbility;
|
use LIT\MCP\Integrations\Divi\Pages\DiviDynamicSourcesAbility;
|
||||||
|
use LIT\MCP\Integrations\Divi\Pages\DiviPageLoader;
|
||||||
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
|
use LIT\MCP\Integrations\Divi\Pages\DynamicContentMap;
|
||||||
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
use LIT\MCP\Integrations\WordPress\Schema\ContentSchema;
|
||||||
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
|
use LIT\MCP\Integrations\WordPress\Schema\FieldDefinition;
|
||||||
@@ -16,6 +17,9 @@ use Mockery;
|
|||||||
|
|
||||||
final class DiviDynamicSourcesAbilityTest extends TestCase
|
final class DiviDynamicSourcesAbilityTest extends TestCase
|
||||||
{
|
{
|
||||||
|
/** @var list<int> Post IDs Divi's registry was asked for. */
|
||||||
|
private array $registryCalls = [];
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
parent::setUp();
|
parent::setUp();
|
||||||
@@ -51,19 +55,118 @@ final class DiviDynamicSourcesAbilityTest extends TestCase
|
|||||||
private function ability(
|
private function ability(
|
||||||
array $deniedRoles = [],
|
array $deniedRoles = [],
|
||||||
?string $storage = 'meta',
|
?string $storage = 'meta',
|
||||||
?array $registry = null
|
?array $registry = null,
|
||||||
|
?DiviPageLoader $loader = null
|
||||||
): DiviDynamicSourcesAbility {
|
): DiviDynamicSourcesAbility {
|
||||||
$registry ??= DiviFixtures::dynamicContentRegistry();
|
$registry ??= DiviFixtures::dynamicContentRegistry();
|
||||||
|
|
||||||
return new DiviDynamicSourcesAbility(
|
return new DiviDynamicSourcesAbility(
|
||||||
$this->schema(),
|
$this->schema(),
|
||||||
new DynamicContentMap(static fn (int $id): array => $registry),
|
new DynamicContentMap(function (int $id) use ($registry): array {
|
||||||
|
$this->registryCalls[] = $id;
|
||||||
|
|
||||||
|
return $registry;
|
||||||
|
}),
|
||||||
LibraryFixtures::roles($deniedRoles),
|
LibraryFixtures::roles($deniedRoles),
|
||||||
static fn (string $postType): int => 831,
|
static fn (string $postType): int => 831,
|
||||||
static fn (string $postType): ?string => $storage
|
static fn (string $postType): ?string => $storage,
|
||||||
|
$loader
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ruling X6: the ability with a loader over post 7 – s0.r0.c0 runs a term loop, s0.r0.c1 none – and a
|
||||||
|
* registry with one post and one term loop option (Divi 5.15.1 groups and fields).
|
||||||
|
*/
|
||||||
|
private function withLoopPage(): DiviDynamicSourcesAbility
|
||||||
|
{
|
||||||
|
$loop = ['module' => ['advanced' => ['loop' => ['desktop' => ['value' => [
|
||||||
|
'enable' => 'on', 'queryType' => 'post_taxonomies',
|
||||||
|
]]]]]];
|
||||||
|
$text = '<!-- wp:divi/text {"builderVersion":"5.2.1"} --><!-- /wp:divi/text -->';
|
||||||
|
$column = static fn (array $attrs): string => '<!-- wp:divi/column '
|
||||||
|
. json_encode($attrs + ['builderVersion' => '5.2.1']) . ' -->' . $text . '<!-- /wp:divi/column -->';
|
||||||
|
DiviFixtures::givenPost(
|
||||||
|
'<!-- wp:divi/placeholder --><!-- wp:divi/section {"builderVersion":"5.2.1"} --><!-- wp:divi/row '
|
||||||
|
. '{"builderVersion":"5.2.1"} -->' . $column($loop) . $column([])
|
||||||
|
. '<!-- /wp:divi/row --><!-- /wp:divi/section --><!-- /wp:divi/placeholder -->'
|
||||||
|
);
|
||||||
|
$field = static fn (string $type): array => ['label' => 'x', 'type' => $type, 'default' => ''];
|
||||||
|
$common = ['before' => $field('text'), 'after' => $field('text'), 'loop_position' => $field('text')];
|
||||||
|
$registry = DiviFixtures::dynamicContentRegistry() + [
|
||||||
|
'loop_post_date' => ['label' => 'Datum', 'group' => 'Loop', 'fields' => $common + [
|
||||||
|
'date_format' => $field('select'),
|
||||||
|
]],
|
||||||
|
'loop_term_name' => ['label' => 'Name', 'group' => 'Loop Terms', 'fields' => $common],
|
||||||
|
];
|
||||||
|
|
||||||
|
return $this->ability([], 'meta', $registry, new DiviPageLoader(DiviFixtures::schema()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ruling X6: post_id + node add the loop sources of the node's loop kind, read from that entry. */
|
||||||
|
public function testANodeInALoopAddsTheLoopSourcesOfItsKind(): void
|
||||||
|
{
|
||||||
|
$result = $this->withLoopPage()->execute(['post_type' => 'page', 'post_id' => 7, 'node' => 's0.r0.c0.m0']);
|
||||||
|
|
||||||
|
self::assertSame(7, $result['sample_post_id'], 'the entry itself, not a sample');
|
||||||
|
self::assertSame([7], array_values(array_unique($this->registryCalls)));
|
||||||
|
self::assertSame(
|
||||||
|
['node' => 's0.r0.c0.m0', 'query_type' => 'post_taxonomies', 'label' => 'Terme'],
|
||||||
|
$result['loop']
|
||||||
|
);
|
||||||
|
// The fixture registry also has the meta key option of each kind: only the term one fits, no short form.
|
||||||
|
self::assertSame(
|
||||||
|
['loop_term_name', 'loop_term_meta_key_manual_custom_field'],
|
||||||
|
array_column($result['loop_sources'], 'name')
|
||||||
|
);
|
||||||
|
self::assertSame('{{loop:term_name}}', $result['loop_sources'][0]['placeholder']);
|
||||||
|
self::assertNull($result['loop_sources'][1]['placeholder']);
|
||||||
|
self::assertEquals(
|
||||||
|
['before' => 'richtext', 'after' => 'richtext', 'loop_position' => 'text'],
|
||||||
|
(array) $result['loop_sources'][0]['settings']
|
||||||
|
);
|
||||||
|
self::assertSame([], array_filter(
|
||||||
|
array_column($result['sources'], 'name'),
|
||||||
|
static fn (string $name): bool => str_starts_with($name, 'loop_')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testANodeOutsideALoopGetsNoLoopSources(): void
|
||||||
|
{
|
||||||
|
$result = $this->withLoopPage()->execute(['post_type' => 'page', 'post_id' => 7, 'node' => 's0.r0.c1.m0']);
|
||||||
|
|
||||||
|
self::assertSame(['node' => 's0.r0.c1.m0', 'query_type' => null, 'label' => null], $result['loop']);
|
||||||
|
self::assertSame([], $result['loop_sources']);
|
||||||
|
self::assertStringContainsString('kein Schleifen-Kontext', implode(' ', $result['notes']));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testWithoutANodeTheAnswerHasNoLoopPart(): void
|
||||||
|
{
|
||||||
|
$result = $this->ability()->execute(['post_type' => 'page']);
|
||||||
|
|
||||||
|
self::assertArrayNotHasKey('loop', $result);
|
||||||
|
self::assertArrayNotHasKey('loop_sources', $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPostIdAndNodeComeTogetherAndNeedTheEntry(): void
|
||||||
|
{
|
||||||
|
$ability = $this->withLoopPage();
|
||||||
|
|
||||||
|
foreach ([['post_id' => 7], ['node' => 's0']] as $half) {
|
||||||
|
self::assertToolError($ability->permission(['post_type' => 'page'] + $half), 'validation_failed');
|
||||||
|
self::assertToolError($ability->execute(['post_type' => 'page'] + $half), 'validation_failed');
|
||||||
|
}
|
||||||
|
$node = ['post_type' => 'page', 'post_id' => 7, 'node' => 's0'];
|
||||||
|
self::assertToolError($ability->execute(['node' => 's9'] + $node), 'not_found');
|
||||||
|
self::assertToolError($ability->execute(['node' => 'x'] + $node), 'validation_failed');
|
||||||
|
self::assertToolError($ability->execute(['post_type' => 'post'] + $node), 'validation_failed');
|
||||||
|
|
||||||
|
Functions\when('current_user_can')->alias(static fn (string $cap): bool => $cap !== 'edit_post');
|
||||||
|
self::assertToolError($ability->permission($node), 'forbidden');
|
||||||
|
self::assertToolError($ability->execute($node), 'forbidden');
|
||||||
|
self::assertSame([], $this->registryCalls, 'nothing is read for a refused entry');
|
||||||
|
}
|
||||||
|
|
||||||
public function testListsDiviSourcesWithTheirPlaceholders(): void
|
public function testListsDiviSourcesWithTheirPlaceholders(): void
|
||||||
{
|
{
|
||||||
$result = $this->ability()->execute(['post_type' => 'page']);
|
$result = $this->ability()->execute(['post_type' => 'page']);
|
||||||
|
|||||||
@@ -203,6 +203,14 @@ final class DiviToolFeaturesTest extends TestCase
|
|||||||
'lit/divi-design-restore', ['snapshot_id' => 'theme_options-20261016T100000Z-aaaaaa'],
|
'lit/divi-design-restore', ['snapshot_id' => 'theme_options-20261016T100000Z-aaaaaa'],
|
||||||
['theme-option-read', 'theme-option-write'],
|
['theme-option-read', 'theme-option-write'],
|
||||||
],
|
],
|
||||||
|
'dynamic-sources of a node' => [
|
||||||
|
'lit/divi-dynamic-sources', ['post_type' => 'page', 'post_id' => $page, 'node' => 's0'],
|
||||||
|
['dynamic-sources', 'roles', 'page-read'],
|
||||||
|
],
|
||||||
|
'dynamic-sources of a node in a layout' => [
|
||||||
|
'lit/divi-dynamic-sources', ['post_type' => 'et_body_layout', 'post_id' => $layout, 'node' => 's0'],
|
||||||
|
['dynamic-sources', 'roles', 'page-read'],
|
||||||
|
],
|
||||||
'library-list' => ['lit/library-list', [], ['library-read', 'roles']],
|
'library-list' => ['lit/library-list', [], ['library-read', 'roles']],
|
||||||
'library-list of one entry' => [
|
'library-list of one entry' => [
|
||||||
'lit/library-list', ['id' => $library], ['library-read', 'roles', 'page-read', 'design-read'],
|
'lit/library-list', ['id' => $library], ['library-read', 'roles', 'page-read', 'design-read'],
|
||||||
|
|||||||
Reference in New Issue
Block a user