Compare commits
4 Commits
development
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 28fcc72ced | |||
| fdd004b345 | |||
| 2755ef709f | |||
| df59fd7303 |
@@ -7,7 +7,7 @@
|
|||||||
# INGROUP: moko-platform.Release
|
# INGROUP: moko-platform.Release
|
||||||
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
# REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
|
||||||
# PATH: /templates/workflows/universal/pre-release.yml.template
|
# PATH: /templates/workflows/universal/pre-release.yml.template
|
||||||
# VERSION: 09.23.00
|
# VERSION: 05.01.00
|
||||||
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
# BRIEF: Manual pre-release -- builds dev/alpha/beta/rc packages from any branch
|
||||||
|
|
||||||
name: "Universal: Pre-Release"
|
name: "Universal: Pre-Release"
|
||||||
@@ -17,6 +17,10 @@ on:
|
|||||||
types: [closed]
|
types: [closed]
|
||||||
branches:
|
branches:
|
||||||
- dev
|
- dev
|
||||||
|
pull_request_target:
|
||||||
|
types: [synchronize, opened, reopened]
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
stability:
|
stability:
|
||||||
@@ -43,7 +47,8 @@ jobs:
|
|||||||
runs-on: release
|
runs-on: release
|
||||||
if: >-
|
if: >-
|
||||||
github.event_name == 'workflow_dispatch' ||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
(github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev')
|
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'dev') ||
|
||||||
|
(github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main')
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -51,22 +56,29 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
token: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
|
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || '' }}
|
||||||
|
|
||||||
- name: Setup moko-platform tools
|
- name: Setup moko-platform tools
|
||||||
env:
|
env:
|
||||||
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
MOKO_CLONE_TOKEN: ${{ secrets.MOKOGITEA_TOKEN }}
|
||||||
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
MOKO_CLONE_HOST: git.mokoconsulting.tech/MokoConsulting
|
||||||
run: |
|
run: |
|
||||||
if ! command -v composer &> /dev/null; then
|
# Use pre-installed /opt/moko-platform if available (updated by cron every 6h)
|
||||||
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
|
if [ -f “/opt/moko-platform/cli/version_bump.php” ] && [ -f “/opt/moko-platform/vendor/autoload.php” ]; then
|
||||||
|
echo “Using pre-installed /opt/moko-platform”
|
||||||
|
echo “MOKO_CLI=/opt/moko-platform/cli” >> “$GITHUB_ENV”
|
||||||
|
else
|
||||||
|
echo “Falling back to fresh clone”
|
||||||
|
if ! command -v composer &> /dev/null; then
|
||||||
|
sudo apt-get update -qq && sudo apt-get install -y -qq php-cli php-mbstring php-xml php-zip php-curl composer >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
rm -rf /tmp/moko-platform-api
|
||||||
|
git clone --depth 1 --branch main --quiet \
|
||||||
|
“https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git” \
|
||||||
|
/tmp/moko-platform-api
|
||||||
|
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
||||||
|
echo “MOKO_CLI=/tmp/moko-platform-api/cli” >> “$GITHUB_ENV”
|
||||||
fi
|
fi
|
||||||
# Always fetch latest CLI tools — never use stale cache from previous runs
|
|
||||||
rm -rf /tmp/moko-platform-api
|
|
||||||
git clone --depth 1 --branch main --quiet \
|
|
||||||
"https://x-access-token:${MOKO_CLONE_TOKEN}@${MOKO_CLONE_HOST}/moko-platform.git" \
|
|
||||||
/tmp/moko-platform-api
|
|
||||||
cd /tmp/moko-platform-api && composer install --no-dev --no-interaction --quiet
|
|
||||||
echo "MOKO_CLI=/tmp/moko-platform-api/cli" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
- name: Detect platform
|
- name: Detect platform
|
||||||
id: platform
|
id: platform
|
||||||
@@ -76,24 +88,40 @@ jobs:
|
|||||||
- name: Resolve metadata and bump version
|
- name: Resolve metadata and bump version
|
||||||
id: meta
|
id: meta
|
||||||
run: |
|
run: |
|
||||||
STABILITY="${{ inputs.stability || 'development' }}"
|
# Auto-detect stability: RC for PRs targeting main, else use input or default to development
|
||||||
|
if [ "${{ github.event_name }}" = "pull_request_target" ] && [ "${{ github.event.pull_request.base.ref }}" = "main" ]; then
|
||||||
|
STABILITY="release-candidate"
|
||||||
|
else
|
||||||
|
STABILITY="${{ inputs.stability || 'development' }}"
|
||||||
|
fi
|
||||||
|
|
||||||
case "$STABILITY" in
|
case "$STABILITY" in
|
||||||
development) TAG="development" ;;
|
development) SUFFIX="-dev"; TAG="development" ;;
|
||||||
alpha) TAG="alpha" ;;
|
alpha) SUFFIX="-alpha"; TAG="alpha" ;;
|
||||||
beta) TAG="beta" ;;
|
beta) SUFFIX="-beta"; TAG="beta" ;;
|
||||||
release-candidate) TAG="release-candidate" ;;
|
release-candidate) SUFFIX="-rc"; TAG="release-candidate" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Set stability suffix, bump preserves it, fix consistency
|
# Bump version via CLI: patch for dev/alpha/beta, minor for RC
|
||||||
|
case "$STABILITY" in
|
||||||
|
release-candidate) BUMP="minor" ;;
|
||||||
|
*) BUMP="patch" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
php ${MOKO_CLI}/version_bump.php --path . $([ "$BUMP" = "minor" ] && echo "--minor") 2>/dev/null || true
|
||||||
|
|
||||||
|
# Set stability suffix and verify consistency
|
||||||
|
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo "00.00.01")
|
||||||
|
VERSION=$(echo "$VERSION" | sed 's/-\(dev\|alpha\|beta\|rc\)$//')
|
||||||
|
|
||||||
php ${MOKO_CLI}/version_set_platform.php \
|
php ${MOKO_CLI}/version_set_platform.php \
|
||||||
--path . --version "$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null || echo '00.00.01')" \
|
--path . --version "$VERSION" --branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
||||||
--branch "${{ github.ref_name }}" --stability "$STABILITY" 2>/dev/null || true
|
|
||||||
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
php ${MOKO_CLI}/version_check.php --path . --fix 2>/dev/null || true
|
||||||
|
|
||||||
# Read final version (includes suffix, e.g. 01.02.15-dev)
|
# Append suffix for output
|
||||||
VERSION=$(php ${MOKO_CLI}/version_read.php --path . 2>/dev/null)
|
if [ -n "$SUFFIX" ]; then
|
||||||
[ -z "$VERSION" ] && VERSION="00.00.01"
|
VERSION="${VERSION}${SUFFIX}"
|
||||||
|
fi
|
||||||
|
|
||||||
# Commit version bump
|
# Commit version bump
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
||||||
@@ -118,11 +146,12 @@ jobs:
|
|||||||
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
echo "stability=${STABILITY}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "suffix=${SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||||
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
|
||||||
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
echo "zip_name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
echo "ext_element=${EXT_ELEMENT}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION} ==="
|
echo "=== Pre-Release: ${EXT_ELEMENT} ${VERSION}${SUFFIX} ==="
|
||||||
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
id: release
|
id: release
|
||||||
@@ -135,6 +164,41 @@ jobs:
|
|||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
--repo "${GITEA_REPO}" --branch dev --prerelease
|
--repo "${GITEA_REPO}" --branch dev --prerelease
|
||||||
|
|
||||||
|
- name: Update release notes from CHANGELOG.md
|
||||||
|
run: |
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
VERSION="${{ steps.meta.outputs.version }}"
|
||||||
|
API_BASE="${GITEA_URL}/api/v1/repos/${GITEA_ORG}/${GITEA_REPO}"
|
||||||
|
|
||||||
|
# Extract [Unreleased] section from changelog (everything between [Unreleased] and next ## heading)
|
||||||
|
if [ -f "CHANGELOG.md" ]; then
|
||||||
|
NOTES=$(awk '/^## \[Unreleased\]/{found=1; next} /^## \[/{if(found) exit} found{print}' CHANGELOG.md)
|
||||||
|
[ -z "$NOTES" ] && NOTES="Release ${VERSION}"
|
||||||
|
else
|
||||||
|
NOTES="Release ${VERSION}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update release body via API
|
||||||
|
RELEASE_ID=$(curl -sf -H "Authorization: token ${{ secrets.MOKOGITEA_TOKEN }}" \
|
||||||
|
"${API_BASE}/releases/tags/${TAG}" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [ -n "$RELEASE_ID" ]; then
|
||||||
|
python3 -c "
|
||||||
|
import json, urllib.request
|
||||||
|
body = open('/dev/stdin').read()
|
||||||
|
payload = json.dumps({'body': body}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
'${API_BASE}/releases/${RELEASE_ID}',
|
||||||
|
data=payload, method='PATCH',
|
||||||
|
headers={
|
||||||
|
'Authorization': 'token ${{ secrets.MOKOGITEA_TOKEN }}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
urllib.request.urlopen(req)
|
||||||
|
" <<< "$NOTES"
|
||||||
|
echo "Release notes updated from CHANGELOG.md"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Build package and upload
|
- name: Build package and upload
|
||||||
id: package
|
id: package
|
||||||
run: |
|
run: |
|
||||||
@@ -146,55 +210,8 @@ jobs:
|
|||||||
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
--token "${{ secrets.MOKOGITEA_TOKEN }}" --api-base "$API_BASE" \
|
||||||
--repo "${GITEA_REPO}" --output /tmp || true
|
--repo "${GITEA_REPO}" --output /tmp || true
|
||||||
|
|
||||||
- name: Update updates.xml
|
# updates.xml is generated dynamically by MokoGitea license server
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
# No need to build, commit, or sync updates.xml from workflows
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.meta.outputs.version }}"
|
|
||||||
STABILITY="${{ steps.meta.outputs.stability }}"
|
|
||||||
SHA256="${{ steps.package.outputs.sha256_zip }}"
|
|
||||||
|
|
||||||
if [ ! -f "updates.xml" ]; then
|
|
||||||
echo "No updates.xml -- skipping"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
SHA_FLAG=""
|
|
||||||
[ -n "$SHA256" ] && SHA_FLAG="--sha ${SHA256}"
|
|
||||||
|
|
||||||
php ${MOKO_CLI}/updates_xml_build.php \
|
|
||||||
--path . --version "${VERSION}" --stability "${STABILITY}" \
|
|
||||||
--gitea-url "${GITEA_URL}" --org "${GITEA_ORG}" --repo "${GITEA_REPO}" \
|
|
||||||
${SHA_FLAG}
|
|
||||||
|
|
||||||
# Commit and push
|
|
||||||
if ! git diff --quiet updates.xml 2>/dev/null; then
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
git add updates.xml
|
|
||||||
git commit -m "chore: update ${STABILITY} channel ${VERSION} [skip ci]"
|
|
||||||
git push origin HEAD 2>&1 || echo "WARNING: push failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: "Sync updates.xml to all branches"
|
|
||||||
if: steps.platform.outputs.platform == 'joomla'
|
|
||||||
run: |
|
|
||||||
CURRENT_BRANCH="${{ github.ref_name }}"
|
|
||||||
git config --local user.email "gitea-actions[bot]@mokoconsulting.tech"
|
|
||||||
git config --local user.name "gitea-actions[bot]"
|
|
||||||
|
|
||||||
for BRANCH in main dev; do
|
|
||||||
[ "$BRANCH" = "$CURRENT_BRANCH" ] && continue
|
|
||||||
echo "Syncing updates.xml -> ${BRANCH}"
|
|
||||||
git fetch origin "${BRANCH}" 2>/dev/null || continue
|
|
||||||
git checkout "origin/${BRANCH}" -- updates.xml 2>/dev/null || continue
|
|
||||||
git checkout "${CURRENT_BRANCH}" -- updates.xml
|
|
||||||
if ! git diff --quiet updates.xml 2>/dev/null; then
|
|
||||||
git add updates.xml
|
|
||||||
git commit -m "chore: sync updates.xml from ${CURRENT_BRANCH} [skip ci]"
|
|
||||||
git push origin HEAD:refs/heads/${BRANCH} 2>&1 || echo "WARNING: push to ${BRANCH} failed"
|
|
||||||
fi
|
|
||||||
git checkout "${CURRENT_BRANCH}" 2>/dev/null
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: "Delete lesser pre-release channels (cascade)"
|
- name: "Delete lesser pre-release channels (cascade)"
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
<fieldset name="filters" label="COM_MOKOBACKUP_FIELDSET_FILTERS">
|
<fieldset name="filters" label="COM_MOKOBACKUP_FIELDSET_FILTERS">
|
||||||
<field
|
<field
|
||||||
name="exclude_dirs"
|
name="exclude_dirs"
|
||||||
type="ExcludeList"
|
type="DirectoryFilter"
|
||||||
label="COM_MOKOBACKUP_FIELD_EXCLUDE_DIRS"
|
label="COM_MOKOBACKUP_FIELD_EXCLUDE_DIRS"
|
||||||
description="COM_MOKOBACKUP_FIELD_EXCLUDE_DIRS_DESC"
|
description="COM_MOKOBACKUP_FIELD_EXCLUDE_DIRS_DESC"
|
||||||
filter="raw"
|
filter="raw"
|
||||||
|
|||||||
@@ -102,7 +102,10 @@ COM_MOKOBACKUP_FIELD_INCLUDE_MOKORESTORE_DESC="Include MokoRestore (standalone r
|
|||||||
|
|
||||||
; Exclusion filter fields
|
; Exclusion filter fields
|
||||||
COM_MOKOBACKUP_FIELD_EXCLUDE_DIRS="Exclude Directories"
|
COM_MOKOBACKUP_FIELD_EXCLUDE_DIRS="Exclude Directories"
|
||||||
COM_MOKOBACKUP_FIELD_EXCLUDE_DIRS_DESC="One directory path per line (relative to Joomla root). These directories will be skipped during file backup."
|
COM_MOKOBACKUP_FIELD_EXCLUDE_DIRS_DESC="Browse and check directories to exclude from file backup. You can also type paths manually."
|
||||||
|
COM_MOKOBACKUP_FILTER_EXCLUDED="Excluded"
|
||||||
|
COM_MOKOBACKUP_FILTER_INCLUDED="Included"
|
||||||
|
COM_MOKOBACKUP_FILTER_ADD_MANUAL="Add Path"
|
||||||
COM_MOKOBACKUP_FIELD_EXCLUDE_FILES="Exclude Files"
|
COM_MOKOBACKUP_FIELD_EXCLUDE_FILES="Exclude Files"
|
||||||
COM_MOKOBACKUP_FIELD_EXCLUDE_FILES_DESC="One filename or pattern per line. Supports wildcards (e.g. *.bak, *.tmp)."
|
COM_MOKOBACKUP_FIELD_EXCLUDE_FILES_DESC="One filename or pattern per line. Supports wildcards (e.g. *.bak, *.tmp)."
|
||||||
COM_MOKOBACKUP_FIELD_EXCLUDE_TABLES="Exclude Tables"
|
COM_MOKOBACKUP_FIELD_EXCLUDE_TABLES="Exclude Tables"
|
||||||
|
|||||||
+6
-1
@@ -1,7 +1,12 @@
|
|||||||
-- MokoJoomBackup 01.01.08
|
-- MokoJoomBackup 01.01.02
|
||||||
|
-- Consolidated schema updates: NULL defaults, notifications, archive name format
|
||||||
|
|
||||||
-- Fix: allow NULL defaults for manifest and log columns
|
-- Fix: allow NULL defaults for manifest and log columns
|
||||||
ALTER TABLE `#__mokobackup_records` MODIFY `manifest` LONGTEXT DEFAULT NULL;
|
ALTER TABLE `#__mokobackup_records` MODIFY `manifest` LONGTEXT DEFAULT NULL;
|
||||||
ALTER TABLE `#__mokobackup_records` MODIFY `log` MEDIUMTEXT DEFAULT NULL;
|
ALTER TABLE `#__mokobackup_records` MODIFY `log` MEDIUMTEXT DEFAULT NULL;
|
||||||
|
|
||||||
-- Add user group notifications column to profiles
|
-- Add user group notifications column to profiles
|
||||||
ALTER TABLE `#__mokobackup_profiles` ADD COLUMN `notify_user_groups` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Comma-separated Joomla user group IDs' AFTER `notify_email`;
|
ALTER TABLE `#__mokobackup_profiles` ADD COLUMN `notify_user_groups` VARCHAR(255) NOT NULL DEFAULT '' COMMENT 'Comma-separated Joomla user group IDs' AFTER `notify_email`;
|
||||||
|
|
||||||
|
-- Add archive_name_format column with placeholder support
|
||||||
|
ALTER TABLE `#__mokobackup_profiles` ADD COLUMN `archive_name_format` VARCHAR(512) NOT NULL DEFAULT '[host]_[datetime]_profile[profile_id]' COMMENT 'Filename format with placeholders' AFTER `backup_dir`;
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
-- MokoJoomBackup 01.01.09
|
|
||||||
-- Add archive_name_format column with placeholder support
|
|
||||||
ALTER TABLE `#__mokobackup_profiles` ADD COLUMN `archive_name_format` VARCHAR(512) NOT NULL DEFAULT '[host]_[datetime]_profile[profile_id]' COMMENT 'Filename format with placeholders' AFTER `backup_dir`;
|
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package MokoJoomBackup
|
||||||
|
* @subpackage com_mokobackup
|
||||||
|
* @author Moko Consulting <hello@mokoconsulting.tech>
|
||||||
|
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
|
||||||
|
* @license GNU General Public License version 3 or later; see LICENSE
|
||||||
|
*
|
||||||
|
* Interactive directory tree field with checkboxes for exclude/include filtering.
|
||||||
|
* Loads the directory tree from the server via AJAX (browseDir endpoint).
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Joomla\Component\MokoBackup\Administrator\Field;
|
||||||
|
|
||||||
|
defined('_JEXEC') or die;
|
||||||
|
|
||||||
|
use Joomla\CMS\Form\FormField;
|
||||||
|
use Joomla\CMS\Language\Text;
|
||||||
|
|
||||||
|
class DirectoryFilterField extends FormField
|
||||||
|
{
|
||||||
|
protected $type = 'DirectoryFilter';
|
||||||
|
|
||||||
|
protected function getInput(): string
|
||||||
|
{
|
||||||
|
$id = htmlspecialchars($this->id, ENT_QUOTES, 'UTF-8');
|
||||||
|
$name = htmlspecialchars($this->name, ENT_QUOTES, 'UTF-8');
|
||||||
|
$mode = htmlspecialchars((string) ($this->element['mode'] ?? 'exclude'), ENT_QUOTES, 'UTF-8');
|
||||||
|
|
||||||
|
// Parse current values (newline-separated)
|
||||||
|
$items = [];
|
||||||
|
|
||||||
|
if (!empty($this->value)) {
|
||||||
|
$items = array_values(array_filter(array_map('trim', explode("\n", str_replace("\r", '', $this->value)))));
|
||||||
|
}
|
||||||
|
|
||||||
|
$itemsJson = json_encode($items);
|
||||||
|
$jRoot = json_encode(JPATH_ROOT);
|
||||||
|
|
||||||
|
$labelExclude = Text::_('COM_MOKOBACKUP_FILTER_EXCLUDED');
|
||||||
|
$labelInclude = Text::_('COM_MOKOBACKUP_FILTER_INCLUDED');
|
||||||
|
$labelManual = Text::_('COM_MOKOBACKUP_FILTER_ADD_MANUAL');
|
||||||
|
$addLabel = Text::_('JGLOBAL_FIELD_ADD');
|
||||||
|
$placeholder = htmlspecialchars((string) ($this->element['hint'] ?? 'path/to/directory'), ENT_QUOTES, 'UTF-8');
|
||||||
|
|
||||||
|
return <<<HTML
|
||||||
|
<div id="{$id}_wrap">
|
||||||
|
<input type="hidden" name="{$name}" id="{$id}" value="" />
|
||||||
|
|
||||||
|
<!-- Manual entry row -->
|
||||||
|
<div class="input-group input-group-sm mb-2">
|
||||||
|
<input type="text" class="form-control" id="{$id}_manual" placeholder="{$placeholder}" />
|
||||||
|
<button type="button" class="btn btn-outline-success" id="{$id}_addBtn">
|
||||||
|
<span class="icon-plus" aria-hidden="true"></span> {$addLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selected items (pills) -->
|
||||||
|
<div id="{$id}_pills" class="mb-2 d-flex flex-wrap gap-1"></div>
|
||||||
|
|
||||||
|
<!-- Browsable tree -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header py-1 px-2 d-flex justify-content-between align-items-center">
|
||||||
|
<small class="fw-bold text-muted" id="{$id}_cwd"></small>
|
||||||
|
<button type="button" class="btn btn-sm btn-link p-0" id="{$id}_upBtn" style="display:none;">
|
||||||
|
<span class="icon-arrow-up-4" aria-hidden="true"></span> ..
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="{$id}_tree" class="list-group list-group-flush" style="max-height:300px; overflow-y:auto;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#{$id}_wrap .mb-dir-pill {
|
||||||
|
display: inline-flex; align-items: center; gap: 0.3rem;
|
||||||
|
padding: 0.2rem 0.5rem; border-radius: 4px; font-size: 0.8rem;
|
||||||
|
font-family: monospace; cursor: default;
|
||||||
|
}
|
||||||
|
#{$id}_wrap .mb-dir-pill.excluded { background: #f8d7da; color: #842029; border: 1px solid #f5c2c7; }
|
||||||
|
#{$id}_wrap .mb-dir-pill.included { background: #d1e7dd; color: #0f5132; border: 1px solid #badbcc; }
|
||||||
|
#{$id}_wrap .mb-dir-pill .btn-close { font-size: 0.6rem; }
|
||||||
|
#{$id}_wrap .mb-dir-row { display: flex; align-items: center; padding: 0.35rem 0.75rem; gap: 0.5rem; border-bottom: 1px solid #eee; }
|
||||||
|
#{$id}_wrap .mb-dir-row:hover { background: #f8f9fa; }
|
||||||
|
#{$id}_wrap .mb-dir-row .mb-dir-name { cursor: pointer; flex: 1; font-size: 0.9rem; }
|
||||||
|
#{$id}_wrap .mb-dir-row .mb-dir-name:hover { color: #0d6efd; text-decoration: underline; }
|
||||||
|
#{$id}_wrap .mb-dir-check { width: 1rem; height: 1rem; cursor: pointer; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
const id = '{$id}';
|
||||||
|
const hidden = document.getElementById(id);
|
||||||
|
const pills = document.getElementById(id + '_pills');
|
||||||
|
const tree = document.getElementById(id + '_tree');
|
||||||
|
const cwdEl = document.getElementById(id + '_cwd');
|
||||||
|
const upBtn = document.getElementById(id + '_upBtn');
|
||||||
|
const manualInput = document.getElementById(id + '_manual');
|
||||||
|
const addBtn = document.getElementById(id + '_addBtn');
|
||||||
|
const jRoot = {$jRoot};
|
||||||
|
|
||||||
|
let selected = new Set({$itemsJson});
|
||||||
|
let currentPath = jRoot;
|
||||||
|
let parentPath = null;
|
||||||
|
|
||||||
|
function sync() {
|
||||||
|
hidden.value = Array.from(selected).join('\\n');
|
||||||
|
renderPills();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPills() {
|
||||||
|
while (pills.firstChild) pills.removeChild(pills.firstChild);
|
||||||
|
selected.forEach(function(path) {
|
||||||
|
const pill = document.createElement('span');
|
||||||
|
pill.className = 'mb-dir-pill excluded';
|
||||||
|
|
||||||
|
const icon = document.createElement('span');
|
||||||
|
icon.className = 'icon-folder';
|
||||||
|
icon.setAttribute('aria-hidden', 'true');
|
||||||
|
pill.appendChild(icon);
|
||||||
|
|
||||||
|
pill.appendChild(document.createTextNode(' ' + path + ' '));
|
||||||
|
|
||||||
|
const closeBtn = document.createElement('button');
|
||||||
|
closeBtn.type = 'button';
|
||||||
|
closeBtn.className = 'btn-close btn-close-sm';
|
||||||
|
closeBtn.setAttribute('aria-label', 'Remove');
|
||||||
|
closeBtn.addEventListener('click', function() {
|
||||||
|
selected.delete(path);
|
||||||
|
sync();
|
||||||
|
refreshTree();
|
||||||
|
});
|
||||||
|
pill.appendChild(closeBtn);
|
||||||
|
|
||||||
|
pills.appendChild(pill);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRelative(absPath) {
|
||||||
|
if (absPath.indexOf(jRoot) === 0) {
|
||||||
|
let rel = absPath.substring(jRoot.length);
|
||||||
|
if (rel.charAt(0) === '/') rel = rel.substring(1);
|
||||||
|
return rel;
|
||||||
|
}
|
||||||
|
return absPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTreeMessage(text, cls) {
|
||||||
|
while (tree.firstChild) tree.removeChild(tree.firstChild);
|
||||||
|
const msg = document.createElement('div');
|
||||||
|
msg.className = 'p-2 ' + cls;
|
||||||
|
msg.textContent = text;
|
||||||
|
tree.appendChild(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDir(path) {
|
||||||
|
setTreeMessage('Loading...', 'text-muted');
|
||||||
|
currentPath = path;
|
||||||
|
|
||||||
|
const form = new URLSearchParams();
|
||||||
|
form.append('task', 'ajax.browseDir');
|
||||||
|
form.append('path', path);
|
||||||
|
const tokenName = Joomla.getOptions('csrf.token') || '';
|
||||||
|
if (tokenName) form.append(tokenName, '1');
|
||||||
|
|
||||||
|
fetch('index.php?option=com_mokobackup&format=json', {
|
||||||
|
method: 'POST', body: form,
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }
|
||||||
|
})
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.error) {
|
||||||
|
setTreeMessage(data.message || 'Error', 'text-danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parentPath = data.parent || null;
|
||||||
|
cwdEl.textContent = data.current || path;
|
||||||
|
upBtn.style.display = parentPath ? '' : 'none';
|
||||||
|
renderTree(data.dirs || []);
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
setTreeMessage('Error: ' + err.message, 'text-danger');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshTree() {
|
||||||
|
loadDir(currentPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTree(dirs) {
|
||||||
|
while (tree.firstChild) tree.removeChild(tree.firstChild);
|
||||||
|
if (dirs.length === 0) {
|
||||||
|
setTreeMessage('(empty)', 'text-muted');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dirs.forEach(function(dir) {
|
||||||
|
const rel = toRelative(dir.path);
|
||||||
|
const isExcluded = selected.has(rel);
|
||||||
|
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'mb-dir-row' + (isExcluded ? ' bg-danger bg-opacity-10' : '');
|
||||||
|
|
||||||
|
const cb = document.createElement('input');
|
||||||
|
cb.type = 'checkbox';
|
||||||
|
cb.className = 'mb-dir-check form-check-input';
|
||||||
|
cb.checked = isExcluded;
|
||||||
|
cb.title = isExcluded ? 'Excluded — uncheck to include' : 'Check to exclude';
|
||||||
|
cb.addEventListener('change', function() {
|
||||||
|
if (cb.checked) {
|
||||||
|
selected.add(rel);
|
||||||
|
} else {
|
||||||
|
selected.delete(rel);
|
||||||
|
}
|
||||||
|
sync();
|
||||||
|
refreshTree();
|
||||||
|
});
|
||||||
|
|
||||||
|
const icon = document.createElement('span');
|
||||||
|
icon.className = isExcluded ? 'icon-unpublish text-danger' : 'icon-folder text-warning';
|
||||||
|
icon.setAttribute('aria-hidden', 'true');
|
||||||
|
|
||||||
|
const nameEl = document.createElement('span');
|
||||||
|
nameEl.className = 'mb-dir-name';
|
||||||
|
nameEl.textContent = dir.name;
|
||||||
|
nameEl.addEventListener('click', function() { loadDir(dir.path); });
|
||||||
|
|
||||||
|
row.appendChild(cb);
|
||||||
|
row.appendChild(icon);
|
||||||
|
row.appendChild(nameEl);
|
||||||
|
tree.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
upBtn.addEventListener('click', function() {
|
||||||
|
if (parentPath) loadDir(parentPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
addBtn.addEventListener('click', function() {
|
||||||
|
const val = manualInput.value.trim();
|
||||||
|
if (val && !selected.has(val)) {
|
||||||
|
selected.add(val);
|
||||||
|
manualInput.value = '';
|
||||||
|
sync();
|
||||||
|
refreshTree();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
manualInput.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Enter') { e.preventDefault(); addBtn.click(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
sync();
|
||||||
|
loadDir(jRoot);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
HTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,79 +34,81 @@ $ajaxUrl = Route::_('index.php?option=com_mokobackup&format=json', false);
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Row 1: Status Cards (clickable) -->
|
<!-- Row 1: Status Cards (clickable) -->
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-md-3 mb-3">
|
||||||
<a href="<?php echo Route::_('index.php?option=com_mokobackup&view=backups'); ?>" class="text-decoration-none">
|
<div class="card h-100 mb-tile" role="link" data-href="<?php echo $this->lastBackup ? Route::_('index.php?option=com_mokobackup&view=backup&id=' . $this->lastBackup->id) : Route::_('index.php?option=com_mokobackup&view=backups'); ?>">
|
||||||
<div class="card h-100" style="cursor:pointer;" onmouseover="this.classList.add('shadow')" onmouseout="this.classList.remove('shadow')">
|
<div class="card-body text-center">
|
||||||
<div class="card-body text-center">
|
<span class="icon-database fs-1 text-primary" aria-hidden="true"></span>
|
||||||
<span class="icon-database fs-1 text-primary" aria-hidden="true"></span>
|
<h5 class="card-title mt-2"><?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_LAST_BACKUP'); ?></h5>
|
||||||
<h5 class="card-title mt-2"><?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_LAST_BACKUP'); ?></h5>
|
<?php if ($this->lastBackup) : ?>
|
||||||
<?php if ($this->lastBackup) : ?>
|
<p class="card-text text-success fw-bold">
|
||||||
<p class="card-text text-success fw-bold">
|
<?php echo HTMLHelper::_('date', $this->lastBackup->backupend, Text::_('DATE_FORMAT_LC4')); ?>
|
||||||
<?php echo HTMLHelper::_('date', $this->lastBackup->backupend, Text::_('DATE_FORMAT_LC4')); ?>
|
|
||||||
</p>
|
|
||||||
<small class="text-muted">
|
|
||||||
<?php echo $this->escape($this->lastBackup->profile_title); ?>
|
|
||||||
—
|
|
||||||
<?php echo HTMLHelper::_('number.bytes', $this->lastBackup->total_size); ?>
|
|
||||||
</small>
|
|
||||||
<?php else : ?>
|
|
||||||
<p class="card-text text-warning"><?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_NO_BACKUPS'); ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3 mb-3">
|
|
||||||
<a href="<?php echo Route::_('index.php?option=com_scheduler&view=tasks'); ?>" class="text-decoration-none">
|
|
||||||
<div class="card h-100" style="cursor:pointer;" onmouseover="this.classList.add('shadow')" onmouseout="this.classList.remove('shadow')">
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<span class="icon-calendar fs-1 text-info" aria-hidden="true"></span>
|
|
||||||
<h5 class="card-title mt-2"><?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_NEXT_SCHEDULED'); ?></h5>
|
|
||||||
<?php if ($this->nextScheduled) : ?>
|
|
||||||
<p class="card-text fw-bold">
|
|
||||||
<?php echo HTMLHelper::_('date', $this->nextScheduled->next_execution, Text::_('DATE_FORMAT_LC4')); ?>
|
|
||||||
</p>
|
|
||||||
<small class="text-muted"><?php echo $this->escape($this->nextScheduled->title); ?></small>
|
|
||||||
<?php else : ?>
|
|
||||||
<p class="card-text text-muted"><?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_NO_SCHEDULED'); ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3 mb-3">
|
|
||||||
<a href="<?php echo Route::_('index.php?option=com_mokobackup&view=backups'); ?>" class="text-decoration-none">
|
|
||||||
<div class="card h-100" style="cursor:pointer;" onmouseover="this.classList.add('shadow')" onmouseout="this.classList.remove('shadow')">
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<span class="icon-copy fs-1 text-secondary" aria-hidden="true"></span>
|
|
||||||
<h5 class="card-title mt-2"><?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_TOTAL_BACKUPS'); ?></h5>
|
|
||||||
<p class="card-text fw-bold fs-3"><?php echo (int) $this->stats->total_count; ?></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3 mb-3">
|
|
||||||
<a href="<?php echo Route::_('index.php?option=com_mokobackup&view=backups'); ?>" class="text-decoration-none">
|
|
||||||
<div class="card h-100" style="cursor:pointer;" onmouseover="this.classList.add('shadow')" onmouseout="this.classList.remove('shadow')">
|
|
||||||
<div class="card-body text-center">
|
|
||||||
<span class="icon-folder-open fs-1 text-warning" aria-hidden="true"></span>
|
|
||||||
<h5 class="card-title mt-2"><?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_STORAGE'); ?></h5>
|
|
||||||
<p class="card-text fw-bold fs-3">
|
|
||||||
<?php echo HTMLHelper::_('number.bytes', (int) $this->stats->total_size); ?>
|
|
||||||
</p>
|
</p>
|
||||||
<?php if ($this->stats->fail_count_7d > 0) : ?>
|
<small class="text-muted">
|
||||||
<span class="badge bg-danger">
|
<?php echo $this->escape($this->lastBackup->profile_title); ?>
|
||||||
<?php echo Text::sprintf('COM_MOKOBACKUP_DASHBOARD_FAILURES_7D', $this->stats->fail_count_7d); ?>
|
—
|
||||||
</span>
|
<?php echo HTMLHelper::_('number.bytes', $this->lastBackup->total_size); ?>
|
||||||
<?php endif; ?>
|
</small>
|
||||||
</div>
|
<?php else : ?>
|
||||||
|
<p class="card-text text-warning"><?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_NO_BACKUPS'); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="card h-100 mb-tile" role="link" data-href="<?php echo Route::_('index.php?option=com_scheduler&view=tasks'); ?>">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<span class="icon-calendar fs-1 text-info" aria-hidden="true"></span>
|
||||||
|
<h5 class="card-title mt-2"><?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_NEXT_SCHEDULED'); ?></h5>
|
||||||
|
<?php if ($this->nextScheduled) : ?>
|
||||||
|
<p class="card-text fw-bold">
|
||||||
|
<?php echo HTMLHelper::_('date', $this->nextScheduled->next_execution, Text::_('DATE_FORMAT_LC4')); ?>
|
||||||
|
</p>
|
||||||
|
<small class="text-muted"><?php echo $this->escape($this->nextScheduled->title); ?></small>
|
||||||
|
<?php else : ?>
|
||||||
|
<p class="card-text text-muted"><?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_NO_SCHEDULED'); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="card h-100 mb-tile" role="link" data-href="<?php echo Route::_('index.php?option=com_mokobackup&view=backups'); ?>">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<span class="icon-copy fs-1 text-secondary" aria-hidden="true"></span>
|
||||||
|
<h5 class="card-title mt-2"><?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_TOTAL_BACKUPS'); ?></h5>
|
||||||
|
<p class="card-text fw-bold fs-3"><?php echo (int) $this->stats->total_count; ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<div class="card h-100 mb-tile" role="link" data-href="<?php echo Route::_('index.php?option=com_mokobackup&view=backups'); ?>">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<span class="icon-folder-open fs-1 text-warning" aria-hidden="true"></span>
|
||||||
|
<h5 class="card-title mt-2"><?php echo Text::_('COM_MOKOBACKUP_DASHBOARD_STORAGE'); ?></h5>
|
||||||
|
<p class="card-text fw-bold fs-3">
|
||||||
|
<?php echo HTMLHelper::_('number.bytes', (int) $this->stats->total_size); ?>
|
||||||
|
</p>
|
||||||
|
<?php if ($this->stats->fail_count_7d > 0) : ?>
|
||||||
|
<span class="badge bg-danger">
|
||||||
|
<?php echo Text::sprintf('COM_MOKOBACKUP_DASHBOARD_FAILURES_7D', $this->stats->fail_count_7d); ?>
|
||||||
|
</span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.mb-tile { cursor: pointer; transition: box-shadow 0.2s, transform 0.1s; }
|
||||||
|
.mb-tile:hover { box-shadow: 0 .5rem 1rem rgba(0,0,0,.15); transform: translateY(-2px); }
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
document.querySelectorAll('.mb-tile').forEach(function(tile) {
|
||||||
|
tile.addEventListener('click', function() { window.location.href = this.dataset.href; });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- Row 2: Quick Actions -->
|
<!-- Row 2: Quick Actions -->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
|||||||
Reference in New Issue
Block a user