diff --git a/src/Integrations/Divi/Compose/LoopScope.php b/src/Integrations/Divi/Compose/LoopScope.php index e36516a..394ea57 100644 --- a/src/Integrations/Divi/Compose/LoopScope.php +++ b/src/Integrations/Divi/Compose/LoopScope.php @@ -94,6 +94,18 @@ final class LoopScope return $this->queryType === null ? [] : [$this->kind(), ...$this->outerKinds]; } + /** + * Identity of the context: the loop and the kinds of its whole chain (audit M1). Two scopes with the same key + * resolve every loop_* reference the same way; no loop is "-|". + */ + public function key(): string + { + return ($this->queryType ?? '-') . '|' . implode(',', array_map( + static fn (?string $kind): string => $kind ?? '?', + $this->kinds() + )); + } + /** Whether there is a loop whose loop_* options the tools allow. */ public function active(): bool { diff --git a/src/Integrations/Divi/Pages/DiviUpdateAbility.php b/src/Integrations/Divi/Pages/DiviUpdateAbility.php index 0790622..207518d 100644 --- a/src/Integrations/Divi/Pages/DiviUpdateAbility.php +++ b/src/Integrations/Divi/Pages/DiviUpdateAbility.php @@ -231,7 +231,7 @@ final class DiviUpdateAbility extends DiviWriteAbility */ private static function composeInLoops(NodeIndex $index, DiviComposition $composition, array $pending): array { - /** @var array $composers query type ('' = no loop) => Composer of that scope */ + /** @var array $composers LoopScope::key() => Composer of that scope */ $composers = []; $scopes = []; $results = []; @@ -247,7 +247,7 @@ final class DiviUpdateAbility extends DiviWriteAbility } foreach ($pending as $position => [$block, $change, $field]) { $scope = self::loopAt($index, $block, $newAttrs); - if ($scope->queryType() !== $scopes[$position]->queryType()) { + if ($scope->key() !== $scopes[$position]->key()) { $results[$position] = self::composerFor($composition, $scope, $composers) ->update($block, $change, $field); } @@ -256,9 +256,9 @@ final class DiviUpdateAbility extends DiviWriteAbility return $results; } - /** @param array $composers One Composer per loop scope (S31 as reworded by X3). */ + /** @param array $composers One Composer per loop context, chain included (S31 as reworded by X3). */ private static function composerFor(DiviComposition $composition, LoopScope $scope, array &$composers): Composer { - return $composers[$scope->queryType() ?? ''] ??= $composition->inLoop($scope)->composer(); + return $composers[$scope->key()] ??= $composition->inLoop($scope)->composer(); } } diff --git a/tests/Unit/Integrations/Divi/Pages/DiviLoopWriteTest.php b/tests/Unit/Integrations/Divi/Pages/DiviLoopWriteTest.php index ef1b2d8..276124e 100644 --- a/tests/Unit/Integrations/Divi/Pages/DiviLoopWriteTest.php +++ b/tests/Unit/Integrations/Divi/Pages/DiviLoopWriteTest.php @@ -47,6 +47,9 @@ final class DiviLoopWriteTest extends DiviWriteAbilityTestCase 'date_format' => $field('select'), 'custom_date_format' => $field('text'), ]), 'loop_term_name' => $option('loop_term_name', 'Loop Terms', $common), + 'loop_post_meta_key_manual_custom_field' => $option('loop_post_meta_key_manual_custom_field', 'Loop', $common + [ + 'select_loop_meta_key' => $field('select'), 'loop_meta_key' => $field('text'), + ]), ]; } @@ -530,4 +533,55 @@ final class DiviLoopWriteTest extends DiviWriteAbilityTestCase self::assertTrue($result['ok'], (string) json_encode($result)); self::assertStringContainsString('loop_post_meta_key_manual_custom_field', implode(' | ', $result['warnings'])); } + + /** Two sections: a post loop on its own, and a post loop inside a term loop (s1). */ + private function useTwoSectionPage(): void + { + $inner = static fn (): string => self::block('text', self::loop(self::POST_LOOP)); + $this->usePage('' + . self::block('section', [], self::block('row', [], self::block('column', [], $inner()))) + . self::block('section', self::loop(self::TERM_LOOP), self::block('row', [], self::block('column', [], $inner()))) + . ''); + } + + private static function metaChange(string $id): array + { + $ref = VariableRef::serialize( + 'content', + 'loop_post_meta_key_manual_custom_field', + ['before' => '', 'after' => '', 'loop_meta_key' => 'telefon'] + ); + + return ['id' => $id, 'attrs' => ['content.innerContent' => ['desktop' => $ref]]]; + } + + /** Re-review (M1): the composers of one update call are told apart by the whole chain, not the inner kind. */ + public function testMetaKeyContentIsCheckedAgainstTheWholeChainInOneCall(): void + { + $this->useTwoSectionPage(); + $inTerm = $this->changeAll([self::metaChange('s1.r0.c0.m0')]); + self::assertStringContainsString('Typ: Beiträge', self::problems($inTerm), 'refused when sent alone'); + + $both = $this->changeAll([self::metaChange('s0.r0.c0.m0'), self::metaChange('s1.r0.c0.m0')]); + self::assertStringContainsString('changes[1]', implode(' ', array_keys(self::errorFields($both)))); + self::assertStringContainsString('passt nicht zur Schleife', self::problems($both)); + self::assertSame([], $this->saved); + + $plain = $this->changeAll([self::metaChange('s0.r0.c0.m0')]); + self::assertTrue($plain['ok'], (string) json_encode($plain)); + } + + /** Re-review (M1): an outer loop switched to another kind in the same call refuses meta key content below. */ + public function testSwitchingTheOuterLoopRefusesMetaKeyContentOfTheInnerLoop(): void + { + $this->useTwoSectionPage(); + + $refused = $this->changeAll([ + ['id' => 's0', 'attrs' => ['module.advanced.loop' => ['desktop' => self::TERM_LOOP]]], + self::metaChange('s0.r0.c0.m0'), + ]); + + self::assertStringContainsString('changes[1]', implode(' ', array_keys(self::errorFields($refused)))); + self::assertSame([], $this->saved); + } }