Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f6e8c2f171 |
@@ -1,63 +0,0 @@
|
|||||||
# MokoOnyx
|
|
||||||
|
|
||||||
Joomla site template — successor to MokoCassiopeia. Base template for all WaaS client deployments.
|
|
||||||
|
|
||||||
## Quick Reference
|
|
||||||
|
|
||||||
| Field | Value |
|
|
||||||
|---|---|
|
|
||||||
| **Element** | `tpl_mokoonyx` |
|
|
||||||
| **Type** | Joomla site template |
|
|
||||||
| **Language** | PHP 8.1+ / CSS / JS |
|
|
||||||
| **Branch** | develop on `dev`, merge to `main` (protected) |
|
|
||||||
| **Wiki** | [MokoOnyx Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/wiki) |
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
make build # Build template ZIP
|
|
||||||
make lint # Run linters
|
|
||||||
make validate # Validate structure
|
|
||||||
make release # Full release pipeline
|
|
||||||
make minify # Minify CSS/JS assets
|
|
||||||
make clean # Clean build artifacts
|
|
||||||
composer install # Install PHP dependencies
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
Joomla **site template** — the base layer that client theme packages override:
|
|
||||||
|
|
||||||
- `src/templateDetails.xml` — template manifest
|
|
||||||
- `src/index.php` — main template entry point
|
|
||||||
- `src/error.php` — error page
|
|
||||||
- `src/offline.php` — maintenance page
|
|
||||||
- `src/component.php` — print/component-only layout
|
|
||||||
- `src/html/` — template overrides for core components
|
|
||||||
- `src/media/css/` — base stylesheets
|
|
||||||
- `src/media/js/` — base scripts
|
|
||||||
- `src/media/images/` — template images
|
|
||||||
- `src/language/` — translations
|
|
||||||
|
|
||||||
### Client Theme Packages
|
|
||||||
|
|
||||||
Client repos (`client-clarksvillefurs`, `client-optainfunding`, etc.) install `type="file"` packages that overlay client-specific CSS, images, and JS into the MokoOnyx media directory. MokoOnyx provides the structure; client themes customize the appearance.
|
|
||||||
|
|
||||||
### Minification
|
|
||||||
|
|
||||||
`MokoMinifyHelper` handles runtime CSS/JS minification in Joomla. Build-time minification via `make minify`. Never commit `*.min.css`/`*.min.js` — they're generated.
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, `*.min.css`/`*.min.js`
|
|
||||||
- **Attribution**: `Authored-by: Moko Consulting`
|
|
||||||
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
|
||||||
- **Minification**: handled at build time (CI) and runtime (MokoMinifyHelper)
|
|
||||||
- **Wiki**: documentation lives in the Gitea wiki, not `docs/` files
|
|
||||||
- **Standards**: [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
|
|
||||||
|
|
||||||
## Coding Standards
|
|
||||||
|
|
||||||
- PHP 8.1+ minimum
|
|
||||||
- SPDX license headers on all PHP files
|
|
||||||
- `defined('_JEXEC') or die;` on all PHP files
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# DISABLED - auto-release handles dev recreation
|
# DISABLED - auto-release handles dev recreation from main
|
||||||
name: Cascade (DISABLED)
|
name: Cascade (DISABLED)
|
||||||
on: workflow_dispatch
|
on: workflow_dispatch
|
||||||
jobs:
|
jobs:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<display-name>Template - MokoOnyx</display-name>
|
<display-name>Template - MokoOnyx</display-name>
|
||||||
<org>MokoConsulting</org>
|
<org>MokoConsulting</org>
|
||||||
<description>MokoOnyx - Joomla site template (successor to MokoCassiopeia)</description>
|
<description>MokoOnyx - Joomla site template (successor to MokoCassiopeia)</description>
|
||||||
<version>02.19.07</version>
|
<version>02.17.00</version>
|
||||||
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
|
||||||
</identity>
|
</identity>
|
||||||
<governance>
|
<governance>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
# | Reads manifest.xml (joomla|dolibarr|generic) to branch logic. |
|
||||||
# | |
|
# | |
|
||||||
# | Platform-specific: |
|
# | Platform-specific: |
|
||||||
# | joomla: XML manifest, type-prefixed packages |
|
# | joomla: XML manifest, updates.xml, type-prefixed packages |
|
||||||
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
# | dolibarr: mod*.class.php, update.txt, dev version reset |
|
||||||
# | generic: README-only, no update stream |
|
# | generic: README-only, no update stream |
|
||||||
# | |
|
# | |
|
||||||
@@ -71,25 +71,20 @@ jobs:
|
|||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
if ! command -v composer &> /dev/null; then
|
||||||
echo Using pre-installed /opt/moko-platform
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
|
||||||
else
|
|
||||||
echo Falling back to fresh clone
|
|
||||||
if ! command -v composer > /dev/null 2>&1; then
|
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
|
||||||
fi
|
|
||||||
rm -rf /tmp/moko-platform-api
|
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
|
||||||
cd /tmp/moko-platform-api
|
|
||||||
composer install --no-dev --no-interaction --quiet
|
|
||||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
|
||||||
fi
|
fi
|
||||||
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
|
rm -rf /tmp/moko-platform-api
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
|
/tmp/moko-platform-api
|
||||||
|
cd /tmp/moko-platform-api
|
||||||
|
composer install --no-dev --no-interaction --quiet
|
||||||
|
|
||||||
- name: Rename branch to rc
|
- name: Rename branch to rc
|
||||||
run: |
|
run: |
|
||||||
php ${MOKO_CLI}/branch_rename.php \
|
php /tmp/moko-platform-api/cli/branch_rename.php \
|
||||||
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
--from "${{ github.event.pull_request.head.ref || 'dev' }}" --to rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
--api-base "${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}" \
|
||||||
@@ -105,7 +100,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish RC release
|
- name: Publish RC release
|
||||||
run: |
|
run: |
|
||||||
php ${MOKO_CLI}/release_publish.php \
|
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||||
--path . --stability rc --bump minor --branch rc \
|
--path . --stability rc --bump minor --branch rc \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
@@ -113,7 +108,7 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
echo "## Promoted to Release Candidate" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Branch renamed to rc, minor bump, RC release built" >> $GITHUB_STEP_SUMMARY
|
echo "Branch renamed to rc, minor bump, RC + lesser stream releases built, updates.xml synced" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
# ── Merged PR → Build & Release (or promote RC to stable) ────────────────────
|
||||||
release:
|
release:
|
||||||
@@ -136,80 +131,31 @@ jobs:
|
|||||||
git config --local user.name "gitea-actions[bot]"
|
git config --local user.name "gitea-actions[bot]"
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
- name: Check for merge conflict markers
|
|
||||||
run: |
|
|
||||||
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
|
||||||
if [ -n "$CONFLICTS" ]; then
|
|
||||||
echo "::error::Merge conflict markers found — aborting release"
|
|
||||||
echo "## Release Blocked: Conflict Markers" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "No conflict markers found"
|
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
- name: Setup moko-platform tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.GH_MIRROR_TOKEN }}"}}'
|
||||||
run: |
|
run: |
|
||||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
# Ensure PHP + Composer are available
|
||||||
echo Using pre-installed /opt/moko-platform
|
if ! command -v composer &> /dev/null; then
|
||||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
else
|
|
||||||
echo Falling back to fresh clone
|
|
||||||
if ! command -v composer > /dev/null 2>&1; then
|
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
|
||||||
fi
|
|
||||||
rm -rf /tmp/moko-platform-api
|
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
|
||||||
cd /tmp/moko-platform-api
|
|
||||||
composer install --no-dev --no-interaction --quiet
|
|
||||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
|
||||||
fi
|
fi
|
||||||
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
|
rm -rf /tmp/moko-platform-api
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
|
/tmp/moko-platform-api
|
||||||
|
cd /tmp/moko-platform-api
|
||||||
|
composer install --no-dev --no-interaction --quiet
|
||||||
|
|
||||||
|
|
||||||
- name: "Publish stable release"
|
- name: "Publish stable release"
|
||||||
run: |
|
run: |
|
||||||
php ${MOKO_CLI}/release_publish.php \
|
php /tmp/moko-platform-api/cli/release_publish.php \
|
||||||
--path . --stability stable --bump minor --branch main \
|
--path . --stability stable --bump minor --branch main \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
--token "${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
- name: Update release notes from CHANGELOG.md
|
|
||||||
run: |
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
|
||||||
|
|
||||||
# Extract [Unreleased] section from changelog
|
|
||||||
if [ -f "CHANGELOG.md" ]; then
|
|
||||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
|
||||||
[ -z "$NOTES" ] && NOTES="Stable release"
|
|
||||||
else
|
|
||||||
NOTES="Stable release"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update release body via API
|
|
||||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
|
||||||
"${API_BASE}/releases/tags/stable" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
|
||||||
|
|
||||||
if [ -n "$RELEASE_ID" ]; then
|
|
||||||
python3 -c "
|
|
||||||
import json, urllib.request
|
|
||||||
body = open('/dev/stdin').read()
|
|
||||||
payload = json.dumps({'body': body}).encode()
|
|
||||||
req = urllib.request.Request(
|
|
||||||
'${API_BASE}/releases/${RELEASE_ID}',
|
|
||||||
data=payload, method='PATCH',
|
|
||||||
headers={
|
|
||||||
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
})
|
|
||||||
urllib.request.urlopen(req)
|
|
||||||
" <<< "$NOTES"
|
|
||||||
echo "Release notes updated from CHANGELOG.md"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
# -- STEP 9: Mirror to GitHub (stable only) --------------------------------
|
||||||
- name: "Step 9: Mirror release to GitHub"
|
- name: "Step 9: Mirror release to GitHub"
|
||||||
if: >-
|
if: >-
|
||||||
@@ -221,7 +167,7 @@ jobs:
|
|||||||
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
RELEASE_TAG="${{ steps.version.outputs.release_tag }}"
|
||||||
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
GH_REPO="${{ vars.GH_MIRROR_REPO || github.repository }}"
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php ${MOKO_CLI}/release_mirror.php \
|
php /tmp/moko-platform-api/cli/release_mirror.php \
|
||||||
--version "$VERSION" --tag "$RELEASE_TAG" \
|
--version "$VERSION" --tag "$RELEASE_TAG" \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
--gh-token "${{ secrets.GH_MIRROR_TOKEN }}" --gh-repo "$GH_REPO" \
|
||||||
@@ -295,7 +241,7 @@ jobs:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
php ${MOKO_CLI}/version_reset_dev.php \
|
php /tmp/moko-platform-api/cli/version_reset_dev.php \
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "${API_BASE}" \
|
||||||
--branch dev --path . 2>&1 || true
|
--branch dev --path . 2>&1 || true
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
# DISABLED — auto-release Step 11 recreates dev from main after every release.
|
# DISABLED - auto-release handles dev recreation from main
|
||||||
# Cascade-dev is redundant and causes version conflicts when both main and dev
|
name: Cascade (DISABLED)
|
||||||
# have different version numbers in templateDetails.xml / manifest.xml.
|
|
||||||
name: "Cascade Main → Dev (DISABLED)"
|
|
||||||
on: workflow_dispatch
|
on: workflow_dispatch
|
||||||
jobs:
|
jobs:
|
||||||
noop:
|
noop:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- run: echo "Cascade disabled — auto-release handles dev recreation"
|
- run: echo disabled
|
||||||
|
|||||||
@@ -5,7 +5,17 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Automation
|
# INGROUP: moko-platform.Automation
|
||||||
# VERSION: 02.19.07
|
<<<<<<< HEAD
|
||||||
|
<<<<<<< HEAD
|
||||||
|
# VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
# VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
# VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
# VERSION: 02.17.00
|
||||||
|
>>>>>>> origin/main
|
||||||
|
>>>>>>> origin/main
|
||||||
# BRIEF: Auto-create feature branch when an issue is opened
|
# BRIEF: Auto-create feature branch when an issue is opened
|
||||||
|
|
||||||
name: "Universal: Issue Branch"
|
name: "Universal: Issue Branch"
|
||||||
|
|||||||
+236
-508
@@ -1,508 +1,236 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.CI
|
# INGROUP: moko-platform.CI
|
||||||
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
# REPO: https://git.mokoconsulting.tech/mokoconsulting-tech/moko-platform
|
||||||
# PATH: /templates/workflows/universal/pr-check.yml.template
|
# PATH: /templates/workflows/universal/pr-check.yml.template
|
||||||
# VERSION: 09.23.00
|
# VERSION: 05.00.00
|
||||||
# BRIEF: PR gate — branch policy + code validation before merge
|
# BRIEF: PR gate — branch policy + code validation before merge
|
||||||
|
|
||||||
name: "Universal: PR Check"
|
name: "Universal: PR Check"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, reopened, edited]
|
types: [opened, synchronize, reopened, edited]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ── Branch Policy ──────────────────────────────────────────────────────
|
# ── Branch Policy ──────────────────────────────────────────────────────
|
||||||
branch-policy:
|
branch-policy:
|
||||||
name: Branch Policy
|
name: Branch Policy
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check branch merge target
|
- name: Check branch merge target
|
||||||
run: |
|
run: |
|
||||||
HEAD="${{ github.head_ref }}"
|
HEAD="${{ github.head_ref }}"
|
||||||
BASE="${{ github.base_ref }}"
|
BASE="${{ github.base_ref }}"
|
||||||
|
|
||||||
echo "PR: ${HEAD} → ${BASE}"
|
echo "PR: ${HEAD} → ${BASE}"
|
||||||
|
|
||||||
ALLOWED=true
|
ALLOWED=true
|
||||||
REASON=""
|
REASON=""
|
||||||
|
|
||||||
case "$HEAD" in
|
case "$HEAD" in
|
||||||
feature/*|feat/*)
|
feature/*|feat/*)
|
||||||
if [ "$BASE" != "dev" ]; then
|
if [ "$BASE" != "dev" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Feature branches must target 'dev', not '${BASE}'"
|
REASON="Feature branches must target 'dev', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
fix/*|bugfix/*)
|
fix/*|bugfix/*)
|
||||||
if [ "$BASE" != "dev" ]; then
|
if [ "$BASE" != "dev" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Fix branches must target 'dev', not '${BASE}'"
|
REASON="Fix branches must target 'dev', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
patch/*)
|
patch/*)
|
||||||
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
|
if [ "$BASE" != "dev" ] && [ "$BASE" != "rc" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
|
REASON="Patch branches must target 'dev' or 'rc', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
hotfix/*)
|
hotfix/*)
|
||||||
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
if [ "$BASE" != "dev" ] && [ "$BASE" != "main" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
|
REASON="Hotfix branches can only target 'dev' or 'main', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
rc)
|
rc)
|
||||||
if [ "$BASE" != "main" ]; then
|
if [ "$BASE" != "main" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="RC branch can only merge into 'main', not '${BASE}'"
|
REASON="RC branch can only merge into 'main', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
dev)
|
dev)
|
||||||
if [ "$BASE" != "main" ]; then
|
if [ "$BASE" != "main" ]; then
|
||||||
ALLOWED=false
|
ALLOWED=false
|
||||||
REASON="Dev branch can only merge into 'main', not '${BASE}'"
|
REASON="Dev branch can only merge into 'main', not '${BASE}'"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
if [ "$ALLOWED" = false ]; then
|
if [ "$ALLOWED" = false ]; then
|
||||||
echo "::error::${REASON}"
|
echo "::error::${REASON}"
|
||||||
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
|
echo "## Branch Policy Violation" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
|
echo "${REASON}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
echo "### Allowed merge paths:" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`feature/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`fix/*\` → \`dev\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`hotfix/*\` → \`dev\` or \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`dev\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
echo "- \`rc/*\` → \`main\`" >> $GITHUB_STEP_SUMMARY
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
echo "Branch policy: OK (${HEAD} → ${BASE})"
|
||||||
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
echo "## Branch Policy: Passed" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
# ── Code Validation ────────────────────────────────────────────────────
|
# ── Code Validation ────────────────────────────────────────────────────
|
||||||
validate:
|
validate:
|
||||||
name: Validate PR
|
name: Validate PR
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Check for merge conflict markers
|
- name: Detect platform
|
||||||
run: |
|
id: platform
|
||||||
CONFLICTS=$(grep -rn '<<<<<<< \|>>>>>>> \|^=======$' --include='*.php' --include='*.xml' --include='*.css' --include='*.js' --include='*.json' --include='*.md' --include='*.yml' --include='*.yaml' --include='*.ini' --include='*.txt' . 2>/dev/null | grep -v '.git/' || true)
|
run: |
|
||||||
if [ -n "$CONFLICTS" ]; then
|
# Read platform from XML manifest (<platform> tag) or plain text fallback
|
||||||
echo "::error::Merge conflict markers found in source files"
|
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
|
||||||
echo "## Conflict Markers Found" >> $GITHUB_STEP_SUMMARY
|
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
|
||||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
||||||
echo "$CONFLICTS" >> $GITHUB_STEP_SUMMARY
|
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
- name: Setup PHP
|
||||||
fi
|
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
||||||
echo "No conflict markers found"
|
run: |
|
||||||
|
if ! command -v php &> /dev/null; then
|
||||||
- name: Detect platform
|
sudo apt-get update -qq
|
||||||
id: platform
|
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
||||||
run: |
|
fi
|
||||||
# Read platform from XML manifest (<platform> tag) or plain text fallback
|
|
||||||
PLATFORM=$(sed -n 's/.*<platform>\([^<]*\)<\/platform>.*/\1/p' .mokogitea/manifest.xml 2>/dev/null | head -1)
|
- name: PHP syntax check
|
||||||
[ -z "$PLATFORM" ] && PLATFORM=$(cat .mokogitea/manifest.xml 2>/dev/null | tr -d '[:space:]')
|
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
||||||
[ -z "$PLATFORM" ] && PLATFORM="generic"
|
run: |
|
||||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
ERRORS=0
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
- name: Setup PHP
|
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
||||||
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
ERRORS=$((ERRORS + 1))
|
||||||
run: |
|
fi
|
||||||
if ! command -v php &> /dev/null; then
|
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
||||||
sudo apt-get update -qq
|
echo "PHP lint: ${ERRORS} error(s)"
|
||||||
sudo apt-get install -y -qq php-cli php-mbstring php-xml >/dev/null 2>&1
|
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
||||||
fi
|
|
||||||
|
- name: Validate platform manifest
|
||||||
- name: PHP syntax check
|
run: |
|
||||||
if: steps.platform.outputs.platform == 'joomla' || steps.platform.outputs.platform == 'dolibarr'
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
run: |
|
case "$PLATFORM" in
|
||||||
ERRORS=0
|
joomla)
|
||||||
while IFS= read -r -d '' file; do
|
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
||||||
if ! php -l "$file" 2>&1 | grep -q "No syntax errors"; then
|
if [ -z "$MANIFEST" ]; then
|
||||||
ERRORS=$((ERRORS + 1))
|
echo "::warning::No Joomla manifest found (WaaS site)"
|
||||||
fi
|
exit 0
|
||||||
done < <(find . -name "*.php" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
fi
|
||||||
echo "PHP lint: ${ERRORS} error(s)"
|
echo "Manifest: ${MANIFEST}"
|
||||||
[ "$ERRORS" -eq 0 ] || { echo "::error::PHP syntax errors found"; exit 1; }
|
if command -v php &> /dev/null; then
|
||||||
|
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
|
||||||
- name: Joomla JEXEC guard check
|
fi
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
for ELEMENT in name version description; do
|
||||||
run: |
|
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
|
||||||
ERRORS=0
|
done
|
||||||
while IFS= read -r -d '' file; do
|
echo "Joomla manifest valid"
|
||||||
# Skip vendor, node_modules, and index.html stub files
|
;;
|
||||||
case "$file" in ./vendor/*|./node_modules/*) continue ;; esac
|
dolibarr)
|
||||||
# Check first 10 lines for JEXEC or JPATH guard
|
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
|
||||||
if ! head -20 "$file" | grep -qE "defined\s*\(\s*['\"](_JEXEC|JPATH_BASE|\\\\JPATH_PLATFORM)['\"]"; then
|
if [ -z "$MOD_FILE" ]; then
|
||||||
echo "::error file=${file}::Missing JEXEC guard: ${file}"
|
echo "::error::No mod*.class.php found"
|
||||||
ERRORS=$((ERRORS + 1))
|
exit 1
|
||||||
fi
|
fi
|
||||||
done < <(find . -name "*.php" -path "*/src/*" -not -path "./.git/*" -not -path "./vendor/*" -print0)
|
echo "Dolibarr module: ${MOD_FILE}"
|
||||||
if [ "$ERRORS" -gt 0 ]; then
|
;;
|
||||||
echo "::error::${ERRORS} PHP file(s) missing defined('_JEXEC') or die guard"
|
*)
|
||||||
echo "## JEXEC Guard Check: Failed" >> $GITHUB_STEP_SUMMARY
|
echo "Generic platform — no manifest validation"
|
||||||
echo "${ERRORS} file(s) in src/ are missing the Joomla execution guard." >> $GITHUB_STEP_SUMMARY
|
;;
|
||||||
exit 1
|
esac
|
||||||
fi
|
|
||||||
echo "JEXEC guard: OK"
|
- name: Check update stream format
|
||||||
|
run: |
|
||||||
- name: Joomla directory listing protection
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
case "$PLATFORM" in
|
||||||
run: |
|
joomla)
|
||||||
MISSING=0
|
if [ -f "updates.xml" ]; then
|
||||||
SOURCE_DIR="src"
|
if command -v php &> /dev/null; then
|
||||||
[ ! -d "$SOURCE_DIR" ] && exit 0
|
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
|
||||||
while IFS= read -r dir; do
|
fi
|
||||||
if [ ! -f "${dir}/index.html" ]; then
|
echo "updates.xml valid"
|
||||||
echo "::warning::Missing index.html in ${dir} (directory listing protection)"
|
fi
|
||||||
MISSING=$((MISSING + 1))
|
;;
|
||||||
fi
|
dolibarr)
|
||||||
done < <(find "$SOURCE_DIR" -type d -not -path "./.git/*" -not -path "*/vendor/*" -not -path "*/node_modules/*")
|
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
|
||||||
if [ "$MISSING" -gt 0 ]; then
|
;;
|
||||||
echo "## Directory Protection" >> $GITHUB_STEP_SUMMARY
|
esac
|
||||||
echo "${MISSING} director(ies) missing index.html" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
- name: Check changelog has unreleased entry
|
||||||
echo "Directory protection: ${MISSING} missing (advisory)"
|
run: |
|
||||||
|
if [ ! -f "CHANGELOG.md" ]; then
|
||||||
- name: Joomla script file and asset checks
|
echo "::warning::No CHANGELOG.md found"
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
exit 0
|
||||||
run: |
|
fi
|
||||||
ERRORS=0
|
# Check for content under [Unreleased] section
|
||||||
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
|
||||||
[ -z "$MANIFEST" ] && exit 0
|
echo "::error::CHANGELOG.md missing [Unreleased] section"
|
||||||
MANIFEST_DIR=$(dirname "$MANIFEST")
|
exit 1
|
||||||
|
fi
|
||||||
# Check scriptfile exists if declared
|
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
|
||||||
SCRIPTFILE=$(sed -n 's/.*<scriptfile>\([^<]*\)<\/scriptfile>.*/\1/p' "$MANIFEST" 2>/dev/null)
|
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
|
||||||
if [ -n "$SCRIPTFILE" ]; then
|
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
|
||||||
if [ ! -f "${MANIFEST_DIR}/${SCRIPTFILE}" ]; then
|
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
|
||||||
echo "::error::Manifest declares <scriptfile>${SCRIPTFILE}</scriptfile> but file not found at ${MANIFEST_DIR}/${SCRIPTFILE}"
|
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
|
||||||
ERRORS=$((ERRORS + 1))
|
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
|
||||||
else
|
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Script file: ${MANIFEST_DIR}/${SCRIPTFILE} (OK)"
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
|
||||||
|
|
||||||
# Require joomla.asset.json and validate it
|
- name: Verify package source
|
||||||
ASSET_JSON=$(find "$MANIFEST_DIR" -name "joomla.asset.json" -not -path "./.git/*" 2>/dev/null | head -1)
|
run: |
|
||||||
if [ -z "$ASSET_JSON" ]; then
|
SOURCE_DIR="src"
|
||||||
echo "::error::joomla.asset.json not found — Joomla asset system is required"
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
ERRORS=$((ERRORS + 1))
|
if [ ! -d "$SOURCE_DIR" ]; then
|
||||||
else
|
echo "::warning::No src/ or htdocs/ directory"
|
||||||
if command -v php &> /dev/null; then
|
exit 0
|
||||||
php -r "json_decode(file_get_contents('$ASSET_JSON')); if(json_last_error()!==JSON_ERROR_NONE){echo json_last_error_msg();exit(1);}" 2>&1 || {
|
fi
|
||||||
echo "::error::joomla.asset.json is not valid JSON"
|
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
||||||
ERRORS=$((ERRORS + 1))
|
echo "Source: ${FILE_COUNT} files"
|
||||||
}
|
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
|
||||||
fi
|
|
||||||
echo "joomla.asset.json: valid"
|
# ── Pre-Release RC Build ─────────────────────────────────────────────────
|
||||||
fi
|
pre-release:
|
||||||
|
name: Build RC Package
|
||||||
# Validate all XML files in src/ are well-formed
|
runs-on: ubuntu-latest
|
||||||
XML_ERRORS=0
|
needs: [branch-policy, validate]
|
||||||
if command -v php &> /dev/null; then
|
|
||||||
while IFS= read -r -d '' xmlfile; do
|
steps:
|
||||||
if ! php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$xmlfile'); if(!\$x){foreach(libxml_get_errors() as \$e) echo trim(\$e->message) . ' in $xmlfile'; exit(1);}" 2>&1; then
|
- name: Trigger RC pre-release
|
||||||
XML_ERRORS=$((XML_ERRORS + 1))
|
env:
|
||||||
fi
|
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
done < <(find "$MANIFEST_DIR" -name "*.xml" -not -path "./.git/*" -print0)
|
REPO: ${{ github.repository }}
|
||||||
fi
|
BRANCH: ${{ github.head_ref }}
|
||||||
if [ "$XML_ERRORS" -gt 0 ]; then
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
echo "::error::${XML_ERRORS} XML file(s) are malformed"
|
run: |
|
||||||
ERRORS=$((ERRORS + 1))
|
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
||||||
else
|
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "XML well-formedness: OK"
|
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
|
||||||
|
|
||||||
[ "$ERRORS" -gt 0 ] && exit 1
|
|
||||||
echo "Joomla asset checks: OK"
|
|
||||||
|
|
||||||
- name: Validate platform manifest
|
|
||||||
run: |
|
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
||||||
case "$PLATFORM" in
|
|
||||||
joomla)
|
|
||||||
MANIFEST=$(find . -maxdepth 3 -name "*.xml" ! -path "./.git/*" -exec grep -l '<extension' {} \; 2>/dev/null | head -1)
|
|
||||||
if [ -z "$MANIFEST" ]; then
|
|
||||||
echo "::warning::No Joomla manifest found (WaaS site)"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "Manifest: ${MANIFEST}"
|
|
||||||
if command -v php &> /dev/null; then
|
|
||||||
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('$MANIFEST'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::Manifest XML is malformed"; exit 1; }
|
|
||||||
fi
|
|
||||||
for ELEMENT in name version description; do
|
|
||||||
grep -q "<${ELEMENT}>" "$MANIFEST" || { echo "::error::Missing <${ELEMENT}> in manifest"; exit 1; }
|
|
||||||
done
|
|
||||||
# Block legacy raw/branch update server URLs on MokoGitea
|
|
||||||
RAW_URLS=$(grep -n 'raw/branch' "$MANIFEST" | grep -i 'mokoconsulting\|mokogitea\|git\.mokoconsulting\.tech' || true)
|
|
||||||
if [ -n "$RAW_URLS" ]; then
|
|
||||||
echo "::error::Manifest contains legacy raw/branch update server URL on MokoGitea. Use the Gitea Pages URL instead (e.g. /{REPO}/updates.xml not /{REPO}/raw/branch/main/updates.xml)"
|
|
||||||
echo "$RAW_URLS"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Joomla manifest valid"
|
|
||||||
;;
|
|
||||||
dolibarr)
|
|
||||||
MOD_FILE=$(find . -maxdepth 4 -name "mod*.class.php" ! -path "./.git/*" -exec grep -l 'extends DolibarrModules' {} \; 2>/dev/null | head -1)
|
|
||||||
if [ -z "$MOD_FILE" ]; then
|
|
||||||
echo "::error::No mod*.class.php found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Dolibarr module: ${MOD_FILE}"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Generic platform — no manifest validation"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
- name: Check update stream format
|
|
||||||
run: |
|
|
||||||
PLATFORM="${{ steps.platform.outputs.platform }}"
|
|
||||||
case "$PLATFORM" in
|
|
||||||
joomla)
|
|
||||||
if [ -f "updates.xml" ]; then
|
|
||||||
if command -v php &> /dev/null; then
|
|
||||||
php -r "libxml_use_internal_errors(true); \$x = simplexml_load_file('updates.xml'); if(!\$x){foreach(libxml_get_errors() as \$e) echo \$e->message; exit(1);}" || { echo "::error::updates.xml is malformed"; exit 1; }
|
|
||||||
fi
|
|
||||||
echo "updates.xml valid"
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
dolibarr)
|
|
||||||
[ -f "update.txt" ] && echo "update.txt present" || echo "::warning::No update.txt"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
- name: Validate Joomla language files
|
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
|
||||||
run: |
|
|
||||||
ERRORS=0
|
|
||||||
WARNINGS=0
|
|
||||||
|
|
||||||
# Require both en-GB and en-US language directories
|
|
||||||
LANG_ROOT=$(find . -path "*/language" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
|
||||||
if [ -z "$LANG_ROOT" ]; then
|
|
||||||
echo "No language/ directory found — skipping"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -d "$LANG_ROOT/en-GB" ]; then
|
|
||||||
echo "::error::Missing en-GB language directory (${LANG_ROOT}/en-GB)"
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
if [ ! -d "$LANG_ROOT/en-US" ]; then
|
|
||||||
echo "::error::Missing en-US language directory (${LANG_ROOT}/en-US)"
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check that en-GB and en-US have matching .ini files
|
|
||||||
if [ -d "$LANG_ROOT/en-GB" ] && [ -d "$LANG_ROOT/en-US" ]; then
|
|
||||||
for GB_INI in "$LANG_ROOT/en-GB"/*.ini; do
|
|
||||||
[ ! -f "$GB_INI" ] && continue
|
|
||||||
US_INI="$LANG_ROOT/en-US/$(basename "$GB_INI")"
|
|
||||||
if [ ! -f "$US_INI" ]; then
|
|
||||||
echo "::error::$(basename "$GB_INI") exists in en-GB but missing from en-US"
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
for US_INI in "$LANG_ROOT/en-US"/*.ini; do
|
|
||||||
[ ! -f "$US_INI" ] && continue
|
|
||||||
GB_INI="$LANG_ROOT/en-GB/$(basename "$US_INI")"
|
|
||||||
if [ ! -f "$GB_INI" ]; then
|
|
||||||
echo "::error::$(basename "$US_INI") exists in en-US but missing from en-GB"
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Find all .ini language files
|
|
||||||
INI_FILES=$(find . -path "*/language/*/*.ini" -not -path "./.git/*" 2>/dev/null)
|
|
||||||
if [ -z "$INI_FILES" ]; then
|
|
||||||
echo "No .ini language files found"
|
|
||||||
[ "$ERRORS" -gt 0 ] && exit 1
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Found $(echo "$INI_FILES" | wc -l) language file(s)"
|
|
||||||
|
|
||||||
for FILE in $INI_FILES; do
|
|
||||||
FNAME=$(basename "$FILE")
|
|
||||||
LINENUM=0
|
|
||||||
SEEN_KEYS=""
|
|
||||||
|
|
||||||
while IFS= read -r line || [ -n "$line" ]; do
|
|
||||||
LINENUM=$((LINENUM + 1))
|
|
||||||
|
|
||||||
# Skip empty lines and comments
|
|
||||||
[ -z "$line" ] && continue
|
|
||||||
echo "$line" | grep -qE '^\s*;' && continue
|
|
||||||
echo "$line" | grep -qE '^\s*$' && continue
|
|
||||||
|
|
||||||
# Must match KEY="VALUE" format
|
|
||||||
if ! echo "$line" | grep -qE '^[A-Z_][A-Z0-9_]*=".*"$'; then
|
|
||||||
echo "::error file=${FILE},line=${LINENUM}::Malformed line: ${line}"
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Extract key and check for duplicates
|
|
||||||
KEY=$(echo "$line" | sed 's/=.*//')
|
|
||||||
if echo "$SEEN_KEYS" | grep -qx "$KEY"; then
|
|
||||||
echo "::error file=${FILE},line=${LINENUM}::Duplicate key: ${KEY}"
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
SEEN_KEYS="${SEEN_KEYS}
|
|
||||||
${KEY}"
|
|
||||||
done < "$FILE"
|
|
||||||
|
|
||||||
echo " ${FILE}: checked ${LINENUM} lines"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Cross-check en-GB vs en-US key consistency
|
|
||||||
GB_DIR=$(find . -path "*/language/en-GB" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
|
||||||
US_DIR=$(find . -path "*/language/en-US" -type d -not -path "./.git/*" 2>/dev/null | head -1)
|
|
||||||
|
|
||||||
if [ -n "$GB_DIR" ] && [ -n "$US_DIR" ]; then
|
|
||||||
for GB_FILE in "$GB_DIR"/*.ini; do
|
|
||||||
[ ! -f "$GB_FILE" ] && continue
|
|
||||||
FNAME=$(basename "$GB_FILE")
|
|
||||||
US_FILE="$US_DIR/$FNAME"
|
|
||||||
[ ! -f "$US_FILE" ] && continue
|
|
||||||
|
|
||||||
GB_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$GB_FILE" 2>/dev/null | sort)
|
|
||||||
US_KEYS=$(grep -oP '^[A-Z_][A-Z0-9_]*(?==)' "$US_FILE" 2>/dev/null | sort)
|
|
||||||
|
|
||||||
# Keys in en-GB but not en-US
|
|
||||||
MISSING_US=$(comm -23 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
|
|
||||||
if [ -n "$MISSING_US" ]; then
|
|
||||||
echo "::warning::Keys in en-GB/$FNAME but missing from en-US/$FNAME:"
|
|
||||||
echo "$MISSING_US" | while read -r k; do echo " - $k"; done
|
|
||||||
WARNINGS=$((WARNINGS + 1))
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Keys in en-US but not en-GB
|
|
||||||
MISSING_GB=$(comm -13 <(echo "$GB_KEYS") <(echo "$US_KEYS"))
|
|
||||||
if [ -n "$MISSING_GB" ]; then
|
|
||||||
echo "::warning::Keys in en-US/$FNAME but missing from en-GB/$FNAME:"
|
|
||||||
echo "$MISSING_GB" | while read -r k; do echo " - $k"; done
|
|
||||||
WARNINGS=$((WARNINGS + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
{
|
|
||||||
echo "### Language File Validation"
|
|
||||||
echo "| Metric | Count |"
|
|
||||||
echo "|---|---|"
|
|
||||||
echo "| Files checked | $(echo "$INI_FILES" | wc -l) |"
|
|
||||||
echo "| Errors | ${ERRORS} |"
|
|
||||||
echo "| Warnings | ${WARNINGS} |"
|
|
||||||
} >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
if [ "$ERRORS" -gt 0 ]; then
|
|
||||||
echo "::error::Language validation failed with ${ERRORS} error(s)"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Language files: OK (${WARNINGS} warning(s))"
|
|
||||||
|
|
||||||
- name: Check changelog has unreleased entry
|
|
||||||
run: |
|
|
||||||
if [ ! -f "CHANGELOG.md" ]; then
|
|
||||||
echo "::warning::No CHANGELOG.md found"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
# Check for content under [Unreleased] section
|
|
||||||
if ! grep -q "## \[Unreleased\]" CHANGELOG.md; then
|
|
||||||
echo "::error::CHANGELOG.md missing [Unreleased] section"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
# Check there's at least one entry (Added/Changed/Fixed/Removed) under Unreleased
|
|
||||||
UNRELEASED_CONTENT=$(sed -n '/## \[Unreleased\]/,/## \[/p' CHANGELOG.md | grep -cE '^\s*-\s' || true)
|
|
||||||
if [ "$UNRELEASED_CONTENT" -eq 0 ]; then
|
|
||||||
echo "::error::CHANGELOG.md [Unreleased] section has no entries. Add a changelog entry describing your changes."
|
|
||||||
echo "## Changelog Check: Failed" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "The \`[Unreleased]\` section in CHANGELOG.md has no entries." >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Add a line like \`- Description of your change\` under a heading (\`### Added\`, \`### Changed\`, \`### Fixed\`, etc.)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Changelog: ${UNRELEASED_CONTENT} entry/entries in [Unreleased]"
|
|
||||||
|
|
||||||
- name: Verify package source
|
|
||||||
run: |
|
|
||||||
SOURCE_DIR="src"
|
|
||||||
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
|
||||||
if [ ! -d "$SOURCE_DIR" ]; then
|
|
||||||
echo "::warning::No src/ or htdocs/ directory"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
FILE_COUNT=$(find "$SOURCE_DIR" -type f | wc -l)
|
|
||||||
echo "Source: ${FILE_COUNT} files"
|
|
||||||
[ "$FILE_COUNT" -gt 0 ] || { echo "::error::Source directory is empty"; exit 1; }
|
|
||||||
|
|
||||||
# ── Pre-Release RC Build ─────────────────────────────────────────────────
|
|
||||||
pre-release:
|
|
||||||
name: Build RC Package
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [branch-policy, validate]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Trigger RC pre-release
|
|
||||||
env:
|
|
||||||
GA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
REPO: ${{ github.repository }}
|
|
||||||
BRANCH: ${{ github.head_ref }}
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
run: |
|
|
||||||
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO}/actions/workflows/pre-release.yml/dispatches" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "{\"ref\":\"${BRANCH}\",\"inputs\":{\"stability\":\"release-candidate\"}}"
|
|
||||||
echo "### Pre-Release" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Triggered RC build on branch \`${BRANCH}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# ── Issue Reporter ──────────────────────────────────────────────────────
|
|
||||||
report-issues:
|
|
||||||
name: Report Issues
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [branch-policy, validate]
|
|
||||||
if: >-
|
|
||||||
always() &&
|
|
||||||
needs.validate.result == 'failure'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
sparse-checkout: automation/ci-issue-reporter.sh
|
|
||||||
sparse-checkout-cone-mode: false
|
|
||||||
|
|
||||||
- name: "File issue for PR validation failure"
|
|
||||||
env:
|
|
||||||
GITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
|
||||||
run: |
|
|
||||||
chmod +x automation/ci-issue-reporter.sh
|
|
||||||
./automation/ci-issue-reporter.sh \
|
|
||||||
--gate "PR Validation" \
|
|
||||||
--workflow "PR Check" \
|
|
||||||
--severity error \
|
|
||||||
--details "PR validation failed (syntax, manifest, changelog, or source checks). See the CI run for the specific check that failed."
|
|
||||||
|
|||||||
@@ -1,243 +1,233 @@
|
|||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
#
|
#
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
#
|
#
|
||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: moko-platform.Release
|
# INGROUP: moko-platform.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||||
# VERSION: 05.01.00
|
# VERSION: 05.01.00
|
||||||
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
||||||
|
|
||||||
name: "Universal: Pre-Release"
|
name: "Universal: Pre-Release"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [closed]
|
types: [closed]
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
pull_request_target:
|
workflow_dispatch:
|
||||||
types: [synchronize, opened, reopened]
|
inputs:
|
||||||
branches:
|
stability:
|
||||||
- main
|
description: 'Pre-release channel'
|
||||||
workflow_dispatch:
|
required: true
|
||||||
inputs:
|
type: choice
|
||||||
stability:
|
options:
|
||||||
description: 'Pre-release channel'
|
- development
|
||||||
required: true
|
- alpha
|
||||||
type: choice
|
- beta
|
||||||
options:
|
- release-candidate
|
||||||
- development
|
|
||||||
- alpha
|
permissions:
|
||||||
- beta
|
contents: write
|
||||||
- release-candidate
|
|
||||||
|
env:
|
||||||
permissions:
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
contents: write
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
env:
|
|
||||||
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
jobs:
|
||||||
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
build:
|
||||||
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
|
||||||
|
runs-on: release
|
||||||
jobs:
|
if: >-
|
||||||
build:
|
github.event_name == 'workflow_dispatch' ||
|
||||||
name: "Build Pre-Release (${{ inputs.stability || 'development' }})"
|
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
|
||||||
runs-on: release
|
|
||||||
if: >-
|
steps:
|
||||||
github.event_name == 'workflow_dispatch' ||
|
- name: Checkout
|
||||||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') ||
|
uses: actions/checkout@v4
|
||||||
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
steps:
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
- name: Setup moko-platform tools
|
||||||
with:
|
env:
|
||||||
fetch-depth: 0
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
|
run: |
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
- name: Setup moko-platform tools
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
env:
|
fi
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
rm -rf /tmp/moko-platform-api
|
||||||
run: |
|
git clone --depth 1 --branch main --quiet \
|
||||||
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
if [ -f /opt/moko-platform/cli/version_bump.php ] && [ -f /opt/moko-platform/cli/manifest_element.php ] && [ -f /opt/moko-platform/vendor/autoload.php ]; then
|
/tmp/moko-platform-api
|
||||||
echo Using pre-installed /opt/moko-platform
|
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||||
echo MOKO_CLI=/opt/moko-platform/cli >> $GITHUB_ENV
|
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
||||||
else
|
|
||||||
echo Falling back to fresh clone
|
- name: Detect platform
|
||||||
if ! command -v composer > /dev/null 2>&1; then
|
id: platform
|
||||||
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer > /dev/null 2>&1
|
run: |
|
||||||
fi
|
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||||
rm -rf /tmp/moko-platform-api
|
|
||||||
CLONE_URL=https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git
|
- name: Resolve metadata and bump version
|
||||||
git clone --depth 1 --branch main --quiet $CLONE_URL /tmp/moko-platform-api
|
id: meta
|
||||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
run: |
|
||||||
echo MOKO_CLI=/tmp/moko-platform-api/cli >> $GITHUB_ENV
|
STABILITY="${{ inputs.stability || 'development' }}"
|
||||||
fi
|
|
||||||
|
case "$STABILITY" in
|
||||||
- name: Detect platform
|
development) SUFFIX="-dev"; TAG="development" ;;
|
||||||
id: platform
|
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
||||||
run: |
|
beta) SUFFIX="-beta"; TAG="beta" ;;
|
||||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||||
|
esac
|
||||||
- name: Resolve metadata and bump version
|
|
||||||
id: meta
|
# Read current version (bump already handled by push workflow)
|
||||||
run: |
|
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
|
||||||
# Auto-detect stability: RC for PRs targeting main, else use input or default to development
|
[ -z "$VERSION" ] && VERSION="00.00.01"
|
||||||
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then
|
|
||||||
STABILITY="release-candidate"
|
# Strip any existing suffix from version before applying stability
|
||||||
else
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
STABILITY="${{ inputs.stability || 'development' }}"
|
|
||||||
fi
|
php ${MOKO_CLI}/version_set_platform.php \
|
||||||
|
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
||||||
case "$STABILITY" in
|
|
||||||
development) SUFFIX="-dev"; TAG="development" ;;
|
# Verify version consistency across all files
|
||||||
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||||
beta) SUFFIX="-beta"; TAG="beta" ;;
|
|
||||||
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
# Update VERSION variable with suffix
|
||||||
esac
|
if [ -n "$SUFFIX" ]; then
|
||||||
|
VERSION="${VERSION}${SUFFIX}"
|
||||||
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
|
fi
|
||||||
case "$STABILITY" in
|
|
||||||
release-candidate) BUMP="minor" ;;
|
# Commit version bump
|
||||||
*) BUMP="patch" ;;
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
esac
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
|
git add -A
|
||||||
|
git diff --cached --quiet || {
|
||||||
# Set stability suffix and verify consistency
|
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
||||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
|
git push origin HEAD 2>&1
|
||||||
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
}
|
||||||
|
|
||||||
php ${MOKO_CLI}/version_set_platform.php \
|
# Auto-detect element via manifest_element.php
|
||||||
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
php ${MOKO_CLI}/manifest_element.php \
|
||||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
--path . --version "$VERSION" --stability "$STABILITY" \
|
||||||
|
--repo "${GITEA_REPO}" --github-output
|
||||||
# Ensure licensing tags (updateservers, dlid) if enabled in manifest.xml
|
|
||||||
php ${MOKO_CLI}/manifest_licensing.php --path . --fix 2>/dev/null || true
|
# Read back element outputs
|
||||||
|
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||||
# Append suffix for output
|
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
||||||
if [ -n "$SUFFIX" ]; then
|
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
||||||
VERSION="${VERSION}${SUFFIX}"
|
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
|
||||||
fi
|
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
# Commit version bump
|
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||||
git config --local user.name "gitea-actions[bot]"
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
git add -A
|
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
||||||
git diff --cached --quiet || {
|
|
||||||
git commit -m "chore(version): pre-release bump to ${VERSION} [skip ci]"
|
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
||||||
git push origin HEAD 2>&1
|
|
||||||
}
|
- name: Create release
|
||||||
|
id: release
|
||||||
# Auto-detect element via manifest_element.php
|
run: |
|
||||||
php ${MOKO_CLI}/manifest_element.php \
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
--path . --version "$VERSION" --stability "$STABILITY" \
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
--repo "${GITEA_REPO}" --github-output
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
php ${MOKO_CLI}/release_create.php \
|
||||||
# Read back element outputs
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
EXT_ELEMENT=$(grep '^ext_element=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
ZIP_NAME=$(grep '^zip_name=' "$GITHUB_OUTPUT" | tail -1 | cut -d= -f2)
|
--repo "${GITEA_REPO}" --branch dev --prerelease
|
||||||
[ -z "$EXT_ELEMENT" ] && EXT_ELEMENT=$(echo "${GITEA_REPO}" | tr '[:upper:]' '[:lower:]' | tr -d ' -')
|
|
||||||
[ -z "$ZIP_NAME" ] && ZIP_NAME="${EXT_ELEMENT}-${VERSION}.zip"
|
- name: Build package and upload
|
||||||
|
id: package
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
run: |
|
||||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
php ${MOKO_CLI}/release_package.php \
|
||||||
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
--repo "${GITEA_REPO}" --output /tmp || true
|
||||||
|
|
||||||
- name: Create release
|
- name: Update updates.xml
|
||||||
id: release
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||||
php ${MOKO_CLI}/release_create.php \
|
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
if [ ! -f "updates.xml" ]; then
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
echo "No updates.xml -- skipping"
|
||||||
--repo "${GITEA_REPO}" --branch dev --prerelease
|
exit 0
|
||||||
|
fi
|
||||||
- name: Update release notes from CHANGELOG.md
|
|
||||||
run: |
|
SHA_FLAG=""
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
php ${MOKO_CLI}/updates_xml_build.php \
|
||||||
|
--path . --version "${VERSION}" --stability "${STABILITY}" \
|
||||||
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
|
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||||
if [ -f "CHANGELOG.md" ]; then
|
${SHA_FLAG}
|
||||||
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
|
||||||
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
# Commit and push
|
||||||
else
|
if ! git diff --quiet updates.xml 2>/dev/null; then
|
||||||
NOTES="Release ${VERSION}"
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
fi
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git add updates.xml
|
||||||
# Update release body via API
|
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
||||||
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
git push origin HEAD 2>&1 || echo "WARNING: push failed"
|
||||||
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
fi
|
||||||
|
|
||||||
if [ -n "$RELEASE_ID" ]; then
|
- name: "Sync updates.xml to all branches"
|
||||||
python3 -c "
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
import json, urllib.request
|
run: |
|
||||||
body = open('/dev/stdin').read()
|
CURRENT_BRANCH="${{ github.ref_name }}"
|
||||||
payload = json.dumps({'body': body}).encode()
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
req = urllib.request.Request(
|
git config --local user.name "gitea-actions[bot]"
|
||||||
'${API_BASE}/releases/${RELEASE_ID}',
|
|
||||||
data=payload, method='PATCH',
|
for BRANCH in main dev; do
|
||||||
headers={
|
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
|
||||||
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
echo "Syncing updates.xml -> ${BRANCH}"
|
||||||
'Content-Type': 'application/json'
|
git fetch origin "${BRANCH}" 2>/dev/null || continue
|
||||||
})
|
git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
|
||||||
urllib.request.urlopen(req)
|
git checkout "${CURRENT_BRANCH}" -- updates.xml
|
||||||
" <<< "$NOTES"
|
if ! git diff --quiet updates.xml 2>/dev/null; then
|
||||||
echo "Release notes updated from CHANGELOG.md"
|
git add updates.xml
|
||||||
fi
|
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
|
||||||
|
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
|
||||||
- name: Build package and upload
|
fi
|
||||||
id: package
|
git checkout "${CURRENT_BRANCH}" 2>/dev/null
|
||||||
run: |
|
done
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
- name: "Delete lesser pre-release channels (cascade)"
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
continue-on-error: true
|
||||||
php ${MOKO_CLI}/release_package.php \
|
run: |
|
||||||
--path . --version "$VERSION" --tag "$TAG" \
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
--repo "${GITEA_REPO}" --output /tmp || true
|
|
||||||
|
php ${MOKO_CLI}/release_cascade.php \
|
||||||
# updates.xml is generated dynamically by MokoGitea license server
|
--stability "${{ steps.meta.outputs.stability }}" \
|
||||||
# No need to build, commit, or sync updates.xml from workflows
|
--token "${TOKEN}" \
|
||||||
|
--api-base "${API_BASE}"
|
||||||
- name: "Delete lesser pre-release channels (cascade)"
|
|
||||||
continue-on-error: true
|
- name: Summary
|
||||||
run: |
|
if: always()
|
||||||
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
run: |
|
||||||
TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
php ${MOKO_CLI}/release_cascade.php \
|
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
||||||
--stability "${{ steps.meta.outputs.stability }}" \
|
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||||
--token "${TOKEN}" \
|
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
--api-base "${API_BASE}"
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
- name: Summary
|
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||||
if: always()
|
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
run: |
|
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
ZIP_NAME="${{ steps.meta.outputs.zip_name }}"
|
|
||||||
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
|
||||||
echo "## Pre-Release Complete" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Version | \`${VERSION}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Channel | ${STABILITY} |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| Package | \`${ZIP_NAME}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "| SHA-256 | \`${SHA256:-n/a}\` |" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,312 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: moko-platform.Universal
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
|
# PATH: /templates/workflows/update-server.yml
|
||||||
|
# VERSION: 05.00.00
|
||||||
|
# BRIEF: Pre-release build + update server XML for dev/alpha/beta/rc branches
|
||||||
|
#
|
||||||
|
# Thin wrapper around moko-platform CLI tools.
|
||||||
|
# Builds packages, updates updates.xml, and optionally deploys via SFTP.
|
||||||
|
#
|
||||||
|
# Joomla filters update entries by the user's "Minimum Stability" setting.
|
||||||
|
|
||||||
|
name: "Update Server"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'dev'
|
||||||
|
- 'dev/**'
|
||||||
|
- 'alpha/**'
|
||||||
|
- 'beta/**'
|
||||||
|
- 'rc/**'
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'htdocs/**'
|
||||||
|
pull_request:
|
||||||
|
types: [closed]
|
||||||
|
branches:
|
||||||
|
- 'dev'
|
||||||
|
- 'dev/**'
|
||||||
|
- 'alpha/**'
|
||||||
|
- 'beta/**'
|
||||||
|
- 'rc/**'
|
||||||
|
paths:
|
||||||
|
- 'src/**'
|
||||||
|
- 'htdocs/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
stability:
|
||||||
|
description: 'Stability tag'
|
||||||
|
required: true
|
||||||
|
default: 'development'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- development
|
||||||
|
- alpha
|
||||||
|
- beta
|
||||||
|
- rc
|
||||||
|
- stable
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
GITEA_URL: ${{ vars.GITEA_URL || 'https://git.mokoconsulting.tech' }}
|
||||||
|
GITEA_ORG: ${{ vars.GITEA_ORG || github.repository_owner }}
|
||||||
|
GITEA_REPO: ${{ vars.GITEA_REPO || github.event.repository.name }}
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-xml:
|
||||||
|
name: Update Server
|
||||||
|
runs-on: release
|
||||||
|
if: >-
|
||||||
|
github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' || github.event_name == 'push'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup moko-platform tools
|
||||||
|
env:
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
|
COMPOSER_AUTH: '{"http-basic":{"git.mokoconsulting.tech":{"username":"token","password":"${{ secrets.MOKOGITEA_TOKEN }}"}}}'
|
||||||
|
run: |
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
# Always fetch latest CLI tools — never use stale cache from previous runs
|
||||||
|
rm -rf /tmp/moko-platform
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
||||||
|
/tmp/moko-platform 2>/dev/null || true
|
||||||
|
if [ -d "/tmp/moko-platform" ] && [ -f "/tmp/moko-platform/composer.json" ]; then
|
||||||
|
cd /tmp/moko-platform && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
echo "MOKO_CLI=/tmp/moko-platform/cli" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Detect platform
|
||||||
|
id: platform
|
||||||
|
run: php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||||
|
|
||||||
|
- name: Resolve stability and bump version
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
BRANCH="${{ github.ref_name }}"
|
||||||
|
|
||||||
|
# Configure git for bot pushes
|
||||||
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
|
git config --local user.name "gitea-actions[bot]"
|
||||||
|
git remote set-url origin "https://x-access-token:${{ secrets.MOKOGITEA_TOKEN }}@git.mokoconsulting.tech/${{ github.repository }}.git"
|
||||||
|
|
||||||
|
# Auto-bump patch version
|
||||||
|
php ${MOKO_CLI}/version_bump.php --path . 2>/dev/null || true
|
||||||
|
|
||||||
|
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "0.0.0")
|
||||||
|
|
||||||
|
# Strip any existing suffix before applying stability
|
||||||
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
|
|
||||||
|
# Determine stability from branch or manual input
|
||||||
|
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||||
|
STABILITY="${{ inputs.stability }}"
|
||||||
|
elif [[ "$BRANCH" == rc/* ]]; then
|
||||||
|
STABILITY="rc"
|
||||||
|
elif [[ "$BRANCH" == beta/* ]]; then
|
||||||
|
STABILITY="beta"
|
||||||
|
elif [[ "$BRANCH" == alpha/* ]]; then
|
||||||
|
STABILITY="alpha"
|
||||||
|
else
|
||||||
|
STABILITY="development"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Version suffix per stability stream
|
||||||
|
case "$STABILITY" in
|
||||||
|
development) SUFFIX="-dev"; TAG="development" ;;
|
||||||
|
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
||||||
|
beta) SUFFIX="-beta"; TAG="beta" ;;
|
||||||
|
rc) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||||
|
*) SUFFIX=""; TAG="stable" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Propagate version with stability suffix to all manifest files
|
||||||
|
php ${MOKO_CLI}/version_set_platform.php \
|
||||||
|
--path . --version "$VERSION" --branch "$BRANCH" --stability "$STABILITY" 2>/dev/null || true
|
||||||
|
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
|
# Re-read version (now includes suffix from version_set_platform)
|
||||||
|
if [ -n "$SUFFIX" ]; then
|
||||||
|
VERSION="${VERSION}${SUFFIX}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "display_version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Commit version bump if changed
|
||||||
|
git add -A
|
||||||
|
git diff --cached --quiet || {
|
||||||
|
git commit -m "chore(version): auto-bump ${VERSION} [skip ci]" \
|
||||||
|
--author="gitea-actions[bot] <gitea-actions[bot]@mokoconsulting.tech>"
|
||||||
|
git push
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Create release and upload package
|
||||||
|
id: package
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# Create or update Gitea release
|
||||||
|
php ${MOKO_CLI}/release_create.php \
|
||||||
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||||
|
|
||||||
|
# Build package and upload
|
||||||
|
php ${MOKO_CLI}/release_package.php \
|
||||||
|
--path . --version "$VERSION" --tag "$TAG" \
|
||||||
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
|
--repo "${GITEA_REPO}" --output /tmp || true
|
||||||
|
|
||||||
|
- name: Update updates.xml
|
||||||
|
if: steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
||||||
|
|
||||||
|
if [ ! -f "updates.xml" ]; then
|
||||||
|
echo "No updates.xml — skipping"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
SHA_FLAG=""
|
||||||
|
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
||||||
|
|
||||||
|
php ${MOKO_CLI}/updates_xml_build.php \
|
||||||
|
--path . --version "${VERSION}" --stability "${STABILITY}" \
|
||||||
|
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
||||||
|
${SHA_FLAG}
|
||||||
|
|
||||||
|
# Commit and push updates.xml
|
||||||
|
git add updates.xml
|
||||||
|
git diff --cached --quiet || {
|
||||||
|
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
||||||
|
git push
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Sync updates.xml to main
|
||||||
|
if: github.ref_name != 'main' && steps.platform.outputs.platform == 'joomla'
|
||||||
|
run: |
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
GITEA_TOKEN="${{ secrets.MOKOGITEA_TOKEN }}"
|
||||||
|
|
||||||
|
FILE_SHA=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
"${API_BASE}/contents/updates.xml?ref=main" | python3 -c "import sys,json; print(json.load(sys.stdin).get('sha',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$FILE_SHA" ] && [ -f "updates.xml" ]; then
|
||||||
|
python3 -c "
|
||||||
|
import base64, json, urllib.request, sys
|
||||||
|
with open('updates.xml', 'rb') as f:
|
||||||
|
content = base64.b64encode(f.read()).decode()
|
||||||
|
payload = json.dumps({
|
||||||
|
'content': content,
|
||||||
|
'sha': '${FILE_SHA}',
|
||||||
|
'message': 'chore: sync updates.xml from ${{ steps.meta.outputs.stability }} [skip ci]',
|
||||||
|
'branch': 'main'
|
||||||
|
}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
'${API_BASE}/contents/updates.xml',
|
||||||
|
data=payload, method='PUT',
|
||||||
|
headers={
|
||||||
|
'Authorization': 'token ${GITEA_TOKEN}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
urllib.request.urlopen(req)
|
||||||
|
print('updates.xml synced to main')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'WARNING: sync to main failed: {e}', file=sys.stderr)
|
||||||
|
"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: SFTP deploy to dev server
|
||||||
|
if: contains(github.ref, 'dev/') || github.ref == 'refs/heads/dev'
|
||||||
|
env:
|
||||||
|
DEV_HOST: ${{ vars.DEV_FTP_HOST }}
|
||||||
|
DEV_PATH: ${{ vars.DEV_FTP_PATH }}
|
||||||
|
DEV_SUFFIX: ${{ vars.DEV_FTP_SUFFIX }}
|
||||||
|
DEV_USER: ${{ vars.DEV_FTP_USERNAME }}
|
||||||
|
DEV_PORT: ${{ vars.DEV_FTP_PORT }}
|
||||||
|
DEV_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||||
|
DEV_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
# Permission check: admin or maintain role required
|
||||||
|
ACTOR="${{ github.actor }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
PERMISSION=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
"${API_BASE}/collaborators/${ACTOR}/permission" 2>/dev/null | \
|
||||||
|
python3 -c "import sys,json; print(json.load(sys.stdin).get('permission','read'))" 2>/dev/null || echo "read")
|
||||||
|
case "$PERMISSION" in
|
||||||
|
admin|maintain|write) ;;
|
||||||
|
*)
|
||||||
|
echo "Deploy denied: ${ACTOR} has '${PERMISSION}' — requires admin, maintain, or write"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
[ -z "$DEV_HOST" ] || [ -z "$DEV_PATH" ] && { echo "DEV FTP not configured — skipping SFTP"; exit 0; }
|
||||||
|
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && exit 0
|
||||||
|
|
||||||
|
PORT="${DEV_PORT:-22}"
|
||||||
|
REMOTE="${DEV_PATH%/}"
|
||||||
|
[ -n "$DEV_SUFFIX" ] && REMOTE="${REMOTE}/${DEV_SUFFIX#/}"
|
||||||
|
|
||||||
|
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
||||||
|
"$DEV_HOST" "$PORT" "$DEV_USER" "$REMOTE" > /tmp/sftp-config.json
|
||||||
|
if [ -n "$DEV_KEY" ]; then
|
||||||
|
echo "$DEV_KEY" > /tmp/deploy_key && chmod 600 /tmp/deploy_key
|
||||||
|
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
||||||
|
else
|
||||||
|
printf ',"password":"%s"}' "$DEV_PASS" >> /tmp/sftp-config.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
PLATFORM=$(php ${MOKO_CLI}/platform_detect.php --path . 2>/dev/null || true)
|
||||||
|
if [ "$PLATFORM" = "waas-component" ] && [ -f "${MOKO_CLI}/../deploy/deploy-joomla.php" ]; then
|
||||||
|
php ${MOKO_CLI}/../deploy/deploy-joomla.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||||
|
elif [ -f "${MOKO_CLI}/../deploy/deploy-sftp.php" ]; then
|
||||||
|
php ${MOKO_CLI}/../deploy/deploy-sftp.php --path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json
|
||||||
|
fi
|
||||||
|
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||||
|
echo "SFTP deploy to dev complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
STABILITY="${{ steps.meta.outputs.stability }}"
|
||||||
|
DISPLAY="${{ steps.meta.outputs.display_version }}"
|
||||||
|
echo "## Update Server" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Stability | \`${STABILITY}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Version | \`${DISPLAY}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
+53
-14
@@ -8,26 +8,65 @@
|
|||||||
DEFGROUP: Joomla.Template.Site
|
DEFGROUP: Joomla.Template.Site
|
||||||
INGROUP: MokoOnyx.Documentation
|
INGROUP: MokoOnyx.Documentation
|
||||||
PATH: ./CHANGELOG.md
|
PATH: ./CHANGELOG.md
|
||||||
VERSION: 02.19.07
|
<<<<<<< HEAD
|
||||||
|
<<<<<<< HEAD
|
||||||
|
VERSION: 02.17.00
|
||||||
BRIEF: Changelog file documenting version history of MokoOnyx
|
BRIEF: Changelog file documenting version history of MokoOnyx
|
||||||
-->
|
-->
|
||||||
|
|
||||||
# Changelog — MokoOnyx (VERSION: 02.19.07)
|
# Changelog — MokoOnyx (VERSION: 02.17.00)
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Fixed
|
=======
|
||||||
- Strip Joomla-injected `p-2` padding class from Font Awesome icons in all menu overrides (default, mainmenu, horizontal)
|
VERSION: 02.17.00
|
||||||
|
BRIEF: Changelog file documenting version history of MokoOnyx
|
||||||
|
-->
|
||||||
|
|
||||||
### Changed
|
# Changelog — MokoOnyx (VERSION: 02.17.00)
|
||||||
- Migrated update server URL from raw file endpoint to Gitea Pages
|
>>>>>>> origin/main
|
||||||
- Release workflow no longer manages updates.xml (decoupled to Gitea Pages)
|
=======
|
||||||
- Added conflict-marker guard to PR check and release workflows
|
VERSION: 02.17.00
|
||||||
- Added Joomla language file validation (syntax, duplicates, en-GB/en-US consistency)
|
BRIEF: Changelog file documenting version history of MokoOnyx
|
||||||
- Added JEXEC guard, joomla.asset.json, XML well-formedness, and script file CI checks
|
-->
|
||||||
- Removed RS_FTP_PATH_SUFFIX from repo health requirements
|
|
||||||
|
|
||||||
## [02.20.00] --- 2026-06-04
|
# Changelog — MokoOnyx (VERSION: 02.17.00)
|
||||||
|
>>>>>>> origin/main
|
||||||
## [02.18.00] --- 2026-06-02
|
## [02.17.00] --- 2026-05-30
|
||||||
|
|
||||||
## [02.15.00] --- 2026-05-30
|
## [02.15.00] --- 2026-05-30
|
||||||
|
|
||||||
|
=======
|
||||||
|
VERSION: 02.17.00
|
||||||
|
BRIEF: Changelog file documenting version history of MokoOnyx
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Changelog — MokoOnyx (VERSION: 02.17.00)
|
||||||
|
>>>>>>> origin/main
|
||||||
|
## [02.14.00] --- 2026-05-30
|
||||||
|
|
||||||
|
## [02.13.00] --- 2026-05-30
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Hero image (`hero.jpg`) to template images
|
||||||
|
- `.fa-solid`, `.fa-regular`, `.fa-brands`, `.fa-light` icon margin spacing
|
||||||
|
- `.blog-item .item-image` fixed 250px height with object-fit cover
|
||||||
|
- Hide header on home page option (`hideHeaderHome` template param)
|
||||||
|
- Hide main menu on home page option (`hideMenuHome` template param)
|
||||||
|
- Three distinct menu overrides: mainmenu (collapsible), horizontal (always visible), default (vertical)
|
||||||
|
- `<php_minimum>8.1.0</php_minimum>` to templateDetails.xml
|
||||||
|
- `<changelogurl>` support in updates.xml
|
||||||
|
- CONTRIBUTING.md with universal workflow and version policy
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Release pipeline rework: independent update streams, CLI-driven workflows
|
||||||
|
- Version bumps only trigger on `src/` changes (not docs/config)
|
||||||
|
- Branch protection: CI bot only for push, force push disabled
|
||||||
|
- Auto-bump supports dev, rc, feature/*, patch/* branches
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Joomla update loop caused by version mismatch between ZIP and updates.xml
|
||||||
|
- Duplicate hamburger menu on mobile
|
||||||
|
- Stacked version suffixes (-dev-dev-dev)
|
||||||
|
- Template name doubling (Template - Template - MokoOnyx)
|
||||||
|
|
||||||
|
## [02.08.00] --- 2026-05-29
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code when working with this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**MokoOnyx** -- MokoOnyx - Joomla site template (successor to MokoCassiopeia)
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| **Platform** | joomla |
|
||||||
|
| **Language** | PHP |
|
||||||
|
| **Default branch** | main |
|
||||||
|
| **License** | GPL-3.0-or-later |
|
||||||
|
| **Wiki** | [MokoOnyx Wiki](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/wiki) |
|
||||||
|
| **Standards** | [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home) |
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer install # Install PHP dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
This is a Joomla extension. Key directories:
|
||||||
|
- `src/` -- extension source (deployed to Joomla)
|
||||||
|
- `src/*.xml` -- manifest file (version, files, params)
|
||||||
|
- `src/src/` or `src/services/` -- PHP classes
|
||||||
|
- `src/language/` -- translation strings
|
||||||
|
- `src/media/` -- CSS/JS/images
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- **Workflow directory**: `.mokogitea/` (not `.gitea/` or `.github/`)
|
||||||
|
|
||||||
|
- **Never commit** `.claude/`, `.mcp.json`, `TODO.md`, or `*.min.css`/`*.min.js`
|
||||||
|
- **Attribution**: use `Authored-by: Moko Consulting` in commits
|
||||||
|
- **Branch strategy**: develop on `dev`, merge to `main` for release
|
||||||
|
- **Minification**: handled at build time (CI) and runtime (MokoMinifyHelper for Joomla templates)
|
||||||
|
- **Wiki**: documentation lives in the Gitea wiki, not in `docs/` files
|
||||||
|
- **Standards**: this repo follows [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)
|
||||||
+161
-161
@@ -1,161 +1,161 @@
|
|||||||
# Contributing to Moko Consulting Projects
|
# Contributing to Moko Consulting Projects
|
||||||
|
|
||||||
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
|
Thank you for your interest in contributing. All Moko Consulting repositories follow this universal workflow and version policy.
|
||||||
|
|
||||||
## Branching Workflow
|
## Branching Workflow
|
||||||
|
|
||||||
```
|
```
|
||||||
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
|
feature/* ──PR──> dev ──draft PR──> (renamed to rc) ──merge──> main
|
||||||
```
|
```
|
||||||
|
|
||||||
### Step by step
|
### Step by step
|
||||||
|
|
||||||
1. **Create a feature branch** from `dev`:
|
1. **Create a feature branch** from `dev`:
|
||||||
```bash
|
```bash
|
||||||
git checkout dev && git pull
|
git checkout dev && git pull
|
||||||
git checkout -b feature/my-change
|
git checkout -b feature/my-change
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Work and commit** on your feature branch. Push to origin.
|
2. **Work and commit** on your feature branch. Push to origin.
|
||||||
|
|
||||||
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
|
3. **Open a PR**: `feature/my-change` → `dev`. After review and checks, merge it.
|
||||||
|
|
||||||
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
|
4. **When ready for release**, open a **draft PR**: `dev` → `main`.
|
||||||
- This automatically renames the source branch to `rc` (release candidate)
|
- This automatically renames the source branch to `rc` (release candidate)
|
||||||
- An RC pre-release is built and uploaded
|
- An RC pre-release is built and uploaded
|
||||||
|
|
||||||
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
|
5. **Alpha and beta branches** are created by manually renaming the branch before the RC stage:
|
||||||
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
|
- Rename `dev` to `alpha` for early testing → alpha pre-release is built
|
||||||
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
|
- Rename `alpha` to `beta` for feature-complete testing → beta pre-release is built
|
||||||
- When the draft PR is created, the branch is renamed to `rc`
|
- When the draft PR is created, the branch is renamed to `rc`
|
||||||
|
|
||||||
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
|
6. **Once PR checks pass** on the `rc` branch, mark the PR as ready and merge to `main`.
|
||||||
|
|
||||||
7. **Merging to main** triggers the stable release pipeline:
|
7. **Merging to main** triggers the stable release pipeline:
|
||||||
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
|
- Minor version bump (e.g., `02.09.xx` → `02.10.00`)
|
||||||
- Stability suffix stripped (clean version)
|
- Stability suffix stripped (clean version)
|
||||||
- Gitea release created with ZIP/tar.gz packages
|
- Gitea release created with ZIP/tar.gz packages
|
||||||
- `updates.xml` updated (Joomla extensions)
|
- `updates.xml` updated (Joomla extensions)
|
||||||
- `dev` branch recreated from `main`
|
- `dev` branch recreated from `main`
|
||||||
|
|
||||||
### Branch summary
|
### Branch summary
|
||||||
|
|
||||||
| Branch | Purpose | Created by |
|
| Branch | Purpose | Created by |
|
||||||
|--------|---------|-----------|
|
|--------|---------|-----------|
|
||||||
| `feature/*` | New features and fixes | Developer |
|
| `feature/*` | New features and fixes | Developer |
|
||||||
| `dev` | Integration branch | Auto-recreated after release |
|
| `dev` | Integration branch | Auto-recreated after release |
|
||||||
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
|
| `alpha` | Alpha pre-release testing | Manual rename from `dev` |
|
||||||
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
|
| `beta` | Beta pre-release testing | Manual rename from `alpha` |
|
||||||
| `rc` | Release candidate | Auto-renamed on draft PR to main |
|
| `rc` | Release candidate | Auto-renamed on draft PR to main |
|
||||||
| `main` | Stable releases | Protected, merge only |
|
| `main` | Stable releases | Protected, merge only |
|
||||||
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
|
| `version/XX.YY.ZZ` | Archived release snapshots | Auto-created by CI |
|
||||||
|
|
||||||
### Protected branches
|
### Protected branches
|
||||||
|
|
||||||
| Branch | Direct push | Merge via |
|
| Branch | Direct push | Merge via |
|
||||||
|--------|------------|-----------|
|
|--------|------------|-----------|
|
||||||
| `main` | Blocked (CI bot whitelisted) | PR merge only |
|
| `main` | Blocked (CI bot whitelisted) | PR merge only |
|
||||||
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
|
| `dev` | Blocked (CI bot whitelisted) | PR merge from feature/* |
|
||||||
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
|
| `rc` | Blocked (CI bot whitelisted) | Auto-created on draft PR |
|
||||||
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
|
| `alpha` | Blocked (CI bot whitelisted) | Manual rename |
|
||||||
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
|
| `beta` | Blocked (CI bot whitelisted) | Manual rename |
|
||||||
| `feature/*` | Open | N/A (source branch) |
|
| `feature/*` | Open | N/A (source branch) |
|
||||||
|
|
||||||
## Version Policy
|
## Version Policy
|
||||||
|
|
||||||
### Format
|
### Format
|
||||||
|
|
||||||
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
|
All versions use `XX.YY.ZZ` — three two-digit segments, zero-padded:
|
||||||
|
|
||||||
- **XX** — Major version (breaking changes)
|
- **XX** — Major version (breaking changes)
|
||||||
- **YY** — Minor version (new features, bumped on release to main)
|
- **YY** — Minor version (new features, bumped on release to main)
|
||||||
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
|
- **ZZ** — Patch version (auto-incremented on every push to dev/feature branches)
|
||||||
|
|
||||||
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
|
Rollover: patch `99` → `00` increments minor; minor `99` → `00` increments major.
|
||||||
|
|
||||||
### Stability suffixes
|
### Stability suffixes
|
||||||
|
|
||||||
Each branch appends a suffix to indicate stability:
|
Each branch appends a suffix to indicate stability:
|
||||||
|
|
||||||
| Branch | Suffix | Example |
|
| Branch | Suffix | Example |
|
||||||
|--------|--------|---------|
|
|--------|--------|---------|
|
||||||
| `main` | (none) | `02.09.00` |
|
| `main` | (none) | `02.09.00` |
|
||||||
| `dev` | `-dev` | `02.09.01-dev` |
|
| `dev` | `-dev` | `02.09.01-dev` |
|
||||||
| `feature/*` | `-dev` | `02.09.01-dev` |
|
| `feature/*` | `-dev` | `02.09.01-dev` |
|
||||||
| `alpha` | `-alpha` | `02.09.01-alpha` |
|
| `alpha` | `-alpha` | `02.09.01-alpha` |
|
||||||
| `beta` | `-beta` | `02.09.01-beta` |
|
| `beta` | `-beta` | `02.09.01-beta` |
|
||||||
| `rc` | `-rc` | `02.09.01-rc` |
|
| `rc` | `-rc` | `02.09.01-rc` |
|
||||||
|
|
||||||
### Auto version bump
|
### Auto version bump
|
||||||
|
|
||||||
On every push to `dev`, `feature/*`, or `patch/*`:
|
On every push to `dev`, `feature/*`, or `patch/*`:
|
||||||
|
|
||||||
1. Patch version incremented
|
1. Patch version incremented
|
||||||
2. Stability suffix `-dev` applied
|
2. Stability suffix `-dev` applied
|
||||||
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
|
3. All version-bearing files updated (manifests, CHANGELOG, PHP headers, etc.)
|
||||||
4. Commit created with `[skip ci]` to avoid loops
|
4. Commit created with `[skip ci]` to avoid loops
|
||||||
|
|
||||||
### Release version flow
|
### Release version flow
|
||||||
|
|
||||||
Version bumps happen at specific release events:
|
Version bumps happen at specific release events:
|
||||||
|
|
||||||
| Event | Bump | Example |
|
| Event | Bump | Example |
|
||||||
|-------|------|---------|
|
|-------|------|---------|
|
||||||
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
|
| Feature merged to dev | Patch bump after dev release | `02.09.01-dev` → release → `02.09.02-dev` |
|
||||||
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
|
| Dev promoted to RC | Minor bump | `02.09.02-dev` → `02.10.00-rc` |
|
||||||
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
|
| RC merged to main | Minor bump | `02.10.00-rc` → `02.11.00` (stable) |
|
||||||
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
|
| Dev recreated from main | Patch bump | `02.11.00` → `02.11.01-dev` |
|
||||||
|
|
||||||
### Release stream copies
|
### Release stream copies
|
||||||
|
|
||||||
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
|
When a higher-stability release is published, copies are created for all lesser streams with the same base version:
|
||||||
|
|
||||||
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
|
- **RC `02.10.00-rc`** also creates: `02.10.00-dev`, `02.10.00-alpha`, `02.10.00-beta`
|
||||||
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
|
- **Stable `02.11.00`** also creates: `02.11.00-dev`, `02.11.00-alpha`, `02.11.00-beta`, `02.11.00-rc`
|
||||||
|
|
||||||
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
|
This ensures Joomla sites on ANY stability channel see the update (Joomla only shows versions higher than what's installed).
|
||||||
|
|
||||||
### Version files
|
### Version files
|
||||||
|
|
||||||
The version tools update all files containing version stamps:
|
The version tools update all files containing version stamps:
|
||||||
|
|
||||||
- `.mokogitea/manifest.xml` (canonical source)
|
- `.mokogitea/manifest.xml` (canonical source)
|
||||||
- Joomla XML manifests (`<version>` tag)
|
- Joomla XML manifests (`<version>` tag)
|
||||||
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
|
- `README.md`, `CHANGELOG.md` (`VERSION:` pattern)
|
||||||
- `package.json`, `pyproject.toml`
|
- `package.json`, `pyproject.toml`
|
||||||
- Any text file with a `VERSION: XX.YY.ZZ` label
|
- Any text file with a `VERSION: XX.YY.ZZ` label
|
||||||
|
|
||||||
Files synced from other repos (with a `# REPO:` header) are not touched.
|
Files synced from other repos (with a `# REPO:` header) are not touched.
|
||||||
|
|
||||||
## Code Standards
|
## Code Standards
|
||||||
|
|
||||||
- **PHP**: PSR-12, tabs for indentation
|
- **PHP**: PSR-12, tabs for indentation
|
||||||
- **Copyright**: all files must include the Moko Consulting copyright header
|
- **Copyright**: all files must include the Moko Consulting copyright header
|
||||||
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
|
- **License**: SPDX identifier `GPL-3.0-or-later` (or as specified per repo)
|
||||||
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
|
- **Attribution**: use `Authored-by: Moko Consulting` in commits, not individual names
|
||||||
|
|
||||||
## Commit Messages
|
## Commit Messages
|
||||||
|
|
||||||
Use conventional commit format:
|
Use conventional commit format:
|
||||||
|
|
||||||
```
|
```
|
||||||
type(scope): short description
|
type(scope): short description
|
||||||
|
|
||||||
Optional body with context.
|
Optional body with context.
|
||||||
|
|
||||||
Authored-by: Moko Consulting
|
Authored-by: Moko Consulting
|
||||||
```
|
```
|
||||||
|
|
||||||
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
|
Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `test`, `ci`
|
||||||
|
|
||||||
Special flags in commit messages:
|
Special flags in commit messages:
|
||||||
- `[skip ci]` — skip all CI workflows
|
- `[skip ci]` — skip all CI workflows
|
||||||
- `[skip bump]` — skip auto version bump only
|
- `[skip bump]` — skip auto version bump only
|
||||||
|
|
||||||
## Reporting Issues
|
## Reporting Issues
|
||||||
|
|
||||||
Use the repository's issue tracker with the appropriate template.
|
Use the repository's issue tracker with the appropriate template.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Moko Consulting <hello@mokoconsulting.tech>*
|
*Moko Consulting <hello@mokoconsulting.tech>*
|
||||||
|
|||||||
+11
-1
@@ -10,7 +10,17 @@
|
|||||||
INGROUP: MokoOnyx.Governance
|
INGROUP: MokoOnyx.Governance
|
||||||
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
||||||
FILE: SECURITY.md
|
FILE: SECURITY.md
|
||||||
VERSION: 02.19.07
|
<<<<<<< HEAD
|
||||||
|
<<<<<<< HEAD
|
||||||
|
VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
VERSION: 02.17.00
|
||||||
|
>>>>>>> origin/main
|
||||||
|
>>>>>>> origin/main
|
||||||
BRIEF: Security policy and vulnerability reporting process for MokoOnyx.
|
BRIEF: Security policy and vulnerability reporting process for MokoOnyx.
|
||||||
PATH: /SECURITY.md
|
PATH: /SECURITY.md
|
||||||
NOTE: This policy is process oriented and does not replace secure engineering practices.
|
NOTE: This policy is process oriented and does not replace secure engineering practices.
|
||||||
|
|||||||
@@ -1,237 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# ============================================================================
|
|
||||||
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
#
|
|
||||||
# FILE INFORMATION
|
|
||||||
# DEFGROUP: Automation.CI
|
|
||||||
# INGROUP: moko-platform.Automation
|
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
|
||||||
# PATH: /automation/ci-issue-reporter.sh
|
|
||||||
# VERSION: 09.23.00
|
|
||||||
# BRIEF: Creates or updates a Gitea issue when a CI gate fails.
|
|
||||||
# Deduplicates by searching open issues with the "ci-auto" label
|
|
||||||
# whose title matches the gate. If a matching issue exists, a comment
|
|
||||||
# is appended instead of opening a duplicate.
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# ── Defaults ────────────────────────────────────────────────────────────────
|
|
||||||
GITEA_URL="${GITEA_URL:-https://git.mokoconsulting.tech}"
|
|
||||||
GITEA_TOKEN="${GITEA_TOKEN:-}"
|
|
||||||
REPO="${GITHUB_REPOSITORY:-}"
|
|
||||||
RUN_URL="${GITHUB_SERVER_URL:-${GITEA_URL}}/${REPO}/actions/runs/${GITHUB_RUN_ID:-0}"
|
|
||||||
LABEL_NAME="ci-auto"
|
|
||||||
LABEL_COLOR="#e11d48"
|
|
||||||
|
|
||||||
GATE=""
|
|
||||||
DETAILS=""
|
|
||||||
SEVERITY="error"
|
|
||||||
WORKFLOW=""
|
|
||||||
|
|
||||||
# ── Parse arguments ─────────────────────────────────────────────────────────
|
|
||||||
usage() {
|
|
||||||
cat <<EOF
|
|
||||||
Usage: ci-issue-reporter.sh --gate NAME --details TEXT [OPTIONS]
|
|
||||||
|
|
||||||
Required:
|
|
||||||
--gate CI gate name (e.g. "Code Quality", "Self-Health")
|
|
||||||
--details Human-readable failure description
|
|
||||||
|
|
||||||
Optional:
|
|
||||||
--severity "error" (default) or "warning"
|
|
||||||
--workflow Workflow name for the issue title
|
|
||||||
--repo owner/repo (default: \$GITHUB_REPOSITORY)
|
|
||||||
--run-url URL to the CI run (auto-detected from env)
|
|
||||||
--token Gitea API token (default: \$GITEA_TOKEN)
|
|
||||||
--url Gitea base URL (default: \$GITEA_URL)
|
|
||||||
EOF
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--gate) GATE="$2"; shift 2 ;;
|
|
||||||
--details) DETAILS="$2"; shift 2 ;;
|
|
||||||
--severity) SEVERITY="$2"; shift 2 ;;
|
|
||||||
--workflow) WORKFLOW="$2"; shift 2 ;;
|
|
||||||
--repo) REPO="$2"; shift 2 ;;
|
|
||||||
--run-url) RUN_URL="$2"; shift 2 ;;
|
|
||||||
--token) GITEA_TOKEN="$2"; shift 2 ;;
|
|
||||||
--url) GITEA_URL="$2"; shift 2 ;;
|
|
||||||
-h|--help) usage ;;
|
|
||||||
*) echo "Unknown option: $1"; usage ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
[[ -z "$GATE" ]] && { echo "ERROR: --gate is required"; usage; }
|
|
||||||
[[ -z "$DETAILS" ]] && { echo "ERROR: --details is required"; usage; }
|
|
||||||
[[ -z "$GITEA_TOKEN" ]] && { echo "ERROR: GITEA_TOKEN not set"; exit 1; }
|
|
||||||
[[ -z "$REPO" ]] && { echo "ERROR: GITHUB_REPOSITORY not set"; exit 1; }
|
|
||||||
|
|
||||||
API="${GITEA_URL}/api/v1/repos/${REPO}"
|
|
||||||
|
|
||||||
# ── Build title ─────────────────────────────────────────────────────────────
|
|
||||||
if [[ -n "$WORKFLOW" ]]; then
|
|
||||||
TITLE="[CI] ${WORKFLOW}: ${GATE} failed"
|
|
||||||
else
|
|
||||||
TITLE="[CI] ${GATE} failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ── Ensure label exists ─────────────────────────────────────────────────────
|
|
||||||
ensure_label() {
|
|
||||||
local exists
|
|
||||||
exists=$(curl -sf -o /dev/null -w '%{http_code}' \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API}/labels" 2>/dev/null || echo "000")
|
|
||||||
|
|
||||||
if [[ "$exists" == "200" ]]; then
|
|
||||||
# Check if label already exists
|
|
||||||
local found
|
|
||||||
found=$(curl -sf \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API}/labels" 2>/dev/null \
|
|
||||||
| grep -o "\"name\":\"${LABEL_NAME}\"" || true)
|
|
||||||
|
|
||||||
if [[ -z "$found" ]]; then
|
|
||||||
curl -sf -X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API}/labels" \
|
|
||||||
-d "{\"name\":\"${LABEL_NAME}\",\"color\":\"${LABEL_COLOR}\",\"description\":\"Auto-created by CI issue reporter\"}" \
|
|
||||||
> /dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Search for existing open issue ──────────────────────────────────────────
|
|
||||||
find_existing_issue() {
|
|
||||||
# URL-encode the gate name for the query
|
|
||||||
local query
|
|
||||||
query=$(printf '%s' "[CI] ${GATE}" | sed 's/ /%20/g; s/\[/%5B/g; s/\]/%5D/g')
|
|
||||||
|
|
||||||
local response
|
|
||||||
response=$(curl -sf \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API}/issues?type=issues&state=open&labels=${LABEL_NAME}&q=${query}&limit=5" \
|
|
||||||
2>/dev/null || echo "[]")
|
|
||||||
|
|
||||||
# Extract the first matching issue number
|
|
||||||
echo "$response" \
|
|
||||||
| grep -oP '"number":\s*\K[0-9]+' \
|
|
||||||
| head -1
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Build issue body ────────────────────────────────────────────────────────
|
|
||||||
build_body() {
|
|
||||||
local severity_badge
|
|
||||||
if [[ "$SEVERITY" == "error" ]]; then
|
|
||||||
severity_badge="**Severity:** Error"
|
|
||||||
else
|
|
||||||
severity_badge="**Severity:** Warning"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat <<BODY
|
|
||||||
## CI Gate Failure: ${GATE}
|
|
||||||
|
|
||||||
${severity_badge}
|
|
||||||
**Workflow:** ${WORKFLOW:-unknown}
|
|
||||||
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
|
||||||
**Commit:** \`${GITHUB_SHA:0:8}\`
|
|
||||||
**Run:** [View CI run](${RUN_URL})
|
|
||||||
|
|
||||||
### Details
|
|
||||||
|
|
||||||
${DETAILS}
|
|
||||||
|
|
||||||
### Resolution
|
|
||||||
|
|
||||||
Fix the issue described above and push a new commit. This issue will be closed automatically when the gate passes, or can be closed manually.
|
|
||||||
|
|
||||||
---
|
|
||||||
*Auto-created by [ci-issue-reporter](${GITEA_URL}/${REPO}/src/branch/main/automation/ci-issue-reporter.sh)*
|
|
||||||
BODY
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Build comment body (for existing issues) ────────────────────────────────
|
|
||||||
build_comment() {
|
|
||||||
cat <<COMMENT
|
|
||||||
### CI failure recurrence
|
|
||||||
|
|
||||||
**Branch:** ${GITHUB_REF_NAME:-unknown}
|
|
||||||
**Commit:** \`${GITHUB_SHA:0:8}\`
|
|
||||||
**Run:** [View CI run](${RUN_URL})
|
|
||||||
|
|
||||||
${DETAILS}
|
|
||||||
COMMENT
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Main ────────────────────────────────────────────────────────────────────
|
|
||||||
ensure_label
|
|
||||||
|
|
||||||
EXISTING=$(find_existing_issue)
|
|
||||||
|
|
||||||
if [[ -n "$EXISTING" ]]; then
|
|
||||||
# Append comment to existing issue
|
|
||||||
COMMENT_BODY=$(build_comment)
|
|
||||||
COMMENT_JSON=$(printf '%s' "$COMMENT_BODY" | python3 -c "
|
|
||||||
import sys, json
|
|
||||||
print(json.dumps({'body': sys.stdin.read()}))" 2>/dev/null)
|
|
||||||
|
|
||||||
HTTP=$(curl -sf -o /dev/null -w '%{http_code}' -X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API}/issues/${EXISTING}/comments" \
|
|
||||||
-d "${COMMENT_JSON}" 2>/dev/null || echo "000")
|
|
||||||
|
|
||||||
if [[ "$HTTP" == "201" ]]; then
|
|
||||||
echo "Commented on existing issue #${EXISTING}"
|
|
||||||
else
|
|
||||||
echo "WARNING: Failed to comment on issue #${EXISTING} (HTTP ${HTTP})"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# Create new issue
|
|
||||||
ISSUE_BODY=$(build_body)
|
|
||||||
ISSUE_JSON=$(python3 -c "
|
|
||||||
import sys, json
|
|
||||||
body = sys.stdin.read()
|
|
||||||
print(json.dumps({
|
|
||||||
'title': sys.argv[1],
|
|
||||||
'body': body,
|
|
||||||
'labels': []
|
|
||||||
}))" "$TITLE" <<< "$ISSUE_BODY" 2>/dev/null)
|
|
||||||
|
|
||||||
# Create the issue
|
|
||||||
RESPONSE=$(curl -sf -X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API}/issues" \
|
|
||||||
-d "${ISSUE_JSON}" 2>/dev/null || echo "{}")
|
|
||||||
|
|
||||||
ISSUE_NUM=$(echo "$RESPONSE" | grep -oP '"number":\s*\K[0-9]+' | head -1)
|
|
||||||
|
|
||||||
if [[ -n "$ISSUE_NUM" ]]; then
|
|
||||||
# Apply label (separate call — more reliable across Gitea versions)
|
|
||||||
LABEL_ID=$(curl -sf \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
"${API}/labels" 2>/dev/null \
|
|
||||||
| grep -oP "\"id\":\s*\K[0-9]+(?=[^}]*\"name\":\s*\"${LABEL_NAME}\")" \
|
|
||||||
| head -1 || true)
|
|
||||||
|
|
||||||
if [[ -n "$LABEL_ID" ]]; then
|
|
||||||
curl -sf -X POST \
|
|
||||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
"${API}/issues/${ISSUE_NUM}/labels" \
|
|
||||||
-d "{\"labels\":[${LABEL_ID}]}" \
|
|
||||||
> /dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Created issue #${ISSUE_NUM}: ${TITLE}"
|
|
||||||
else
|
|
||||||
echo "WARNING: Failed to create issue"
|
|
||||||
echo "Response: ${RESPONSE}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
@@ -236,11 +236,11 @@ use Joomla\CMS\Log\Log;
|
|||||||
|
|
||||||
// Update the update server
|
// Update the update server
|
||||||
try {
|
try {
|
||||||
$onyxUpdatesUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/updates.xml';
|
$onyxUpdatesUrl = 'https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml';
|
||||||
$query = $db->getQuery(true)
|
$query = $db->getQuery(true)
|
||||||
->update('#__update_sites')
|
->update('#__update_sites')
|
||||||
->set($db->quoteName('location') . ' = ' . $db->quote($onyxUpdatesUrl))
|
->set($db->quoteName('location') . ' = ' . $db->quote($onyxUpdatesUrl))
|
||||||
->set($db->quoteName('name') . ' = ' . $db->quote('Template - MokoOnyx'))
|
->set($db->quoteName('name') . ' = ' . $db->quote($newDisplay))
|
||||||
->where($db->quoteName('location') . ' LIKE ' . $db->quote('%MokoCassiopeia%'));
|
->where($db->quoteName('location') . ' LIKE ' . $db->quote('%MokoCassiopeia%'));
|
||||||
$db->setQuery($query)->execute();
|
$db->setQuery($query)->execute();
|
||||||
$n = $db->getAffectedRows();
|
$n = $db->getAffectedRows();
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ class MokoMinifyHelper
|
|||||||
$js = preg_replace('/\s*([{}();,=+\-*\/<>!&|?:])\s*/', '$1', $js);
|
$js = preg_replace('/\s*([{}();,=+\-*\/<>!&|?:])\s*/', '$1', $js);
|
||||||
|
|
||||||
// Restore necessary spaces (after keywords)
|
// Restore necessary spaces (after keywords)
|
||||||
$js = preg_replace('/\b(var|let|const|return|typeof|instanceof|new|delete|throw|case|in|of)\b([^\s;})><=!&|?:,])/', '$1 $2', $js);
|
$js = preg_replace('/(var|let|const|return|typeof|instanceof|new|delete|throw|case|in|of)([^\s;})><=!&|?:,])/', '$1 $2', $js);
|
||||||
|
|
||||||
return trim($js);
|
return trim($js);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
*
|
||||||
|
* This file is part of a Moko Consulting project.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* FILE INFORMATION
|
||||||
|
* DEFGROUP: MokoOnyx.Override
|
||||||
|
* INGROUP: MokoOnyx
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
||||||
|
* PATH: /html/com_joomgallery/category/default.php
|
||||||
|
<<<<<<< HEAD
|
||||||
|
<<<<<<< HEAD
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
>>>>>>> origin/main
|
||||||
|
>>>>>>> origin/main
|
||||||
|
* BRIEF: Category view override — password gate then loads default_cat sub-layout
|
||||||
|
*/
|
||||||
|
|
||||||
|
// No direct access
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Router\Route;
|
||||||
|
use Joomla\CMS\Language\Text;
|
||||||
|
use Joomla\CMS\HTML\HTMLHelper;
|
||||||
|
|
||||||
|
// Import CSS & JS
|
||||||
|
$wa = $this->document->getWebAssetManager();
|
||||||
|
$wa->useStyle('com_joomgallery.site');
|
||||||
|
$wa->useStyle('com_joomgallery.jg-icon-font');
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php if ($this->item->pw_protected) : ?>
|
||||||
|
<div class="com-joomgallery-category--locked">
|
||||||
|
<form action="<?php echo Route::_('index.php?task=category.unlock&catid=' . $this->item->id); ?>" method="post" class="row g-3 align-items-end" autocomplete="off">
|
||||||
|
<div class="col-12">
|
||||||
|
<h3><i class="jg-icon-lock me-2" aria-hidden="true"></i><?php echo Text::_('COM_JOOMGALLERY_CATEGORY_PASSWORD_PROTECTED'); ?></h3>
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<label for="jg_password" class="form-label"><?php echo Text::_('JGLOBAL_PASSWORD'); ?></label>
|
||||||
|
<input type="password" name="password" id="jg_password" class="form-control" required />
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">
|
||||||
|
<button type="submit" class="btn btn-primary" id="jg_unlock_button">
|
||||||
|
<i class="jg-icon-unlock me-1" aria-hidden="true"></i><?php echo Text::_('COM_JOOMGALLERY_CATEGORY_BUTTON_UNLOCK'); ?>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<?php echo HTMLHelper::_('form.token'); ?>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<?php else : ?>
|
||||||
|
<?php echo $this->loadTemplate('cat'); ?>
|
||||||
|
<?php endif; ?>
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
*
|
||||||
|
* This file is part of a Moko Consulting project.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* FILE INFORMATION
|
||||||
|
* DEFGROUP: MokoOnyx.Override
|
||||||
|
* INGROUP: MokoOnyx
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
||||||
|
* PATH: /html/com_joomgallery/category/default_cat.php
|
||||||
|
<<<<<<< HEAD
|
||||||
|
<<<<<<< HEAD
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
>>>>>>> origin/main
|
||||||
|
>>>>>>> origin/main
|
||||||
|
* BRIEF: Category sub-layout — subcategories grid + images grid with pagination
|
||||||
|
*/
|
||||||
|
|
||||||
|
// No direct access
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\CMS\Router\Route;
|
||||||
|
use Joomla\CMS\Language\Text;
|
||||||
|
use Joomla\CMS\Session\Session;
|
||||||
|
use Joomla\CMS\HTML\HTMLHelper;
|
||||||
|
use Joomla\CMS\Layout\LayoutHelper;
|
||||||
|
use Joomgallery\Component\Joomgallery\Administrator\Helper\JoomHelper;
|
||||||
|
|
||||||
|
// Image params
|
||||||
|
$image_type = $this->params['configs']->get('jg_category_view_type_image', 'thumbnail', 'STRING');
|
||||||
|
$gallery_class = $this->params['configs']->get('jg_category_view_class', 'masonry', 'STRING');
|
||||||
|
$num_columns = $this->params['configs']->get('jg_category_view_num_columns', 3, 'INT');
|
||||||
|
$image_class = $this->params['configs']->get('jg_category_view_image_class', 0, 'INT');
|
||||||
|
$justified_height = $this->params['configs']->get('jg_category_view_justified_height', 200, 'INT');
|
||||||
|
$justified_gap = $this->params['configs']->get('jg_category_view_justified_gap', 5, 'INT');
|
||||||
|
$image_link = $this->params['configs']->get('jg_category_view_image_link', 'defaultview', 'STRING');
|
||||||
|
$lightbox_image = $this->params['configs']->get('jg_category_view_lightbox_image', 'detail', 'STRING');
|
||||||
|
$pagination_type = $this->params['configs']->get('jg_category_view_pagination', 0, 'INT');
|
||||||
|
$show_subcategories = $this->params['configs']->get('jg_category_view_subcategories', 1, 'INT');
|
||||||
|
$subcategory_type_image = $this->params['configs']->get('jg_category_view_type_subcategory_image', 'thumbnail', 'STRING');
|
||||||
|
$num_columns_subcats = $this->params['configs']->get('jg_category_view_subcategories_num_columns', 3, 'INT');
|
||||||
|
|
||||||
|
// Import CSS & JS
|
||||||
|
$wa = $this->document->getWebAssetManager();
|
||||||
|
|
||||||
|
if ($gallery_class == 'masonry') {
|
||||||
|
$wa->useScript('com_joomgallery.masonry');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($gallery_class == 'justified') {
|
||||||
|
$wa->useScript('com_joomgallery.justified');
|
||||||
|
$wa->addInlineStyle('.jg-images[class*=" justified-"] .jg-image-caption-hover { right: ' . $justified_gap . 'px; }');
|
||||||
|
}
|
||||||
|
|
||||||
|
$lightbox = false;
|
||||||
|
if ($image_link == 'lightgallery') {
|
||||||
|
$lightbox = true;
|
||||||
|
$wa->useScript('com_joomgallery.lightgallery');
|
||||||
|
$wa->useScript('com_joomgallery.lg-thumbnail');
|
||||||
|
$wa->useStyle('com_joomgallery.lightgallery-bundle');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialise the grid script
|
||||||
|
$iniJS = 'window.joomGrid = {';
|
||||||
|
$iniJS .= ' itemid: ' . $this->item->id . ',';
|
||||||
|
$iniJS .= ' pagination: ' . $pagination_type . ',';
|
||||||
|
$iniJS .= ' layout: "' . $gallery_class . '",';
|
||||||
|
$iniJS .= ' num_columns: ' . $num_columns . ',';
|
||||||
|
$iniJS .= ' lightbox: ' . ($lightbox ? 'true' : 'false') . ',';
|
||||||
|
$iniJS .= ' justified: {height: ' . $justified_height . ', gap: ' . $justified_gap . '}';
|
||||||
|
$iniJS .= '};';
|
||||||
|
|
||||||
|
$wa->addInlineScript($iniJS, ['position' => 'before'], [], ['com_joomgallery.joomgrid']);
|
||||||
|
$wa->useScript('com_joomgallery.joomgrid');
|
||||||
|
|
||||||
|
// Access check
|
||||||
|
$canEdit = $this->getAcl()->checkACL('edit', 'com_joomgallery.category', $this->item->id, $this->item->parent_id, true);
|
||||||
|
$canAdd = $this->getAcl()->checkACL('add', 'com_joomgallery.image', 0, $this->item->id, true);
|
||||||
|
$canDelete = $this->getAcl()->checkACL('delete', 'com_joomgallery.category', $this->item->id, $this->item->parent_id, true);
|
||||||
|
$canCheckin = $this->getAcl()->checkACL('editstate', 'com_joomgallery.category', $this->item->id, $this->item->parent_id, true) || $this->item->checked_out == Factory::getUser()->id;
|
||||||
|
$returnURL = base64_encode(JoomHelper::getViewRoute('category', $this->item->id, $this->item->parent_id, $this->item->language, $this->getLayout()));
|
||||||
|
|
||||||
|
$hasSubcats = $show_subcategories && !empty($this->item->children->items) && count($this->item->children->items) > 0;
|
||||||
|
$hasImages = !empty($this->item->images->items) && count($this->item->images->items) > 0;
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="com-joomgallery-category" itemscope itemtype="https://schema.org/ImageGallery">
|
||||||
|
<?php // Page heading ?>
|
||||||
|
<?php if ($this->params['menu']->get('show_page_heading')) : ?>
|
||||||
|
<div class="page-header">
|
||||||
|
<h1><?php echo $this->escape($this->params['menu']->get('page_heading')); ?></h1>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php // Category title ?>
|
||||||
|
<h2 itemprop="name"><?php echo $this->escape($this->item->title); ?></h2>
|
||||||
|
|
||||||
|
<?php // Category description ?>
|
||||||
|
<?php if (!empty($this->item->description)) : ?>
|
||||||
|
<div class="com-joomgallery-category__description mb-3" itemprop="description">
|
||||||
|
<?php echo $this->item->description; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php // Admin buttons ?>
|
||||||
|
<?php if ($canEdit || $canCheckin || $canAdd || $canDelete) : ?>
|
||||||
|
<div class="com-joomgallery-category__actions btn-toolbar mb-3" role="toolbar" aria-label="<?php echo Text::_('JTOOLBAR'); ?>">
|
||||||
|
<?php if ($canCheckin && $this->item->checked_out > 0) : ?>
|
||||||
|
<a class="btn btn-outline-secondary btn-sm me-2" href="<?php echo Route::_('index.php?option=com_joomgallery&task=category.checkin&id=' . $this->item->id . '&return=' . $returnURL . '&' . Session::getFormToken() . '=1'); ?>">
|
||||||
|
<i class="jg-icon-checkin me-1" aria-hidden="true"></i><?php echo Text::_('JLIB_HTML_CHECKIN'); ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($canEdit) : ?>
|
||||||
|
<a class="btn btn-outline-primary btn-sm me-2<?php echo ($this->item->checked_out > 0) ? ' disabled' : ''; ?>" href="<?php echo Route::_('index.php?option=com_joomgallery&task=category.edit&id=' . $this->item->id . '&return=' . $returnURL); ?>">
|
||||||
|
<i class="jg-icon-edit me-1" aria-hidden="true"></i><?php echo Text::_('JGLOBAL_EDIT'); ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($canAdd) : ?>
|
||||||
|
<a class="btn btn-outline-success btn-sm me-2<?php echo ($this->item->checked_out > 0) ? ' disabled' : ''; ?>" href="<?php echo Route::_('index.php?option=com_joomgallery&task=image.add&catid=' . $this->item->id . '&return=' . $returnURL); ?>">
|
||||||
|
<i class="jg-icon-upload me-1" aria-hidden="true"></i><?php echo Text::_('COM_JOOMGALLERY_IMG_UPLOAD_IMAGE'); ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($canDelete) : ?>
|
||||||
|
<a class="btn btn-outline-danger btn-sm<?php echo ($this->item->checked_out > 0) ? ' disabled' : ''; ?>" href="#deleteCatModal" role="button" data-bs-toggle="modal">
|
||||||
|
<i class="jg-icon-delete me-1" aria-hidden="true"></i><?php echo Text::_('JACTION_DELETE'); ?>
|
||||||
|
</a>
|
||||||
|
<?php echo HTMLHelper::_(
|
||||||
|
'bootstrap.renderModal',
|
||||||
|
'deleteCatModal',
|
||||||
|
[
|
||||||
|
'title' => Text::_('JACTION_DELETE'),
|
||||||
|
'modalWidth' => '50',
|
||||||
|
'bodyHeight' => '100',
|
||||||
|
'footer' => '<button class="btn btn-secondary" data-bs-dismiss="modal">' . Text::_('JCANCEL') . '</button>'
|
||||||
|
. '<a href="' . Route::_('index.php?option=com_joomgallery&task=category.remove&id=' . $this->item->id . '&return=' . $returnURL . '&' . Session::getFormToken() . '=1', false, 2) . '" class="btn btn-danger">' . Text::_('JACTION_DELETE') . '</a>',
|
||||||
|
],
|
||||||
|
Text::_('COM_JOOMGALLERY_COMMON_ALERT_SURE_DELETE_SELECTED_ITEM')
|
||||||
|
); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php // Subcategories ?>
|
||||||
|
<?php if ($hasSubcats) : ?>
|
||||||
|
<section class="com-joomgallery-category__subcategories mb-4" aria-label="<?php echo Text::_('COM_JOOMGALLERY_SUBCATEGORIES'); ?>">
|
||||||
|
<h3><?php echo Text::_('COM_JOOMGALLERY_SUBCATEGORIES'); ?></h3>
|
||||||
|
<?php
|
||||||
|
$catsData = [
|
||||||
|
'id' => (int) $this->item->id,
|
||||||
|
'items' => $this->item->children->items,
|
||||||
|
'num_columns' => (int) $num_columns_subcats,
|
||||||
|
'image_type' => $subcategory_type_image,
|
||||||
|
];
|
||||||
|
echo LayoutHelper::render('joomgallery.grids.categories', $catsData);
|
||||||
|
?>
|
||||||
|
</section>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php // Images ?>
|
||||||
|
<?php if ($hasImages) : ?>
|
||||||
|
<section class="com-joomgallery-category__images" aria-label="<?php echo Text::_('COM_JOOMGALLERY_IMAGES'); ?>">
|
||||||
|
<?php if ($hasSubcats) : ?>
|
||||||
|
<h3><?php echo Text::_('COM_JOOMGALLERY_IMAGES'); ?></h3>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
$imgsData = [
|
||||||
|
'id' => (int) $this->item->id,
|
||||||
|
'layout' => $gallery_class,
|
||||||
|
'items' => $this->item->images->items,
|
||||||
|
'num_columns' => (int) $num_columns,
|
||||||
|
'caption_align' => 'center',
|
||||||
|
'image_class' => $image_class,
|
||||||
|
'image_type' => $image_type,
|
||||||
|
'lightbox_type' => $lightbox_image,
|
||||||
|
'image_link' => $image_link,
|
||||||
|
'image_title' => false,
|
||||||
|
'title_link' => 'defaultview',
|
||||||
|
'image_desc' => false,
|
||||||
|
'image_date' => false,
|
||||||
|
'image_author' => false,
|
||||||
|
'image_tags' => false,
|
||||||
|
];
|
||||||
|
echo LayoutHelper::render('joomgallery.grids.images', $imgsData);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php // Pagination ?>
|
||||||
|
<nav class="mt-4" aria-label="<?php echo Text::_('JLIB_HTML_PAGINATION'); ?>">
|
||||||
|
<?php echo $this->item->images->pagination->getListFooter(); ?>
|
||||||
|
</nav>
|
||||||
|
</section>
|
||||||
|
<?php elseif (!$hasSubcats) : ?>
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
<p class="mb-0"><?php echo Text::_('COM_JOOMGALLERY_GALLERY_NO_IMAGES'); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php // Back to parent category ?>
|
||||||
|
<?php if ($this->item->parent_id > 0 && $this->item->parent_id != 1) : ?>
|
||||||
|
<div class="mt-4">
|
||||||
|
<a class="btn btn-outline-secondary" href="<?php echo Route::_('index.php?option=com_joomgallery&view=category&id=' . (int) $this->item->parent_id); ?>">
|
||||||
|
<i class="jg-icon-arrow-left-alt me-1" aria-hidden="true"></i><?php echo Text::_('COM_JOOMGALLERY_BACK'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
if (window.joomGrid.layout != 'justified') {
|
||||||
|
document.querySelectorAll('.' + window.joomGrid.imgclass).forEach(function(image) {
|
||||||
|
image.addEventListener('load', function() {
|
||||||
|
this.closest('.' + window.joomGrid.imgboxclass).classList.add('loaded');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<!DOCTYPE html><title></title>
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
*
|
||||||
|
* This file is part of a Moko Consulting project.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* FILE INFORMATION
|
||||||
|
* DEFGROUP: MokoOnyx.Override
|
||||||
|
* INGROUP: MokoOnyx
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
||||||
|
* PATH: /html/com_joomgallery/gallery/default.php
|
||||||
|
<<<<<<< HEAD
|
||||||
|
<<<<<<< HEAD
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
>>>>>>> origin/main
|
||||||
|
>>>>>>> origin/main
|
||||||
|
* BRIEF: Gallery view override — main image grid with masonry/justified layout
|
||||||
|
*/
|
||||||
|
|
||||||
|
// No direct access
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Router\Route;
|
||||||
|
use Joomla\CMS\Language\Text;
|
||||||
|
use Joomla\CMS\Layout\LayoutHelper;
|
||||||
|
|
||||||
|
// Image params
|
||||||
|
$image_type = $this->params['configs']->get('jg_gallery_view_type_image', 'thumbnail', 'STRING');
|
||||||
|
$gallery_class = $this->params['configs']->get('jg_gallery_view_class', 'masonry', 'STRING');
|
||||||
|
$num_columns = $this->params['configs']->get('jg_gallery_view_num_columns', 3, 'INT');
|
||||||
|
$image_class = $this->params['configs']->get('jg_gallery_view_image_class', 0, 'INT');
|
||||||
|
$justified_height = $this->params['configs']->get('jg_gallery_view_justified_height', 200, 'INT');
|
||||||
|
$justified_gap = $this->params['configs']->get('jg_gallery_view_justified_gap', 5, 'INT');
|
||||||
|
$image_link = $this->params['configs']->get('jg_gallery_view_image_link', 'defaultview', 'STRING');
|
||||||
|
$lightbox_image = $this->params['configs']->get('jg_category_view_lightbox_image', 'detail', 'STRING');
|
||||||
|
$browse_categories_link = $this->params['configs']->get('jg_gallery_view_browse_categories_link', 1, 'INT');
|
||||||
|
|
||||||
|
// Import CSS & JS
|
||||||
|
$wa = $this->document->getWebAssetManager();
|
||||||
|
$wa->useStyle('com_joomgallery.site');
|
||||||
|
$wa->useStyle('com_joomgallery.jg-icon-font');
|
||||||
|
|
||||||
|
if ($gallery_class == 'masonry') {
|
||||||
|
$wa->useScript('com_joomgallery.masonry');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($gallery_class == 'justified') {
|
||||||
|
$wa->useScript('com_joomgallery.justified');
|
||||||
|
$wa->addInlineStyle('.jg-images[class*=" justified-"] .jg-image-caption-hover { right: ' . $justified_gap . 'px; }');
|
||||||
|
}
|
||||||
|
|
||||||
|
$lightbox = false;
|
||||||
|
if ($image_link == 'lightgallery') {
|
||||||
|
$lightbox = true;
|
||||||
|
$wa->useScript('com_joomgallery.lightgallery');
|
||||||
|
$wa->useScript('com_joomgallery.lg-thumbnail');
|
||||||
|
$wa->useStyle('com_joomgallery.lightgallery-bundle');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialise the grid script
|
||||||
|
$iniJS = 'window.joomGrid = {';
|
||||||
|
$iniJS .= ' itemid: ' . $this->item->id . ',';
|
||||||
|
$iniJS .= ' pagination: 0,';
|
||||||
|
$iniJS .= ' layout: "' . $gallery_class . '",';
|
||||||
|
$iniJS .= ' num_columns: ' . $num_columns . ',';
|
||||||
|
$iniJS .= ' lightbox: ' . ($lightbox ? 'true' : 'false') . ',';
|
||||||
|
$iniJS .= ' justified: {height: ' . $justified_height . ', gap: ' . $justified_gap . '}';
|
||||||
|
$iniJS .= '};';
|
||||||
|
|
||||||
|
$wa->addInlineScript($iniJS, ['position' => 'before'], [], ['com_joomgallery.joomgrid']);
|
||||||
|
$wa->useScript('com_joomgallery.joomgrid');
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="com-joomgallery-gallery" itemscope itemtype="https://schema.org/ImageGallery">
|
||||||
|
<?php if ($this->params['menu']->get('show_page_heading')) : ?>
|
||||||
|
<div class="page-header">
|
||||||
|
<h1><?php echo $this->escape($this->params['menu']->get('page_heading')); ?></h1>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php // Browse categories link (top) ?>
|
||||||
|
<?php if ($browse_categories_link == '1') : ?>
|
||||||
|
<div class="text-center mb-4">
|
||||||
|
<a class="btn btn-outline-primary" href="<?php echo Route::_('index.php?option=com_joomgallery&view=category&id=1'); ?>">
|
||||||
|
<i class="jg-icon-folder me-1" aria-hidden="true"></i><?php echo Text::_('COM_JOOMGALLERY_GALLERY_VIEW_BROWSE_CATEGORIES'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (count($this->item->images->items) == 0) : ?>
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
<p class="mb-0"><?php echo Text::_('COM_JOOMGALLERY_GALLERY_NO_IMAGES'); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php else : ?>
|
||||||
|
<?php
|
||||||
|
$imgsData = [
|
||||||
|
'id' => (int) $this->item->id,
|
||||||
|
'layout' => $gallery_class,
|
||||||
|
'items' => $this->item->images->items,
|
||||||
|
'num_columns' => (int) $num_columns,
|
||||||
|
'caption_align' => 'center',
|
||||||
|
'image_class' => $image_class,
|
||||||
|
'image_type' => $image_type,
|
||||||
|
'lightbox_type' => $lightbox_image,
|
||||||
|
'image_link' => $image_link,
|
||||||
|
'image_title' => false,
|
||||||
|
'title_link' => 'defaultview',
|
||||||
|
'image_desc' => false,
|
||||||
|
'image_date' => false,
|
||||||
|
'image_author' => false,
|
||||||
|
'image_tags' => false,
|
||||||
|
];
|
||||||
|
?>
|
||||||
|
<?php echo LayoutHelper::render('joomgallery.grids.images', $imgsData); ?>
|
||||||
|
|
||||||
|
<?php // Pagination ?>
|
||||||
|
<nav class="mt-4" aria-label="<?php echo Text::_('JLIB_HTML_PAGINATION'); ?>">
|
||||||
|
<?php echo $this->item->images->pagination->getListFooter(); ?>
|
||||||
|
</nav>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php // Browse categories link (bottom) ?>
|
||||||
|
<?php if ($browse_categories_link == '2') : ?>
|
||||||
|
<div class="text-center mt-4">
|
||||||
|
<a class="btn btn-outline-primary" href="<?php echo Route::_('index.php?option=com_joomgallery&view=category&id=1'); ?>">
|
||||||
|
<i class="jg-icon-folder me-1" aria-hidden="true"></i><?php echo Text::_('COM_JOOMGALLERY_GALLERY_VIEW_BROWSE_CATEGORIES'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
if (window.joomGrid.layout != 'justified') {
|
||||||
|
document.querySelectorAll('.' + window.joomGrid.imgclass).forEach(function(image) {
|
||||||
|
image.addEventListener('load', function() {
|
||||||
|
this.closest('.' + window.joomGrid.imgboxclass).classList.add('loaded');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<!DOCTYPE html><title></title>
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
*
|
||||||
|
* This file is part of a Moko Consulting project.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* FILE INFORMATION
|
||||||
|
* DEFGROUP: MokoOnyx.Override
|
||||||
|
* INGROUP: MokoOnyx
|
||||||
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
||||||
|
* PATH: /html/com_joomgallery/image/default.php
|
||||||
|
<<<<<<< HEAD
|
||||||
|
<<<<<<< HEAD
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
>>>>>>> origin/main
|
||||||
|
>>>>>>> origin/main
|
||||||
|
* BRIEF: Image detail view override — single image with metadata, tags, custom fields
|
||||||
|
*/
|
||||||
|
|
||||||
|
// No direct access
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Factory;
|
||||||
|
use Joomla\CMS\Router\Route;
|
||||||
|
use Joomla\CMS\Language\Text;
|
||||||
|
use Joomla\CMS\Session\Session;
|
||||||
|
use Joomla\CMS\HTML\HTMLHelper;
|
||||||
|
use Joomla\CMS\Layout\FileLayout;
|
||||||
|
use Joomla\CMS\User\UserFactoryInterface;
|
||||||
|
use Joomla\Component\Fields\Administrator\Helper\FieldsHelper;
|
||||||
|
use Joomgallery\Component\Joomgallery\Administrator\Helper\JoomHelper;
|
||||||
|
|
||||||
|
// Image params
|
||||||
|
$image_type = $this->params['configs']->get('jg_detail_view_type_image', 'detail', 'STRING');
|
||||||
|
$show_title = $this->params['configs']->get('jg_detail_view_show_title', 0, 'INT');
|
||||||
|
$show_category = $this->params['configs']->get('jg_detail_view_show_category', 0, 'INT');
|
||||||
|
$show_description = $this->params['configs']->get('jg_detail_view_show_description', 0, 'INT');
|
||||||
|
$show_imgdate = $this->params['configs']->get('jg_detail_view_show_imgdate', 0, 'INT');
|
||||||
|
$show_imgauthor = $this->params['configs']->get('jg_detail_view_show_imgauthor', 0, 'INT');
|
||||||
|
$show_created_by = $this->params['configs']->get('jg_detail_view_show_created_by', 0, 'INT');
|
||||||
|
$show_votes = $this->params['configs']->get('jg_detail_view_show_votes', 0, 'INT');
|
||||||
|
$show_rating = $this->params['configs']->get('jg_detail_view_show_rating', 0, 'INT');
|
||||||
|
$show_hits = $this->params['configs']->get('jg_detail_view_show_hits', 0, 'INT');
|
||||||
|
$show_downloads = $this->params['configs']->get('jg_detail_view_show_downloads', 0, 'INT');
|
||||||
|
$show_tags = $this->params['configs']->get('jg_detail_view_show_tags', 0, 'INT');
|
||||||
|
$show_metadata = $this->params['configs']->get('jg_detail_view_show_metadata', 0, 'INT');
|
||||||
|
|
||||||
|
// Import CSS & JS
|
||||||
|
$wa = $this->document->getWebAssetManager();
|
||||||
|
$wa->useStyle('com_joomgallery.site');
|
||||||
|
$wa->useStyle('com_joomgallery.jg-icon-font');
|
||||||
|
|
||||||
|
// Access check
|
||||||
|
$canEdit = $this->getAcl()->checkACL('edit', 'com_joomgallery.image', $this->item->id, $this->item->catid, true);
|
||||||
|
$canDelete = $this->getAcl()->checkACL('delete', 'com_joomgallery.image', $this->item->id, $this->item->catid, true);
|
||||||
|
$canCheckin = $this->getAcl()->checkACL('editstate', 'com_joomgallery.image', $this->item->id, $this->item->catid, true) || $this->item->checked_out == Factory::getUser()->id;
|
||||||
|
$returnURL = base64_encode(JoomHelper::getViewRoute('image', $this->item->id, $this->item->catid, $this->item->language, $this->getLayout()));
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
$tagLayout = new FileLayout('joomgallery.content.tags');
|
||||||
|
$tags = $tagLayout->render($this->item->tags);
|
||||||
|
|
||||||
|
// Metadata
|
||||||
|
$metadataLayout = new FileLayout('joomgallery.content.metadata');
|
||||||
|
$metadata = $metadataLayout->render($this->item->imgmetadata);
|
||||||
|
|
||||||
|
// Custom Fields
|
||||||
|
$fields = FieldsHelper::getFields('com_joomgallery.image', $this->item);
|
||||||
|
|
||||||
|
// Check if we have any info rows to show
|
||||||
|
$hasInfo = $show_category || $show_imgdate || $show_imgauthor || $show_created_by
|
||||||
|
|| $show_votes || $show_rating || $show_hits || $show_downloads
|
||||||
|
|| $show_tags || $show_metadata || count($fields) > 0;
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="com-joomgallery-image" itemscope itemtype="https://schema.org/ImageObject">
|
||||||
|
<?php if ($show_title) : ?>
|
||||||
|
<h2 itemprop="name"><?php echo $this->escape($this->item->title); ?></h2>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php // Back to category ?>
|
||||||
|
<a class="btn btn-outline-primary btn-sm mb-3" href="<?php echo Route::_('index.php?option=com_joomgallery&view=category&id=' . (int) $this->item->catid); ?>">
|
||||||
|
<i class="jg-icon-arrow-left-alt me-1" aria-hidden="true"></i><?php echo Text::_('COM_JOOMGALLERY_IMAGE_BACK_TO_CATEGORY') . ' ' . $this->escape($this->item->cattitle); ?>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<?php // Admin buttons ?>
|
||||||
|
<?php if ($canEdit || $canCheckin || $canDelete) : ?>
|
||||||
|
<div class="com-joomgallery-image__actions btn-toolbar mb-3" role="toolbar" aria-label="<?php echo Text::_('JTOOLBAR'); ?>">
|
||||||
|
<?php if ($canCheckin && $this->item->checked_out > 0) : ?>
|
||||||
|
<a class="btn btn-outline-secondary btn-sm me-2" href="<?php echo Route::_('index.php?option=com_joomgallery&task=image.checkin&id=' . $this->item->id . '&return=' . $returnURL . '&' . Session::getFormToken() . '=1'); ?>">
|
||||||
|
<i class="jg-icon-checkin me-1" aria-hidden="true"></i><?php echo Text::_('JLIB_HTML_CHECKIN'); ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($canEdit) : ?>
|
||||||
|
<a class="btn btn-outline-primary btn-sm me-2<?php echo ($this->item->checked_out > 0) ? ' disabled' : ''; ?>" href="<?php echo Route::_('index.php?option=com_joomgallery&task=image.edit&id=' . $this->item->id . '&return=' . $returnURL); ?>">
|
||||||
|
<i class="jg-icon-edit me-1" aria-hidden="true"></i><?php echo Text::_('JGLOBAL_EDIT'); ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($canDelete) : ?>
|
||||||
|
<a class="btn btn-outline-danger btn-sm<?php echo ($this->item->checked_out > 0) ? ' disabled' : ''; ?>" href="#deleteImgModal" role="button" data-bs-toggle="modal">
|
||||||
|
<i class="jg-icon-delete me-1" aria-hidden="true"></i><?php echo Text::_('JACTION_DELETE'); ?>
|
||||||
|
</a>
|
||||||
|
<?php echo HTMLHelper::_(
|
||||||
|
'bootstrap.renderModal',
|
||||||
|
'deleteImgModal',
|
||||||
|
[
|
||||||
|
'title' => Text::_('JACTION_DELETE'),
|
||||||
|
'modalWidth' => '50',
|
||||||
|
'bodyHeight' => '100',
|
||||||
|
'footer' => '<button class="btn btn-secondary" data-bs-dismiss="modal">' . Text::_('JCANCEL') . '</button>'
|
||||||
|
. '<a href="' . Route::_('index.php?option=com_joomgallery&task=image.remove&id=' . $this->item->id . '&return=' . $returnURL . '&' . Session::getFormToken() . '=1', false, 2) . '" class="btn btn-danger">' . Text::_('COM_JOOMGALLERY_COMMON_DELETE_IMAGE_TIPCAPTION') . '</a>',
|
||||||
|
],
|
||||||
|
Text::_('COM_JOOMGALLERY_COMMON_ALERT_SURE_DELETE_SELECTED_ITEM')
|
||||||
|
); ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php // Image ?>
|
||||||
|
<figure class="figure com-joomgallery-image__figure text-center w-100 mb-4">
|
||||||
|
<div id="jg-loader"></div>
|
||||||
|
<img
|
||||||
|
src="<?php echo JoomHelper::getImg($this->item, $image_type); ?>"
|
||||||
|
class="figure-img img-fluid rounded"
|
||||||
|
alt="<?php echo $this->escape($this->item->title); ?>"
|
||||||
|
style="width:auto;"
|
||||||
|
itemprop="contentUrl"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
<?php if ($show_description && !empty($this->item->description)) : ?>
|
||||||
|
<figcaption class="figure-caption" itemprop="description"><?php echo $this->item->description; ?></figcaption>
|
||||||
|
<?php endif; ?>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<?php // Image info table ?>
|
||||||
|
<?php if ($hasInfo) : ?>
|
||||||
|
<div class="com-joomgallery-image__info">
|
||||||
|
<h3><?php echo Text::_('COM_JOOMGALLERY_IMAGE_INFO'); ?></h3>
|
||||||
|
<table class="table table-striped">
|
||||||
|
<tbody>
|
||||||
|
<?php if ($show_category) : ?>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php echo Text::_('JCATEGORY'); ?></th>
|
||||||
|
<td>
|
||||||
|
<a href="<?php echo Route::_('index.php?option=com_joomgallery&view=category&id=' . (int) $this->item->catid); ?>">
|
||||||
|
<?php echo $this->escape($this->item->cattitle); ?>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($show_imgdate) : ?>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php echo Text::_('COM_JOOMGALLERY_DATE'); ?></th>
|
||||||
|
<td>
|
||||||
|
<time datetime="<?php echo HTMLHelper::_('date', $this->item->date, 'c'); ?>" itemprop="dateCreated">
|
||||||
|
<?php echo HTMLHelper::_('date', $this->item->date, Text::_('DATE_FORMAT_LC4')); ?>
|
||||||
|
</time>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($show_imgauthor) : ?>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php echo Text::_('JAUTHOR'); ?></th>
|
||||||
|
<td itemprop="author"><?php echo $this->escape($this->item->author); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($show_created_by) : ?>
|
||||||
|
<?php $user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($this->item->created_by); ?>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php echo Text::_('COM_JOOMGALLERY_OWNER'); ?></th>
|
||||||
|
<td><?php echo $this->escape($user->name); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($show_votes) : ?>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php echo Text::_('COM_JOOMGALLERY_VOTES'); ?></th>
|
||||||
|
<td><?php echo $this->escape($this->item->votes); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($show_rating) : ?>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php echo Text::_('COM_JOOMGALLERY_IMAGE_RATING'); ?></th>
|
||||||
|
<td><?php echo $this->escape($this->item->rating); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($show_hits) : ?>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php echo Text::_('JGLOBAL_HITS'); ?></th>
|
||||||
|
<td><?php echo (int) $this->item->hits; ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($show_downloads) : ?>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php echo Text::_('COM_JOOMGALLERY_DOWNLOADS'); ?></th>
|
||||||
|
<td><?php echo (int) $this->item->downloads; ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($show_tags) : ?>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php echo Text::_('COM_JOOMGALLERY_TAGS'); ?></th>
|
||||||
|
<td><?php echo $tags; ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($show_metadata) : ?>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php echo Text::_('COM_JOOMGALLERY_IMGMETADATA'); ?></th>
|
||||||
|
<td><?php echo $metadata; ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php // Custom fields ?>
|
||||||
|
<?php if (count($fields) > 0) : ?>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" colspan="2"><strong><?php echo Text::_('JGLOBAL_FIELDS'); ?></strong></th>
|
||||||
|
</tr>
|
||||||
|
<?php foreach ($fields as $field) : ?>
|
||||||
|
<?php if ($this->component->getAccess()->checkViewLevel($field->access) && $field->params->get('display') > 0) : ?>
|
||||||
|
<tr class="<?php echo $this->escape($field->params->get('render_class')); ?>">
|
||||||
|
<?php if ($field->params->get('showlabel', true)) : ?>
|
||||||
|
<th scope="row" class="<?php echo $this->escape($field->params->get('label_render_class')); ?>"><?php echo $this->escape($field->title); ?></th>
|
||||||
|
<?php else : ?>
|
||||||
|
<th scope="row"></th>
|
||||||
|
<?php endif; ?>
|
||||||
|
<td class="<?php echo $this->escape($field->params->get('value_render_class')); ?>"><?php echo $field->value; ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.onload = function() {
|
||||||
|
var el = document.querySelector('#jg-loader');
|
||||||
|
if (el) el.classList.add('hidden');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<!DOCTYPE html><title></title>
|
||||||
@@ -10,7 +10,17 @@
|
|||||||
* INGROUP: MokoOnyx
|
* INGROUP: MokoOnyx
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
||||||
* PATH: /html/layouts/joomla/module/card.php
|
* PATH: /html/layouts/joomla/module/card.php
|
||||||
* VERSION: 02.19.07
|
<<<<<<< HEAD
|
||||||
|
<<<<<<< HEAD
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
>>>>>>> origin/main
|
||||||
|
>>>>>>> origin/main
|
||||||
* BRIEF: Custom card module chrome — renders module titles for all modules
|
* BRIEF: Custom card module chrome — renders module titles for all modules
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,17 @@
|
|||||||
* INGROUP: MokoOnyx.Layouts
|
* INGROUP: MokoOnyx.Layouts
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
||||||
* PATH: /src/html/layouts/mokoonyx/article-metadata.php
|
* PATH: /src/html/layouts/mokoonyx/article-metadata.php
|
||||||
* VERSION: 02.19.07
|
<<<<<<< HEAD
|
||||||
|
<<<<<<< HEAD
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
>>>>>>> origin/main
|
||||||
|
>>>>>>> origin/main
|
||||||
* BRIEF: Article metadata footer layout -- renders jcfields grouped by field group
|
* BRIEF: Article metadata footer layout -- renders jcfields grouped by field group
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ if ($item->anchor_rel) {
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
|
||||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ $anchor_css = $item->anchor_css ?: '';
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
|
||||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ $anchor_css = $item->anchor_css ?: '';
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
|
||||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ if ($item->anchor_rel) {
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
|
||||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ if ($item->anchor_rel) {
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
|
||||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ $anchor_css = $item->anchor_css ?: '';
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
|
||||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ $anchor_css = $item->anchor_css ?: '';
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
|
||||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ if ($item->anchor_rel) {
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
|
||||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
$linktype = '<span class="' . $item->menu_icon . '" aria-hidden="true"></span>' . $item->title;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ if ($item->anchor_rel) {
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
|
||||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
|
||||||
// The link is an icon
|
// The link is an icon
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
// If the link text is to be displayed, the icon is added with aria-hidden
|
// If the link text is to be displayed, the icon is added with aria-hidden
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ $anchor_css = $item->anchor_css ?: '';
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
|
||||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
|
||||||
// The link is an icon
|
// The link is an icon
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
// If the link text is to be displayed, the icon is added with aria-hidden
|
// If the link text is to be displayed, the icon is added with aria-hidden
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ $anchor_css = $item->anchor_css ?: '';
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
|
||||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
|
||||||
// The link is an icon
|
// The link is an icon
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
// If the link text is to be displayed, the icon is added with aria-hidden
|
// If the link text is to be displayed, the icon is added with aria-hidden
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ if ($item->anchor_rel) {
|
|||||||
$linktype = $item->title;
|
$linktype = $item->title;
|
||||||
|
|
||||||
if ($item->menu_icon) {
|
if ($item->menu_icon) {
|
||||||
// Strip Joomla-injected padding classes that conflict with FA icon sizing
|
|
||||||
$item->menu_icon = trim(preg_replace('/\bp-[0-5]\b/', '', $item->menu_icon));
|
|
||||||
// The link is an icon
|
// The link is an icon
|
||||||
if ($itemParams->get('menu_text', 1)) {
|
if ($itemParams->get('menu_text', 1)) {
|
||||||
// If the link text is to be displayed, the icon is added with aria-hidden
|
// If the link text is to be displayed, the icon is added with aria-hidden
|
||||||
|
|||||||
@@ -10,7 +10,17 @@
|
|||||||
* INGROUP: MokoOnyx.Accessibility
|
* INGROUP: MokoOnyx.Accessibility
|
||||||
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
* REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx
|
||||||
* PATH: ./media/css/a11y-high-contrast.css
|
* PATH: ./media/css/a11y-high-contrast.css
|
||||||
* VERSION: 02.19.07
|
<<<<<<< HEAD
|
||||||
|
<<<<<<< HEAD
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
=======
|
||||||
|
* VERSION: 02.17.00
|
||||||
|
>>>>>>> origin/main
|
||||||
|
>>>>>>> origin/main
|
||||||
* BRIEF: High-contrast stylesheet for accessibility toolbar
|
* BRIEF: High-contrast stylesheet for accessibility toolbar
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
-281
@@ -34,9 +34,6 @@ class Tpl_MokoonyxInstallerScript implements InstallerScriptInterface
|
|||||||
private const OLD_DISPLAY = 'MokoCassiopeia';
|
private const OLD_DISPLAY = 'MokoCassiopeia';
|
||||||
private const NEW_DISPLAY = 'MokoOnyx';
|
private const NEW_DISPLAY = 'MokoOnyx';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public function preflight(string $type, InstallerAdapter $parent): bool
|
public function preflight(string $type, InstallerAdapter $parent): bool
|
||||||
{
|
{
|
||||||
if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) {
|
if (version_compare(PHP_VERSION, self::MIN_PHP, '<')) {
|
||||||
@@ -86,23 +83,16 @@ class Tpl_MokoonyxInstallerScript implements InstallerScriptInterface
|
|||||||
public function uninstall(InstallerAdapter $parent): bool
|
public function uninstall(InstallerAdapter $parent): bool
|
||||||
{
|
{
|
||||||
$this->logMessage('MokoOnyx template uninstalled.');
|
$this->logMessage('MokoOnyx template uninstalled.');
|
||||||
$this->saveDownloadKey();
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function postflight(string $type, InstallerAdapter $parent): bool
|
public function postflight(string $type, InstallerAdapter $parent): bool
|
||||||
{
|
{
|
||||||
$this->restoreDownloadKey();
|
|
||||||
$this->warnMissingLicenseKey();
|
|
||||||
|
|
||||||
if ($type === 'install' || $type === 'update') {
|
if ($type === 'install' || $type === 'update') {
|
||||||
$this->migrateFromCassiopeia();
|
$this->migrateFromCassiopeia();
|
||||||
$this->replaceCassiopeiaReferences();
|
$this->replaceCassiopeiaReferences();
|
||||||
$this->clearFaviconStamp();
|
$this->clearFaviconStamp();
|
||||||
$this->cleanMediaFolder();
|
$this->cleanMediaFolder();
|
||||||
$this->removeDeletedFiles();
|
|
||||||
$this->removeDuplicateExtensions();
|
|
||||||
$this->lockExtension();
|
$this->lockExtension();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,189 +483,6 @@ class Tpl_MokoonyxInstallerScript implements InstallerScriptInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove duplicate MokoOnyx extension entries from #__extensions.
|
|
||||||
*
|
|
||||||
* Re-installs or migrations can leave ghost rows. We keep the one
|
|
||||||
* that is locked (the active template) and delete any extras.
|
|
||||||
* Also removes stale MokoCassiopeia entries if present.
|
|
||||||
*/
|
|
||||||
private function removeDuplicateExtensions(): void
|
|
||||||
{
|
|
||||||
$db = Factory::getDbo();
|
|
||||||
|
|
||||||
// Find all MokoOnyx template entries
|
|
||||||
$rows = $db->setQuery(
|
|
||||||
$db->getQuery(true)
|
|
||||||
->select(['extension_id', 'locked'])
|
|
||||||
->from('#__extensions')
|
|
||||||
->where($db->quoteName('element') . ' = ' . $db->quote(self::NEW_NAME))
|
|
||||||
->where($db->quoteName('type') . ' = ' . $db->quote('template'))
|
|
||||||
->order('locked DESC, extension_id ASC')
|
|
||||||
)->loadObjectList();
|
|
||||||
|
|
||||||
if (count($rows) > 1) {
|
|
||||||
$keep = (int) $rows[0]->extension_id;
|
|
||||||
$removed = 0;
|
|
||||||
|
|
||||||
for ($i = 1; $i < count($rows); $i++) {
|
|
||||||
$staleId = (int) $rows[$i]->extension_id;
|
|
||||||
|
|
||||||
$db->setQuery(
|
|
||||||
$db->getQuery(true)
|
|
||||||
->delete('#__update_sites_extensions')
|
|
||||||
->where('extension_id = ' . $staleId)
|
|
||||||
)->execute();
|
|
||||||
|
|
||||||
$db->setQuery(
|
|
||||||
$db->getQuery(true)
|
|
||||||
->delete('#__extensions')
|
|
||||||
->where('extension_id = ' . $staleId)
|
|
||||||
)->execute();
|
|
||||||
|
|
||||||
$removed++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($removed > 0) {
|
|
||||||
$this->logMessage("Removed {$removed} duplicate MokoOnyx extension(s). Kept ID {$keep}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove faulty "template-mokoonyx" duplicate (wrong element name from bad install)
|
|
||||||
$faultyId = (int) $db->setQuery(
|
|
||||||
$db->getQuery(true)
|
|
||||||
->select('extension_id')
|
|
||||||
->from('#__extensions')
|
|
||||||
->where($db->quoteName('element') . ' = ' . $db->quote('template-mokoonyx'))
|
|
||||||
->where($db->quoteName('type') . ' = ' . $db->quote('template'))
|
|
||||||
)->loadResult();
|
|
||||||
|
|
||||||
if ($faultyId) {
|
|
||||||
$db->setQuery(
|
|
||||||
$db->getQuery(true)
|
|
||||||
->delete('#__update_sites_extensions')
|
|
||||||
->where('extension_id = ' . $faultyId)
|
|
||||||
)->execute();
|
|
||||||
|
|
||||||
$db->setQuery(
|
|
||||||
$db->getQuery(true)
|
|
||||||
->delete('#__extensions')
|
|
||||||
->where('extension_id = ' . $faultyId)
|
|
||||||
)->execute();
|
|
||||||
|
|
||||||
$db->setQuery(
|
|
||||||
$db->getQuery(true)
|
|
||||||
->delete('#__template_styles')
|
|
||||||
->where($db->quoteName('template') . ' = ' . $db->quote('template-mokoonyx'))
|
|
||||||
)->execute();
|
|
||||||
|
|
||||||
$this->logMessage('Removed faulty template-mokoonyx duplicate extension.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove stale MokoCassiopeia if not set as default
|
|
||||||
$oldExt = (int) $db->setQuery(
|
|
||||||
$db->getQuery(true)
|
|
||||||
->select('extension_id')
|
|
||||||
->from('#__extensions')
|
|
||||||
->where($db->quoteName('element') . ' = ' . $db->quote(self::OLD_NAME))
|
|
||||||
->where($db->quoteName('type') . ' = ' . $db->quote('template'))
|
|
||||||
)->loadResult();
|
|
||||||
|
|
||||||
if ($oldExt) {
|
|
||||||
$isDefault = (int) $db->setQuery(
|
|
||||||
$db->getQuery(true)
|
|
||||||
->select('COUNT(*)')
|
|
||||||
->from('#__template_styles')
|
|
||||||
->where($db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME))
|
|
||||||
->where($db->quoteName('home') . ' = 1')
|
|
||||||
)->loadResult();
|
|
||||||
|
|
||||||
if ($isDefault === 0) {
|
|
||||||
$db->setQuery(
|
|
||||||
$db->getQuery(true)
|
|
||||||
->delete('#__update_sites_extensions')
|
|
||||||
->where('extension_id = ' . $oldExt)
|
|
||||||
)->execute();
|
|
||||||
|
|
||||||
$db->setQuery(
|
|
||||||
$db->getQuery(true)
|
|
||||||
->delete('#__extensions')
|
|
||||||
->where('extension_id = ' . $oldExt)
|
|
||||||
)->execute();
|
|
||||||
|
|
||||||
$db->setQuery(
|
|
||||||
$db->getQuery(true)
|
|
||||||
->delete('#__template_styles')
|
|
||||||
->where($db->quoteName('template') . ' = ' . $db->quote(self::OLD_NAME))
|
|
||||||
)->execute();
|
|
||||||
|
|
||||||
$this->logMessage('Removed stale MokoCassiopeia extension and styles.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove files and directories that were shipped in previous versions
|
|
||||||
* but have since been deleted from the package.
|
|
||||||
*
|
|
||||||
* Joomla's installer never deletes files on upgrade — it only
|
|
||||||
* adds/overwrites. This method fills that gap so stale overrides
|
|
||||||
* and deprecated assets don't linger on disk.
|
|
||||||
*
|
|
||||||
* Maintain this list: when you delete a file from the repo, add its
|
|
||||||
* path here (relative to the template root) so existing installs
|
|
||||||
* get cleaned up on the next update.
|
|
||||||
*/
|
|
||||||
private function removeDeletedFiles(): void
|
|
||||||
{
|
|
||||||
$templateRoot = JPATH_ROOT . '/templates/' . self::NEW_NAME;
|
|
||||||
|
|
||||||
// Paths relative to templates/mokoonyx/
|
|
||||||
$deletedFiles = [
|
|
||||||
// JoomGallery template overrides — removed in 02.19.00
|
|
||||||
'html/com_joomgallery/category/default.php',
|
|
||||||
'html/com_joomgallery/category/default_cat.php',
|
|
||||||
'html/com_joomgallery/category/index.html',
|
|
||||||
'html/com_joomgallery/gallery/default.php',
|
|
||||||
'html/com_joomgallery/gallery/index.html',
|
|
||||||
'html/com_joomgallery/image/default.php',
|
|
||||||
'html/com_joomgallery/image/index.html',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Directories to remove (only if empty after file deletion)
|
|
||||||
$deletedDirs = [
|
|
||||||
'html/com_joomgallery/image',
|
|
||||||
'html/com_joomgallery/gallery',
|
|
||||||
'html/com_joomgallery/category',
|
|
||||||
'html/com_joomgallery',
|
|
||||||
];
|
|
||||||
|
|
||||||
$removed = 0;
|
|
||||||
|
|
||||||
foreach ($deletedFiles as $relPath) {
|
|
||||||
$file = $templateRoot . '/' . $relPath;
|
|
||||||
if (is_file($file)) {
|
|
||||||
@unlink($file);
|
|
||||||
$removed++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($deletedDirs as $relPath) {
|
|
||||||
$dir = $templateRoot . '/' . $relPath;
|
|
||||||
if (is_dir($dir)) {
|
|
||||||
// Only remove if empty
|
|
||||||
$entries = @scandir($dir);
|
|
||||||
if ($entries && count($entries) <= 2) { // . and .. only
|
|
||||||
@rmdir($dir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($removed > 0) {
|
|
||||||
$this->logMessage("Removed {$removed} deprecated file(s) from previous versions.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function logMessage(string $message, string $priority = 'info'): void
|
private function logMessage(string $message, string $priority = 'info'): void
|
||||||
{
|
{
|
||||||
$priorities = [
|
$priorities = [
|
||||||
@@ -692,92 +499,4 @@ class Tpl_MokoonyxInstallerScript implements InstallerScriptInterface
|
|||||||
|
|
||||||
Log::add($message, $priorities[$priority] ?? Log::INFO, 'mokoonyx');
|
Log::add($message, $priorities[$priority] ?? Log::INFO, 'mokoonyx');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private ?string $savedDownloadKey = null;
|
|
||||||
|
|
||||||
private function saveDownloadKey(): void
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
$db = \Joomla\CMS\Factory::getDbo();
|
|
||||||
$db->setQuery(
|
|
||||||
$db->getQuery(true)
|
|
||||||
->select($db->quoteName('us.extra_query'))
|
|
||||||
->from($db->quoteName('#__update_sites', 'us'))
|
|
||||||
->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON use.update_site_id = us.update_site_id')
|
|
||||||
->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id')
|
|
||||||
->where($db->quoteName('e.element') . ' = ' . $db->quote('mokoonyx'))
|
|
||||||
->setLimit(1)
|
|
||||||
);
|
|
||||||
$key = $db->loadResult();
|
|
||||||
if (!empty($key)) { $this->savedDownloadKey = $key; }
|
|
||||||
}
|
|
||||||
catch (\Throwable $e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function restoreDownloadKey(): void
|
|
||||||
{
|
|
||||||
if ($this->savedDownloadKey === null) { return; }
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
$db = \Joomla\CMS\Factory::getDbo();
|
|
||||||
$db->setQuery(
|
|
||||||
$db->getQuery(true)
|
|
||||||
->select($db->quoteName('us.update_site_id'))
|
|
||||||
->from($db->quoteName('#__update_sites', 'us'))
|
|
||||||
->join('INNER', $db->quoteName('#__update_sites_extensions', 'use') . ' ON use.update_site_id = us.update_site_id')
|
|
||||||
->join('INNER', $db->quoteName('#__extensions', 'e') . ' ON e.extension_id = use.extension_id')
|
|
||||||
->where($db->quoteName('e.element') . ' = ' . $db->quote('mokoonyx'))
|
|
||||||
->setLimit(1)
|
|
||||||
);
|
|
||||||
$siteId = (int) $db->loadResult();
|
|
||||||
if ($siteId > 0)
|
|
||||||
{
|
|
||||||
$db->setQuery(
|
|
||||||
$db->getQuery(true)
|
|
||||||
->update($db->quoteName('#__update_sites'))
|
|
||||||
->set($db->quoteName('extra_query') . ' = ' . $db->quote($this->savedDownloadKey))
|
|
||||||
->where($db->quoteName('update_site_id') . ' = ' . $siteId)
|
|
||||||
)->execute();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (\Throwable $e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function warnMissingLicenseKey(): void
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
$db = \Joomla\CMS\Factory::getDbo();
|
|
||||||
$db->setQuery(
|
|
||||||
$db->getQuery(true)
|
|
||||||
->select([$db->quoteName('update_site_id'), $db->quoteName('extra_query')])
|
|
||||||
->from($db->quoteName('#__update_sites'))
|
|
||||||
->where('(' . $db->quoteName('name') . ' LIKE ' . $db->quote('%MokoOnyx%') . ' OR ' . $db->quoteName('location') . ' LIKE ' . $db->quote('%MokoOnyx%') . ')')
|
|
||||||
->setLimit(1)
|
|
||||||
);
|
|
||||||
$site = $db->loadObject();
|
|
||||||
|
|
||||||
if ($site)
|
|
||||||
{
|
|
||||||
$eq = (string) ($site->extra_query ?? '');
|
|
||||||
if (!empty($eq) && strpos($eq, 'dlid=') !== false) { parse_str($eq, $p); if (!empty($p['dlid'])) { return; } }
|
|
||||||
$editUrl = 'index.php?option=com_installer&task=updatesite.edit&update_site_id=' . (int) $site->update_site_id;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$editUrl = 'index.php?option=com_installer&view=updatesites';
|
|
||||||
}
|
|
||||||
|
|
||||||
\Joomla\CMS\Factory::getApplication()->enqueueMessage(
|
|
||||||
'<strong>Moko Consulting License Key Required</strong> — '
|
|
||||||
. 'No download key is configured. Updates will not be available until a valid license key is entered. '
|
|
||||||
. '<a href="' . $editUrl . '" class="btn btn-sm btn-warning ms-2">Enter License Key</a>',
|
|
||||||
'warning'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
catch (\Throwable $e) {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,11 +31,16 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="template" client="site" method="upgrade">
|
<extension type="template" client="site" method="upgrade">
|
||||||
<updateservers>
|
<updateservers>
|
||||||
<server type="extension" name="Template - MokoOnyx">https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/updates.xml</server>
|
<server type="extension" priority="1" name="MokoOnyx Update Server (MokoGitea)">
|
||||||
|
https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/updates.xml
|
||||||
|
</server>
|
||||||
</updateservers>
|
</updateservers>
|
||||||
<dlid prefix="dlid=" suffix=""/>
|
|
||||||
<name>mokoonyx</name>
|
<name>mokoonyx</name>
|
||||||
<version>02.19.07-dev</version>
|
<<<<<<< HEAD
|
||||||
|
<version>02.17.00-rc</version>
|
||||||
|
=======
|
||||||
|
<version>02.17.00-rc</version>
|
||||||
|
>>>>>>> origin/main
|
||||||
<scriptfile>script.php</scriptfile>
|
<scriptfile>script.php</scriptfile>
|
||||||
<creationDate>2026-05-16</creationDate>
|
<creationDate>2026-05-16</creationDate>
|
||||||
<author>Jonathan Miller || Moko Consulting</author>
|
<author>Jonathan Miller || Moko Consulting</author>
|
||||||
|
|||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
<?xml version='1.0' encoding='UTF-8'?>
|
||||||
|
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
VERSION: 02.17.00
|
||||||
|
-->
|
||||||
|
|
||||||
|
<updates>
|
||||||
|
<update>
|
||||||
|
<name>Template - MokoOnyx</name>
|
||||||
|
<description>Template - MokoOnyx dev build.</description>
|
||||||
|
<element>mokoonyx</element>
|
||||||
|
<type>template</type>
|
||||||
|
<client>site</client>
|
||||||
|
<version>02.16.00-dev</version>
|
||||||
|
<creationDate>2026-05-30</creationDate>
|
||||||
|
<infourl title="Template - MokoOnyx">https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/development</infourl>
|
||||||
|
<downloads>
|
||||||
|
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/development/tpl_mokoonyx-02.16.00-dev.zip</downloadurl>
|
||||||
|
</downloads>
|
||||||
|
<sha256>f8eb73b0a0b61a9f37ee5b720182374cb0b2495fda5e1198711d1298ef5bbd67</sha256>
|
||||||
|
<tags><tag>dev</tag></tags>
|
||||||
|
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||||
|
<maintainer>Moko Consulting</maintainer>
|
||||||
|
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||||
|
<targetplatform name="joomla" version="(5|6)\..*"/>
|
||||||
|
<php_minimum>8.1.0</php_minimum>
|
||||||
|
</update>
|
||||||
|
<update>
|
||||||
|
<name>Template - MokoOnyx</name>
|
||||||
|
<description>Template - MokoOnyx alpha build.</description>
|
||||||
|
<element>mokoonyx</element>
|
||||||
|
<type>template</type>
|
||||||
|
<client>site</client>
|
||||||
|
<version>02.16.00-alpha</version>
|
||||||
|
<creationDate>2026-05-30</creationDate>
|
||||||
|
<infourl title="Template - MokoOnyx">https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/alpha</infourl>
|
||||||
|
<downloads>
|
||||||
|
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/alpha/tpl_mokoonyx-02.16.00-alpha.zip</downloadurl>
|
||||||
|
</downloads>
|
||||||
|
<sha256>f8eb73b0a0b61a9f37ee5b720182374cb0b2495fda5e1198711d1298ef5bbd67</sha256>
|
||||||
|
<tags><tag>alpha</tag></tags>
|
||||||
|
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||||
|
<maintainer>Moko Consulting</maintainer>
|
||||||
|
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||||
|
<targetplatform name="joomla" version="(5|6)\..*"/>
|
||||||
|
<php_minimum>8.1.0</php_minimum>
|
||||||
|
</update>
|
||||||
|
<update>
|
||||||
|
<name>Template - MokoOnyx</name>
|
||||||
|
<description>Template - MokoOnyx beta build.</description>
|
||||||
|
<element>mokoonyx</element>
|
||||||
|
<type>template</type>
|
||||||
|
<client>site</client>
|
||||||
|
<version>02.16.00-beta</version>
|
||||||
|
<creationDate>2026-05-30</creationDate>
|
||||||
|
<infourl title="Template - MokoOnyx">https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/beta</infourl>
|
||||||
|
<downloads>
|
||||||
|
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/beta/tpl_mokoonyx-02.16.00-beta.zip</downloadurl>
|
||||||
|
</downloads>
|
||||||
|
<sha256>f8eb73b0a0b61a9f37ee5b720182374cb0b2495fda5e1198711d1298ef5bbd67</sha256>
|
||||||
|
<tags><tag>beta</tag></tags>
|
||||||
|
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||||
|
<maintainer>Moko Consulting</maintainer>
|
||||||
|
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||||
|
<targetplatform name="joomla" version="(5|6)\..*"/>
|
||||||
|
<php_minimum>8.1.0</php_minimum>
|
||||||
|
</update>
|
||||||
|
<update>
|
||||||
|
<name>Template - MokoOnyx</name>
|
||||||
|
<description>Template - MokoOnyx rc build.</description>
|
||||||
|
<element>mokoonyx</element>
|
||||||
|
<type>template</type>
|
||||||
|
<client>site</client>
|
||||||
|
<version>02.16.00-rc</version>
|
||||||
|
<creationDate>2026-05-30</creationDate>
|
||||||
|
<infourl title="Template - MokoOnyx">https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/release-candidate</infourl>
|
||||||
|
<downloads>
|
||||||
|
<downloadurl type="full" format="zip">https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/release-candidate/tpl_mokoonyx-02.16.00-rc.zip</downloadurl>
|
||||||
|
</downloads>
|
||||||
|
<sha256>f8eb73b0a0b61a9f37ee5b720182374cb0b2495fda5e1198711d1298ef5bbd67</sha256>
|
||||||
|
<tags><tag>rc</tag></tags>
|
||||||
|
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||||
|
<maintainer>Moko Consulting</maintainer>
|
||||||
|
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||||
|
<targetplatform name="joomla" version="(5|6)\..*"/>
|
||||||
|
<php_minimum>8.1.0</php_minimum>
|
||||||
|
</update>
|
||||||
|
<update>
|
||||||
|
<name>Template - MokoOnyx</name>
|
||||||
|
<description>Template - MokoOnyx stable build.</description>
|
||||||
|
<element>mokoonyx</element>
|
||||||
|
<type>template</type>
|
||||||
|
<client>site</client>
|
||||||
|
<version>02.16.00</version>
|
||||||
|
<creationDate>2026-05-30</creationDate>
|
||||||
|
<infourl title='Template - MokoOnyx'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/tag/stable</infourl>
|
||||||
|
<downloads>
|
||||||
|
<downloadurl type='full' format='zip'>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/releases/download/stable/tpl_mokoonyx-02.16.00.zip</downloadurl>
|
||||||
|
</downloads>
|
||||||
|
<sha256>f8eb73b0a0b61a9f37ee5b720182374cb0b2495fda5e1198711d1298ef5bbd67</sha256>
|
||||||
|
<tags><tag>stable</tag></tags>
|
||||||
|
<changelogurl>https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx/raw/branch/main/CHANGELOG.md</changelogurl>
|
||||||
|
<maintainer>Moko Consulting</maintainer>
|
||||||
|
<maintainerurl>https://mokoconsulting.tech</maintainerurl>
|
||||||
|
<targetplatform name="joomla" version="(5|6)\..*" />
|
||||||
|
<php_minimum>8.1.0</php_minimum>
|
||||||
|
</update>
|
||||||
|
</updates>
|
||||||
@@ -136,11 +136,11 @@ A read-only reference tab displaying all available CSS custom properties organiz
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Built with [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform) -- Moko Consulting*
|
*Built with [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API) -- Moko Consulting*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
| Revision | Date | Author | Description |
|
| Revision | Date | Author | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|||||||
@@ -223,11 +223,11 @@ Additional variables are defined for: VirtueMart (`--vm-*`), Gable (`--gab-*`),
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Built with [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform) -- Moko Consulting*
|
*Built with [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API) -- Moko Consulting*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
| Revision | Date | Author | Description |
|
| Revision | Date | Author | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|||||||
@@ -114,11 +114,11 @@ For additional overrides beyond theme variables, use these files (also update-sa
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Built with [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform) -- Moko Consulting*
|
*Built with [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API) -- Moko Consulting*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
| Revision | Date | Author | Description |
|
| Revision | Date | Author | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|||||||
+5
-5
@@ -46,7 +46,7 @@ MokoOnyx/
|
|||||||
│ ├── dark.custom.css # Custom dark palette template
|
│ ├── dark.custom.css # Custom dark palette template
|
||||||
│ └── brand-showcase.html # Brand showcase HTML template
|
│ └── brand-showcase.html # Brand showcase HTML template
|
||||||
├── Makefile # Build and validation automation
|
├── Makefile # Build and validation automation
|
||||||
├── composer.json # PHP dependencies (moko-platform)
|
├── composer.json # PHP dependencies (MokoStandards)
|
||||||
├── package.json # Node.js dependencies (minification)
|
├── package.json # Node.js dependencies (minification)
|
||||||
├── phpcs.xml # PHP CodeSniffer configuration
|
├── phpcs.xml # PHP CodeSniffer configuration
|
||||||
├── phpstan.neon # PHPStan static analysis configuration
|
├── phpstan.neon # PHPStan static analysis configuration
|
||||||
@@ -63,7 +63,7 @@ MokoOnyx/
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **PHP** 8.1+
|
- **PHP** 8.1+
|
||||||
- **Composer** (for moko-platform CLI and dependencies)
|
- **Composer** (for MokoStandards CLI and dependencies)
|
||||||
- **Node.js** (optional, for build-time minification with terser/clean-css)
|
- **Node.js** (optional, for build-time minification with terser/clean-css)
|
||||||
- **Make** (GNU Make or compatible)
|
- **Make** (GNU Make or compatible)
|
||||||
- **zip** (or PowerShell for Windows)
|
- **zip** (or PowerShell for Windows)
|
||||||
@@ -110,7 +110,7 @@ Creates `dist/mokoonyx-{version}-beta.zip` (skips minification).
|
|||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
MokoOnyx uses the **moko-platform CLI** for code quality checks.
|
MokoOnyx uses the **MokoStandards Enterprise API** for code quality checks.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all validation checks
|
# Run all validation checks
|
||||||
@@ -277,11 +277,11 @@ vendor/bin/codecept run
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Built with [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform) -- Moko Consulting*
|
*Built with [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API) -- Moko Consulting*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
| Revision | Date | Author | Description |
|
| Revision | Date | Author | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|||||||
+2
-2
@@ -88,11 +88,11 @@ Key parameters include:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
> **[moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki)** -- central standards hub for all Moko Consulting projects.
|
> **[MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki)** -- central standards hub for all Moko Consulting projects.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
| Revision | Date | Author | Description |
|
| Revision | Date | Author | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|||||||
@@ -55,11 +55,11 @@ MokoOnyx includes an automatic update server. Joomla will notify you when new ve
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Built with [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform) -- Moko Consulting*
|
*Built with [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API) -- Moko Consulting*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
| Revision | Date | Author | Description |
|
| Revision | Date | Author | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|||||||
+2
-2
@@ -108,11 +108,11 @@ Once you have confirmed everything is working:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Built with [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform) -- Moko Consulting*
|
*Built with [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API) -- Moko Consulting*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
| Revision | Date | Author | Description |
|
| Revision | Date | Author | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|||||||
@@ -102,11 +102,11 @@ This immediately deletes all `.min` files. The template will load the unminified
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Built with [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform) -- Moko Consulting*
|
*Built with [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API) -- Moko Consulting*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
| Revision | Date | Author | Description |
|
| Revision | Date | Author | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|||||||
@@ -99,11 +99,11 @@ Each layout has sub-templates for different menu item types: `_component`, `_hea
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Built with [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform) -- Moko Consulting*
|
*Built with [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API) -- Moko Consulting*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
| Revision | Date | Author | Description |
|
| Revision | Date | Author | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
@@ -111,7 +111,7 @@ Each layout has sub-templates for different menu item types: `_component`, `_hea
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [moko-platform](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
*Repo: [MokoOnyx](https://git.mokoconsulting.tech/MokoConsulting/MokoOnyx) · [MokoStandards](https://git.mokoconsulting.tech/MokoConsulting/moko-platform/wiki/Home)*
|
||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
|||||||
Reference in New Issue
Block a user