You've already forked wc-composable-product
v1.2.0 - Fix product selection, cart pricing, admin tabs + CI/CD
Fix three critical bugs that persisted through v1.1.11-v1.1.14: - Product selection always empty: meta_query checked _product_type in postmeta, but WooCommerce uses the product_type taxonomy. Replaced with correct tax_query using NOT IN operator. - Cart price always 0.00: composable_price_calculated flag persisted in session, preventing recalculation on page loads. Removed flag; static variable already handles per-request dedup. - Admin tabs both visible on load: JS now triggers WooCommerce native tab click instead of manually toggling panel visibility. Add Gitea CI/CD release workflow triggered on v* tags. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
205
.gitea/workflows/release.yml
Normal file
205
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
name: Create Release Package
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
uses: shivammathur/setup-php@v2
|
||||||
|
with:
|
||||||
|
php-version: '8.3'
|
||||||
|
extensions: mbstring, xml, zip, intl, gettext
|
||||||
|
tools: composer:v2
|
||||||
|
|
||||||
|
- name: Get version from tag
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION=${GITHUB_REF_NAME#v}
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
echo "Building version: $VERSION"
|
||||||
|
|
||||||
|
- name: Validate composer.json
|
||||||
|
run: composer validate --strict
|
||||||
|
|
||||||
|
- name: Install Composer dependencies (production)
|
||||||
|
run: |
|
||||||
|
composer config platform.php 8.3.0
|
||||||
|
composer install --no-dev --optimize-autoloader --no-interaction
|
||||||
|
|
||||||
|
- name: Install gettext
|
||||||
|
run: apt-get update && apt-get install -y gettext
|
||||||
|
|
||||||
|
- name: Compile translations
|
||||||
|
run: |
|
||||||
|
for po in languages/*.po; do
|
||||||
|
if [ -f "$po" ]; then
|
||||||
|
mo="${po%.po}.mo"
|
||||||
|
echo "Compiling $po to $mo"
|
||||||
|
msgfmt -o "$mo" "$po"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Verify plugin version matches tag
|
||||||
|
run: |
|
||||||
|
PLUGIN_VERSION=$(grep -oP "Version:\s*\K[0-9]+\.[0-9]+\.[0-9]+" wc-composable-product.php | head -1)
|
||||||
|
TAG_VERSION=${{ steps.version.outputs.version }}
|
||||||
|
if [ "$PLUGIN_VERSION" != "$TAG_VERSION" ]; then
|
||||||
|
echo "Error: Plugin version ($PLUGIN_VERSION) does not match tag version ($TAG_VERSION)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Version verified: $PLUGIN_VERSION"
|
||||||
|
|
||||||
|
- name: Create release directory
|
||||||
|
run: mkdir -p releases
|
||||||
|
|
||||||
|
- name: Build release package
|
||||||
|
run: |
|
||||||
|
VERSION=${{ steps.version.outputs.version }}
|
||||||
|
PLUGIN_NAME="wc-composable-product"
|
||||||
|
RELEASE_FILE="releases/${PLUGIN_NAME}-${VERSION}.zip"
|
||||||
|
|
||||||
|
# Move to parent directory for proper zip structure
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
# Create zip with proper WordPress plugin structure
|
||||||
|
zip -r "${PLUGIN_NAME}/${RELEASE_FILE}" "${PLUGIN_NAME}" \
|
||||||
|
-x "${PLUGIN_NAME}/.git/*" \
|
||||||
|
-x "${PLUGIN_NAME}/.gitea/*" \
|
||||||
|
-x "${PLUGIN_NAME}/.github/*" \
|
||||||
|
-x "${PLUGIN_NAME}/.vscode/*" \
|
||||||
|
-x "${PLUGIN_NAME}/.claude/*" \
|
||||||
|
-x "${PLUGIN_NAME}/CLAUDE.md" \
|
||||||
|
-x "${PLUGIN_NAME}/wp-core" \
|
||||||
|
-x "${PLUGIN_NAME}/wp-core/*" \
|
||||||
|
-x "${PLUGIN_NAME}/wp-plugins" \
|
||||||
|
-x "${PLUGIN_NAME}/wp-plugins/*" \
|
||||||
|
-x "${PLUGIN_NAME}/releases/*" \
|
||||||
|
-x "${PLUGIN_NAME}/cache/*" \
|
||||||
|
-x "${PLUGIN_NAME}/composer.lock" \
|
||||||
|
-x "${PLUGIN_NAME}/*.log" \
|
||||||
|
-x "${PLUGIN_NAME}/.gitignore" \
|
||||||
|
-x "${PLUGIN_NAME}/.editorconfig" \
|
||||||
|
-x "${PLUGIN_NAME}/phpcs.xml*" \
|
||||||
|
-x "${PLUGIN_NAME}/phpunit.xml*" \
|
||||||
|
-x "${PLUGIN_NAME}/tests/*" \
|
||||||
|
-x "${PLUGIN_NAME}/*.po~" \
|
||||||
|
-x "${PLUGIN_NAME}/*.bak" \
|
||||||
|
-x "*.DS_Store"
|
||||||
|
|
||||||
|
cd "${PLUGIN_NAME}"
|
||||||
|
echo "Created: ${RELEASE_FILE}"
|
||||||
|
|
||||||
|
- name: Generate checksums
|
||||||
|
run: |
|
||||||
|
VERSION=${{ steps.version.outputs.version }}
|
||||||
|
|
||||||
|
cd releases
|
||||||
|
sha256sum "wc-composable-product-${VERSION}.zip" > "wc-composable-product-${VERSION}.zip.sha256"
|
||||||
|
|
||||||
|
echo "SHA256:"
|
||||||
|
cat "wc-composable-product-${VERSION}.zip.sha256"
|
||||||
|
|
||||||
|
- name: Verify package structure
|
||||||
|
run: |
|
||||||
|
set +o pipefail
|
||||||
|
VERSION=${{ steps.version.outputs.version }}
|
||||||
|
echo "Package contents:"
|
||||||
|
unzip -l "releases/wc-composable-product-${VERSION}.zip" | head -50 || true
|
||||||
|
|
||||||
|
# Verify main file is at correct location
|
||||||
|
if unzip -l "releases/wc-composable-product-${VERSION}.zip" | grep -q "wc-composable-product/wc-composable-product.php"; then
|
||||||
|
echo "✓ Main plugin file at correct location"
|
||||||
|
else
|
||||||
|
echo "✗ Error: Main plugin file not found at wc-composable-product/wc-composable-product.php"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify vendor directory is included
|
||||||
|
if unzip -l "releases/wc-composable-product-${VERSION}.zip" | grep -q "wc-composable-product/vendor/"; then
|
||||||
|
echo "✓ Vendor directory included"
|
||||||
|
else
|
||||||
|
echo "✗ Error: Vendor directory not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Extract changelog for release notes
|
||||||
|
id: changelog
|
||||||
|
run: |
|
||||||
|
VERSION=${{ steps.version.outputs.version }}
|
||||||
|
# Extract changelog section for this version
|
||||||
|
NOTES=$(sed -n "/^## \[${VERSION}\]/,/^## \[/p" CHANGELOG.md | sed '$ d' | tail -n +2)
|
||||||
|
if [ -z "$NOTES" ]; then
|
||||||
|
NOTES="Release version ${VERSION}"
|
||||||
|
fi
|
||||||
|
# Save to file for multi-line output
|
||||||
|
echo "$NOTES" > release_notes.txt
|
||||||
|
echo "Release notes extracted"
|
||||||
|
|
||||||
|
- name: Create Gitea Release
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.SRC_GITEA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
VERSION=${{ steps.version.outputs.version }}
|
||||||
|
TAG_NAME=${{ github.ref_name }}
|
||||||
|
PRERELEASE="false"
|
||||||
|
if [[ "$TAG_NAME" == *-* ]]; then
|
||||||
|
PRERELEASE="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read release notes
|
||||||
|
BODY=$(cat release_notes.txt)
|
||||||
|
|
||||||
|
# Check if release already exists for this tag and delete it
|
||||||
|
EXISTING_RELEASE=$(curl -s \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG_NAME}")
|
||||||
|
|
||||||
|
EXISTING_ID=$(echo "$EXISTING_RELEASE" | jq -r '.id // empty')
|
||||||
|
|
||||||
|
if [ -n "$EXISTING_ID" ] && [ "$EXISTING_ID" != "null" ]; then
|
||||||
|
echo "Deleting existing release ID: $EXISTING_ID"
|
||||||
|
curl -s -X DELETE \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/${EXISTING_ID}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create release via Gitea API
|
||||||
|
RELEASE_RESPONSE=$(curl -s -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"tag_name\": \"${TAG_NAME}\", \"name\": \"Release ${VERSION}\", \"body\": $(echo "$BODY" | jq -Rs .), \"draft\": false, \"prerelease\": ${PRERELEASE}}" \
|
||||||
|
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases")
|
||||||
|
|
||||||
|
RELEASE_ID=$(echo "$RELEASE_RESPONSE" | jq -r '.id')
|
||||||
|
|
||||||
|
if [ "$RELEASE_ID" == "null" ] || [ -z "$RELEASE_ID" ]; then
|
||||||
|
echo "Failed to create release:"
|
||||||
|
echo "$RELEASE_RESPONSE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Created release ID: $RELEASE_ID"
|
||||||
|
|
||||||
|
# Upload attachments
|
||||||
|
for file in "releases/wc-composable-product-${VERSION}.zip" "releases/wc-composable-product-${VERSION}.zip.sha256"; do
|
||||||
|
if [ -f "$file" ]; then
|
||||||
|
FILENAME=$(basename "$file")
|
||||||
|
echo "Uploading $FILENAME..."
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary "@$file" \
|
||||||
|
"${GITHUB_SERVER_URL}/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets?name=${FILENAME}"
|
||||||
|
echo "Uploaded $FILENAME"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Release created successfully: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/releases/tag/${TAG_NAME}"
|
||||||
0
.gitignore
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
39
CHANGELOG.md
39
CHANGELOG.md
@@ -5,6 +5,45 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.2.0] - 2026-03-01
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **CRITICAL**: Product selection always empty regardless of configuration (categories, tags, or SKUs)
|
||||||
|
- Root cause: Product query used a `meta_query` checking `_product_type` in `wp_postmeta`, but WooCommerce stores product types in the `product_type` **taxonomy** — the `!=` comparison with a non-existent meta key caused an INNER JOIN returning zero results
|
||||||
|
- Fix: Replaced broken `meta_query` with correct `tax_query` using `product_type` taxonomy to exclude composable products
|
||||||
|
- **CRITICAL**: Cart price always 0.00 despite correct frontend price calculation
|
||||||
|
- Root cause: `composable_price_calculated` flag was persisted to the cart session, preventing price recalculation on subsequent page loads — but `set_price()` only modifies the in-memory product object and is lost between requests
|
||||||
|
- Fix: Removed per-item session flag; the existing static `$already_calculated` flag already prevents duplicate calculation within a single request
|
||||||
|
- **Admin tab rendering**: Both General and Composable Options panels visible on initial page load
|
||||||
|
- Root cause: JavaScript manually showed `#composable_product_data` via `.show()` without hiding the General panel
|
||||||
|
- Fix: Trigger WooCommerce's native tab click instead, so the tab system handles panel visibility correctly
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Gitea CI/CD release workflow** (`.gitea/workflows/release.yml`)
|
||||||
|
- Triggered on `v*` tags
|
||||||
|
- Installs PHP 8.3 with production Composer dependencies
|
||||||
|
- Compiles `.po` → `.mo` translations
|
||||||
|
- Verifies plugin version matches tag
|
||||||
|
- Builds release ZIP with proper WordPress directory structure
|
||||||
|
- Generates SHA-256 checksums
|
||||||
|
- Verifies package contains main plugin file and vendor directory
|
||||||
|
- Extracts changelog for release notes
|
||||||
|
- Creates Gitea release with attachments via API
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Debug logging from v1.1.14 (no longer needed after root cause identified)
|
||||||
|
|
||||||
|
### Technical
|
||||||
|
|
||||||
|
- Modified files: includes/Product_Type.php, includes/Cart_Handler.php, assets/js/admin.js
|
||||||
|
- New file: .gitea/workflows/release.yml
|
||||||
|
- Product query now correctly uses `tax_query` with `product_type` taxonomy (`NOT IN` operator)
|
||||||
|
- Cart price recalculated on every request via `woocommerce_before_calculate_totals` hook
|
||||||
|
- Admin JS uses `$('ul.product_data_tabs li.composable_options a').trigger('click')` for native WooCommerce tab handling
|
||||||
|
|
||||||
## [1.1.14] - 2025-12-31
|
## [1.1.14] - 2025-12-31
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
63
CLAUDE.md
63
CLAUDE.md
@@ -255,8 +255,9 @@ unzip -l wc-composable-product-vX.X.X.zip
|
|||||||
- ✅ ~~Small rendering Bug in admin area. If you load the side, on first view it shows the first both tabs.~~ **FIXED in v1.1.8**
|
- ✅ ~~Small rendering Bug in admin area. If you load the side, on first view it shows the first both tabs.~~ **FIXED in v1.1.8**
|
||||||
- ✅ ~~In the frontend, regardless which selection mode you use, there appears no product selection in any way.~~ **FIXED in v1.1.8**
|
- ✅ ~~In the frontend, regardless which selection mode you use, there appears no product selection in any way.~~ **FIXED in v1.1.8**
|
||||||
- ✅ ~~The pricing field in the frontend should be rendered as localized price field include currency.~~ **FIXED in v1.1.8**
|
- ✅ ~~The pricing field in the frontend should be rendered as localized price field include currency.~~ **FIXED in v1.1.8**
|
||||||
- Still no product selection in frontend. Current mode 'by Category', but 'by tag' also didn't work
|
- ✅ ~~Still no product selection in frontend. Current mode 'by Category', but 'by tag' also didn't work~~ **FIXED in v1.2.0** - Root cause: meta_query checked `_product_type` in postmeta, but WooCommerce stores product types in the `product_type` taxonomy. The `!=` comparison with a non-existent meta key caused INNER JOIN returning zero results. Fixed by using correct `tax_query`.
|
||||||
- The tab rendering is still no correct. first both tabs are shown on initial page load. After clicking a tab, they behave as expected. Update: I Think there is a collision with the dynamicly changing the criteria with the related field and the tab switching function.
|
- ✅ ~~The tab rendering is still no correct. first both tabs are shown on initial page load. After clicking a tab, they behave as expected.~~ **FIXED in v1.2.0** - JS now triggers WooCommerce's native tab click instead of manually toggling panel visibility.
|
||||||
|
- ✅ ~~Cart price always 0.00 despite correct frontend price calculation~~ **FIXED in v1.2.0** - `composable_price_calculated` session flag prevented price recalculation on subsequent page loads.
|
||||||
|
|
||||||
## Session History
|
## Session History
|
||||||
|
|
||||||
@@ -1883,6 +1884,64 @@ This session demonstrated the importance of changing debugging strategy after mu
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### v1.2.0 - Critical Bug Fixes & CI/CD (2026-03-01)
|
||||||
|
|
||||||
|
#### Session 15: Root Cause Discovery and Automated Releases
|
||||||
|
|
||||||
|
**Major bug fix release** resolving three long-standing issues that persisted through v1.1.11-v1.1.14.
|
||||||
|
|
||||||
|
**Bugs fixed:**
|
||||||
|
|
||||||
|
1. **Product selection always empty** (the real root cause, finally found!)
|
||||||
|
- **Root cause**: `includes/Product_Type.php` line 117-124 had a `meta_query` checking `_product_type != 'composable'` in `wp_postmeta`. But WooCommerce stores product types in the `product_type` **taxonomy**, not in postmeta. The `!=` comparison on a non-existent meta key generates an `INNER JOIN` on `wp_postmeta` that matches zero rows — returning no products at all.
|
||||||
|
- **Fix**: Replaced `meta_query` with correct `tax_query` using `product_type` taxonomy with `NOT IN` operator.
|
||||||
|
- **Why v1.1.11-v1.1.14 failed**: All previous fix attempts addressed symptoms (variable products, stock filtering, debug logging) but never examined the actual WP_Query construction. The meta_query was the root cause all along.
|
||||||
|
|
||||||
|
2. **Cart price always 0.00**
|
||||||
|
- **Root cause**: `includes/Cart_Handler.php` stored a `composable_price_calculated` flag in the cart session. On the next page load (cart, checkout), this flag was restored from session and prevented price recalculation — but `set_price()` only modifies the in-memory product object and is lost between requests.
|
||||||
|
- **Fix**: Removed the per-item `composable_price_calculated` flag entirely. The existing `static $already_calculated` flag already handles the "don't run twice in the same request" concern.
|
||||||
|
|
||||||
|
3. **Admin tab rendering on initial page load**
|
||||||
|
- **Root cause**: JavaScript called `$('#composable_product_data').show()` which made the composable panel visible without hiding the General panel that WooCommerce shows by default.
|
||||||
|
- **Fix**: Trigger WooCommerce's native tab click (`$('ul.product_data_tabs li.composable_options a').trigger('click')`) so the tab system handles panel visibility correctly.
|
||||||
|
|
||||||
|
**New feature:**
|
||||||
|
|
||||||
|
4. **Gitea CI/CD release workflow** (`.gitea/workflows/release.yml`)
|
||||||
|
- Triggered on `v*` tags
|
||||||
|
- Installs PHP 8.3, Composer deps (production), compiles translations
|
||||||
|
- Verifies plugin version matches tag
|
||||||
|
- Builds release ZIP with proper WordPress directory structure
|
||||||
|
- Generates SHA-256 checksums, verifies package structure
|
||||||
|
- Creates Gitea release with ZIP and checksum attachments
|
||||||
|
- Uses `SRC_GITEA_TOKEN` secret for Gitea API
|
||||||
|
|
||||||
|
**Files modified:**
|
||||||
|
|
||||||
|
- includes/Product_Type.php: Replaced `meta_query` with `tax_query`, removed debug logging
|
||||||
|
- includes/Cart_Handler.php: Removed `composable_price_calculated` session flag
|
||||||
|
- assets/js/admin.js: Use native WooCommerce tab click instead of manual panel toggle
|
||||||
|
|
||||||
|
**Files created:**
|
||||||
|
|
||||||
|
- .gitea/workflows/release.yml: Gitea CI/CD release workflow
|
||||||
|
|
||||||
|
**Key lessons learned:**
|
||||||
|
|
||||||
|
1. **WooCommerce stores product types in taxonomy, not postmeta**: This is the single most important lesson from the entire v1.1.11-v1.1.14 debugging saga. `_product_type` does NOT exist in `wp_postmeta` — product types are terms in the `product_type` taxonomy.
|
||||||
|
|
||||||
|
2. **WP_Query `!=` on non-existent meta keys returns zero results**: When you use `'compare' => '!='` in a meta_query, WordPress generates an `INNER JOIN` that only matches posts having that meta key. Posts without the key are excluded entirely.
|
||||||
|
|
||||||
|
3. **Don't persist calculation flags in cart sessions**: `set_price()` only modifies in-memory objects. Any flag that prevents recalculation must NOT be stored in session data — use request-scoped variables (like `static`) instead.
|
||||||
|
|
||||||
|
4. **Use native WooCommerce UI mechanisms**: For tab/panel visibility, trigger WooCommerce's own click handlers rather than manually toggling visibility. WooCommerce's tab system handles hiding all other panels automatically.
|
||||||
|
|
||||||
|
5. **Read the actual query, not just the results**: v1.1.11-v1.1.14 all tried to fix what happened AFTER the query (stock filtering, variation expansion, debug logging), but the query itself was the problem.
|
||||||
|
|
||||||
|
**Status:** v1.2.0 released with all three bugs resolved and CI/CD automation added.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
**For AI Assistants:**
|
**For AI Assistants:**
|
||||||
|
|
||||||
When starting a new session on this project:
|
When starting a new session on this project:
|
||||||
|
|||||||
26
README.md
26
README.md
@@ -10,10 +10,13 @@ This plugin adds a new product type to WooCommerce that allows customers to buil
|
|||||||
|
|
||||||
- **Custom Product Type**: New "Composable Product" type in WooCommerce
|
- **Custom Product Type**: New "Composable Product" type in WooCommerce
|
||||||
- **Flexible Selection**: Define available products by category, tag, or SKU
|
- **Flexible Selection**: Define available products by category, tag, or SKU
|
||||||
|
- **Variable Product Support**: Automatically expands variable products into selectable variations
|
||||||
|
- **Stock Management**: Real-time stock validation, visual indicators, and automatic inventory tracking
|
||||||
- **Configurable Limits**: Set global or per-product selection limits
|
- **Configurable Limits**: Set global or per-product selection limits
|
||||||
- **Pricing Options**: Fixed price or sum of selected products
|
- **Pricing Options**: Fixed price or sum of selected products with full locale-aware formatting
|
||||||
- **Multi-language Support**: Fully translatable with i18n support
|
- **Multi-language Support**: Fully translated in 6 locales (de_DE, de_CH, fr_CH, it_CH + informal variants)
|
||||||
- **Modern UI**: Clean interface built with Twig templates and vanilla JavaScript
|
- **Modern UI**: Clean interface built with Twig templates and vanilla JavaScript
|
||||||
|
- **CI/CD**: Automated release workflow for Gitea
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -43,6 +46,7 @@ This plugin adds a new product type to WooCommerce that allows customers to buil
|
|||||||
### Global Settings
|
### Global Settings
|
||||||
|
|
||||||
Navigate to WooCommerce > Settings > Composable Products to configure:
|
Navigate to WooCommerce > Settings > Composable Products to configure:
|
||||||
|
|
||||||
- Default selection limit
|
- Default selection limit
|
||||||
- Default pricing mode
|
- Default pricing mode
|
||||||
- Display options
|
- Display options
|
||||||
@@ -60,10 +64,28 @@ composer install
|
|||||||
### Translation
|
### Translation
|
||||||
|
|
||||||
Generate POT file:
|
Generate POT file:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
wp i18n make-pot . languages/wc-composable-product.pot
|
wp i18n make-pot . languages/wc-composable-product.pot
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Compile translations:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for po in languages/*.po; do msgfmt -o "${po%.po}.mo" "$po"; done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating Releases
|
||||||
|
|
||||||
|
Releases are automated via Gitea CI/CD. Push an annotated tag to trigger:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag -a v1.2.0 -m "Release v1.2.0"
|
||||||
|
git push origin v1.2.0
|
||||||
|
```
|
||||||
|
|
||||||
|
The workflow builds the release ZIP, compiles translations, generates checksums, and creates a Gitea release with attachments.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
GPL v3 or later - see LICENSE file for details
|
GPL v3 or later - see LICENSE file for details
|
||||||
|
|||||||
0
assets/css/admin.css
Normal file → Executable file
0
assets/css/admin.css
Normal file → Executable file
0
assets/css/frontend.css
Normal file → Executable file
0
assets/css/frontend.css
Normal file → Executable file
8
assets/js/admin.js
Normal file → Executable file
8
assets/js/admin.js
Normal file → Executable file
@@ -17,12 +17,14 @@
|
|||||||
if (productType === 'composable') {
|
if (productType === 'composable') {
|
||||||
$('.show_if_composable').show();
|
$('.show_if_composable').show();
|
||||||
$('.hide_if_composable').hide();
|
$('.hide_if_composable').hide();
|
||||||
$('#composable_product_data').show();
|
// Show the composable tab, then click it so WooCommerce's
|
||||||
$('.product_data_tabs .composable_options a').show();
|
// native tab system hides all other panels properly
|
||||||
|
$('.product_data_tabs li.composable_options').show();
|
||||||
|
$('ul.product_data_tabs li.composable_options a').trigger('click');
|
||||||
} else {
|
} else {
|
||||||
$('.show_if_composable').hide();
|
$('.show_if_composable').hide();
|
||||||
|
$('.product_data_tabs li.composable_options').hide();
|
||||||
$('#composable_product_data').hide();
|
$('#composable_product_data').hide();
|
||||||
$('.product_data_tabs .composable_options a').hide();
|
|
||||||
}
|
}
|
||||||
}).trigger('change');
|
}).trigger('change');
|
||||||
|
|
||||||
|
|||||||
0
assets/js/frontend.js
Normal file → Executable file
0
assets/js/frontend.js
Normal file → Executable file
@@ -200,7 +200,7 @@ class Cart_Handler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use static flag to prevent multiple executions
|
// Use static flag to prevent multiple executions within the same request
|
||||||
static $already_calculated = false;
|
static $already_calculated = false;
|
||||||
if ($already_calculated) {
|
if ($already_calculated) {
|
||||||
return;
|
return;
|
||||||
@@ -208,13 +208,10 @@ class Cart_Handler {
|
|||||||
|
|
||||||
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
|
foreach ($cart->get_cart() as $cart_item_key => $cart_item) {
|
||||||
if (isset($cart_item['data']) && $cart_item['data']->get_type() === 'composable') {
|
if (isset($cart_item['data']) && $cart_item['data']->get_type() === 'composable') {
|
||||||
if (isset($cart_item['composable_products']) && !isset($cart_item['composable_price_calculated'])) {
|
if (isset($cart_item['composable_products'])) {
|
||||||
$product = $cart_item['data'];
|
$product = $cart_item['data'];
|
||||||
$price = $product->calculate_composed_price($cart_item['composable_products']);
|
$price = $product->calculate_composed_price($cart_item['composable_products']);
|
||||||
$cart_item['data']->set_price($price);
|
$cart_item['data']->set_price($price);
|
||||||
|
|
||||||
// Mark as calculated to prevent re-calculation by other plugins
|
|
||||||
$cart->cart_contents[$cart_item_key]['composable_price_calculated'] = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,24 +110,20 @@ class Product_Type extends \WC_Product {
|
|||||||
'post_status' => 'publish',
|
'post_status' => 'publish',
|
||||||
'orderby' => 'title',
|
'orderby' => 'title',
|
||||||
'order' => 'ASC',
|
'order' => 'ASC',
|
||||||
'tax_query' => [],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Exclude composable products from selection
|
// Exclude composable products using the product_type taxonomy
|
||||||
$args['meta_query'] = [
|
// (WooCommerce stores product types as taxonomy terms, NOT as postmeta)
|
||||||
|
$args['tax_query'] = [
|
||||||
'relation' => 'AND',
|
'relation' => 'AND',
|
||||||
[
|
[
|
||||||
'key' => '_product_type',
|
'taxonomy' => 'product_type',
|
||||||
'value' => 'composable',
|
'field' => 'slug',
|
||||||
'compare' => '!=',
|
'terms' => ['composable'],
|
||||||
|
'operator' => 'NOT IN',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
// Debug logging
|
|
||||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
|
||||||
error_log('Composable Product Criteria: ' . print_r($criteria, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ($criteria['type']) {
|
switch ($criteria['type']) {
|
||||||
case 'category':
|
case 'category':
|
||||||
if (!empty($criteria['categories'])) {
|
if (!empty($criteria['categories'])) {
|
||||||
@@ -154,28 +150,20 @@ class Product_Type extends \WC_Product {
|
|||||||
case 'sku':
|
case 'sku':
|
||||||
if (!empty($criteria['skus'])) {
|
if (!empty($criteria['skus'])) {
|
||||||
$skus = array_map('trim', explode(',', $criteria['skus']));
|
$skus = array_map('trim', explode(',', $criteria['skus']));
|
||||||
$args['meta_query'][] = [
|
$args['meta_query'] = [
|
||||||
'key' => '_sku',
|
[
|
||||||
'value' => $skus,
|
'key' => '_sku',
|
||||||
'compare' => 'IN',
|
'value' => $skus,
|
||||||
|
'compare' => 'IN',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug logging
|
|
||||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
|
||||||
error_log('Composable Product Query Args: ' . print_r($args, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
$query = new \WP_Query($args);
|
$query = new \WP_Query($args);
|
||||||
$products = [];
|
$products = [];
|
||||||
|
|
||||||
// Debug logging
|
|
||||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
|
||||||
error_log('Composable Product Query Found: ' . $query->found_posts . ' posts');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($query->have_posts()) {
|
if ($query->have_posts()) {
|
||||||
foreach ($query->posts as $post) {
|
foreach ($query->posts as $post) {
|
||||||
$product = wc_get_product($post->ID);
|
$product = wc_get_product($post->ID);
|
||||||
@@ -186,36 +174,21 @@ class Product_Type extends \WC_Product {
|
|||||||
|
|
||||||
// Handle variable products by including their variations
|
// Handle variable products by including their variations
|
||||||
if ($product->is_type('variable')) {
|
if ($product->is_type('variable')) {
|
||||||
// Get variation IDs directly from the product
|
|
||||||
$variation_ids = $product->get_children();
|
$variation_ids = $product->get_children();
|
||||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
|
||||||
error_log('Variable product ' . $product->get_id() . ' has ' . count($variation_ids) . ' variations');
|
|
||||||
}
|
|
||||||
foreach ($variation_ids as $variation_id) {
|
foreach ($variation_ids as $variation_id) {
|
||||||
$variation = wc_get_product($variation_id);
|
$variation = wc_get_product($variation_id);
|
||||||
if ($variation && $variation->is_purchasable()) {
|
if ($variation && $variation->is_purchasable()) {
|
||||||
$products[] = $variation;
|
$products[] = $variation;
|
||||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
|
||||||
error_log('Added variation ' . $variation_id . ' - ' . $variation->get_name());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} elseif ($product->is_purchasable()) {
|
} elseif ($product->is_purchasable()) {
|
||||||
// Simple and other product types
|
|
||||||
$products[] = $product;
|
$products[] = $product;
|
||||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
|
||||||
error_log('Added simple product ' . $product->get_id() . ' - ' . $product->get_name());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wp_reset_postdata();
|
wp_reset_postdata();
|
||||||
|
|
||||||
if (defined('WP_DEBUG') && WP_DEBUG) {
|
|
||||||
error_log('Total products available: ' . count($products));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $products;
|
return $products;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
0
languages/wc-composable-product-de_CH.po
Normal file → Executable file
0
languages/wc-composable-product-de_CH.po
Normal file → Executable file
Binary file not shown.
0
languages/wc-composable-product-de_CH_informal.po
Normal file → Executable file
0
languages/wc-composable-product-de_CH_informal.po
Normal file → Executable file
Binary file not shown.
0
languages/wc-composable-product-de_DE.po
Normal file → Executable file
0
languages/wc-composable-product-de_DE.po
Normal file → Executable file
Binary file not shown.
0
languages/wc-composable-product-de_DE_informal.po
Normal file → Executable file
0
languages/wc-composable-product-de_DE_informal.po
Normal file → Executable file
Binary file not shown.
0
languages/wc-composable-product-fr_CH.po
Normal file → Executable file
0
languages/wc-composable-product-fr_CH.po
Normal file → Executable file
Binary file not shown.
0
languages/wc-composable-product-it_CH.po
Normal file → Executable file
0
languages/wc-composable-product-it_CH.po
Normal file → Executable file
0
languages/wc-composable-product.pot
Normal file → Executable file
0
languages/wc-composable-product.pot
Normal file → Executable file
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
aec3bae001f0013322a73fa941169688 wc-composable-product-v1.0.0.zip
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
4a0f7ec2171aeabfdfe155419fd6124f35f3e14501ee2ca324bbab447259a8bb wc-composable-product-v1.0.0.zip
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
0a60816bbc5a01c0057c1ffa72679d93 releases/wc-composable-product-v1.1.0.zip
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
645fdd68aca95cba77d961f3a48d41b9c12b3d17552572b7c039575dcfcab693 releases/wc-composable-product-v1.1.0.zip
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
db09928aea6fffbf9c2e754d2264f2bc wc-composable-product-v1.1.1.zip
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
761eef69da910ecfdb20ceeed70b5d0381c7cab895e81a040d132cb0f88d749b wc-composable-product-v1.1.1.zip
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
271aad47684ee8318a8824861d5fc387 wc-composable-product-v1.1.10.zip
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
63bfe97aa9fd98e74750786ed0e1579b069505e85558316f7042787994c856ac wc-composable-product-v1.1.10.zip
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
63b105311dc1cc8ac67c05528ad02e30 wc-composable-product-v1.1.11.zip
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
214002a28a0426b4d2423f234d1dff63e4a8e58c6301cbd6eaed8db670db88c6 wc-composable-product-v1.1.11.zip
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
546b9f9dd4ef0ec174d574af301a7bbc wc-composable-product-v1.1.12.zip
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
c445f1744d28cb53ef314f2dbb253aae31a7750f49f615f5c11a109274736f75 wc-composable-product-v1.1.12.zip
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
49d0e5220e927a3b20c25ed5d475f72b wc-composable-product-v1.1.13.zip
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
6011f23f19da9c61c1953f9de110d073bb594fa5e75bf9745d37f666e2869873 wc-composable-product-v1.1.13.zip
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
7b8bbd9c1e0a5db59f89ae677f095430 wc-composable-product-v1.1.14.zip
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
7c943fc5a85d5a48125aaf9f2e42434b370c4fa168ca33cd1e3485deb55302a5 wc-composable-product-v1.1.14.zip
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
37cef191778b448dcbd2ae10141f64c6 wc-composable-product-v1.1.2.zip
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
191eae035b34ce8b33b90cf9d85ed54e493c1b471cda0efe5c992a512e91cc36 wc-composable-product-v1.1.2.zip
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
9bbed416019a796b4d4a5ef72e016e1f wc-composable-product-v1.1.3.zip
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
0ca23ca12570f0e9c518514ffc5209d78c76c3295954d10ec74a28013a762956 wc-composable-product-v1.1.3.zip
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
eae384e342450abd4ac83af0266ac764 wc-composable-product-v1.1.6.zip
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
d64f4f5f1a00d392989cb613780e5726106a08c6aace08e0c74c80553a0b0f1e wc-composable-product-v1.1.6.zip
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
871fbb3b910380c0e43bcf1538408eda releases/wc-composable-product-v1.1.7.zip
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
866e7dd34431f4c881629fd8b59ddd3a27c7a45b7324a3d88cd064a3e01c1b83 releases/wc-composable-product-v1.1.7.zip
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
78eee5eee4762c308c5d37d1aac06b04 wc-composable-product-v1.1.8.zip
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
d7d06e2a5d336609249f803b681cdf270dbe60d6fc28bdd6c451c6744d2fdab6 wc-composable-product-v1.1.8.zip
|
|
||||||
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
a5b08f3613d1b1e8aba0c2b7b82a1582 wc-composable-product-v1.1.9.zip
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
f9fc497c0531c7ea828e164137f3db6e0a2755b899690dfb7d6411baf0c7a65a wc-composable-product-v1.1.9.zip
|
|
||||||
0
templates/product-selector.twig
Normal file → Executable file
0
templates/product-selector.twig
Normal file → Executable file
@@ -4,7 +4,7 @@
|
|||||||
* Plugin Name: WooCommerce Composable Products
|
* Plugin Name: WooCommerce Composable Products
|
||||||
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-composable-product
|
* Plugin URI: https://src.bundespruefstelle.ch/magdev/wc-composable-product
|
||||||
* Description: Create composable products where customers select a limited number of items from a configurable set
|
* Description: Create composable products where customers select a limited number of items from a configurable set
|
||||||
* Version: 1.1.14
|
* Version: 1.2.0
|
||||||
* Author: Marco Graetsch
|
* Author: Marco Graetsch
|
||||||
* Author URI: https://src.bundespruefstelle.ch/magdev
|
* Author URI: https://src.bundespruefstelle.ch/magdev
|
||||||
* License: GPL v3 or later
|
* License: GPL v3 or later
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
defined('ABSPATH') || exit;
|
defined('ABSPATH') || exit;
|
||||||
|
|
||||||
// Define plugin constants
|
// Define plugin constants
|
||||||
define('WC_COMPOSABLE_PRODUCT_VERSION', '1.1.14');
|
define('WC_COMPOSABLE_PRODUCT_VERSION', '1.2.0');
|
||||||
define('WC_COMPOSABLE_PRODUCT_FILE', __FILE__);
|
define('WC_COMPOSABLE_PRODUCT_FILE', __FILE__);
|
||||||
define('WC_COMPOSABLE_PRODUCT_PATH', plugin_dir_path(__FILE__));
|
define('WC_COMPOSABLE_PRODUCT_PATH', plugin_dir_path(__FILE__));
|
||||||
define('WC_COMPOSABLE_PRODUCT_URL', plugin_dir_url(__FILE__));
|
define('WC_COMPOSABLE_PRODUCT_URL', plugin_dir_url(__FILE__));
|
||||||
|
|||||||
Reference in New Issue
Block a user