diff --git a/Api/Data/PromptRuleInterface.php b/Api/Data/PromptRuleInterface.php
new file mode 100644
index 0000000..d7694ba
--- /dev/null
+++ b/Api/Data/PromptRuleInterface.php
@@ -0,0 +1,26 @@
+request->getParam('rule_id');
+ if (!$ruleId) {
+ return [];
+ }
+
+ return [
+ 'label' => __('Delete Rule'),
+ 'class' => 'delete',
+ 'on_click' => sprintf(
+ "deleteConfirm('%s', '%s', {data: {}})",
+ __('Are you sure you want to delete this rule?'),
+ $this->urlBuilder->getUrl('*/*/delete', ['rule_id' => $ruleId])
+ ),
+ 'sort_order' => 20,
+ ];
+ }
+}
diff --git a/Block/Adminhtml/PromptRule/Edit/SaveButton.php b/Block/Adminhtml/PromptRule/Edit/SaveButton.php
new file mode 100644
index 0000000..c56159c
--- /dev/null
+++ b/Block/Adminhtml/PromptRule/Edit/SaveButton.php
@@ -0,0 +1,23 @@
+ __('Save Rule'),
+ 'class' => 'save primary',
+ 'data_attribute' => [
+ 'mage-init' => ['button' => ['event' => 'save']],
+ 'form-role' => 'save',
+ ],
+ 'sort_order' => 90,
+ ];
+ }
+}
diff --git a/Controller/Adminhtml/PromptRule/Delete.php b/Controller/Adminhtml/PromptRule/Delete.php
new file mode 100644
index 0000000..4104386
--- /dev/null
+++ b/Controller/Adminhtml/PromptRule/Delete.php
@@ -0,0 +1,52 @@
+getRequest()->getParam('rule_id');
+ $redirect = $this->resultRedirectFactory->create()->setPath('*/*/');
+
+ if (!$ruleId) {
+ $this->messageManager->addErrorMessage(__('Rule ID is required.'));
+ return $redirect;
+ }
+
+ $rule = $this->ruleFactory->create();
+ $this->ruleResource->load($rule, $ruleId);
+
+ if (!$rule->getRuleId()) {
+ $this->messageManager->addErrorMessage(__('This rule no longer exists.'));
+ return $redirect;
+ }
+
+ try {
+ $this->ruleResource->delete($rule);
+ $this->messageManager->addSuccessMessage(__('The rule has been deleted.'));
+ } catch (\Exception $e) {
+ $this->messageManager->addErrorMessage($e->getMessage());
+ }
+
+ return $redirect;
+ }
+}
diff --git a/Controller/Adminhtml/PromptRule/Edit.php b/Controller/Adminhtml/PromptRule/Edit.php
new file mode 100644
index 0000000..9b20602
--- /dev/null
+++ b/Controller/Adminhtml/PromptRule/Edit.php
@@ -0,0 +1,48 @@
+getRequest()->getParam('rule_id');
+ $rule = $this->ruleFactory->create();
+
+ if ($ruleId) {
+ $this->ruleResource->load($rule, $ruleId);
+ if (!$rule->getRuleId()) {
+ $this->messageManager->addErrorMessage(__('This rule no longer exists.'));
+ return $this->resultRedirectFactory->create()->setPath('*/*/');
+ }
+ }
+
+ $resultPage = $this->resultPageFactory->create();
+ $resultPage->setActiveMenu('MageOS_CatalogDataAI::prompt_rules');
+ $resultPage->getConfig()->getTitle()->prepend(
+ $ruleId ? __('Edit Rule: %1', $rule->getName()) : __('New Prompt Rule')
+ );
+
+ return $resultPage;
+ }
+}
diff --git a/Controller/Adminhtml/PromptRule/Index.php b/Controller/Adminhtml/PromptRule/Index.php
new file mode 100644
index 0000000..8c91601
--- /dev/null
+++ b/Controller/Adminhtml/PromptRule/Index.php
@@ -0,0 +1,30 @@
+resultPageFactory->create();
+ $resultPage->setActiveMenu('MageOS_CatalogDataAI::prompt_rules');
+ $resultPage->getConfig()->getTitle()->prepend(__('AI Prompt Rules'));
+ return $resultPage;
+ }
+}
diff --git a/Controller/Adminhtml/PromptRule/MassDelete.php b/Controller/Adminhtml/PromptRule/MassDelete.php
new file mode 100644
index 0000000..dbaee82
--- /dev/null
+++ b/Controller/Adminhtml/PromptRule/MassDelete.php
@@ -0,0 +1,40 @@
+filter->getCollection($this->collectionFactory->create());
+ $deleted = 0;
+
+ foreach ($collection as $rule) {
+ $this->ruleResource->delete($rule);
+ $deleted++;
+ }
+
+ $this->messageManager->addSuccessMessage(__('A total of %1 rule(s) have been deleted.', $deleted));
+ return $this->resultRedirectFactory->create()->setPath('*/*/');
+ }
+}
diff --git a/Controller/Adminhtml/PromptRule/NewAction.php b/Controller/Adminhtml/PromptRule/NewAction.php
new file mode 100644
index 0000000..518f9c0
--- /dev/null
+++ b/Controller/Adminhtml/PromptRule/NewAction.php
@@ -0,0 +1,19 @@
+resultFactory->create(ResultFactory::TYPE_FORWARD)->forward('edit');
+ }
+}
diff --git a/Controller/Adminhtml/PromptRule/Preview.php b/Controller/Adminhtml/PromptRule/Preview.php
new file mode 100644
index 0000000..857a61c
--- /dev/null
+++ b/Controller/Adminhtml/PromptRule/Preview.php
@@ -0,0 +1,60 @@
+jsonFactory->create();
+ $sku = $this->getRequest()->getParam('sku');
+ $prompt = $this->getRequest()->getParam('prompt');
+
+ if (!$sku || !$prompt) {
+ return $result->setData([
+ 'success' => false,
+ 'message' => 'SKU and prompt are required.',
+ ]);
+ }
+
+ try {
+ $product = $this->productRepository->get($sku);
+ $resolvedPrompt = $this->enricher->parsePrompt($prompt, $product);
+
+ return $result->setData([
+ 'success' => true,
+ 'resolved_prompt' => $resolvedPrompt,
+ 'product_name' => $product->getName(),
+ ]);
+ } catch (\Magento\Framework\Exception\NoSuchEntityException $e) {
+ return $result->setData([
+ 'success' => false,
+ 'message' => sprintf('Product with SKU "%s" not found.', $sku),
+ ]);
+ } catch (\Exception $e) {
+ return $result->setData([
+ 'success' => false,
+ 'message' => $e->getMessage(),
+ ]);
+ }
+ }
+}
diff --git a/Controller/Adminhtml/PromptRule/Save.php b/Controller/Adminhtml/PromptRule/Save.php
new file mode 100644
index 0000000..0ec0b87
--- /dev/null
+++ b/Controller/Adminhtml/PromptRule/Save.php
@@ -0,0 +1,68 @@
+getRequest()->getPostValue();
+ $redirect = $this->resultRedirectFactory->create();
+
+ if (!$data) {
+ return $redirect->setPath('*/*/');
+ }
+
+ $ruleId = (int)($data['rule_id'] ?? 0);
+ $rule = $this->ruleFactory->create();
+
+ if ($ruleId) {
+ $this->ruleResource->load($rule, $ruleId);
+ if (!$rule->getRuleId()) {
+ $this->messageManager->addErrorMessage(__('This rule no longer exists.'));
+ return $redirect->setPath('*/*/');
+ }
+ }
+
+ if (isset($data['store_ids']) && is_array($data['store_ids'])) {
+ $data['store_ids'] = implode(',', $data['store_ids']);
+ }
+
+ if (isset($data['rule']['conditions'])) {
+ $rule->loadPost(['conditions' => $data['rule']['conditions']]);
+ }
+
+ $rule->addData($data);
+
+ try {
+ $this->ruleResource->save($rule);
+ $this->messageManager->addSuccessMessage(__('The rule has been saved.'));
+
+ if ($this->getRequest()->getParam('back')) {
+ return $redirect->setPath('*/*/edit', ['rule_id' => $rule->getRuleId()]);
+ }
+ return $redirect->setPath('*/*/');
+ } catch (\Exception $e) {
+ $this->messageManager->addErrorMessage($e->getMessage());
+ return $redirect->setPath('*/*/edit', ['rule_id' => $ruleId]);
+ }
+ }
+}
diff --git a/Model/Config/Source/EnrichableAttributes.php b/Model/Config/Source/EnrichableAttributes.php
new file mode 100644
index 0000000..ffd3257
--- /dev/null
+++ b/Model/Config/Source/EnrichableAttributes.php
@@ -0,0 +1,39 @@
+searchCriteriaBuilder
+ ->addFilter('frontend_input', ['text', 'textarea'], 'in')
+ ->create();
+
+ $attributes = $this->attributeRepository->getList($searchCriteria);
+
+ $options = [['value' => '', 'label' => __('-- Please Select --')]];
+ foreach ($attributes->getItems() as $attribute) {
+ $options[] = [
+ 'value' => $attribute->getAttributeCode(),
+ 'label' => ($attribute->getDefaultFrontendLabel() ?? $attribute->getAttributeCode()),
+ ];
+ }
+
+ usort($options, fn ($a, $b) => strcmp((string)$a['label'], (string)$b['label']));
+
+ return $options;
+ }
+}
diff --git a/Model/Product/Enricher.php b/Model/Product/Enricher.php
index f6d65f1..1a2054c 100644
--- a/Model/Product/Enricher.php
+++ b/Model/Product/Enricher.php
@@ -17,12 +17,14 @@ class Enricher
* @param Config $config
* @param HashGenerator $hashGenerator
* @param EnrichmentRecorder $enrichmentRecorder
+ * @param PromptResolver $promptResolver
*/
public function __construct(
private readonly AiClientInterface $aiClient,
private readonly Config $config,
private readonly HashGenerator $hashGenerator,
- private readonly EnrichmentRecorder $enrichmentRecorder
+ private readonly EnrichmentRecorder $enrichmentRecorder,
+ private readonly PromptResolver $promptResolver
) {
}
@@ -47,7 +49,7 @@ public function enrichAttribute(Product $product, string $attributeCode): void
return;
}
- $prompt = $this->config->getProductPrompt($attributeCode, (int) $product->getStoreId());
+ $prompt = $this->promptResolver->resolve($attributeCode, $product);
if (!$prompt) {
return;
}
diff --git a/Model/Product/PromptResolver.php b/Model/Product/PromptResolver.php
new file mode 100644
index 0000000..f7e9e30
--- /dev/null
+++ b/Model/Product/PromptResolver.php
@@ -0,0 +1,38 @@
+getStoreId();
+
+ $collection = $this->ruleCollectionFactory->create();
+ $collection->addFieldToFilter('attribute_code', $attributeCode);
+ $collection->addFieldToFilter('is_active', 1);
+ $collection->setOrder('priority', 'DESC');
+
+ /** @var PromptRule $rule */
+ foreach ($collection as $rule) {
+ if ($rule->matchesStore($storeId) && $rule->matchesProduct($product)) {
+ return $rule->getPrompt();
+ }
+ }
+
+ return $this->config->getProductPrompt($attributeCode);
+ }
+}
diff --git a/Model/PromptRule.php b/Model/PromptRule.php
new file mode 100644
index 0000000..761df20
--- /dev/null
+++ b/Model/PromptRule.php
@@ -0,0 +1,85 @@
+_init(PromptRuleResource::class);
+ }
+
+ public function getConditionsInstance(): \Magento\Rule\Model\Condition\Combine
+ {
+ return $this->_conditionFactory->create(Combine::class);
+ }
+
+ public function getActionsInstance(): \Magento\Rule\Model\Action\Collection
+ {
+ return $this->_actionFactory->create(\Magento\Rule\Model\Action\Collection::class);
+ }
+
+ public function getRuleId(): ?int
+ {
+ return $this->getData(self::RULE_ID) ? (int)$this->getData(self::RULE_ID) : null;
+ }
+
+ public function getName(): string
+ {
+ return (string)$this->getData(self::NAME);
+ }
+
+ public function getAttributeCode(): string
+ {
+ return (string)$this->getData(self::ATTRIBUTE_CODE);
+ }
+
+ public function getStoreIds(): string
+ {
+ return (string)$this->getData(self::STORE_IDS);
+ }
+
+ public function getConditionsSerialized(): ?string
+ {
+ return $this->getData(self::CONDITIONS_SERIALIZED);
+ }
+
+ public function getPrompt(): string
+ {
+ return (string)$this->getData(self::PROMPT);
+ }
+
+ public function getPriority(): int
+ {
+ return (int)$this->getData(self::PRIORITY);
+ }
+
+ public function getIsActive(): bool
+ {
+ return (bool)$this->getData(self::IS_ACTIVE);
+ }
+
+ public function matchesProduct(\Magento\Catalog\Model\Product $product): bool
+ {
+ return $this->getConditions()->validate($product);
+ }
+
+ public function matchesStore(int $storeId): bool
+ {
+ $storeIds = $this->getStoreIds();
+ if ($storeIds === '' || $storeIds === '0') {
+ return true;
+ }
+ $ids = array_map('intval', explode(',', $storeIds));
+ return in_array(0, $ids, true) || in_array($storeId, $ids, true);
+ }
+}
diff --git a/Model/PromptRule/DataProvider.php b/Model/PromptRule/DataProvider.php
new file mode 100644
index 0000000..beec62e
--- /dev/null
+++ b/Model/PromptRule/DataProvider.php
@@ -0,0 +1,54 @@
+collection = $collectionFactory->create();
+ parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data);
+ }
+
+ public function getData(): array
+ {
+ if (!empty($this->loadedData)) {
+ return $this->loadedData;
+ }
+
+ $ruleId = (int)$this->request->getParam('rule_id');
+ if ($ruleId) {
+ $rule = $this->ruleFactory->create();
+ $this->ruleResource->load($rule, $ruleId);
+
+ if ($rule->getRuleId()) {
+ $data = $rule->getData();
+ if (isset($data['store_ids']) && is_string($data['store_ids'])) {
+ $data['store_ids'] = explode(',', $data['store_ids']);
+ }
+ $this->loadedData[$ruleId] = $data;
+ }
+ }
+
+ return $this->loadedData;
+ }
+}
diff --git a/Model/ResourceModel/PromptRule.php b/Model/ResourceModel/PromptRule.php
new file mode 100644
index 0000000..c4aa5a5
--- /dev/null
+++ b/Model/ResourceModel/PromptRule.php
@@ -0,0 +1,15 @@
+_init('mageos_catalogai_prompt_rule', 'rule_id');
+ }
+}
diff --git a/Model/ResourceModel/PromptRule/Collection.php b/Model/ResourceModel/PromptRule/Collection.php
new file mode 100644
index 0000000..cacc197
--- /dev/null
+++ b/Model/ResourceModel/PromptRule/Collection.php
@@ -0,0 +1,19 @@
+_init(PromptRule::class, PromptRuleResource::class);
+ }
+}
diff --git a/Model/ResourceModel/PromptRule/Grid/Collection.php b/Model/ResourceModel/PromptRule/Grid/Collection.php
new file mode 100644
index 0000000..44edd5c
--- /dev/null
+++ b/Model/ResourceModel/PromptRule/Grid/Collection.php
@@ -0,0 +1,73 @@
+_mainTable = $mainTable;
+ $this->_setIdFieldName('rule_id');
+ $this->setModel(\Magento\Framework\View\Element\UiComponent\DataProvider\Document::class);
+ }
+
+ public function getAggregations(): AggregationInterface
+ {
+ return $this->aggregations;
+ }
+
+ public function setAggregations($aggregations): self
+ {
+ $this->aggregations = $aggregations;
+ return $this;
+ }
+
+ public function getSearchCriteria(): ?SearchCriteriaInterface
+ {
+ return null;
+ }
+
+ public function setSearchCriteria(SearchCriteriaInterface $searchCriteria): self
+ {
+ return $this;
+ }
+
+ public function getTotalCount(): int
+ {
+ return $this->getSize();
+ }
+
+ public function setTotalCount($totalCount): self
+ {
+ return $this;
+ }
+
+ public function setItems(?array $items = null): self
+ {
+ return $this;
+ }
+}
diff --git a/Test/Unit/Model/Product/EnricherTest.php b/Test/Unit/Model/Product/EnricherTest.php
index b983fe2..d97357d 100644
--- a/Test/Unit/Model/Product/EnricherTest.php
+++ b/Test/Unit/Model/Product/EnricherTest.php
@@ -14,6 +14,7 @@
use MageOS\CatalogDataAI\Model\Product\Enricher;
use MageOS\CatalogDataAI\Model\Product\EnrichmentRecorder;
use MageOS\CatalogDataAI\Model\Product\HashGenerator;
+use MageOS\CatalogDataAI\Model\Product\PromptResolver;
use MageOS\CatalogDataAI\Test\Unit\Trait\ProductMockTrait;
use OpenAI\Exceptions\ErrorException;
use PHPUnit\Framework\MockObject\MockObject;
@@ -27,6 +28,7 @@ class EnricherTest extends TestCase
private Config&MockObject $config;
private HashGenerator&MockObject $hashGenerator;
private EnrichmentRecorder&MockObject $enrichmentRecorder;
+ private PromptResolver&MockObject $promptResolver;
private Enricher $enricher;
protected function setUp(): void
@@ -35,12 +37,14 @@ protected function setUp(): void
$this->config = $this->createMock(Config::class);
$this->hashGenerator = $this->createMock(HashGenerator::class);
$this->enrichmentRecorder = $this->createMock(EnrichmentRecorder::class);
+ $this->promptResolver = $this->createMock(PromptResolver::class);
$this->enricher = new Enricher(
$this->aiClient,
$this->config,
$this->hashGenerator,
- $this->enrichmentRecorder
+ $this->enrichmentRecorder,
+ $this->promptResolver
);
}
@@ -87,7 +91,7 @@ public function testEnrichAttributeSkipsExistingValueWithoutOverwrite(): void
'mageos_catalogai_overwrite' => false,
]);
- $this->config->expects($this->never())->method('getProductPrompt');
+ $this->promptResolver->expects($this->never())->method('resolve');
$this->hashGenerator->expects($this->never())->method('generate');
$this->enricher->enrichAttribute($product, 'description');
@@ -97,9 +101,8 @@ public function testEnrichAttributeSkipsWhenNoPromptConfigured(): void
{
$product = $this->createProductMock(['store_id' => 1], storeId: 1);
- $this->config->expects($this->once())
- ->method('getProductPrompt')
- ->with('description', 1)
+ $this->promptResolver->expects($this->once())
+ ->method('resolve')
->willReturn(null);
$this->hashGenerator->expects($this->never())->method('generate');
@@ -198,8 +201,7 @@ public function testEnrichAttributeProcessesWhenOverwriteSet(): void
trackSetData: true
);
- $this->config->method('getProductPrompt')
- ->with('description', 1)
+ $this->promptResolver->method('resolve')
->willReturn('Describe {{name}}');
$this->config->method('isCacheEnabled')->willReturn(true);
$this->config->method('getSystemPrompt')->willReturn('system');
@@ -304,8 +306,7 @@ public function testCacheDisabledApiSuccessSetsProductData(): void
{
$product = $this->createProductMock(['name' => 'Widget'], storeId: 1, id: 42, trackSetData: true);
- $this->config->method('getProductPrompt')
- ->with('description', 1)
+ $this->promptResolver->method('resolve')
->willReturn('Describe {{name}}');
$this->config->method('isCacheEnabled')->willReturn(false);
$this->config->method('getSystemPrompt')->willReturn('system');
@@ -327,8 +328,7 @@ public function testCacheDisabledApiReturnsNullDoesNothing(): void
{
$product = $this->createProductMock(['name' => 'Widget'], storeId: 1, id: 42, trackSetData: true);
- $this->config->method('getProductPrompt')
- ->with('description', 1)
+ $this->promptResolver->method('resolve')
->willReturn('Describe {{name}}');
$this->config->method('isCacheEnabled')->willReturn(false);
$this->config->method('getSystemPrompt')->willReturn('system');
@@ -358,11 +358,14 @@ public function testExecuteIteratesAllConfiguredAttributes(): void
->with(1)
->willReturn(['description', 'short_description']);
- $this->config->method('getProductPrompt')
- ->willReturnMap([
- ['description', 1, 'Describe {{name}}'],
- ['short_description', 1, 'Short {{name}}'],
- ]);
+ $this->promptResolver->method('resolve')
+ ->willReturnCallback(function (string $code) {
+ return match ($code) {
+ 'description' => 'Describe {{name}}',
+ 'short_description' => 'Short {{name}}',
+ default => null,
+ };
+ });
$this->config->method('isCacheEnabled')->willReturn(false);
$this->config->method('getSystemPrompt')->willReturn('system');
@@ -389,8 +392,7 @@ public function testExecuteRetriesOnErrorException(): void
->with(1)
->willReturn(['description']);
- $this->config->method('getProductPrompt')
- ->with('description', 1)
+ $this->promptResolver->method('resolve')
->willReturn('Describe {{name}}');
$this->config->method('isCacheEnabled')->willReturn(false);
$this->config->method('getSystemPrompt')->willReturn('system');
@@ -442,8 +444,7 @@ private function setUpCacheHitScenario(
string $hash,
int $storeId
): void {
- $this->config->method('getProductPrompt')
- ->with('description', $storeId)
+ $this->promptResolver->method('resolve')
->willReturn('Describe {{name}}');
$this->config->method('isCacheEnabled')->willReturn(true);
$this->config->method('getSystemPrompt')->willReturn('system');
@@ -459,8 +460,7 @@ private function setUpCacheMissScenario(
int $storeId,
?string $apiReturn
): void {
- $this->config->method('getProductPrompt')
- ->with('description', $storeId)
+ $this->promptResolver->method('resolve')
->willReturn('Describe {{name}}');
$this->config->method('isCacheEnabled')->willReturn(true);
$this->config->method('getSystemPrompt')->willReturn('system');
diff --git a/Test/Unit/Model/Product/PromptResolverTest.php b/Test/Unit/Model/Product/PromptResolverTest.php
new file mode 100644
index 0000000..df3dc53
--- /dev/null
+++ b/Test/Unit/Model/Product/PromptResolverTest.php
@@ -0,0 +1,75 @@
+createMock(Product::class);
+ $product->method('getStoreId')->willReturn(1);
+
+ $lowRule = $this->createMock(PromptRule::class);
+ $lowRule->method('getIsActive')->willReturn(true);
+ $lowRule->method('getPriority')->willReturn(10);
+ $lowRule->method('getPrompt')->willReturn('low priority prompt');
+ $lowRule->method('matchesStore')->willReturn(true);
+ $lowRule->method('matchesProduct')->willReturn(true);
+
+ $highRule = $this->createMock(PromptRule::class);
+ $highRule->method('getIsActive')->willReturn(true);
+ $highRule->method('getPriority')->willReturn(50);
+ $highRule->method('getPrompt')->willReturn('high priority prompt');
+ $highRule->method('matchesStore')->willReturn(true);
+ $highRule->method('matchesProduct')->willReturn(true);
+
+ $collection = $this->createMock(Collection::class);
+ $collection->method('addFieldToFilter')->willReturnSelf();
+ $collection->method('setOrder')->willReturnSelf();
+ $collection->method('getIterator')->willReturn(new \ArrayIterator([$highRule, $lowRule]));
+
+ $collectionFactory = $this->createMock(CollectionFactory::class);
+ $collectionFactory->method('create')->willReturn($collection);
+
+ $config = $this->createMock(Config::class);
+
+ $resolver = new PromptResolver($collectionFactory, $config);
+ $result = $resolver->resolve('description', $product);
+
+ $this->assertEquals('high priority prompt', $result);
+ }
+
+ public function test_falls_back_to_config_when_no_rule_matches(): void
+ {
+ $product = $this->createMock(Product::class);
+ $product->method('getStoreId')->willReturn(1);
+
+ $collection = $this->createMock(Collection::class);
+ $collection->method('addFieldToFilter')->willReturnSelf();
+ $collection->method('setOrder')->willReturnSelf();
+ $collection->method('getIterator')->willReturn(new \ArrayIterator([]));
+
+ $collectionFactory = $this->createMock(CollectionFactory::class);
+ $collectionFactory->method('create')->willReturn($collection);
+
+ $config = $this->createMock(Config::class);
+ $config->method('getProductPrompt')
+ ->with('description')
+ ->willReturn('default config prompt');
+
+ $resolver = new PromptResolver($collectionFactory, $config);
+ $result = $resolver->resolve('description', $product);
+
+ $this->assertEquals('default config prompt', $result);
+ }
+}
diff --git a/Ui/Component/Listing/Column/PromptRuleActions.php b/Ui/Component/Listing/Column/PromptRuleActions.php
new file mode 100644
index 0000000..8dce3e4
--- /dev/null
+++ b/Ui/Component/Listing/Column/PromptRuleActions.php
@@ -0,0 +1,54 @@
+getData('name')] = [
+ 'edit' => [
+ 'href' => $this->urlBuilder->getUrl(
+ 'catalogai/promptrule/edit',
+ ['rule_id' => $item['rule_id']]
+ ),
+ 'label' => __('Edit'),
+ ],
+ 'delete' => [
+ 'href' => $this->urlBuilder->getUrl(
+ 'catalogai/promptrule/delete',
+ ['rule_id' => $item['rule_id']]
+ ),
+ 'label' => __('Delete'),
+ 'confirm' => [
+ 'title' => __('Delete Rule'),
+ 'message' => __('Are you sure you want to delete this rule?'),
+ ],
+ ],
+ ];
+ }
+ }
+ }
+ return $dataSource;
+ }
+}
diff --git a/Ui/DataProvider/PromptRule/Form/Modifier/Conditions.php b/Ui/DataProvider/PromptRule/Form/Modifier/Conditions.php
new file mode 100644
index 0000000..c675975
--- /dev/null
+++ b/Ui/DataProvider/PromptRule/Form/Modifier/Conditions.php
@@ -0,0 +1,86 @@
+request->getParam('rule_id');
+ if ($ruleId && isset($data[$ruleId])) {
+ $rule = $this->ruleFactory->create();
+ $this->ruleResource->load($rule, $ruleId);
+
+ $conditions = $rule->getConditions()->asArray();
+ $data[$ruleId]['rule']['conditions'] = $this->convertConditions($conditions);
+ }
+
+ return $data;
+ }
+
+ public function modifyMeta(array $meta): array
+ {
+ $meta['conditions_fieldset'] = [
+ 'arguments' => [
+ 'data' => [
+ 'config' => [
+ 'label' => __('Conditions'),
+ 'componentType' => 'fieldset',
+ 'collapsible' => true,
+ 'sortOrder' => 20,
+ ],
+ ],
+ ],
+ 'children' => [
+ 'conditions_notice' => [
+ 'arguments' => [
+ 'data' => [
+ 'config' => [
+ 'componentType' => 'container',
+ 'component' => 'Magento_Ui/js/form/components/html',
+ 'content' => (string)__(
+ 'Define product conditions that must match for this rule to apply. '
+ . 'If no conditions are set, the rule applies to all products.'
+ ),
+ ],
+ ],
+ ],
+ ],
+ ],
+ ];
+
+ return $meta;
+ }
+
+ private function convertConditions(array $conditions): array
+ {
+ $result = [];
+ if (isset($conditions['type'])) {
+ $result['type'] = $conditions['type'];
+ $result['attribute'] = $conditions['attribute'] ?? '';
+ $result['operator'] = $conditions['operator'] ?? '';
+ $result['value'] = $conditions['value'] ?? '';
+ $result['aggregator'] = $conditions['aggregator'] ?? 'all';
+ if (isset($conditions['conditions'])) {
+ foreach ($conditions['conditions'] as $key => $condition) {
+ $result['conditions'][$key] = $this->convertConditions($condition);
+ }
+ }
+ }
+ return $result;
+ }
+}
diff --git a/etc/adminhtml/di.xml b/etc/adminhtml/di.xml
index ff8db02..da5ea52 100644
--- a/etc/adminhtml/di.xml
+++ b/etc/adminhtml/di.xml
@@ -11,4 +11,21 @@
+