variant options wip
CI / testsuite (mysql, 8.1, ) (push) Failing after 0s Details
CI / testsuite (mysql, 8.4, ) (push) Failing after 0s Details
CI / testsuite (pgsql, 8.1, ) (push) Failing after 0s Details
CI / testsuite (pgsql, 8.4, ) (push) Failing after 0s Details
CI / testsuite (sqlite, 8.1, ) (push) Failing after 0s Details
CI / testsuite (sqlite, 8.1, prefer-lowest) (push) Failing after 0s Details
CI / testsuite (sqlite, 8.4, ) (push) Failing after 0s Details
CI / Coding Standard & Static Analysis (push) Failing after 0s Details

This commit is contained in:
Brandon Shipley 2025-07-05 20:08:56 -07:00
parent c9d34f7115
commit 5adc791c20
Signed by: bmfs
GPG Key ID: 14E38571D8BB0DE4
16 changed files with 462 additions and 1 deletions

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
use Migrations\BaseMigration;
class CreateProductCategoryVariantOptions extends BaseMigration
{
/**
* Change Method.
*
* More information on this method is available here:
* https://book.cakephp.org/migrations/4/en/migrations.html#the-change-method
* @return void
*/
public function change(): void
{
$table = $this->table('product_category_variant_options', ['id' => false, 'primary_key' => ['id']]);
$table->addColumn('id', 'uuid', [
'default' => null,
'null' => false,
]);
$table->addColumn('product_category_variant_id', 'uuid', [
'default' => null,
'null' => false,
]);
$table->addColumn('variant_value', 'string', [
'default' => null,
'limit' => 255,
'null' => false,
]);
$table->addColumn('variant_label', 'string', [
'default' => null,
'limit' => 255,
'null' => true,
]);
$table->addColumn('created', 'datetime', [
'default' => null,
'null' => false,
]);
$table->addColumn('modified', 'datetime', [
'default' => null,
'null' => false,
]);
$table->addColumn('deleted', 'datetime', [
'default' => null,
'null' => true,
]);
$table->addColumn('enabled', 'boolean', [
'default' => true,
'null' => false,
]);
$table->addForeignKey('product_category_variant_id', 'product_category_variants');
$table->create();
}
}

View File

@ -17,6 +17,7 @@ use Cake\ORM\Entity;
*
* @property ProductCategory|EntityInterface $product_category
* @property Product|EntityInterface $product
* @property ProductCategoryVariantOption[]|EntityInterface[] $product_category_variant_options
*/
class ProductCategoryVariant extends Entity
{
@ -34,8 +35,10 @@ class ProductCategoryVariant extends Entity
'product_category_id' => true,
'product_id' => true,
'enabled' => true,
// entities
'product_category' => false,
'product' => false,
'product_category_variant_options' => true,
];
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace CakeProducts\Model\Entity;
use Cake\ORM\Entity;
/**
* ProductCategoryVariantOption Entity
*
* @property string $id
* @property string $product_category_variant_id
* @property string $variant_value
* @property \Cake\I18n\DateTime $created
* @property \Cake\I18n\DateTime $modified
* @property \Cake\I18n\DateTime|null $deleted
*
* @property \App\Model\Entity\ProductCategoryVariant $product_category_variant
*/
class ProductCategoryVariantOption extends Entity
{
/**
* Fields that can be mass assigned using newEntity() or patchEntity().
*
* Note that when '*' is set to true, this allows all unspecified fields to
* be mass assigned. For security purposes, it is advised to set '*' to false
* (or remove it), and explicitly make individual fields accessible as needed.
*
* @var array<string, bool>
*/
protected array $_accessible = [
'product_category_variant_id' => true,
'variant_value' => true,
'created' => true,
'modified' => true,
'deleted' => true,
// entities
'product_category_variant' => false,
];
}

View File

@ -91,6 +91,14 @@ class ProductCategoriesTable extends Table
'dependent' => true,
'cascadeCallbacks' => true,
]);
$this->hasMany('ProductCategoryVariants', [
'foreignKey' => 'product_category_id',
'bindingKey' => 'internal_id',
'className' => 'CakeProducts.ProductCategoryVariants',
'dependent' => true,
'cascadeCallbacks' => true,
]);
$this->behaviors()->Tree->setConfig('scope', ['product_catalog_id' => $this->treeCatalogId]);
$this->addBehavior('Muffin/Trash.Trash');
}

View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace CakeProducts\Model\Table;
use CakeProducts\Model\Entity\ProductCategoryVariantOption;
use CakeProducts\Model\Table\ProductCategoryVariantsTable;
use Cake\Datasource\EntityInterface;
use Cake\Datasource\ResultSetInterface;
use Cake\ORM\Association\BelongsTo;
use Cake\ORM\Behavior\TimestampBehavior;
use Cake\ORM\Query\SelectQuery;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;
use Closure;
use Psr\SimpleCache\CacheInterface;
/**
* ProductCategoryVariantOptions Model
*
* @property ProductCategoryVariantsTable&BelongsTo $ProductCategoryVariants
*
* @method ProductCategoryVariantOption newEmptyEntity()
* @method ProductCategoryVariantOption newEntity(array $data, array $options = [])
* @method array<ProductCategoryVariantOption> newEntities(array $data, array $options = [])
* @method ProductCategoryVariantOption get(mixed $primaryKey, array|string $finder = 'all', CacheInterface|string|null $cache = null, Closure|string|null $cacheKey = null, mixed ...$args)
* @method ProductCategoryVariantOption findOrCreate($search, ?callable $callback = null, array $options = [])
* @method ProductCategoryVariantOption patchEntity(EntityInterface $entity, array $data, array $options = [])
* @method array<ProductCategoryVariantOption> patchEntities(iterable $entities, array $data, array $options = [])
* @method ProductCategoryVariantOption|false save(EntityInterface $entity, array $options = [])
* @method ProductCategoryVariantOption saveOrFail(EntityInterface $entity, array $options = [])
* @method iterable<ProductCategoryVariantOption>|ResultSetInterface<ProductCategoryVariantOption>|false saveMany(iterable $entities, array $options = [])
* @method iterable<ProductCategoryVariantOption>|ResultSetInterface<ProductCategoryVariantOption> saveManyOrFail(iterable $entities, array $options = [])
* @method iterable<ProductCategoryVariantOption>|ResultSetInterface<ProductCategoryVariantOption>|false deleteMany(iterable $entities, array $options = [])
* @method iterable<ProductCategoryVariantOption>|ResultSetInterface<ProductCategoryVariantOption> deleteManyOrFail(iterable $entities, array $options = [])
*
* @mixin TimestampBehavior
*/
class ProductCategoryVariantOptionsTable extends Table
{
/**
* Initialize method
*
* @param array<string, mixed> $config The configuration for the Table.
* @return void
*/
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable('product_category_variant_options');
$this->setDisplayField('variant_value');
$this->setPrimaryKey('id');
$this->addBehavior('Timestamp');
$this->belongsTo('ProductCategoryVariants', [
'foreignKey' => 'product_category_variant_id',
'joinType' => 'INNER',
]);
}
/**
* Default validation rules.
*
* @param Validator $validator Validator instance.
* @return Validator
*/
public function validationDefault(Validator $validator): Validator
{
$validator
->uuid('product_category_variant_id')
->notEmptyString('product_category_variant_id');
$validator
->scalar('variant_value')
->maxLength('variant_value', 255)
->requirePresence('variant_value', 'create')
->notEmptyString('variant_value');
$validator
->scalar('variant_label')
->maxLength('variant_label', 255)
->allowEmptyString('variant_label');
$validator
->dateTime('deleted')
->allowEmptyDateTime('deleted');
return $validator;
}
/**
* Returns a rules checker object that will be used for validating
* application integrity.
*
* @param RulesChecker $rules The rules object to be modified.
* @return RulesChecker
*/
public function buildRules(RulesChecker $rules): RulesChecker
{
$rules->add($rules->existsIn(['product_category_variant_id'], 'ProductCategoryVariants'), ['errorField' => 'product_category_variant_id']);
return $rules;
}
}

View File

@ -53,6 +53,11 @@ class ProductCategoryVariantsTable extends Table
'foreignKey' => 'product_id',
'className' => 'CakeProducts.Products',
]);
$this->hasMany('ProductCategoryVariantOptions', [
'foreignKey' => 'product_category_variant_id',
'className' => 'CakeProducts.ProductCategoryVariantOptions',
]);
}
/**
@ -100,4 +105,34 @@ class ProductCategoryVariantsTable extends Table
return $rules;
}
/**
* @param SelectQuery $query
* @param string $internalCategoryId
*
* @return array|\Cake\ORM\Query|SelectQuery
*/
public function findAllCategoryVariantsForCategoryId(SelectQuery $query, string $internalCategoryId)
{
$category = $this->ProductCategories->find()->where(['internal_id' => $internalCategoryId])->firstOrFail();
$this->ProductCategories->behaviors()->get('Tree')->setConfig([
'scope' => [
'product_catalog_id' => $category->product_catalog_id ?? 1,
],
]);
return $this->ProductCategories
->find('path', for: $category->id)
->contain(['ProductCategoryVariants']);
}
/**
* @param string $internalCategoryId
* @return array
*/
public function getAllCategoryVariantsForCategoryId(string $internalCategoryId)
{
return $this->find('allCategoryVariantsForCategoryId', $internalCategoryId)->toArray();
}
}

View File

@ -63,6 +63,14 @@ class ProductsTable extends Table
'dependent' => true,
'cascadeCallbacks' => true,
]);
$this->hasMany('ProductCategoryVariants', [
'foreignKey' => 'product_id',
'className' => 'CakeProducts.ProductCategoryVariants',
'dependent' => true,
'cascadeCallbacks' => true,
]);
$this->getSchema()->setColumnType('product_type_id', EnumType::from(ProductProductTypeId::class));
$this->addBehavior('Muffin/Trash.Trash');

View File

@ -25,3 +25,4 @@
</div>
</div>
</div>
<?= $this->Html->script('CakeProducts.product_category_variant_options.js'); ?>

View File

@ -30,3 +30,4 @@
</div>
</div>
</div>
<?= $this->Html->script('CakeProducts.product_category_variant_options.js'); ?>

View File

@ -7,4 +7,7 @@ echo $this->Form->control('name');
echo $this->Form->control('product_category_id', ['options' => $productCategories, 'empty' => true]);
echo $this->Form->control('product_id', ['options' => $products, 'empty' => true]);
echo $this->Form->control('enabled');
?>
?>
<legend><?= __('Variant Options') . '<small class="ms-2">' . $this->Html->link('Add Option', '#', [
'id' => 'add-option-button',
]) . '</small>'; ?></legend>

View File

@ -0,0 +1,32 @@
<?php
/**
* @var \App\View\AppView $this
* @var \Cake\Datasource\EntityInterface $productCategoryAttributeOption
*/
$cnt = 0;
$prefix = $prefix ?? '';
\Cake\Log\Log::debug('$prefix');
\Cake\Log\Log::debug($prefix);
?>
<div class="container product-category-attribute-options-container" data-test="1" data-prefix="<?= $prefix; ?>">
<div class="row">
<div class="col">
<?= $this->Form->control($prefix . 'attribute_value', [
'label' => 'Value',
]); ?>
</div>
<div class="col">
<?= $this->Form->control($prefix . 'attribute_label', [
'label' => 'Label',
]); ?>
</div>
<div class="col">
<?= $this->Form->control($prefix . 'enabled', [
'type' => 'checkbox',
'checked' => true,
'label' => 'Enabled',
]); ?>
</div>
</div>
</div>

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace CakeProducts\Test\Fixture;
use Cake\TestSuite\Fixture\TestFixture;
/**
* ProductCategoryVariantsFixture
*/
class ProductCategoryVariantOptionsFixture extends TestFixture
{
/**
* Init method
*
* @return void
*/
public function init(): void
{
$this->records = [
[
'id' => '5a386e9f-6e7a-4ae7-9360-c8e529f78d93',
'variant_value' => 'Color',
'variant_label' => null,
'product_category_variant_id' => '5a386e9f-6e7a-4ae7-9360-c8e529f78d93',
'created' => '2025-07-04 12:00:00',
'modified' => '2025-07-04 12:00:00',
'enabled' => 1,
],
[
'id' => '5a386e9f-6e7a-4ae7-9360-c8e529f78d94',
'variant_value' => 'Size',
'variant_label' => null,
'product_category_variant_id' => '5a386e9f-6e7a-4ae7-9360-c8e529f78d93',
'created' => '2025-07-04 12:00:00',
'modified' => '2025-07-04 12:00:00',
'enabled' => 1,
],
];
parent::init();
}
}

View File

@ -67,6 +67,7 @@ class ProductCategoriesTableTest extends TestCase
'ChildProductCategories',
// 'Products',
'ProductCategoryAttributes',
'ProductCategoryVariants',
];
$associations = $this->ProductCategories->associations();

View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace CakeProducts\Test\TestCase\Model\Table;
use CakeProducts\Model\Table\ProductCategoryVariantOptionsTable;
use Cake\TestSuite\TestCase;
/**
* App\Model\Table\ProductCategoryVariantOptionsTable Test Case
*/
class ProductCategoryVariantOptionsTableTest extends TestCase
{
/**
* Test subject
*
* @var \App\Model\Table\ProductCategoryVariantOptionsTable
*/
protected $ProductCategoryVariantOptions;
/**
* Fixtures
*
* @var array<string>
*/
protected array $fixtures = [
// 'ProductCategoryVariantOptions',
// 'ProductCategoryVariants',
];
/**
* setUp method
*
* @return void
*/
protected function setUp(): void
{
parent::setUp();
$config = $this->getTableLocator()->exists('ProductCategoryVariantOptions') ? [] : ['className' => ProductCategoryVariantOptionsTable::class];
$this->ProductCategoryVariantOptions = $this->getTableLocator()->get('ProductCategoryVariantOptions', $config);
}
/**
* tearDown method
*
* @return void
*/
protected function tearDown(): void
{
unset($this->ProductCategoryVariantOptions);
parent::tearDown();
}
/**
* TestInitialize method
*
* @return void
* @uses \App\Model\Table\ProductCategoryVariantOptionsTable::initialize()
*/
public function testInitialize(): void
{
// verify all associations loaded
$expectedAssociations = [
'ProductCategoryVariants',
];
$associations = $this->ProductCategoryVariantOptions->associations();
$this->assertCount(count($expectedAssociations), $associations);
foreach ($expectedAssociations as $expectedAssociation) {
$this->assertTrue($this->ProductCategoryVariantOptions->hasAssociation($expectedAssociation));
}
// verify all behaviors loaded
$expectedBehaviors = [
'Timestamp',
];
$behaviors = $this->ProductCategoryVariantOptions->behaviors();
$this->assertCount(count($expectedBehaviors), $behaviors);
foreach ($expectedBehaviors as $expectedBehavior) {
$this->assertTrue($this->ProductCategoryVariantOptions->hasBehavior($expectedBehavior));
}
}
/**
* Test validationDefault method
*
* @return void
* @uses \App\Model\Table\ProductCategoryVariantOptionsTable::validationDefault()
*/
public function testValidationDefault(): void
{
$this->markTestIncomplete('Not implemented yet.');
}
/**
* Test buildRules method
*
* @return void
* @uses \App\Model\Table\ProductCategoryVariantOptionsTable::buildRules()
*/
public function testBuildRules(): void
{
$this->markTestIncomplete('Not implemented yet.');
}
}

View File

@ -64,6 +64,7 @@ class ProductsTableTest extends TestCase
$expectedAssociations = [
'ProductCategories',
'ProductAttributes',
'ProductCategoryVariants',
];
$associations = $this->Products->associations();

View File

@ -0,0 +1,15 @@
const addOptionButton = document.getElementById('add-option-button');
const variantOptionPrefixInput = document.getElementById('variant_options_prefix');
if (addOptionButton && variantOptionPrefixInput) {
addOptionButton.addEventListener('click', addOptionButtonClicked);
}
function addOptionButtonClicked(e)
{
e.preventDefault();
console.debug('variantOptionPrefixInput.value');
console.debug(variantOptionPrefixInput.value);
variantOptionPrefixInput.value = parseInt(variantOptionPrefixInput.value) + 1;
variantOptionPrefixInput.dispatchEvent(new Event('change'));
console.debug('variantOptionPrefixInput.value');
console.debug(variantOptionPrefixInput.value);
}