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 @@ + + + + MageOS\CatalogDataAI\Model\ResourceModel\PromptRule\Grid\Collection + + + + + + + + MageOS\CatalogDataAI\Ui\DataProvider\PromptRule\Form\Modifier\Conditions + 10 + + + + diff --git a/etc/adminhtml/menu.xml b/etc/adminhtml/menu.xml index 64a9ff4..a810a27 100644 --- a/etc/adminhtml/menu.xml +++ b/etc/adminhtml/menu.xml @@ -8,5 +8,12 @@ parent="Magento_Catalog::catalog" action="catalogai/enrichment/index" resource="MageOS_CatalogDataAI::enrichment_review"/> + diff --git a/etc/db_schema.xml b/etc/db_schema.xml index c454280..97c994b 100644 --- a/etc/db_schema.xml +++ b/etc/db_schema.xml @@ -35,4 +35,36 @@ + + + + + + + + + + + + + + + + + + + + +
diff --git a/etc/db_schema_whitelist.json b/etc/db_schema_whitelist.json index 7d840e1..e068dc8 100644 --- a/etc/db_schema_whitelist.json +++ b/etc/db_schema_whitelist.json @@ -24,5 +24,26 @@ "IDX_CATALOGAI_ENRICHMENT_STATUS": true, "IDX_CATALOGAI_ENRICHMENT_CREATED_AT": true } + }, + "mageos_catalogai_prompt_rule": { + "column": { + "rule_id": true, + "name": true, + "attribute_code": true, + "store_ids": true, + "conditions_serialized": true, + "prompt": true, + "priority": true, + "is_active": true, + "created_at": true, + "updated_at": true + }, + "constraint": { + "PRIMARY": true + }, + "index": { + "MAGEOS_CATALOGAI_PROMPT_RULE_ATTR_CODE": true, + "MAGEOS_CATALOGAI_PROMPT_RULE_IS_ACTIVE": true + } } } diff --git a/view/adminhtml/layout/catalogai_promptrule_edit.xml b/view/adminhtml/layout/catalogai_promptrule_edit.xml new file mode 100644 index 0000000..0e1deaa --- /dev/null +++ b/view/adminhtml/layout/catalogai_promptrule_edit.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/view/adminhtml/layout/catalogai_promptrule_index.xml b/view/adminhtml/layout/catalogai_promptrule_index.xml new file mode 100644 index 0000000..e956cfd --- /dev/null +++ b/view/adminhtml/layout/catalogai_promptrule_index.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/view/adminhtml/layout/catalogai_promptrule_new.xml b/view/adminhtml/layout/catalogai_promptrule_new.xml new file mode 100644 index 0000000..0e1deaa --- /dev/null +++ b/view/adminhtml/layout/catalogai_promptrule_new.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/view/adminhtml/ui_component/mageos_catalogai_prompt_rule_form.xml b/view/adminhtml/ui_component/mageos_catalogai_prompt_rule_form.xml new file mode 100644 index 0000000..68eda35 --- /dev/null +++ b/view/adminhtml/ui_component/mageos_catalogai_prompt_rule_form.xml @@ -0,0 +1,142 @@ + +
+ + + mageos_catalogai_prompt_rule_form.mageos_catalogai_prompt_rule_form_data_source + + Prompt Rule + templates/form/collapsible + + + + + + + mageos_catalogai_prompt_rule_columns + + mageos_catalogai_prompt_rule_listing.mageos_catalogai_prompt_rule_listing_data_source + +
+ + + + + + + rule_id + rule_id + + + + + + true + + + + + + + + + + Delete selected rules? + Delete + + + delete + + + + + + + + + rule_id + + + + + textRange + + asc + + + + + text + + + + + + text + + + + + + textRange + + + + + + select + + select + + + + + + dateRange + date + + + + + + rule_id + + + +