Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 57e106fff7 |
@@ -13,6 +13,12 @@
|
|||||||
name: "Generic: Project CI"
|
name: "Generic: Project CI"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- dev
|
||||||
|
- dev/**
|
||||||
|
- rc/**
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
@@ -164,75 +164,6 @@ jobs:
|
|||||||
echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY
|
echo "**Manifest validation passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Update server & packaging checks
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
echo "### Update Server & Packaging" >> $GITHUB_STEP_SUMMARY
|
|
||||||
WARNINGS=0
|
|
||||||
|
|
||||||
# Find the extension manifest
|
|
||||||
MANIFEST=""
|
|
||||||
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
|
||||||
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
|
||||||
MANIFEST="$XML_FILE"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -z "$MANIFEST" ]; then
|
|
||||||
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
EXT_TYPE=$(grep -oP '<extension[^>]*\btype="\K[^"]+' "$MANIFEST" | head -1)
|
|
||||||
|
|
||||||
# 1. Check <updateservers> exists and uses MokoGitea update server
|
|
||||||
if ! grep -q '<updateservers>' "$MANIFEST" 2>/dev/null; then
|
|
||||||
echo "::warning file=${MANIFEST}::Missing \`<updateservers>\` tag — extension will not receive OTA updates"
|
|
||||||
echo "- **Missing** \`<updateservers>\` — extension will not receive OTA updates" >> $GITHUB_STEP_SUMMARY
|
|
||||||
WARNINGS=$((WARNINGS + 1))
|
|
||||||
else
|
|
||||||
SERVER_URL=$(grep -oP '<server[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1)
|
|
||||||
if [ -z "$SERVER_URL" ]; then
|
|
||||||
echo "::warning file=${MANIFEST}::\`<updateservers>\` is empty — no server URL defined"
|
|
||||||
echo "- **Empty** \`<updateservers>\` — no server URL defined" >> $GITHUB_STEP_SUMMARY
|
|
||||||
WARNINGS=$((WARNINGS + 1))
|
|
||||||
elif ! echo "$SERVER_URL" | grep -q 'git\.mokoconsulting\.tech'; then
|
|
||||||
echo "::warning file=${MANIFEST}::Update server does not use MokoGitea engine: ${SERVER_URL}"
|
|
||||||
echo "- **Non-MokoGitea update server:** \`${SERVER_URL}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo " Expected: \`https://git.mokoconsulting.tech/{org}/{repo}/updates.xml\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
WARNINGS=$((WARNINGS + 1))
|
|
||||||
else
|
|
||||||
echo "- \`<updateservers>\`: MokoGitea engine ✓" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 2. Check <dlid> tag exists
|
|
||||||
if ! grep -q '<dlid' "$MANIFEST" 2>/dev/null; then
|
|
||||||
echo "::warning file=${MANIFEST}::Missing \`<dlid>\` tag — download ID authentication is not configured"
|
|
||||||
echo "- **Missing** \`<dlid>\` — download ID authentication not configured" >> $GITHUB_STEP_SUMMARY
|
|
||||||
WARNINGS=$((WARNINGS + 1))
|
|
||||||
else
|
|
||||||
echo "- \`<dlid>\`: present ✓" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 3. For packages: check <childuninstall> tag
|
|
||||||
if [ "$EXT_TYPE" = "package" ]; then
|
|
||||||
if ! grep -q '<childuninstall>' "$MANIFEST" 2>/dev/null; then
|
|
||||||
echo "::warning file=${MANIFEST}::Package is missing \`<childuninstall>\` — child extensions will not be removed on uninstall"
|
|
||||||
echo "- **Missing** \`<childuninstall>\` — child extensions will remain when package is uninstalled" >> $GITHUB_STEP_SUMMARY
|
|
||||||
WARNINGS=$((WARNINGS + 1))
|
|
||||||
else
|
|
||||||
echo "- \`<childuninstall>\`: present ✓" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "$WARNINGS" -gt 0 ]; then
|
|
||||||
echo "**${WARNINGS} packaging warning(s).** These won't block CI but should be addressed." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "**Update server & packaging checks passed.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Check language files referenced in manifest
|
- name: Check language files referenced in manifest
|
||||||
run: |
|
run: |
|
||||||
echo "### Language File Check" >> $GITHUB_STEP_SUMMARY
|
echo "### Language File Check" >> $GITHUB_STEP_SUMMARY
|
||||||
@@ -716,268 +647,6 @@ jobs:
|
|||||||
echo "**Service provider check passed.**" >> $GITHUB_STEP_SUMMARY
|
echo "**Service provider check passed.**" >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Script file reference check
|
|
||||||
run: |
|
|
||||||
echo "### Script File Reference" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=0
|
|
||||||
|
|
||||||
MANIFEST=""
|
|
||||||
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
|
||||||
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
|
||||||
MANIFEST="$XML_FILE"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -z "$MANIFEST" ]; then
|
|
||||||
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
MANIFEST_DIR=$(dirname "$MANIFEST")
|
|
||||||
SCRIPT_FILE=$(grep -oP '<scriptfile>\K[^<]+' "$MANIFEST" 2>/dev/null | head -1)
|
|
||||||
if [ -z "$SCRIPT_FILE" ]; then
|
|
||||||
echo "No \`<scriptfile>\` referenced — skipping." >> $GITHUB_STEP_SUMMARY
|
|
||||||
elif [ ! -f "${MANIFEST_DIR}/${SCRIPT_FILE}" ]; then
|
|
||||||
echo "::error file=${MANIFEST}::Manifest references \`<scriptfile>${SCRIPT_FILE}</scriptfile>\` but file does not exist"
|
|
||||||
echo "- **Missing** \`${SCRIPT_FILE}\` — referenced in \`<scriptfile>\` but not found" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
else
|
|
||||||
echo "- \`${SCRIPT_FILE}\`: present ✓" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "${ERRORS}" -gt 0 ]; then
|
|
||||||
echo "**${ERRORS} script file issue(s).**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "**Script file reference check passed.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Media folder validation
|
|
||||||
run: |
|
|
||||||
echo "### Media Folder Validation" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=0
|
|
||||||
|
|
||||||
MANIFEST=""
|
|
||||||
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
|
||||||
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
|
||||||
MANIFEST="$XML_FILE"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -z "$MANIFEST" ]; then
|
|
||||||
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
MANIFEST_DIR=$(dirname "$MANIFEST")
|
|
||||||
|
|
||||||
# Check <media> tag and its folder/filename children
|
|
||||||
MEDIA_DEST=$(grep -oP '<media[^>]*\bdestination="\K[^"]+' "$MANIFEST" 2>/dev/null | head -1)
|
|
||||||
MEDIA_FOLDER=$(grep -oP '<media[^>]*\bfolder="\K[^"]+' "$MANIFEST" 2>/dev/null | head -1)
|
|
||||||
|
|
||||||
if [ -z "$MEDIA_DEST" ] && [ -z "$MEDIA_FOLDER" ]; then
|
|
||||||
echo "No \`<media>\` tag found — skipping." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
if [ -n "$MEDIA_FOLDER" ] && [ ! -d "${MANIFEST_DIR}/${MEDIA_FOLDER}" ]; then
|
|
||||||
echo "::error file=${MANIFEST}::\`<media folder=\"${MEDIA_FOLDER}\">\` references missing directory"
|
|
||||||
echo "- **Missing** media folder \`${MEDIA_FOLDER}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
else
|
|
||||||
echo "- Media folder \`${MEDIA_FOLDER:-(inline)}\`: present ✓" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
# Check child references inside <media> block
|
|
||||||
if [ -n "$MEDIA_FOLDER" ]; then
|
|
||||||
MEDIA_FOLDERS=$(sed -n '/<media /,/<\/media>/p' "$MANIFEST" | grep -oP '<folder>\K[^<]+' 2>/dev/null || true)
|
|
||||||
for F in $MEDIA_FOLDERS; do
|
|
||||||
if [ ! -d "${MANIFEST_DIR}/${MEDIA_FOLDER}/${F}" ]; then
|
|
||||||
echo "- **Missing** media subfolder \`${MEDIA_FOLDER}/${F}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
MEDIA_FILES=$(sed -n '/<media /,/<\/media>/p' "$MANIFEST" | grep -oP '<filename>\K[^<]+' 2>/dev/null || true)
|
|
||||||
for F in $MEDIA_FILES; do
|
|
||||||
if [ ! -f "${MANIFEST_DIR}/${MEDIA_FOLDER}/${F}" ]; then
|
|
||||||
echo "- **Missing** media file \`${MEDIA_FOLDER}/${F}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
ERRORS=$((ERRORS + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "${ERRORS}" -gt 0 ]; then
|
|
||||||
echo "**${ERRORS} media reference issue(s).**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
echo "**Media folder validation passed.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Target platform check
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
echo "### Target Platform Check" >> $GITHUB_STEP_SUMMARY
|
|
||||||
WARNINGS=0
|
|
||||||
|
|
||||||
MANIFEST=""
|
|
||||||
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
|
||||||
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
|
||||||
MANIFEST="$XML_FILE"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -z "$MANIFEST" ]; then
|
|
||||||
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
# Check updates.xml for targetplatform if it exists
|
|
||||||
if [ -f "updates.xml" ]; then
|
|
||||||
if ! grep -q '<targetplatform' "updates.xml" 2>/dev/null; then
|
|
||||||
echo "::warning file=updates.xml::No \`<targetplatform>\` found — Joomla updater cannot filter by compatible version"
|
|
||||||
echo "- **Missing** \`<targetplatform>\` in updates.xml" >> $GITHUB_STEP_SUMMARY
|
|
||||||
WARNINGS=$((WARNINGS + 1))
|
|
||||||
else
|
|
||||||
echo "- \`<targetplatform>\` in updates.xml: present ✓" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check manifest for minimum PHP/Joomla version hints
|
|
||||||
if ! grep -qP '<php_minimum>|targetplatform|joomla.*version' "$MANIFEST" 2>/dev/null; then
|
|
||||||
echo "::warning file=${MANIFEST}::No minimum Joomla or PHP version constraint found in manifest"
|
|
||||||
echo "- **Missing** version constraints (\`<php_minimum>\` or \`<targetplatform>\`)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
WARNINGS=$((WARNINGS + 1))
|
|
||||||
else
|
|
||||||
echo "- Version constraints in manifest: present ✓" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "$WARNINGS" -gt 0 ]; then
|
|
||||||
echo "**${WARNINGS} target platform warning(s).**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "**Target platform check passed.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Changelog URL check
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
echo "### Changelog URL Check" >> $GITHUB_STEP_SUMMARY
|
|
||||||
WARNINGS=0
|
|
||||||
|
|
||||||
MANIFEST=""
|
|
||||||
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
|
||||||
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
|
||||||
MANIFEST="$XML_FILE"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -z "$MANIFEST" ]; then
|
|
||||||
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
if ! grep -q '<changelogurl>' "$MANIFEST" 2>/dev/null; then
|
|
||||||
echo "::warning file=${MANIFEST}::Missing \`<changelogurl>\` — Joomla updater will not display changelogs"
|
|
||||||
echo "- **Missing** \`<changelogurl>\` — Joomla 4+ shows changelogs in the update manager when this is set" >> $GITHUB_STEP_SUMMARY
|
|
||||||
WARNINGS=$((WARNINGS + 1))
|
|
||||||
else
|
|
||||||
CHANGELOG_URL=$(grep -oP '<changelogurl>\K[^<]+' "$MANIFEST" | head -1)
|
|
||||||
echo "- \`<changelogurl>\`: \`${CHANGELOG_URL}\` ✓" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "$WARNINGS" -gt 0 ]; then
|
|
||||||
echo "**${WARNINGS} changelog URL warning(s).**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "**Changelog URL check passed.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Duplicate file references check
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
echo "### Duplicate File References" >> $GITHUB_STEP_SUMMARY
|
|
||||||
WARNINGS=0
|
|
||||||
|
|
||||||
MANIFEST=""
|
|
||||||
for XML_FILE in $(find . -maxdepth 2 -name "*.xml" -not -path "./.git/*" -not -path "./vendor/*"); do
|
|
||||||
if grep -q "<extension" "$XML_FILE" 2>/dev/null; then
|
|
||||||
MANIFEST="$XML_FILE"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -z "$MANIFEST" ]; then
|
|
||||||
echo "No manifest found — skipping." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
# Extract all <filename> and <folder> references
|
|
||||||
ALL_REFS=$(grep -oP '<(filename|folder)[^>]*>\K[^<]+' "$MANIFEST" 2>/dev/null | sort || true)
|
|
||||||
if [ -z "$ALL_REFS" ]; then
|
|
||||||
echo "No file/folder references found — skipping." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
DUPES=$(echo "$ALL_REFS" | uniq -d)
|
|
||||||
if [ -n "$DUPES" ]; then
|
|
||||||
while IFS= read -r DUP; do
|
|
||||||
COUNT=$(echo "$ALL_REFS" | grep -cx "$DUP")
|
|
||||||
echo "::warning file=${MANIFEST}::Duplicate reference: \`${DUP}\` appears ${COUNT} times (may be valid if in different sections)"
|
|
||||||
echo "- **Duplicate:** \`${DUP}\` (${COUNT}x) — check if cross-section" >> $GITHUB_STEP_SUMMARY
|
|
||||||
WARNINGS=$((WARNINGS + 1))
|
|
||||||
done <<< "$DUPES"
|
|
||||||
else
|
|
||||||
TOTAL=$(echo "$ALL_REFS" | wc -l)
|
|
||||||
echo "All ${TOTAL} file/folder references are unique." >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "$WARNINGS" -gt 0 ]; then
|
|
||||||
echo "**${WARNINGS} duplicate reference(s) found.** Review for cross-section validity." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "**Duplicate file references check passed.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Empty language keys check
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
echo "### Empty Language Keys" >> $GITHUB_STEP_SUMMARY
|
|
||||||
WARNINGS=0
|
|
||||||
|
|
||||||
LANG_FILES=$(find . -name "*.ini" -not -path "./.git/*" -not -path "./vendor/*" 2>/dev/null)
|
|
||||||
if [ -z "$LANG_FILES" ]; then
|
|
||||||
echo "No .ini language files found — skipping." >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
TOTAL_FILES=0
|
|
||||||
for FILE in $LANG_FILES; do
|
|
||||||
TOTAL_FILES=$((TOTAL_FILES + 1))
|
|
||||||
# Find lines with KEY= but no value (empty or whitespace-only after =)
|
|
||||||
EMPTY_KEYS=$(grep -nP '^[A-Z_]+=\s*$' "$FILE" 2>/dev/null || true)
|
|
||||||
if [ -n "$EMPTY_KEYS" ]; then
|
|
||||||
COUNT=$(echo "$EMPTY_KEYS" | wc -l)
|
|
||||||
echo "::warning file=${FILE}::${COUNT} empty language key(s)"
|
|
||||||
echo "- \`${FILE}\`: ${COUNT} empty key(s)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
while IFS= read -r LINE; do
|
|
||||||
LINE_NUM=$(echo "$LINE" | cut -d: -f1)
|
|
||||||
KEY=$(echo "$LINE" | cut -d: -f2 | cut -d= -f1)
|
|
||||||
echo " - Line ${LINE_NUM}: \`${KEY}\`" >> $GITHUB_STEP_SUMMARY
|
|
||||||
done <<< "$EMPTY_KEYS"
|
|
||||||
WARNINGS=$((WARNINGS + COUNT))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$WARNINGS" -eq 0 ]; then
|
|
||||||
echo "All ${TOTAL_FILES} language file(s) have populated keys." >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [ "$WARNINGS" -gt 0 ]; then
|
|
||||||
echo "**${WARNINGS} empty language key(s) across ${TOTAL_FILES} file(s).**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
else
|
|
||||||
echo "**Empty language keys check passed.**" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
release-readiness:
|
release-readiness:
|
||||||
name: Release Readiness Check
|
name: Release Readiness Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
# Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# FILE INFORMATION
|
||||||
|
# DEFGROUP: Gitea.Workflow
|
||||||
|
# INGROUP: MokoStandards.Deploy
|
||||||
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/MokoStandards-API
|
||||||
|
# PATH: /templates/workflows/joomla/deploy-manual.yml.template
|
||||||
|
# VERSION: 04.07.00
|
||||||
|
# BRIEF: Manual SFTP deploy to dev server for Joomla repos
|
||||||
|
|
||||||
|
name: "Universal: Deploy to Dev (Manual)"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
clear_remote:
|
||||||
|
description: 'Delete all remote files before uploading'
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
name: SFTP Deploy to Dev
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
|
- name: Setup PHP
|
||||||
|
run: |
|
||||||
|
php -v && composer --version
|
||||||
|
|
||||||
|
- name: Setup MokoStandards tools
|
||||||
|
env:
|
||||||
|
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
|
||||||
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN || github.token }}
|
||||||
|
MOKO_CLONE_HOST: ${{ secrets.MOKOGITEA_TOKEN && 'git.mokoconsulting.tech/MokoConsulting' || 'github.com/mokoconsulting-tech' }}
|
||||||
|
COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ secrets.MOKOGITEA_TOKEN || github.token }}"}}'
|
||||||
|
run: |
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/MokoStandards-API.git" \
|
||||||
|
/tmp/mokostandards-api 2>/dev/null || true
|
||||||
|
if [ -d "/tmp/mokostandards-api" ] && [ -f "/tmp/mokostandards-api/composer.json" ]; then
|
||||||
|
cd /tmp/mokostandards-api && composer install --no-dev --no-interaction --quiet 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Check FTP configuration
|
||||||
|
id: check
|
||||||
|
env:
|
||||||
|
HOST: ${{ vars.DEV_FTP_HOST }}
|
||||||
|
PATH_VAR: ${{ vars.DEV_FTP_PATH }}
|
||||||
|
PORT: ${{ vars.DEV_FTP_PORT }}
|
||||||
|
run: |
|
||||||
|
if [ -z "$HOST" ] || [ -z "$PATH_VAR" ]; then
|
||||||
|
echo "DEV_FTP_HOST or DEV_FTP_PATH not configured -- cannot deploy"
|
||||||
|
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "host=$HOST" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
REMOTE="${PATH_VAR%/}"
|
||||||
|
echo "remote=$REMOTE" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
[ -z "$PORT" ] && PORT="22"
|
||||||
|
echo "port=$PORT" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Deploy via SFTP
|
||||||
|
if: steps.check.outputs.skip != 'true'
|
||||||
|
env:
|
||||||
|
SFTP_KEY: ${{ secrets.DEV_FTP_KEY }}
|
||||||
|
SFTP_PASS: ${{ secrets.DEV_FTP_PASSWORD }}
|
||||||
|
SFTP_USER: ${{ vars.DEV_FTP_USERNAME }}
|
||||||
|
run: |
|
||||||
|
SOURCE_DIR="src"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && SOURCE_DIR="htdocs"
|
||||||
|
[ ! -d "$SOURCE_DIR" ] && { echo "No src/ or htdocs/ -- nothing to deploy"; exit 0; }
|
||||||
|
|
||||||
|
printf '{"host":"%s","port":%s,"username":"%s","remotePath":"%s"' \
|
||||||
|
"${{ steps.check.outputs.host }}" "${{ steps.check.outputs.port }}" "$SFTP_USER" "${{ steps.check.outputs.remote }}" \
|
||||||
|
> /tmp/sftp-config.json
|
||||||
|
|
||||||
|
if [ -n "$SFTP_KEY" ]; then
|
||||||
|
echo "$SFTP_KEY" > /tmp/deploy_key
|
||||||
|
chmod 600 /tmp/deploy_key
|
||||||
|
printf ',"privateKeyPath":"/tmp/deploy_key"}' >> /tmp/sftp-config.json
|
||||||
|
else
|
||||||
|
printf ',"password":"%s"}' "$SFTP_PASS" >> /tmp/sftp-config.json
|
||||||
|
fi
|
||||||
|
|
||||||
|
DEPLOY_ARGS=(--path . --src-dir "$SOURCE_DIR" --config /tmp/sftp-config.json)
|
||||||
|
[ "${{ inputs.clear_remote }}" = "true" ] && DEPLOY_ARGS+=(--clear-remote)
|
||||||
|
|
||||||
|
PLATFORM=$(php /tmp/mokostandards-api/cli/platform_detect.php --path . 2>/dev/null || true)
|
||||||
|
if [ "$PLATFORM" = "waas-component" ] && [ -f "/tmp/mokostandards-api/deploy/deploy-joomla.php" ]; then
|
||||||
|
php /tmp/mokostandards-api/deploy/deploy-joomla.php "${DEPLOY_ARGS[@]}"
|
||||||
|
else
|
||||||
|
php /tmp/mokostandards-api/deploy/deploy-sftp.php "${DEPLOY_ARGS[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f /tmp/deploy_key /tmp/sftp-config.json
|
||||||
|
|
||||||
|
- name: Summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
if [ "${{ steps.check.outputs.skip }}" = "true" ]; then
|
||||||
|
echo "### Deploy Skipped -- FTP not configured" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "### Manual Dev Deploy Complete" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|-------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Host | \`${{ steps.check.outputs.host }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Remote | \`${{ steps.check.outputs.remote }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| Clear | ${{ inputs.clear_remote }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
# FILE INFORMATION
|
# FILE INFORMATION
|
||||||
# DEFGROUP: Gitea.Workflow
|
# DEFGROUP: Gitea.Workflow
|
||||||
# INGROUP: mokocli.Automation
|
# INGROUP: mokocli.Automation
|
||||||
# VERSION: 01.00.18
|
# VERSION: 01.00.00
|
||||||
# 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"
|
||||||
|
|||||||
@@ -93,8 +93,20 @@ jobs:
|
|||||||
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
php ${MOKO_CLI}/platform_detect.php --path . --github-output 2>/dev/null || true
|
||||||
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
php ${MOKO_CLI}/manifest_read.php --path . --github-output
|
||||||
|
|
||||||
|
- name: Check platform eligibility (Joomla only)
|
||||||
|
id: eligibility
|
||||||
|
run: |
|
||||||
|
PLATFORM="${{ steps.platform.outputs.platform }}"
|
||||||
|
if [[ "$PLATFORM" == joomla* ]] || [[ "$PLATFORM" == "joomla" ]]; then
|
||||||
|
echo "proceed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "proceed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "::notice::Platform '$PLATFORM' — non-Joomla, skipping pre-release auto-bump"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Resolve metadata and bump version
|
- name: Resolve metadata and bump version
|
||||||
id: meta
|
id: meta
|
||||||
|
if: steps.eligibility.outputs.proceed == 'true'
|
||||||
run: |
|
run: |
|
||||||
# Auto-detect stability from branch name on push, or use input on dispatch
|
# Auto-detect stability from branch name on push, or use input on dispatch
|
||||||
if [ "${{ github.event_name }}" = "push" ]; then
|
if [ "${{ github.event_name }}" = "push" ]; then
|
||||||
@@ -171,6 +183,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
id: release
|
id: release
|
||||||
|
if: steps.eligibility.outputs.proceed == 'true'
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
@@ -181,6 +194,7 @@ jobs:
|
|||||||
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
--repo "${GITEA_REPO}" --branch "${{ github.ref_name }}" --prerelease
|
||||||
|
|
||||||
- name: Update release notes from CHANGELOG.md
|
- name: Update release notes from CHANGELOG.md
|
||||||
|
if: steps.eligibility.outputs.proceed == 'true'
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
@@ -217,6 +231,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build package and upload
|
- name: Build package and upload
|
||||||
id: package
|
id: package
|
||||||
|
if: steps.eligibility.outputs.proceed == 'true'
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
TAG="${{ steps.meta.outputs.tag }}"
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
@@ -230,6 +245,7 @@ jobs:
|
|||||||
# No need to build, commit, or sync updates.xml from workflows
|
# No need to build, commit, or sync updates.xml from workflows
|
||||||
|
|
||||||
- name: "Delete lesser pre-release channels (cascade)"
|
- name: "Delete lesser pre-release channels (cascade)"
|
||||||
|
if: steps.eligibility.outputs.proceed == 'true'
|
||||||
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}"
|
||||||
|
|||||||
@@ -47,13 +47,6 @@ jobs:
|
|||||||
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
echo "platform=$PLATFORM" >> "$GITHUB_OUTPUT"
|
||||||
echo "Platform: ${PLATFORM:-all}"
|
echo "Platform: ${PLATFORM:-all}"
|
||||||
|
|
||||||
- name: Setup PHP
|
|
||||||
run: |
|
|
||||||
if ! command -v php &> /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
|
|
||||||
|
|
||||||
- name: Clone mokocli
|
- name: Clone mokocli
|
||||||
env:
|
env:
|
||||||
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKOGITEA_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
|||||||
+2
-31
@@ -5,35 +5,7 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [1.2.0] - Unreleased
|
## [1.1.0] - Unreleased
|
||||||
|
|
||||||
### Added
|
|
||||||
- Multi-category support with parent/child hierarchy (#1)
|
|
||||||
- Categories admin CRUD — list, edit, color picker, custom marker icon
|
|
||||||
- Location-category junction table (many-to-many)
|
|
||||||
- Categories tab on location edit form (multi-select)
|
|
||||||
- Category filtering on site frontend (`catid` parameter)
|
|
||||||
- Custom map markers per category — SVG/PNG icon support (#2)
|
|
||||||
- Map module JOINs category data for marker icons and colors
|
|
||||||
- `access.xml` with full Joomla ACL permissions (#30)
|
|
||||||
- SQL update schema with `sql/updates/mysql/` versioned files (#31)
|
|
||||||
- REST API via Web Services plugin (`plg_webservices_mokosuitestorelocator`) (#29)
|
|
||||||
- API controller + JSON:API view for locations CRUD at `/api/v1/mokosuitestorelocator/locations`
|
|
||||||
- `LocationBridgeHelper` — static helper for cross-extension integration (#48)
|
|
||||||
- `LocationSavedEvent` — fires `onStoreLocatorLocationSaved` for cache invalidation
|
|
||||||
- Plugin added to package manifest
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
- Map module dispatcher uses aliased table queries with category JOIN
|
|
||||||
- ORDER BY clauses in admin and site models now validated against filter_fields allowlist
|
|
||||||
|
|
||||||
### Security
|
|
||||||
- CSV import: MIME type validation, 2 MB file size limit, delimiter allowlist (#34)
|
|
||||||
- CSV import: formula injection prevention (strips leading `=+\-@\t\r` characters)
|
|
||||||
- ORDER BY injection prevention — replaced `$db->escape()` with allowlist validation
|
|
||||||
- Map module: `$mapHeight` CSS value validated with regex pattern
|
|
||||||
|
|
||||||
## [1.1.0] - 2026-06-23
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Haversine proximity search — filter locations by distance from user's coordinates
|
- Haversine proximity search — filter locations by distance from user's coordinates
|
||||||
@@ -46,10 +18,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- CSV import: auto-detect column headers (title/name/store, address/street, city, etc.)
|
- CSV import: auto-detect column headers (title/name/store, address/street, city, etc.)
|
||||||
- CSV import: per-row validation via LocationTable::bind()->check()->store()
|
- CSV import: per-row validation via LocationTable::bind()->check()->store()
|
||||||
- CSV import view accessible from admin toolbar and submenu
|
- CSV import view accessible from admin toolbar and submenu
|
||||||
- FocalPoint (Shack Locations) migration import
|
|
||||||
- Language strings for directions, geocoding feedback, and import UI
|
- Language strings for directions, geocoding feedback, and import UI
|
||||||
|
|
||||||
## [01.00.00] - 2026-06-23
|
## [1.0.0] - 2026-06-23
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- Admin `LocationController` (FormController) for single-record save/cancel/apply
|
- Admin `LocationController` (FormController) for single-record save/cancel/apply
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
DEFGROUP:
|
DEFGROUP:
|
||||||
INGROUP: Project.Documentation
|
INGROUP: Project.Documentation
|
||||||
REPO:
|
REPO:
|
||||||
VERSION: 01.00.18
|
VERSION: 04.04.01
|
||||||
PATH: ./CODE_OF_CONDUCT.md
|
PATH: ./CODE_OF_CONDUCT.md
|
||||||
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
|
BRIEF: Reference + packaging repo for Moko Consulting Developer GPT Other Default
|
||||||
-->
|
-->
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ A Joomla 4/5 package providing a store locator listing component with coordinati
|
|||||||
| Store Locator Component | component | `com_mokosuitestorelocator` |
|
| Store Locator Component | component | `com_mokosuitestorelocator` |
|
||||||
| Store Locator Map | module (site) | `mod_mokosuitestorelocator_map` |
|
| Store Locator Map | module (site) | `mod_mokosuitestorelocator_map` |
|
||||||
| Store Locator Search | module (site) | `mod_mokosuitestorelocator_search` |
|
| Store Locator Search | module (site) | `mod_mokosuitestorelocator_search` |
|
||||||
| Web Services API | plugin (webservices) | `plg_webservices_mokosuitestorelocator` |
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -28,9 +27,7 @@ A Joomla 4/5 package providing a store locator listing component with coordinati
|
|||||||
### Implemented
|
### Implemented
|
||||||
- **Admin CRUD** — full location management with tabbed edit form (details, address, coordinates, contact, image)
|
- **Admin CRUD** — full location management with tabbed edit form (details, address, coordinates, contact, image)
|
||||||
- **Admin list** — searchable, filterable, sortable locations list with bulk publish/unpublish/delete
|
- **Admin list** — searchable, filterable, sortable locations list with bulk publish/unpublish/delete
|
||||||
- **Multi-category** — categories with parent/child hierarchy, color, custom marker icons, many-to-many assignments
|
- **Site frontend** — locations list and detail views with pagination
|
||||||
- **Custom map markers** — per-category SVG/PNG marker icons on the Leaflet map
|
|
||||||
- **Site frontend** — locations list and detail views with pagination and category filtering
|
|
||||||
- **Schema.org** — LocalBusiness structured data markup on all frontend templates
|
- **Schema.org** — LocalBusiness structured data markup on all frontend templates
|
||||||
- **SEF URLs** — router with menu, standard, and nomenu rules
|
- **SEF URLs** — router with menu, standard, and nomenu rules
|
||||||
- **Menu items** — "All Locations" list and single "Location Detail" picker
|
- **Menu items** — "All Locations" list and single "Location Detail" picker
|
||||||
@@ -40,18 +37,13 @@ A Joomla 4/5 package providing a store locator listing component with coordinati
|
|||||||
- **Get Directions** — Google Maps directions link on detail page and map popups
|
- **Get Directions** — Google Maps directions link on detail page and map popups
|
||||||
- **Auto-geocoding** — coordinates auto-populated from address on save (Nominatim/OSM)
|
- **Auto-geocoding** — coordinates auto-populated from address on save (Nominatim/OSM)
|
||||||
- **CSV import** — bulk-create locations from spreadsheet with auto-detected column mapping
|
- **CSV import** — bulk-create locations from spreadsheet with auto-detected column mapping
|
||||||
- **FocalPoint migration** — one-click import from Shack Locations / FocalPoint
|
|
||||||
- **REST API** — JSON:API endpoints via Joomla Web Services plugin
|
|
||||||
- **ACL permissions** — `access.xml` with standard Joomla permission actions
|
|
||||||
- **SQL update schema** — versioned migration files for safe upgrades
|
|
||||||
- **Shop integration** — `LocationBridgeHelper` for cross-extension data access, `LocationSavedEvent` for cache invalidation
|
|
||||||
- **Security hardening** — CSV injection prevention, MIME validation, ORDER BY allowlists, input sanitization
|
|
||||||
|
|
||||||
### Planned
|
### Planned
|
||||||
- Marker clustering for dense location areas (Leaflet.markercluster)
|
- Marker clustering for dense location areas
|
||||||
- Google Maps provider as alternative to Leaflet
|
- Multi-category support with custom map markers
|
||||||
- CSV export
|
- ACL permissions and SQL upgrade schema
|
||||||
- Photo gallery per location
|
- REST API via Joomla Web Services plugin
|
||||||
|
- MokoSuiteShop integration for multi-store ecommerce
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -23,7 +23,7 @@ DEFGROUP: [PROJECT_NAME]
|
|||||||
INGROUP: [PROJECT_NAME].Documentation
|
INGROUP: [PROJECT_NAME].Documentation
|
||||||
REPO: [REPOSITORY_URL]
|
REPO: [REPOSITORY_URL]
|
||||||
PATH: /SECURITY.md
|
PATH: /SECURITY.md
|
||||||
VERSION: 01.00.18
|
VERSION: 04.04.01
|
||||||
BRIEF: Security vulnerability reporting and handling policy
|
BRIEF: Security vulnerability reporting and handling policy
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<access component="com_mokosuitestorelocator">
|
|
||||||
<section name="component">
|
|
||||||
<action name="core.admin" title="JACTION_ADMIN" />
|
|
||||||
<action name="core.manage" title="JACTION_MANAGE" />
|
|
||||||
<action name="core.create" title="JACTION_CREATE" />
|
|
||||||
<action name="core.delete" title="JACTION_DELETE" />
|
|
||||||
<action name="core.edit" title="JACTION_EDIT" />
|
|
||||||
<action name="core.edit.state" title="JACTION_EDITSTATE" />
|
|
||||||
<action name="core.edit.own" title="JACTION_EDITOWN" />
|
|
||||||
</section>
|
|
||||||
</access>
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- Category edit form -->
|
|
||||||
<form>
|
|
||||||
<fieldset name="details">
|
|
||||||
<field
|
|
||||||
name="id"
|
|
||||||
type="hidden"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<field
|
|
||||||
name="title"
|
|
||||||
type="text"
|
|
||||||
label="JGLOBAL_TITLE"
|
|
||||||
required="true"
|
|
||||||
size="40"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<field
|
|
||||||
name="alias"
|
|
||||||
type="text"
|
|
||||||
label="JFIELD_ALIAS_LABEL"
|
|
||||||
size="40"
|
|
||||||
hint="JFIELD_ALIAS_PLACEHOLDER"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<field
|
|
||||||
name="parent_id"
|
|
||||||
type="sql"
|
|
||||||
label="COM_MOKOJOOMSTORELOCATOR_CATEGORY_PARENT"
|
|
||||||
default="0"
|
|
||||||
query="SELECT id AS value, title AS text FROM #__mokosuitestorelocator_categories WHERE published = 1 ORDER BY ordering"
|
|
||||||
>
|
|
||||||
<option value="0">COM_MOKOJOOMSTORELOCATOR_CATEGORY_NO_PARENT</option>
|
|
||||||
</field>
|
|
||||||
|
|
||||||
<field
|
|
||||||
name="description"
|
|
||||||
type="editor"
|
|
||||||
label="JGLOBAL_DESCRIPTION"
|
|
||||||
filter="safehtml"
|
|
||||||
buttons="true"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<field
|
|
||||||
name="published"
|
|
||||||
type="list"
|
|
||||||
label="JSTATUS"
|
|
||||||
default="1"
|
|
||||||
>
|
|
||||||
<option value="1">JPUBLISHED</option>
|
|
||||||
<option value="0">JUNPUBLISHED</option>
|
|
||||||
</field>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset name="appearance" label="COM_MOKOJOOMSTORELOCATOR_FIELDSET_APPEARANCE">
|
|
||||||
<field
|
|
||||||
name="color"
|
|
||||||
type="color"
|
|
||||||
label="COM_MOKOJOOMSTORELOCATOR_CATEGORY_COLOR"
|
|
||||||
default=""
|
|
||||||
/>
|
|
||||||
|
|
||||||
<field
|
|
||||||
name="marker_icon"
|
|
||||||
type="media"
|
|
||||||
label="COM_MOKOJOOMSTORELOCATOR_CATEGORY_MARKER_ICON"
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<form>
|
|
||||||
<fields name="filter">
|
|
||||||
<field
|
|
||||||
name="search"
|
|
||||||
type="text"
|
|
||||||
label="COM_MOKOJOOMSTORELOCATOR_FILTER_SEARCH_LABEL"
|
|
||||||
hint="JSEARCH_FILTER"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<field
|
|
||||||
name="published"
|
|
||||||
type="list"
|
|
||||||
label="JOPTION_SELECT_PUBLISHED"
|
|
||||||
onchange="this.form.submit();"
|
|
||||||
>
|
|
||||||
<option value="">JOPTION_SELECT_PUBLISHED</option>
|
|
||||||
<option value="1">JPUBLISHED</option>
|
|
||||||
<option value="0">JUNPUBLISHED</option>
|
|
||||||
</field>
|
|
||||||
</fields>
|
|
||||||
|
|
||||||
<fields name="list">
|
|
||||||
<field
|
|
||||||
name="fullordering"
|
|
||||||
type="list"
|
|
||||||
label="JGLOBAL_SORT_BY"
|
|
||||||
default="a.ordering ASC"
|
|
||||||
onchange="this.form.submit();"
|
|
||||||
>
|
|
||||||
<option value="a.title ASC">JGLOBAL_TITLE_ASC</option>
|
|
||||||
<option value="a.title DESC">JGLOBAL_TITLE_DESC</option>
|
|
||||||
<option value="a.ordering ASC">JGRID_HEADING_ORDERING_ASC</option>
|
|
||||||
<option value="a.ordering DESC">JGRID_HEADING_ORDERING_DESC</option>
|
|
||||||
<option value="a.id ASC">JGRID_HEADING_ID_ASC</option>
|
|
||||||
<option value="a.id DESC">JGRID_HEADING_ID_DESC</option>
|
|
||||||
</field>
|
|
||||||
|
|
||||||
<field
|
|
||||||
name="limit"
|
|
||||||
type="limitbox"
|
|
||||||
label="JGLOBAL_LIST_LIMIT"
|
|
||||||
default="25"
|
|
||||||
onchange="this.form.submit();"
|
|
||||||
/>
|
|
||||||
</fields>
|
|
||||||
</form>
|
|
||||||
@@ -43,16 +43,6 @@
|
|||||||
</field>
|
</field>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<fieldset name="categories" label="COM_MOKOJOOMSTORELOCATOR_FIELDSET_CATEGORIES">
|
|
||||||
<field
|
|
||||||
name="categories"
|
|
||||||
type="sql"
|
|
||||||
label="COM_MOKOJOOMSTORELOCATOR_CATEGORIES"
|
|
||||||
multiple="true"
|
|
||||||
query="SELECT id AS value, title AS text FROM #__mokosuitestorelocator_categories WHERE published = 1 ORDER BY ordering"
|
|
||||||
/>
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset name="address" label="COM_MOKOJOOMSTORELOCATOR_FIELDSET_ADDRESS">
|
<fieldset name="address" label="COM_MOKOJOOMSTORELOCATOR_FIELDSET_ADDRESS">
|
||||||
<field
|
<field
|
||||||
name="address"
|
name="address"
|
||||||
|
|||||||
-19
@@ -59,22 +59,3 @@ COM_MOKOJOOMSTORELOCATOR_IMPORT_NO_FILE="No file was uploaded."
|
|||||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_INVALID_FILE="The uploaded file is not a valid CSV."
|
COM_MOKOJOOMSTORELOCATOR_IMPORT_INVALID_FILE="The uploaded file is not a valid CSV."
|
||||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_NO_ROWS="The CSV file contains no data rows."
|
COM_MOKOJOOMSTORELOCATOR_IMPORT_NO_ROWS="The CSV file contains no data rows."
|
||||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_MISSING_TITLE="Row %d: Title is required."
|
COM_MOKOJOOMSTORELOCATOR_IMPORT_MISSING_TITLE="Row %d: Title is required."
|
||||||
|
|
||||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_FP_TITLE="Import from FocalPoint"
|
|
||||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_FP_DESC="Migrate locations from an installed FocalPoint (Shack Locations) component. Coordinates, custom fields (email, website, hours), and metadata are mapped automatically."
|
|
||||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_FP_BUTTON="Import FocalPoint Locations"
|
|
||||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_FP_SUCCESS="%d location(s) imported from FocalPoint."
|
|
||||||
COM_MOKOJOOMSTORELOCATOR_IMPORT_FILE_TOO_LARGE="The uploaded file exceeds the 2 MB size limit."
|
|
||||||
|
|
||||||
COM_MOKOJOOMSTORELOCATOR_CATEGORIES="Categories"
|
|
||||||
COM_MOKOJOOMSTORELOCATOR_CATEGORY_NEW="New Category"
|
|
||||||
COM_MOKOJOOMSTORELOCATOR_CATEGORY_EDIT="Edit Category"
|
|
||||||
COM_MOKOJOOMSTORELOCATOR_CATEGORY_PARENT="Parent Category"
|
|
||||||
COM_MOKOJOOMSTORELOCATOR_CATEGORY_NO_PARENT="- No Parent -"
|
|
||||||
COM_MOKOJOOMSTORELOCATOR_CATEGORY_COLOR="Color"
|
|
||||||
COM_MOKOJOOMSTORELOCATOR_CATEGORY_MARKER_ICON="Custom Marker Icon"
|
|
||||||
COM_MOKOJOOMSTORELOCATOR_CATEGORIES_TABLE_CAPTION="Store Location Categories"
|
|
||||||
COM_MOKOJOOMSTORELOCATOR_ERROR_CATEGORY_TITLE_REQUIRED="A category title is required."
|
|
||||||
|
|
||||||
COM_MOKOJOOMSTORELOCATOR_FIELDSET_CATEGORIES="Categories"
|
|
||||||
COM_MOKOJOOMSTORELOCATOR_FIELDSET_APPEARANCE="Appearance"
|
|
||||||
|
|||||||
@@ -38,38 +38,3 @@ CREATE TABLE IF NOT EXISTS `#__mokosuitestorelocator_locations` (
|
|||||||
KEY `idx_alias` (`alias`(191)),
|
KEY `idx_alias` (`alias`(191)),
|
||||||
KEY `idx_coordinates` (`latitude`, `longitude`)
|
KEY `idx_coordinates` (`latitude`, `longitude`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
-- =========================================================================
|
|
||||||
-- Categories table
|
|
||||||
-- =========================================================================
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `#__mokosuitestorelocator_categories` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`parent_id` int(11) NOT NULL DEFAULT 0,
|
|
||||||
`title` varchar(255) NOT NULL DEFAULT '',
|
|
||||||
`alias` varchar(400) NOT NULL DEFAULT '',
|
|
||||||
`description` text NOT NULL,
|
|
||||||
`color` varchar(7) NOT NULL DEFAULT '',
|
|
||||||
`marker_icon` varchar(255) NOT NULL DEFAULT '',
|
|
||||||
`published` tinyint(4) NOT NULL DEFAULT 1,
|
|
||||||
`ordering` int(11) NOT NULL DEFAULT 0,
|
|
||||||
`level` int(10) unsigned NOT NULL DEFAULT 1,
|
|
||||||
`path` varchar(400) NOT NULL DEFAULT '',
|
|
||||||
`created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
|
|
||||||
`modified` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_published` (`published`),
|
|
||||||
KEY `idx_parent_id` (`parent_id`),
|
|
||||||
KEY `idx_alias` (`alias`(191))
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- =========================================================================
|
|
||||||
-- Location-Category junction table (many-to-many)
|
|
||||||
-- =========================================================================
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `#__mokosuitestorelocator_location_categories` (
|
|
||||||
`location_id` int(11) NOT NULL,
|
|
||||||
`category_id` int(11) NOT NULL,
|
|
||||||
PRIMARY KEY (`location_id`, `category_id`),
|
|
||||||
KEY `idx_category_id` (`category_id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|||||||
@@ -3,6 +3,4 @@
|
|||||||
-- SPDX-License-Identifier: GPL-3.0-or-later
|
-- SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
-- =========================================================================
|
-- =========================================================================
|
||||||
|
|
||||||
DROP TABLE IF EXISTS `#__mokosuitestorelocator_location_categories`;
|
|
||||||
DROP TABLE IF EXISTS `#__mokosuitestorelocator_categories`;
|
|
||||||
DROP TABLE IF EXISTS `#__mokosuitestorelocator_locations`;
|
DROP TABLE IF EXISTS `#__mokosuitestorelocator_locations`;
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
-- MokoSuiteStoreLocator 01.00.00 — Initial release, no schema changes needed.
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
-- MokoSuiteStoreLocator 01.00.01 — Add categories and location-category junction tables.
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `#__mokosuitestorelocator_categories` (
|
|
||||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
|
||||||
`parent_id` int(11) NOT NULL DEFAULT 0,
|
|
||||||
`title` varchar(255) NOT NULL DEFAULT '',
|
|
||||||
`alias` varchar(400) NOT NULL DEFAULT '',
|
|
||||||
`description` text NOT NULL,
|
|
||||||
`color` varchar(7) NOT NULL DEFAULT '',
|
|
||||||
`marker_icon` varchar(255) NOT NULL DEFAULT '',
|
|
||||||
`published` tinyint(4) NOT NULL DEFAULT 1,
|
|
||||||
`ordering` int(11) NOT NULL DEFAULT 0,
|
|
||||||
`level` int(10) unsigned NOT NULL DEFAULT 1,
|
|
||||||
`path` varchar(400) NOT NULL DEFAULT '',
|
|
||||||
`created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
|
|
||||||
`modified` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
|
|
||||||
PRIMARY KEY (`id`),
|
|
||||||
KEY `idx_published` (`published`),
|
|
||||||
KEY `idx_parent_id` (`parent_id`),
|
|
||||||
KEY `idx_alias` (`alias`(191))
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS `#__mokosuitestorelocator_location_categories` (
|
|
||||||
`location_id` int(11) NOT NULL,
|
|
||||||
`category_id` int(11) NOT NULL,
|
|
||||||
PRIMARY KEY (`location_id`, `category_id`),
|
|
||||||
KEY `idx_category_id` (`category_id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
-37
@@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package MokoSuiteStoreLocator
|
|
||||||
* @subpackage com_mokosuitestorelocator
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Moko\Component\MokoSuiteStoreLocator\Administrator\Controller;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\MVC\Controller\AdminController;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Categories list controller.
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
class CategoriesController extends AdminController
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Get the model for this controller.
|
|
||||||
*
|
|
||||||
* @param string $name Model name.
|
|
||||||
* @param string $prefix Model prefix.
|
|
||||||
* @param array $config Configuration.
|
|
||||||
*
|
|
||||||
* @return \Joomla\CMS\MVC\Model\BaseDatabaseModel
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
public function getModel($name = 'Category', $prefix = 'Administrator', $config = ['ignore_request' => true])
|
|
||||||
{
|
|
||||||
return parent::getModel($name, $prefix, $config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package MokoSuiteStoreLocator
|
|
||||||
* @subpackage com_mokosuitestorelocator
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Moko\Component\MokoSuiteStoreLocator\Administrator\Controller;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\MVC\Controller\FormController;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Category edit controller.
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
class CategoryController extends FormController
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -49,12 +49,6 @@ class ImportController extends BaseController
|
|||||||
$file = $this->input->files->get('jform', [], 'array');
|
$file = $this->input->files->get('jform', [], 'array');
|
||||||
$delimiter = $this->input->post->getString('delimiter', ',');
|
$delimiter = $this->input->post->getString('delimiter', ',');
|
||||||
|
|
||||||
// Validate delimiter against allowlist
|
|
||||||
if (!\in_array($delimiter, [',', ';', '|', "\t"], true))
|
|
||||||
{
|
|
||||||
$delimiter = ',';
|
|
||||||
}
|
|
||||||
|
|
||||||
$csvFile = $file['csv_file'] ?? null;
|
$csvFile = $file['csv_file'] ?? null;
|
||||||
|
|
||||||
if (!$csvFile || $csvFile['error'] !== UPLOAD_ERR_OK || !is_uploaded_file($csvFile['tmp_name']))
|
if (!$csvFile || $csvFile['error'] !== UPLOAD_ERR_OK || !is_uploaded_file($csvFile['tmp_name']))
|
||||||
@@ -65,15 +59,6 @@ class ImportController extends BaseController
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enforce 2 MB file size limit
|
|
||||||
if ($csvFile['size'] > 2 * 1024 * 1024)
|
|
||||||
{
|
|
||||||
$this->setMessage(Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_FILE_TOO_LARGE'), 'error');
|
|
||||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitestorelocator&view=import', false));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate file extension
|
// Validate file extension
|
||||||
$ext = strtolower(pathinfo($csvFile['name'], PATHINFO_EXTENSION));
|
$ext = strtolower(pathinfo($csvFile['name'], PATHINFO_EXTENSION));
|
||||||
|
|
||||||
@@ -85,19 +70,6 @@ class ImportController extends BaseController
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate MIME type
|
|
||||||
$finfo = new \finfo(FILEINFO_MIME_TYPE);
|
|
||||||
$mime = $finfo->file($csvFile['tmp_name']);
|
|
||||||
$allowedMimes = ['text/csv', 'text/plain', 'application/csv', 'application/vnd.ms-excel', 'application/octet-stream'];
|
|
||||||
|
|
||||||
if (!$mime || !\in_array($mime, $allowedMimes, true))
|
|
||||||
{
|
|
||||||
$this->setMessage(Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_INVALID_FILE'), 'error');
|
|
||||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitestorelocator&view=import', false));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $model->processImport($csvFile['tmp_name'], $delimiter);
|
$result = $model->processImport($csvFile['tmp_name'], $delimiter);
|
||||||
|
|
||||||
if ($result['imported'] > 0)
|
if ($result['imported'] > 0)
|
||||||
@@ -112,45 +84,4 @@ class ImportController extends BaseController
|
|||||||
|
|
||||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitestorelocator&view=locations', false));
|
$this->setRedirect(Route::_('index.php?option=com_mokosuitestorelocator&view=locations', false));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Import locations from an installed FocalPoint (Shack Locations) component.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*
|
|
||||||
* @since 1.1.0
|
|
||||||
*/
|
|
||||||
public function focalpoint(): void
|
|
||||||
{
|
|
||||||
Session::checkToken() or jexit(Text::_('JINVALID_TOKEN'));
|
|
||||||
|
|
||||||
if (!Factory::getApplication()->getIdentity()->authorise('core.create', 'com_mokosuitestorelocator'))
|
|
||||||
{
|
|
||||||
$this->setMessage(Text::_('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED'), 'error');
|
|
||||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitestorelocator&view=locations', false));
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var \Moko\Component\MokoSuiteStoreLocator\Administrator\Model\ImportModel $model */
|
|
||||||
$model = $this->getModel('Import', 'Administrator');
|
|
||||||
$result = $model->importFromFocalPoint();
|
|
||||||
|
|
||||||
if ($result['imported'] > 0)
|
|
||||||
{
|
|
||||||
$this->setMessage(Text::sprintf('COM_MOKOJOOMSTORELOCATOR_IMPORT_FP_SUCCESS', $result['imported']));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($result['skipped'] > 0)
|
|
||||||
{
|
|
||||||
$this->setMessage(Text::sprintf('COM_MOKOJOOMSTORELOCATOR_IMPORT_SKIPPED', $result['skipped']), 'warning');
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($result['errors'] as $error)
|
|
||||||
{
|
|
||||||
$this->setMessage($error, 'error');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->setRedirect(Route::_('index.php?option=com_mokosuitestorelocator&view=locations', false));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package MokoSuiteStoreLocator
|
|
||||||
* @subpackage com_mokosuitestorelocator
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Moko\Component\MokoSuiteStoreLocator\Administrator\Event;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Event\AbstractEvent;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event fired after a location is saved, for cross-extension integration.
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
class LocationSavedEvent extends AbstractEvent
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Constructor.
|
|
||||||
*
|
|
||||||
* @param string $name The event name.
|
|
||||||
* @param array $arguments Event arguments: ['locationData' => array].
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
public function __construct(string $name, array $arguments = [])
|
|
||||||
{
|
|
||||||
parent::__construct($name, $arguments);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the saved location data.
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
public function getLocationData(): array
|
|
||||||
{
|
|
||||||
return $this->getArgument('locationData', []);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package MokoSuiteStoreLocator
|
|
||||||
* @subpackage com_mokosuitestorelocator
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Moko\Component\MokoSuiteStoreLocator\Administrator\Helper;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\Database\ParameterType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bridge helper for external extensions (e.g. MokoSuiteShop) to query location data.
|
|
||||||
*
|
|
||||||
* All methods are static and return plain objects/arrays with no Joomla model dependencies.
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
class LocationBridgeHelper
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Get all active locations.
|
|
||||||
*
|
|
||||||
* @param bool $publishedOnly Only return published locations.
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
public static function getLocations(bool $publishedOnly = true): array
|
|
||||||
{
|
|
||||||
$db = Factory::getContainer()->get('DatabaseDriver');
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('*')
|
|
||||||
->from($db->quoteName('#__mokosuitestorelocator_locations'));
|
|
||||||
|
|
||||||
if ($publishedOnly)
|
|
||||||
{
|
|
||||||
$query->where($db->quoteName('published') . ' = 1');
|
|
||||||
}
|
|
||||||
|
|
||||||
$query->order($db->quoteName('ordering') . ' ASC');
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
|
|
||||||
return $db->loadObjectList() ?: [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a single location by ID.
|
|
||||||
*
|
|
||||||
* @param int $locationId The location ID.
|
|
||||||
*
|
|
||||||
* @return object|null
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
public static function getById(int $locationId): ?object
|
|
||||||
{
|
|
||||||
$db = Factory::getContainer()->get('DatabaseDriver');
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('*')
|
|
||||||
->from($db->quoteName('#__mokosuitestorelocator_locations'))
|
|
||||||
->where($db->quoteName('id') . ' = :id')
|
|
||||||
->bind(':id', $locationId, ParameterType::INTEGER);
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
|
|
||||||
return $db->loadObject() ?: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get locations within a radius using Haversine formula.
|
|
||||||
*
|
|
||||||
* @param float $lat Latitude of the search origin.
|
|
||||||
* @param float $lng Longitude of the search origin.
|
|
||||||
* @param float $radiusMiles Search radius in miles.
|
|
||||||
* @param int $limit Maximum results.
|
|
||||||
*
|
|
||||||
* @return array Objects with an additional `distance` property (miles).
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
public static function getNearby(float $lat, float $lng, float $radiusMiles = 25, int $limit = 10): array
|
|
||||||
{
|
|
||||||
$db = Factory::getContainer()->get('DatabaseDriver');
|
|
||||||
$query = $db->getQuery(true);
|
|
||||||
|
|
||||||
$haversine = '(3959 * ACOS(LEAST(1, GREATEST(-1, '
|
|
||||||
. 'SIN(RADIANS(' . $db->quoteName('latitude') . ')) * SIN(RADIANS(' . (float) $lat . ')) '
|
|
||||||
. '+ COS(RADIANS(' . $db->quoteName('latitude') . ')) * COS(RADIANS(' . (float) $lat . ')) '
|
|
||||||
. '* COS(RADIANS(' . $db->quoteName('longitude') . ' - ' . (float) $lng . '))'
|
|
||||||
. '))))';
|
|
||||||
|
|
||||||
$query->select('*')
|
|
||||||
->select($haversine . ' AS distance')
|
|
||||||
->from($db->quoteName('#__mokosuitestorelocator_locations'))
|
|
||||||
->where($db->quoteName('published') . ' = 1')
|
|
||||||
->where($db->quoteName('latitude') . ' IS NOT NULL')
|
|
||||||
->where($db->quoteName('longitude') . ' IS NOT NULL')
|
|
||||||
->where($haversine . ' <= ' . (float) $radiusMiles)
|
|
||||||
->order('distance ASC');
|
|
||||||
|
|
||||||
$db->setQuery($query, 0, $limit);
|
|
||||||
|
|
||||||
return $db->loadObjectList() ?: [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get locations by city.
|
|
||||||
*
|
|
||||||
* @param string $city City name.
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
public static function getByCity(string $city): array
|
|
||||||
{
|
|
||||||
$db = Factory::getContainer()->get('DatabaseDriver');
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('*')
|
|
||||||
->from($db->quoteName('#__mokosuitestorelocator_locations'))
|
|
||||||
->where($db->quoteName('published') . ' = 1')
|
|
||||||
->where($db->quoteName('city') . ' = :city')
|
|
||||||
->bind(':city', $city)
|
|
||||||
->order($db->quoteName('ordering') . ' ASC');
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
|
|
||||||
return $db->loadObjectList() ?: [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get locations by state/province.
|
|
||||||
*
|
|
||||||
* @param string $state State/province name.
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
public static function getByState(string $state): array
|
|
||||||
{
|
|
||||||
$db = Factory::getContainer()->get('DatabaseDriver');
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('*')
|
|
||||||
->from($db->quoteName('#__mokosuitestorelocator_locations'))
|
|
||||||
->where($db->quoteName('published') . ' = 1')
|
|
||||||
->where($db->quoteName('state') . ' = :state')
|
|
||||||
->bind(':state', $state)
|
|
||||||
->order($db->quoteName('ordering') . ' ASC');
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
|
|
||||||
return $db->loadObjectList() ?: [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package MokoSuiteStoreLocator
|
|
||||||
* @subpackage com_mokosuitestorelocator
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Moko\Component\MokoSuiteStoreLocator\Administrator\Model;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\MVC\Model\ListModel;
|
|
||||||
use Joomla\Database\QueryInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Categories list model.
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
class CategoriesModel extends ListModel
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Constructor.
|
|
||||||
*
|
|
||||||
* @param array $config Configuration settings.
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
public function __construct($config = [])
|
|
||||||
{
|
|
||||||
if (empty($config['filter_fields']))
|
|
||||||
{
|
|
||||||
$config['filter_fields'] = [
|
|
||||||
'id', 'a.id',
|
|
||||||
'title', 'a.title',
|
|
||||||
'published', 'a.published',
|
|
||||||
'ordering', 'a.ordering',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
parent::__construct($config);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Populate the model state.
|
|
||||||
*
|
|
||||||
* @param string $ordering Default ordering column.
|
|
||||||
* @param string $direction Default ordering direction.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
protected function populateState($ordering = 'a.ordering', $direction = 'ASC')
|
|
||||||
{
|
|
||||||
$search = $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '', 'string');
|
|
||||||
$this->setState('filter.search', $search);
|
|
||||||
|
|
||||||
$published = $this->getUserStateFromRequest($this->context . '.filter.published', 'filter_published', '', 'string');
|
|
||||||
$this->setState('filter.published', $published);
|
|
||||||
|
|
||||||
parent::populateState($ordering, $direction);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build an SQL query to load the list data.
|
|
||||||
*
|
|
||||||
* @return QueryInterface
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
protected function getListQuery(): QueryInterface
|
|
||||||
{
|
|
||||||
$db = $this->getDatabase();
|
|
||||||
$query = $db->getQuery(true);
|
|
||||||
|
|
||||||
$query->select('a.*')
|
|
||||||
->from($db->quoteName('#__mokosuitestorelocator_categories', 'a'));
|
|
||||||
|
|
||||||
// Count locations per category
|
|
||||||
$subQuery = $db->getQuery(true)
|
|
||||||
->select('COUNT(*)')
|
|
||||||
->from($db->quoteName('#__mokosuitestorelocator_location_categories', 'lc'))
|
|
||||||
->where($db->quoteName('lc.category_id') . ' = ' . $db->quoteName('a.id'));
|
|
||||||
|
|
||||||
$query->select('(' . $subQuery . ') AS ' . $db->quoteName('location_count'));
|
|
||||||
|
|
||||||
// Filter by published state
|
|
||||||
$published = $this->getState('filter.published');
|
|
||||||
|
|
||||||
if (is_numeric($published))
|
|
||||||
{
|
|
||||||
$query->where($db->quoteName('a.published') . ' = :published')
|
|
||||||
->bind(':published', $published, \Joomla\Database\ParameterType::INTEGER);
|
|
||||||
}
|
|
||||||
elseif ($published === '')
|
|
||||||
{
|
|
||||||
$query->where($db->quoteName('a.published') . ' IN (0, 1)');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search filter
|
|
||||||
$search = $this->getState('filter.search');
|
|
||||||
|
|
||||||
if (!empty($search))
|
|
||||||
{
|
|
||||||
$search = '%' . trim($search) . '%';
|
|
||||||
$query->where($db->quoteName('a.title') . ' LIKE :search')
|
|
||||||
->bind(':search', $search);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ordering — validate against filter_fields allowlist
|
|
||||||
$orderCol = $this->state->get('list.ordering', 'a.ordering');
|
|
||||||
$orderDir = $this->state->get('list.direction', 'ASC');
|
|
||||||
|
|
||||||
if (!\in_array($orderCol, $this->filter_fields, true))
|
|
||||||
{
|
|
||||||
$orderCol = 'a.ordering';
|
|
||||||
}
|
|
||||||
|
|
||||||
$orderDir = strtoupper($orderDir) === 'DESC' ? 'DESC' : 'ASC';
|
|
||||||
$query->order($db->quoteName($orderCol) . ' ' . $orderDir);
|
|
||||||
|
|
||||||
return $query;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package MokoSuiteStoreLocator
|
|
||||||
* @subpackage com_mokosuitestorelocator
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Moko\Component\MokoSuiteStoreLocator\Administrator\Model;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Form\Form;
|
|
||||||
use Joomla\CMS\MVC\Model\AdminModel;
|
|
||||||
use Joomla\CMS\Table\Table;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Single category edit model.
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
class CategoryModel extends AdminModel
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The type alias for this content type.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
public $typeAlias = 'com_mokosuitestorelocator.category';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the form for this model.
|
|
||||||
*
|
|
||||||
* @param array $data Data for the form.
|
|
||||||
* @param boolean $loadData True if the form is to load its own data.
|
|
||||||
*
|
|
||||||
* @return Form|boolean
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
public function getForm($data = [], $loadData = true)
|
|
||||||
{
|
|
||||||
$form = $this->loadForm(
|
|
||||||
'com_mokosuitestorelocator.category',
|
|
||||||
'category',
|
|
||||||
['control' => 'jform', 'load_data' => $loadData]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (empty($form))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $form;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load the data for the form.
|
|
||||||
*
|
|
||||||
* @return mixed
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
protected function loadFormData()
|
|
||||||
{
|
|
||||||
return $this->getItem();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the table for this model.
|
|
||||||
*
|
|
||||||
* @param string $name The table name.
|
|
||||||
* @param string $prefix The table prefix.
|
|
||||||
* @param array $options Configuration array.
|
|
||||||
*
|
|
||||||
* @return Table
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
public function getTable($name = 'Category', $prefix = 'Administrator', $options = [])
|
|
||||||
{
|
|
||||||
return parent::getTable($name, $prefix, $options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -206,192 +206,9 @@ class ImportModel extends BaseDatabaseModel
|
|||||||
|
|
||||||
foreach ($mapping as $dbField => $csvIndex)
|
foreach ($mapping as $dbField => $csvIndex)
|
||||||
{
|
{
|
||||||
$value = trim($row[$csvIndex] ?? '');
|
$data[$dbField] = trim($row[$csvIndex] ?? '');
|
||||||
$data[$dbField] = $this->sanitizeCsvValue($value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sanitize a CSV value to prevent formula injection.
|
|
||||||
*
|
|
||||||
* Strips leading characters that spreadsheet applications interpret as formulas.
|
|
||||||
*
|
|
||||||
* @param string $value Raw CSV cell value.
|
|
||||||
*
|
|
||||||
* @return string Sanitized value.
|
|
||||||
*
|
|
||||||
* @since 1.1.0
|
|
||||||
*/
|
|
||||||
private function sanitizeCsvValue(string $value): string
|
|
||||||
{
|
|
||||||
if ($value === '')
|
|
||||||
{
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strip leading formula trigger characters
|
|
||||||
return ltrim($value, "=+\-@\t\r");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Import locations from an installed FocalPoint (Shack Locations) component.
|
|
||||||
*
|
|
||||||
* Reads directly from #__focalpoint_locations and #__focalpoint_locationtypes
|
|
||||||
* tables and inserts into #__mokosuitestorelocator_locations using the standard
|
|
||||||
* bind()->check()->store() flow.
|
|
||||||
*
|
|
||||||
* @return array ['imported' => int, 'skipped' => int, 'errors' => string[]]
|
|
||||||
*
|
|
||||||
* @since 1.1.0
|
|
||||||
*/
|
|
||||||
public function importFromFocalPoint(): array
|
|
||||||
{
|
|
||||||
$result = ['imported' => 0, 'skipped' => 0, 'errors' => []];
|
|
||||||
|
|
||||||
$db = $this->getDatabase();
|
|
||||||
|
|
||||||
// Check if FocalPoint tables exist
|
|
||||||
$tables = $db->getTableList();
|
|
||||||
$prefix = $db->getPrefix();
|
|
||||||
$fpTable = $prefix . 'focalpoint_locations';
|
|
||||||
|
|
||||||
if (!\in_array($fpTable, $tables))
|
|
||||||
{
|
|
||||||
$result['errors'][] = 'FocalPoint is not installed — table #__focalpoint_locations not found.';
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load all FocalPoint locations
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select('a.*')
|
|
||||||
->from($db->quoteName('#__focalpoint_locations', 'a'))
|
|
||||||
->order($db->quoteName('a.id') . ' ASC');
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
$fpLocations = $db->loadObjectList();
|
|
||||||
|
|
||||||
if (empty($fpLocations))
|
|
||||||
{
|
|
||||||
$result['errors'][] = 'No locations found in FocalPoint.';
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load location type names for category context
|
|
||||||
$typeQuery = $db->getQuery(true)
|
|
||||||
->select([$db->quoteName('id'), $db->quoteName('title')])
|
|
||||||
->from($db->quoteName('#__focalpoint_locationtypes'));
|
|
||||||
|
|
||||||
$db->setQuery($typeQuery);
|
|
||||||
$typeNames = $db->loadObjectList('id');
|
|
||||||
|
|
||||||
/** @var \Moko\Component\MokoSuiteStoreLocator\Administrator\Table\LocationTable $table */
|
|
||||||
$table = $this->getMVCFactory()->createTable('Location', 'Administrator');
|
|
||||||
|
|
||||||
foreach ($fpLocations as $fpLoc)
|
|
||||||
{
|
|
||||||
$table->reset();
|
|
||||||
$table->id = 0;
|
|
||||||
|
|
||||||
// Parse custom fields JSON for email, website, phone, hours
|
|
||||||
$customData = $this->parseFocalPointCustomFields($fpLoc->customfieldsdata ?? '');
|
|
||||||
|
|
||||||
// Map FocalPoint fields to our schema
|
|
||||||
$data = [
|
|
||||||
'title' => $fpLoc->title,
|
|
||||||
'alias' => $fpLoc->alias ?: '',
|
|
||||||
'description' => trim(($fpLoc->description ?? '') . "\n" . ($fpLoc->fulldescription ?? '')),
|
|
||||||
'address' => $fpLoc->address ?? '',
|
|
||||||
'city' => $customData['city'] ?? '',
|
|
||||||
'state' => $customData['state'] ?? '',
|
|
||||||
'postcode' => $customData['postcode'] ?? $customData['zip'] ?? '',
|
|
||||||
'country' => $customData['country'] ?? '',
|
|
||||||
'latitude' => $fpLoc->latitude != 0 ? $fpLoc->latitude : null,
|
|
||||||
'longitude' => $fpLoc->longitude != 0 ? $fpLoc->longitude : null,
|
|
||||||
'phone' => $customData['phone'] ?? $fpLoc->phone ?? '',
|
|
||||||
'email' => $customData['email'] ?? '',
|
|
||||||
'website' => $customData['website'] ?? $customData['url'] ?? '',
|
|
||||||
'hours' => $customData['hours'] ?? $customData['business_hours'] ?? '',
|
|
||||||
'image' => $fpLoc->image ?? '',
|
|
||||||
'published' => (int) ($fpLoc->state ?? 0),
|
|
||||||
'ordering' => (int) ($fpLoc->ordering ?? 0),
|
|
||||||
'params' => '{}',
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!$table->bind($data))
|
|
||||||
{
|
|
||||||
$result['errors'][] = "FocalPoint #{$fpLoc->id} ({$fpLoc->title}): " . $table->getError();
|
|
||||||
$result['skipped']++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$table->check())
|
|
||||||
{
|
|
||||||
$result['errors'][] = "FocalPoint #{$fpLoc->id} ({$fpLoc->title}): " . $table->getError();
|
|
||||||
$result['skipped']++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$table->store())
|
|
||||||
{
|
|
||||||
$result['errors'][] = "FocalPoint #{$fpLoc->id} ({$fpLoc->title}): " . $table->getError();
|
|
||||||
$result['skipped']++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$result['imported']++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse FocalPoint customfieldsdata JSON into a flat key-value array.
|
|
||||||
*
|
|
||||||
* FocalPoint stores custom field data as JSON. The structure varies by version:
|
|
||||||
* - Simple: {"fieldname": "value", ...}
|
|
||||||
* - Nested: {"fieldname": {"value": "...", "label": "..."}, ...}
|
|
||||||
*
|
|
||||||
* We normalize to lowercase keys with string values for easy field matching.
|
|
||||||
*
|
|
||||||
* @param string $json The customfieldsdata JSON string.
|
|
||||||
*
|
|
||||||
* @return array Flat associative array of field_name => value.
|
|
||||||
*
|
|
||||||
* @since 1.1.0
|
|
||||||
*/
|
|
||||||
private function parseFocalPointCustomFields(string $json): array
|
|
||||||
{
|
|
||||||
if (empty($json) || $json === '{}')
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$decoded = json_decode($json, true);
|
|
||||||
|
|
||||||
if (!\is_array($decoded))
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$fields = [];
|
|
||||||
|
|
||||||
foreach ($decoded as $key => $value)
|
|
||||||
{
|
|
||||||
$normalizedKey = strtolower(trim(str_replace([' ', '-'], '_', $key)));
|
|
||||||
|
|
||||||
if (\is_array($value))
|
|
||||||
{
|
|
||||||
// Nested format: {"value": "...", "label": "..."}
|
|
||||||
$fields[$normalizedKey] = trim((string) ($value['value'] ?? $value[0] ?? ''));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
$fields[$normalizedKey] = trim((string) $value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $fields;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ defined('_JEXEC') or die;
|
|||||||
use Joomla\CMS\Factory;
|
use Joomla\CMS\Factory;
|
||||||
use Joomla\CMS\Form\Form;
|
use Joomla\CMS\Form\Form;
|
||||||
use Joomla\CMS\Http\HttpFactory;
|
use Joomla\CMS\Http\HttpFactory;
|
||||||
use Moko\Component\MokoSuiteStoreLocator\Administrator\Event\LocationSavedEvent;
|
|
||||||
use Joomla\CMS\Language\Text;
|
use Joomla\CMS\Language\Text;
|
||||||
use Joomla\CMS\MVC\Model\AdminModel;
|
use Joomla\CMS\MVC\Model\AdminModel;
|
||||||
use Joomla\CMS\Table\Table;
|
use Joomla\CMS\Table\Table;
|
||||||
@@ -59,6 +58,20 @@ class LocationModel extends AdminModel
|
|||||||
return $form;
|
return $form;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the data for the form.
|
||||||
|
*
|
||||||
|
* @return mixed The data for the form.
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
protected function loadFormData()
|
||||||
|
{
|
||||||
|
$data = $this->getItem();
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the table for this model.
|
* Get the table for this model.
|
||||||
*
|
*
|
||||||
@@ -105,99 +118,7 @@ class LocationModel extends AdminModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract categories before parent::save (it won't know about junction table)
|
return parent::save($data);
|
||||||
$categories = $data['categories'] ?? [];
|
|
||||||
unset($data['categories']);
|
|
||||||
|
|
||||||
if (!parent::save($data))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save category associations
|
|
||||||
$locationId = (int) $this->getState($this->getName() . '.id');
|
|
||||||
$this->saveCategories($locationId, $categories);
|
|
||||||
|
|
||||||
// Fire event for cross-extension integration (e.g. MokoSuiteShop)
|
|
||||||
$data['id'] = $locationId;
|
|
||||||
Factory::getApplication()->getDispatcher()->dispatch(
|
|
||||||
'onStoreLocatorLocationSaved',
|
|
||||||
new LocationSavedEvent('onStoreLocatorLocationSaved', ['locationData' => $data])
|
|
||||||
);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save location-category associations in the junction table.
|
|
||||||
*
|
|
||||||
* @param int $locationId The location ID.
|
|
||||||
* @param array $categories Array of category IDs.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
private function saveCategories(int $locationId, array $categories): void
|
|
||||||
{
|
|
||||||
$db = $this->getDatabase();
|
|
||||||
|
|
||||||
// Remove existing associations
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->delete($db->quoteName('#__mokosuitestorelocator_location_categories'))
|
|
||||||
->where($db->quoteName('location_id') . ' = :locationId')
|
|
||||||
->bind(':locationId', $locationId, \Joomla\Database\ParameterType::INTEGER);
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
$db->execute();
|
|
||||||
|
|
||||||
// Insert new associations
|
|
||||||
if (!empty($categories))
|
|
||||||
{
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->insert($db->quoteName('#__mokosuitestorelocator_location_categories'))
|
|
||||||
->columns([$db->quoteName('location_id'), $db->quoteName('category_id')]);
|
|
||||||
|
|
||||||
foreach ($categories as $catId)
|
|
||||||
{
|
|
||||||
$catId = (int) $catId;
|
|
||||||
|
|
||||||
if ($catId > 0)
|
|
||||||
{
|
|
||||||
$query->values($locationId . ', ' . $catId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
$db->execute();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load the data for the form, including category associations.
|
|
||||||
*
|
|
||||||
* @return mixed
|
|
||||||
*
|
|
||||||
* @since 1.0.0
|
|
||||||
*/
|
|
||||||
protected function loadFormData()
|
|
||||||
{
|
|
||||||
$data = $this->getItem();
|
|
||||||
|
|
||||||
if ($data && (int) $data->id > 0)
|
|
||||||
{
|
|
||||||
$db = $this->getDatabase();
|
|
||||||
$query = $db->getQuery(true)
|
|
||||||
->select($db->quoteName('category_id'))
|
|
||||||
->from($db->quoteName('#__mokosuitestorelocator_location_categories'))
|
|
||||||
->where($db->quoteName('location_id') . ' = :id')
|
|
||||||
->bind(':id', $data->id, \Joomla\Database\ParameterType::INTEGER);
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
|
||||||
$data->categories = $db->loadColumn();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -111,17 +111,10 @@ class LocationsModel extends ListModel
|
|||||||
->bind(':search4', $search);
|
->bind(':search4', $search);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ordering — validate against filter_fields allowlist
|
// Ordering
|
||||||
$orderCol = $this->state->get('list.ordering', 'a.title');
|
$orderCol = $this->state->get('list.ordering', 'a.title');
|
||||||
$orderDir = $this->state->get('list.direction', 'ASC');
|
$orderDir = $this->state->get('list.direction', 'ASC');
|
||||||
|
$query->order($db->escape($orderCol) . ' ' . $db->escape($orderDir));
|
||||||
if (!\in_array($orderCol, $this->filter_fields, true))
|
|
||||||
{
|
|
||||||
$orderCol = 'a.title';
|
|
||||||
}
|
|
||||||
|
|
||||||
$orderDir = strtoupper($orderDir) === 'DESC' ? 'DESC' : 'ASC';
|
|
||||||
$query->order($db->quoteName($orderCol) . ' ' . $orderDir);
|
|
||||||
|
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package MokoSuiteStoreLocator
|
|
||||||
* @subpackage com_mokosuitestorelocator
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Moko\Component\MokoSuiteStoreLocator\Administrator\Table;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\CMS\Filter\OutputFilter;
|
|
||||||
use Joomla\CMS\Language\Text;
|
|
||||||
use Joomla\CMS\Table\Table;
|
|
||||||
use Joomla\Database\DatabaseDriver;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Category table class.
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
class CategoryTable extends Table
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Constructor.
|
|
||||||
*
|
|
||||||
* @param DatabaseDriver $db Database driver object.
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
public function __construct(DatabaseDriver $db)
|
|
||||||
{
|
|
||||||
parent::__construct('#__mokosuitestorelocator_categories', 'id', $db);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Overloaded check method to ensure data integrity.
|
|
||||||
*
|
|
||||||
* @return boolean True if the data is valid.
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
public function check(): bool
|
|
||||||
{
|
|
||||||
if (trim($this->title) === '')
|
|
||||||
{
|
|
||||||
$this->setError(Text::_('COM_MOKOJOOMSTORELOCATOR_ERROR_CATEGORY_TITLE_REQUIRED'));
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trim($this->alias) === '')
|
|
||||||
{
|
|
||||||
$this->alias = $this->title;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->alias = OutputFilter::stringURLSafe($this->alias);
|
|
||||||
|
|
||||||
// Validate color format
|
|
||||||
if ($this->color !== '' && !preg_match('/^#[0-9a-fA-F]{6}$/', $this->color))
|
|
||||||
{
|
|
||||||
$this->color = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$now = Factory::getDate()->toSql();
|
|
||||||
|
|
||||||
if (!(int) $this->id)
|
|
||||||
{
|
|
||||||
if (!$this->created || $this->created === '0000-00-00 00:00:00')
|
|
||||||
{
|
|
||||||
$this->created = $now;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->modified = $now;
|
|
||||||
|
|
||||||
return parent::check();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package MokoSuiteStoreLocator
|
|
||||||
* @subpackage com_mokosuitestorelocator
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Moko\Component\MokoSuiteStoreLocator\Administrator\View\Categories;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Language\Text;
|
|
||||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
|
||||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Categories list view.
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
class HtmlView extends BaseHtmlView
|
|
||||||
{
|
|
||||||
protected $items;
|
|
||||||
protected $pagination;
|
|
||||||
protected $state;
|
|
||||||
public $filterForm;
|
|
||||||
public $activeFilters;
|
|
||||||
|
|
||||||
public function display($tpl = null): void
|
|
||||||
{
|
|
||||||
$this->items = $this->get('Items');
|
|
||||||
$this->pagination = $this->get('Pagination');
|
|
||||||
$this->state = $this->get('State');
|
|
||||||
$this->filterForm = $this->get('FilterForm');
|
|
||||||
$this->activeFilters = $this->get('ActiveFilters');
|
|
||||||
|
|
||||||
$this->addToolbar();
|
|
||||||
|
|
||||||
parent::display($tpl);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function addToolbar(): void
|
|
||||||
{
|
|
||||||
ToolbarHelper::title(Text::_('COM_MOKOJOOMSTORELOCATOR_CATEGORIES'), 'folder');
|
|
||||||
ToolbarHelper::addNew('category.add');
|
|
||||||
ToolbarHelper::publish('categories.publish', 'JTOOLBAR_PUBLISH', true);
|
|
||||||
ToolbarHelper::unpublish('categories.unpublish', 'JTOOLBAR_UNPUBLISH', true);
|
|
||||||
ToolbarHelper::deleteList('', 'categories.delete', 'JTOOLBAR_DELETE');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package MokoSuiteStoreLocator
|
|
||||||
* @subpackage com_mokosuitestorelocator
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Moko\Component\MokoSuiteStoreLocator\Administrator\View\Category;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\CMS\Language\Text;
|
|
||||||
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
|
|
||||||
use Joomla\CMS\Toolbar\ToolbarHelper;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Category edit view.
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
class HtmlView extends BaseHtmlView
|
|
||||||
{
|
|
||||||
protected $form;
|
|
||||||
protected $item;
|
|
||||||
|
|
||||||
public function display($tpl = null): void
|
|
||||||
{
|
|
||||||
$this->form = $this->get('Form');
|
|
||||||
$this->item = $this->get('Item');
|
|
||||||
|
|
||||||
$this->addToolbar();
|
|
||||||
|
|
||||||
parent::display($tpl);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function addToolbar(): void
|
|
||||||
{
|
|
||||||
Factory::getApplication()->input->set('hidemainmenu', true);
|
|
||||||
|
|
||||||
$isNew = ($this->item->id == 0);
|
|
||||||
|
|
||||||
ToolbarHelper::title(
|
|
||||||
Text::_('COM_MOKOJOOMSTORELOCATOR_CATEGORY_' . ($isNew ? 'NEW' : 'EDIT')),
|
|
||||||
'folder'
|
|
||||||
);
|
|
||||||
|
|
||||||
ToolbarHelper::apply('category.apply');
|
|
||||||
ToolbarHelper::save('category.save');
|
|
||||||
ToolbarHelper::save2new('category.save2new');
|
|
||||||
ToolbarHelper::cancel('category.cancel', $isNew ? 'JTOOLBAR_CANCEL' : 'JTOOLBAR_CLOSE');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package MokoSuiteStoreLocator
|
|
||||||
* @subpackage com_mokosuitestorelocator
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\HTML\HTMLHelper;
|
|
||||||
use Joomla\CMS\Language\Text;
|
|
||||||
use Joomla\CMS\Layout\LayoutHelper;
|
|
||||||
use Joomla\CMS\Router\Route;
|
|
||||||
|
|
||||||
/** @var \Moko\Component\MokoSuiteStoreLocator\Administrator\View\Categories\HtmlView $this */
|
|
||||||
?>
|
|
||||||
<form action="<?php echo Route::_('index.php?option=com_mokosuitestorelocator&view=categories'); ?>"
|
|
||||||
method="post" name="adminForm" id="adminForm">
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<div id="j-main-container" class="j-main-container">
|
|
||||||
<?php echo LayoutHelper::render('joomla.searchtools.default', ['view' => $this]); ?>
|
|
||||||
|
|
||||||
<?php if (empty($this->items)) : ?>
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<span class="icon-info-circle" aria-hidden="true"></span>
|
|
||||||
<?php echo Text::_('JGLOBAL_NO_MATCHING_RESULTS'); ?>
|
|
||||||
</div>
|
|
||||||
<?php else : ?>
|
|
||||||
<table class="table" id="categoryList">
|
|
||||||
<caption class="visually-hidden">
|
|
||||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_CATEGORIES_TABLE_CAPTION'); ?>
|
|
||||||
</caption>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<td class="w-1 text-center">
|
|
||||||
<?php echo HTMLHelper::_('grid.checkall'); ?>
|
|
||||||
</td>
|
|
||||||
<th scope="col">
|
|
||||||
<?php echo Text::_('JGLOBAL_TITLE'); ?>
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="w-5 text-center">
|
|
||||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_CATEGORY_COLOR'); ?>
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="w-10 text-center d-none d-md-table-cell">
|
|
||||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_LOCATIONS'); ?>
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="w-5 text-center">
|
|
||||||
<?php echo Text::_('JSTATUS'); ?>
|
|
||||||
</th>
|
|
||||||
<th scope="col" class="w-5 text-center">
|
|
||||||
<?php echo Text::_('JGRID_HEADING_ID'); ?>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php foreach ($this->items as $i => $item) : ?>
|
|
||||||
<tr class="row<?php echo $i % 2; ?>">
|
|
||||||
<td class="w-1 text-center">
|
|
||||||
<?php echo HTMLHelper::_('grid.id', $i, $item->id, false, 'cid', 'cb', $item->title); ?>
|
|
||||||
</td>
|
|
||||||
<th scope="row">
|
|
||||||
<?php if ((int) $item->level > 1) : ?>
|
|
||||||
<?php echo str_repeat('<span class="gi">—</span>', (int) $item->level - 1); ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
<a href="<?php echo Route::_('index.php?option=com_mokosuitestorelocator&task=category.edit&id=' . (int) $item->id); ?>">
|
|
||||||
<?php echo $this->escape($item->title); ?>
|
|
||||||
</a>
|
|
||||||
</th>
|
|
||||||
<td class="text-center">
|
|
||||||
<?php if ($item->color) : ?>
|
|
||||||
<span style="display:inline-block;width:20px;height:20px;border-radius:3px;background-color:<?php echo $this->escape($item->color); ?>;"></span>
|
|
||||||
<?php else : ?>
|
|
||||||
—
|
|
||||||
<?php endif; ?>
|
|
||||||
</td>
|
|
||||||
<td class="text-center d-none d-md-table-cell">
|
|
||||||
<?php echo (int) $item->location_count; ?>
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<?php echo HTMLHelper::_('jgrid.published', $item->published, $i, 'categories.', true, 'cb'); ?>
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<?php echo (int) $item->id; ?>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<?php echo $this->pagination->getListFooter(); ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<input type="hidden" name="task" value="">
|
|
||||||
<input type="hidden" name="boxchecked" value="0">
|
|
||||||
<?php echo HTMLHelper::_('form.token'); ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package MokoSuiteStoreLocator
|
|
||||||
* @subpackage com_mokosuitestorelocator
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\HTML\HTMLHelper;
|
|
||||||
use Joomla\CMS\Language\Text;
|
|
||||||
use Joomla\CMS\Router\Route;
|
|
||||||
|
|
||||||
/** @var \Moko\Component\MokoSuiteStoreLocator\Administrator\View\Category\HtmlView $this */
|
|
||||||
|
|
||||||
HTMLHelper::_('behavior.formvalidator');
|
|
||||||
HTMLHelper::_('behavior.keepalive');
|
|
||||||
?>
|
|
||||||
<form action="<?php echo Route::_('index.php?option=com_mokosuitestorelocator&layout=edit&id=' . (int) $this->item->id); ?>"
|
|
||||||
method="post" name="adminForm" id="adminForm" class="form-validate">
|
|
||||||
|
|
||||||
<?php echo HTMLHelper::_('uitab.startTabSet', 'myTab', ['active' => 'details', 'recall' => true, 'breakpoint' => 768]); ?>
|
|
||||||
|
|
||||||
<?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'details', Text::_('JDETAILS')); ?>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-9">
|
|
||||||
<?php echo $this->form->renderField('title'); ?>
|
|
||||||
<?php echo $this->form->renderField('alias'); ?>
|
|
||||||
<?php echo $this->form->renderField('parent_id'); ?>
|
|
||||||
<?php echo $this->form->renderField('description'); ?>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-3">
|
|
||||||
<?php echo $this->form->renderField('published'); ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
|
||||||
|
|
||||||
<?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'appearance', Text::_('COM_MOKOJOOMSTORELOCATOR_FIELDSET_APPEARANCE')); ?>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-6">
|
|
||||||
<?php echo $this->form->renderField('color'); ?>
|
|
||||||
<?php echo $this->form->renderField('marker_icon'); ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
|
||||||
|
|
||||||
<?php echo HTMLHelper::_('uitab.endTabSet'); ?>
|
|
||||||
|
|
||||||
<input type="hidden" name="task" value="">
|
|
||||||
<?php echo HTMLHelper::_('form.token'); ?>
|
|
||||||
</form>
|
|
||||||
@@ -60,21 +60,6 @@ use Joomla\CMS\Session\Session;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
<div class="card mb-3">
|
|
||||||
<div class="card-body">
|
|
||||||
<h4><?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_FP_TITLE'); ?></h4>
|
|
||||||
<p class="text-muted"><?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_FP_DESC'); ?></p>
|
|
||||||
<form action="<?php echo Route::_('index.php?option=com_mokosuitestorelocator&task=import.focalpoint'); ?>"
|
|
||||||
method="post">
|
|
||||||
<button type="submit" class="btn btn-outline-primary w-100">
|
|
||||||
<span class="icon-download" aria-hidden="true"></span>
|
|
||||||
<?php echo Text::_('COM_MOKOJOOMSTORELOCATOR_IMPORT_FP_BUTTON'); ?>
|
|
||||||
</button>
|
|
||||||
<?php echo HTMLHelper::_('form.token'); ?>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4><?php echo Text::_('JHELP'); ?></h4>
|
<h4><?php echo Text::_('JHELP'); ?></h4>
|
||||||
|
|||||||
@@ -37,14 +37,6 @@ HTMLHelper::_('behavior.keepalive');
|
|||||||
</div>
|
</div>
|
||||||
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
||||||
|
|
||||||
<?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'categories', Text::_('COM_MOKOJOOMSTORELOCATOR_FIELDSET_CATEGORIES')); ?>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-9">
|
|
||||||
<?php echo $this->form->renderField('categories'); ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php echo HTMLHelper::_('uitab.endTab'); ?>
|
|
||||||
|
|
||||||
<?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'address', Text::_('COM_MOKOJOOMSTORELOCATOR_FIELDSET_ADDRESS')); ?>
|
<?php echo HTMLHelper::_('uitab.addTab', 'myTab', 'address', Text::_('COM_MOKOJOOMSTORELOCATOR_FIELDSET_ADDRESS')); ?>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package MokoSuiteStoreLocator
|
|
||||||
* @subpackage com_mokosuitestorelocator
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Moko\Component\MokoSuiteStoreLocator\Api\Controller;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\MVC\Controller\ApiController;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* REST API controller for locations.
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
class LocationsController extends ApiController
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The content type.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
protected $contentType = 'locations';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The default view.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
protected $default_view = 'locations';
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package MokoSuiteStoreLocator
|
|
||||||
* @subpackage com_mokosuitestorelocator
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Moko\Component\MokoSuiteStoreLocator\Api\View\Locations;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\MVC\View\JsonApiView as BaseApiView;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON:API view for locations.
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
class JsonapiView extends BaseApiView
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Fields to render for a single item.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
protected $fieldsToRenderItem = [
|
|
||||||
'id',
|
|
||||||
'title',
|
|
||||||
'alias',
|
|
||||||
'description',
|
|
||||||
'address',
|
|
||||||
'city',
|
|
||||||
'state',
|
|
||||||
'postcode',
|
|
||||||
'country',
|
|
||||||
'latitude',
|
|
||||||
'longitude',
|
|
||||||
'phone',
|
|
||||||
'email',
|
|
||||||
'website',
|
|
||||||
'hours',
|
|
||||||
'image',
|
|
||||||
'published',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fields to render for a list.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
protected $fieldsToRenderList = [
|
|
||||||
'id',
|
|
||||||
'title',
|
|
||||||
'alias',
|
|
||||||
'address',
|
|
||||||
'city',
|
|
||||||
'state',
|
|
||||||
'postcode',
|
|
||||||
'country',
|
|
||||||
'latitude',
|
|
||||||
'longitude',
|
|
||||||
'phone',
|
|
||||||
'published',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="component" method="upgrade">
|
<extension type="component" method="upgrade">
|
||||||
<name>com_mokosuitestorelocator</name>
|
<name>com_mokosuitestorelocator</name>
|
||||||
<version>01.00.18</version>
|
<version>1.0.0</version>
|
||||||
<creationDate>2026-06-23</creationDate>
|
<creationDate>2026-06-23</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
@@ -38,27 +38,14 @@
|
|||||||
</sql>
|
</sql>
|
||||||
</uninstall>
|
</uninstall>
|
||||||
|
|
||||||
<update>
|
|
||||||
<schemas>
|
|
||||||
<schemapath type="mysql">sql/updates/mysql</schemapath>
|
|
||||||
</schemas>
|
|
||||||
</update>
|
|
||||||
|
|
||||||
<files folder="site">
|
<files folder="site">
|
||||||
<folder>language</folder>
|
<folder>language</folder>
|
||||||
<folder>src</folder>
|
<folder>src</folder>
|
||||||
<folder>tmpl</folder>
|
<folder>tmpl</folder>
|
||||||
</files>
|
</files>
|
||||||
|
|
||||||
<api>
|
|
||||||
<files folder="api">
|
|
||||||
<folder>src</folder>
|
|
||||||
</files>
|
|
||||||
</api>
|
|
||||||
|
|
||||||
<administration>
|
<administration>
|
||||||
<files folder="admin">
|
<files folder="admin">
|
||||||
<filename>access.xml</filename>
|
|
||||||
<folder>forms</folder>
|
<folder>forms</folder>
|
||||||
<folder>language</folder>
|
<folder>language</folder>
|
||||||
<folder>services</folder>
|
<folder>services</folder>
|
||||||
@@ -70,7 +57,6 @@
|
|||||||
<menu>COM_MOKOJOOMSTORELOCATOR</menu>
|
<menu>COM_MOKOJOOMSTORELOCATOR</menu>
|
||||||
<submenu>
|
<submenu>
|
||||||
<menu link="option=com_mokosuitestorelocator&view=locations">COM_MOKOJOOMSTORELOCATOR_LOCATIONS</menu>
|
<menu link="option=com_mokosuitestorelocator&view=locations">COM_MOKOJOOMSTORELOCATOR_LOCATIONS</menu>
|
||||||
<menu link="option=com_mokosuitestorelocator&view=categories">COM_MOKOJOOMSTORELOCATOR_CATEGORIES</menu>
|
|
||||||
<menu link="option=com_mokosuitestorelocator&view=import">COM_MOKOJOOMSTORELOCATOR_IMPORT</menu>
|
<menu link="option=com_mokosuitestorelocator&view=import">COM_MOKOJOOMSTORELOCATOR_IMPORT</menu>
|
||||||
</submenu>
|
</submenu>
|
||||||
</administration>
|
</administration>
|
||||||
|
|||||||
@@ -99,9 +99,6 @@ class LocationsModel extends ListModel
|
|||||||
|
|
||||||
$this->setState('filter.radius_unit', in_array($radiusUnit, ['miles', 'km']) ? $radiusUnit : 'miles');
|
$this->setState('filter.radius_unit', in_array($radiusUnit, ['miles', 'km']) ? $radiusUnit : 'miles');
|
||||||
|
|
||||||
$catid = $app->input->getInt('catid', 0);
|
|
||||||
$this->setState('filter.catid', $catid);
|
|
||||||
|
|
||||||
parent::populateState($ordering, $direction);
|
parent::populateState($ordering, $direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,17 +154,6 @@ class LocationsModel extends ListModel
|
|||||||
->bind(':state', $state);
|
->bind(':state', $state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category filter
|
|
||||||
$catid = (int) $this->getState('filter.catid');
|
|
||||||
|
|
||||||
if ($catid > 0)
|
|
||||||
{
|
|
||||||
$query->join('INNER', $db->quoteName('#__mokosuitestorelocator_location_categories', 'lc')
|
|
||||||
. ' ON ' . $db->quoteName('lc.location_id') . ' = ' . $db->quoteName('a.id'))
|
|
||||||
->where($db->quoteName('lc.category_id') . ' = :catid')
|
|
||||||
->bind(':catid', $catid, ParameterType::INTEGER);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proximity / Haversine distance filter
|
// Proximity / Haversine distance filter
|
||||||
$lat = $this->getState('filter.lat');
|
$lat = $this->getState('filter.lat');
|
||||||
$lng = $this->getState('filter.lng');
|
$lng = $this->getState('filter.lng');
|
||||||
@@ -193,17 +179,10 @@ class LocationsModel extends ListModel
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Default ordering — validate against filter_fields allowlist
|
// Default ordering
|
||||||
$orderCol = $this->state->get('list.ordering', 'a.ordering');
|
$orderCol = $this->state->get('list.ordering', 'a.ordering');
|
||||||
$orderDir = $this->state->get('list.direction', 'ASC');
|
$orderDir = $this->state->get('list.direction', 'ASC');
|
||||||
|
$query->order($db->escape($orderCol) . ' ' . $db->escape($orderDir));
|
||||||
if (!\in_array($orderCol, $this->filter_fields, true))
|
|
||||||
{
|
|
||||||
$orderCol = 'a.ordering';
|
|
||||||
}
|
|
||||||
|
|
||||||
$orderDir = strtoupper($orderDir) === 'DESC' ? 'DESC' : 'ASC';
|
|
||||||
$query->order($db->quoteName($orderCol) . ' ' . $orderDir);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $query;
|
return $query;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="module" client="site" method="upgrade">
|
<extension type="module" client="site" method="upgrade">
|
||||||
<name>mod_mokosuitestorelocator_map</name>
|
<name>mod_mokosuitestorelocator_map</name>
|
||||||
<version>01.00.18</version>
|
<version>1.0.0</version>
|
||||||
<creationDate>2026-06-23</creationDate>
|
<creationDate>2026-06-23</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
@@ -26,7 +26,6 @@
|
|||||||
<namespace path="src">Moko\Module\MokoSuiteStoreLocatorMap</namespace>
|
<namespace path="src">Moko\Module\MokoSuiteStoreLocatorMap</namespace>
|
||||||
|
|
||||||
<files>
|
<files>
|
||||||
<folder module="mod_mokosuitestorelocator_map">services</folder>
|
|
||||||
<folder>src</folder>
|
<folder>src</folder>
|
||||||
<folder>tmpl</folder>
|
<folder>tmpl</folder>
|
||||||
<folder>language</folder>
|
<folder>language</folder>
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package MokoSuiteStoreLocator
|
|
||||||
* @subpackage mod_mokosuitestorelocator_map
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Extension\Service\Provider\Module;
|
|
||||||
use Joomla\CMS\Extension\Service\Provider\ModuleDispatcherFactory;
|
|
||||||
use Joomla\CMS\Extension\Service\Provider\HelperFactory;
|
|
||||||
use Joomla\DI\Container;
|
|
||||||
use Joomla\DI\ServiceProviderInterface;
|
|
||||||
|
|
||||||
return new class implements ServiceProviderInterface
|
|
||||||
{
|
|
||||||
public function register(Container $container): void
|
|
||||||
{
|
|
||||||
$container->registerServiceProvider(new ModuleDispatcherFactory('\\Moko\\Module\\MokoSuiteStoreLocatorMap'));
|
|
||||||
$container->registerServiceProvider(new HelperFactory('\\Moko\\Module\\MokoSuiteStoreLocatorMap\\Helper'));
|
|
||||||
$container->registerServiceProvider(new Module());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -41,29 +41,20 @@ class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareI
|
|||||||
$query = $db->getQuery(true);
|
$query = $db->getQuery(true);
|
||||||
|
|
||||||
$query->select([
|
$query->select([
|
||||||
$db->quoteName('a.id'),
|
$db->quoteName('id'),
|
||||||
$db->quoteName('a.title'),
|
$db->quoteName('title'),
|
||||||
$db->quoteName('a.address'),
|
$db->quoteName('address'),
|
||||||
$db->quoteName('a.city'),
|
$db->quoteName('city'),
|
||||||
$db->quoteName('a.state'),
|
$db->quoteName('state'),
|
||||||
$db->quoteName('a.postcode'),
|
$db->quoteName('postcode'),
|
||||||
$db->quoteName('a.phone'),
|
$db->quoteName('phone'),
|
||||||
$db->quoteName('a.latitude'),
|
$db->quoteName('latitude'),
|
||||||
$db->quoteName('a.longitude'),
|
$db->quoteName('longitude'),
|
||||||
])
|
])
|
||||||
->from($db->quoteName('#__mokosuitestorelocator_locations', 'a'))
|
->from($db->quoteName('#__mokosuitestorelocator_locations'))
|
||||||
->where($db->quoteName('a.published') . ' = 1')
|
->where($db->quoteName('published') . ' = 1')
|
||||||
->where($db->quoteName('a.latitude') . ' IS NOT NULL')
|
->where($db->quoteName('latitude') . ' IS NOT NULL')
|
||||||
->where($db->quoteName('a.longitude') . ' IS NOT NULL');
|
->where($db->quoteName('longitude') . ' IS NOT NULL');
|
||||||
|
|
||||||
// Join to get primary category marker icon
|
|
||||||
$query->select([$db->quoteName('c.marker_icon'), $db->quoteName('c.color', 'cat_color')])
|
|
||||||
->join('LEFT', $db->quoteName('#__mokosuitestorelocator_location_categories', 'lc')
|
|
||||||
. ' ON ' . $db->quoteName('lc.location_id') . ' = ' . $db->quoteName('a.id'))
|
|
||||||
->join('LEFT', $db->quoteName('#__mokosuitestorelocator_categories', 'c')
|
|
||||||
. ' ON ' . $db->quoteName('c.id') . ' = ' . $db->quoteName('lc.category_id')
|
|
||||||
. ' AND ' . $db->quoteName('c.published') . ' = 1')
|
|
||||||
->group($db->quoteName('a.id'));
|
|
||||||
|
|
||||||
$db->setQuery($query);
|
$db->setQuery($query);
|
||||||
$locations = $db->loadObjectList() ?: [];
|
$locations = $db->loadObjectList() ?: [];
|
||||||
@@ -72,7 +63,7 @@ class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareI
|
|||||||
|
|
||||||
foreach ($locations as $loc)
|
foreach ($locations as $loc)
|
||||||
{
|
{
|
||||||
$marker = [
|
$markers[] = [
|
||||||
'id' => (int) $loc->id,
|
'id' => (int) $loc->id,
|
||||||
'title' => $loc->title,
|
'title' => $loc->title,
|
||||||
'address' => trim($loc->address . ', ' . $loc->city . ', ' . $loc->state . ' ' . $loc->postcode, ', '),
|
'address' => trim($loc->address . ', ' . $loc->city . ', ' . $loc->state . ' ' . $loc->postcode, ', '),
|
||||||
@@ -80,18 +71,6 @@ class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareI
|
|||||||
'lat' => (float) $loc->latitude,
|
'lat' => (float) $loc->latitude,
|
||||||
'lng' => (float) $loc->longitude,
|
'lng' => (float) $loc->longitude,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!empty($loc->marker_icon))
|
|
||||||
{
|
|
||||||
$marker['marker_icon'] = $loc->marker_icon;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($loc->cat_color))
|
|
||||||
{
|
|
||||||
$marker['cat_color'] = $loc->cat_color;
|
|
||||||
}
|
|
||||||
|
|
||||||
$markers[] = $marker;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$data['locations'] = $markers;
|
$data['locations'] = $markers;
|
||||||
|
|||||||
@@ -15,11 +15,6 @@ $params = $displayData['params'];
|
|||||||
$locations = $displayData['locations'] ?? [];
|
$locations = $displayData['locations'] ?? [];
|
||||||
$moduleId = $displayData['module']->id;
|
$moduleId = $displayData['module']->id;
|
||||||
$mapHeight = $params->get('map_height', '400px');
|
$mapHeight = $params->get('map_height', '400px');
|
||||||
|
|
||||||
if (!preg_match('/^\d+(px|em|rem|vh|%)$/', $mapHeight))
|
|
||||||
{
|
|
||||||
$mapHeight = '400px';
|
|
||||||
}
|
|
||||||
$mapZoom = (int) $params->get('map_zoom', 10);
|
$mapZoom = (int) $params->get('map_zoom', 10);
|
||||||
$provider = $params->get('map_provider', 'leaflet');
|
$provider = $params->get('map_provider', 'leaflet');
|
||||||
$apiKey = $params->get('api_key', '');
|
$apiKey = $params->get('api_key', '');
|
||||||
@@ -74,16 +69,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
locations.forEach(function(loc) {
|
locations.forEach(function(loc) {
|
||||||
var markerOptions = {};
|
var marker = L.marker([loc.lat, loc.lng]).addTo(map);
|
||||||
if (loc.marker_icon) {
|
|
||||||
markerOptions.icon = L.icon({
|
|
||||||
iconUrl: loc.marker_icon,
|
|
||||||
iconSize: [32, 32],
|
|
||||||
iconAnchor: [16, 32],
|
|
||||||
popupAnchor: [0, -32]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
var marker = L.marker([loc.lat, loc.lng], markerOptions).addTo(map);
|
|
||||||
var popup = '<strong>' + esc(loc.title) + '</strong>';
|
var popup = '<strong>' + esc(loc.title) + '</strong>';
|
||||||
if (loc.address) popup += '<br>' + esc(loc.address);
|
if (loc.address) popup += '<br>' + esc(loc.address);
|
||||||
if (loc.phone) popup += '<br><a href="tel:' + esc(loc.phone) + '">' + esc(loc.phone) + '</a>';
|
if (loc.phone) popup += '<br><a href="tel:' + esc(loc.phone) + '">' + esc(loc.phone) + '</a>';
|
||||||
|
|||||||
+1
-2
@@ -14,7 +14,7 @@
|
|||||||
-->
|
-->
|
||||||
<extension type="module" client="site" method="upgrade">
|
<extension type="module" client="site" method="upgrade">
|
||||||
<name>mod_mokosuitestorelocator_search</name>
|
<name>mod_mokosuitestorelocator_search</name>
|
||||||
<version>01.00.18</version>
|
<version>1.0.0</version>
|
||||||
<creationDate>2026-06-23</creationDate>
|
<creationDate>2026-06-23</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
@@ -26,7 +26,6 @@
|
|||||||
<namespace path="src">Moko\Module\MokoSuiteStoreLocatorSearch</namespace>
|
<namespace path="src">Moko\Module\MokoSuiteStoreLocatorSearch</namespace>
|
||||||
|
|
||||||
<files>
|
<files>
|
||||||
<folder module="mod_mokosuitestorelocator_search">services</folder>
|
|
||||||
<folder>src</folder>
|
<folder>src</folder>
|
||||||
<folder>tmpl</folder>
|
<folder>tmpl</folder>
|
||||||
<folder>language</folder>
|
<folder>language</folder>
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package MokoSuiteStoreLocator
|
|
||||||
* @subpackage mod_mokosuitestorelocator_search
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Extension\Service\Provider\Module;
|
|
||||||
use Joomla\CMS\Extension\Service\Provider\ModuleDispatcherFactory;
|
|
||||||
use Joomla\CMS\Extension\Service\Provider\HelperFactory;
|
|
||||||
use Joomla\DI\Container;
|
|
||||||
use Joomla\DI\ServiceProviderInterface;
|
|
||||||
|
|
||||||
return new class implements ServiceProviderInterface
|
|
||||||
{
|
|
||||||
public function register(Container $container): void
|
|
||||||
{
|
|
||||||
$container->registerServiceProvider(new ModuleDispatcherFactory('\\Moko\\Module\\MokoSuiteStoreLocatorSearch'));
|
|
||||||
$container->registerServiceProvider(new HelperFactory('\\Moko\\Module\\MokoSuiteStoreLocatorSearch\\Helper'));
|
|
||||||
$container->registerServiceProvider(new Module());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
-2
@@ -1,2 +0,0 @@
|
|||||||
PLG_WEBSERVICES_MOKOSUITESTORELOCATOR="MokoSuiteStoreLocator - Web Services"
|
|
||||||
PLG_WEBSERVICES_MOKOSUITESTORELOCATOR_DESC="Provides REST API endpoints for the MokoSuiteStoreLocator component."
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- =========================================================================
|
|
||||||
Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
||||||
SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
========================================================================= -->
|
|
||||||
<extension type="plugin" group="webservices" method="upgrade">
|
|
||||||
<name>plg_webservices_mokosuitestorelocator</name>
|
|
||||||
<version>01.00.18</version>
|
|
||||||
<creationDate>2026-06-24</creationDate>
|
|
||||||
<author>Moko Consulting</author>
|
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
|
||||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
|
||||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
|
||||||
<description>PLG_WEBSERVICES_MOKOSUITESTORELOCATOR_DESC</description>
|
|
||||||
|
|
||||||
<namespace path="src">Moko\Plugin\WebServices\MokoSuiteStoreLocator</namespace>
|
|
||||||
|
|
||||||
<files>
|
|
||||||
<folder plugin="mokosuitestorelocator">services</folder>
|
|
||||||
<folder>src</folder>
|
|
||||||
<folder>language</folder>
|
|
||||||
</files>
|
|
||||||
</extension>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package MokoSuiteStoreLocator
|
|
||||||
* @subpackage plg_webservices_mokosuitestorelocator
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Extension\PluginInterface;
|
|
||||||
use Joomla\CMS\Factory;
|
|
||||||
use Joomla\CMS\Plugin\PluginHelper;
|
|
||||||
use Joomla\DI\Container;
|
|
||||||
use Joomla\DI\ServiceProviderInterface;
|
|
||||||
use Joomla\Event\DispatcherInterface;
|
|
||||||
use Moko\Plugin\WebServices\MokoSuiteStoreLocator\Extension\MokoSuiteStoreLocator;
|
|
||||||
|
|
||||||
return new class implements ServiceProviderInterface
|
|
||||||
{
|
|
||||||
public function register(Container $container): void
|
|
||||||
{
|
|
||||||
$container->set(
|
|
||||||
PluginInterface::class,
|
|
||||||
function (Container $container) {
|
|
||||||
$dispatcher = $container->get(DispatcherInterface::class);
|
|
||||||
$plugin = new MokoSuiteStoreLocator(
|
|
||||||
$dispatcher,
|
|
||||||
(array) PluginHelper::getPlugin('webservices', 'mokosuitestorelocator')
|
|
||||||
);
|
|
||||||
$plugin->setApplication(Factory::getApplication());
|
|
||||||
|
|
||||||
return $plugin;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
-57
@@ -1,57 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* @package MokoSuiteStoreLocator
|
|
||||||
* @subpackage plg_webservices_mokosuitestorelocator
|
|
||||||
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
|
||||||
* @license GNU General Public License version 3 or later; see LICENSE
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Moko\Plugin\WebServices\MokoSuiteStoreLocator\Extension;
|
|
||||||
|
|
||||||
defined('_JEXEC') or die;
|
|
||||||
|
|
||||||
use Joomla\CMS\Plugin\CMSPlugin;
|
|
||||||
use Joomla\CMS\Router\ApiRouter;
|
|
||||||
use Joomla\Event\SubscriberInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Web Services plugin for MokoSuiteStoreLocator REST API.
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
final class MokoSuiteStoreLocator extends CMSPlugin implements SubscriberInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Returns the subscribed events.
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
public static function getSubscribedEvents(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'onBeforeApiRoute' => 'onBeforeApiRoute',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register API routes.
|
|
||||||
*
|
|
||||||
* @param \Joomla\CMS\Event\ApiRouterEvent $event The event.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*
|
|
||||||
* @since 1.2.0
|
|
||||||
*/
|
|
||||||
public function onBeforeApiRoute($event): void
|
|
||||||
{
|
|
||||||
$router = $event->getArgument('router') ?? $event->getRouter();
|
|
||||||
|
|
||||||
$router->createCRUDRoutes(
|
|
||||||
'v1/mokosuitestorelocator/locations',
|
|
||||||
'locations',
|
|
||||||
['component' => 'com_mokosuitestorelocator']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,26 +18,25 @@
|
|||||||
<extension type="package" method="upgrade">
|
<extension type="package" method="upgrade">
|
||||||
<name>pkg_mokosuitestorelocator</name>
|
<name>pkg_mokosuitestorelocator</name>
|
||||||
<packagename>mokosuitestorelocator</packagename>
|
<packagename>mokosuitestorelocator</packagename>
|
||||||
<version>01.00.18</version>
|
<version>1.0.0</version>
|
||||||
<creationDate>2026-06-23</creationDate>
|
<creationDate>2026-06-23</creationDate>
|
||||||
<author>Moko Consulting</author>
|
<author>Moko Consulting</author>
|
||||||
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
<authorEmail>hello@mokoconsulting.tech</authorEmail>
|
||||||
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
<authorUrl>https://mokoconsulting.tech</authorUrl>
|
||||||
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
<copyright>Copyright (C) 2026 Moko Consulting. All rights reserved.</copyright>
|
||||||
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
<license>GNU General Public License version 3 or later; see LICENSE</license>
|
||||||
<description>MokoSuiteStoreLocator — store locator component with interactive map and search modules for Joomla 5/6.</description>
|
<description>PKG_MOKOJOOMSTORELOCATOR_DESC</description>
|
||||||
<dlid prefix="dlid=" suffix=""/>
|
|
||||||
<scriptfile>script.php</scriptfile>
|
<scriptfile>script.php</scriptfile>
|
||||||
|
|
||||||
<files>
|
<files>
|
||||||
<file type="component" id="com_mokosuitestorelocator">com_mokosuitestorelocator.zip</file>
|
<file type="component" id="com_mokosuitestorelocator">com_mokosuitestorelocator.zip</file>
|
||||||
<file type="module" id="mod_mokosuitestorelocator_map" client="site">mod_mokosuitestorelocator_map.zip</file>
|
<file type="module" id="mod_mokosuitestorelocator_map" client="site">mod_mokosuitestorelocator_map.zip</file>
|
||||||
<file type="module" id="mod_mokosuitestorelocator_search" client="site">mod_mokosuitestorelocator_search.zip</file>
|
<file type="module" id="mod_mokosuitestorelocator_search" client="site">mod_mokosuitestorelocator_search.zip</file>
|
||||||
<file type="plugin" id="mokosuitestorelocator" group="webservices">plg_webservices_mokosuitestorelocator.zip</file>
|
|
||||||
</files>
|
</files>
|
||||||
|
|
||||||
<updateservers>
|
<updateservers>
|
||||||
<server type="extension" priority="1" name="Package - MokoSuiteStoreLocator">https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteStoreLocator/updates.xml</server>
|
<server type="extension" name="MokoSuiteStoreLocator Updates">https://git.mokoconsulting.tech/MokoConsulting/MokoSuiteStoreLocator/updates.xml</server>
|
||||||
</updateservers>
|
</updateservers>
|
||||||
|
<dlid prefix="dlid=" suffix=""/>
|
||||||
<blockChildUninstall>true</blockChildUninstall>
|
<blockChildUninstall>true</blockChildUninstall>
|
||||||
</extension>
|
</extension>
|
||||||
|
|||||||
Reference in New Issue
Block a user