Release 0.249: banner edit fixes and thumbnail popup
This commit is contained in:
@@ -801,4 +801,4 @@ echo $grid -> draw();
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script>CKEDITOR.dtd.$removeEmpty['span'] = false;</script>
|
<script>CKEDITOR.dtd.$removeEmpty['span'] = false;</script>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ ob_start();
|
|||||||
'id' => 'src_' . $lg['id'],
|
'id' => 'src_' . $lg['id'],
|
||||||
'value' => $this -> banner['languages'][ $lg['id'] ]['src'],
|
'value' => $this -> banner['languages'][ $lg['id'] ]['src'],
|
||||||
'icon_content' => 'przeglądaj',
|
'icon_content' => 'przeglądaj',
|
||||||
'icon_js' => "window.open ( 'http://" . $_SERVER['SERVER_NAME'] . "/libraries/filemanager-9.14.2/dialog.php?type=1&popup=1&field_id=src_" . $lg['id'] . "&akey=" . $rfmAkeyJS . "', 'mywindow', 'location=1,status=1,scrollbars=1, width=1100,height=700');"
|
'icon_js' => "window.open ( '/libraries/filemanager-9.14.2/dialog.php?type=1&popup=1&field_id=src_" . $lg['id'] . "&akey=" . $rfmAkeyJS . "', 'mywindow', 'location=1,status=1,scrollbars=1, width=1100,height=700');"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
?>
|
?>
|
||||||
@@ -182,4 +182,4 @@ echo $grid -> draw();
|
|||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<script>CKEDITOR.dtd.$removeEmpty['span'] = false;</script>
|
<script>CKEDITOR.dtd.$removeEmpty['span'] = false;</script>
|
||||||
|
|||||||
@@ -1,5 +1,105 @@
|
|||||||
<?= \Tpl::view('components/table-list', ['list' => $this->viewModel]); ?>
|
<?= \Tpl::view('components/table-list', ['list' => $this->viewModel]); ?>
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
.banner-thumb-wrap {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-thumb-image {
|
||||||
|
width: 72px;
|
||||||
|
height: 42px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-thumb-popup {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: min(70vw, 760px);
|
||||||
|
max-height: 80vh;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 14px 30px rgba(0, 0, 0, .35);
|
||||||
|
z-index: 3000;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity .1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-thumb-popup.is-visible {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-thumb-popup img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
max-height: calc(80vh - 12px);
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
(function($) {
|
||||||
|
if (!$) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var $popup = $('<div class="banner-thumb-popup" aria-hidden="true"><img src="" alt=""></div>');
|
||||||
|
var $popupImage = $popup.find('img');
|
||||||
|
$('body').append($popup);
|
||||||
|
|
||||||
|
function positionPopup(event) {
|
||||||
|
var offset = 18;
|
||||||
|
var viewportWidth = $(window).width();
|
||||||
|
var viewportHeight = $(window).height();
|
||||||
|
var popupWidth = $popup.outerWidth();
|
||||||
|
var popupHeight = $popup.outerHeight();
|
||||||
|
var left = (event.clientX || 0) + offset;
|
||||||
|
var top = (event.clientY || 0) + offset;
|
||||||
|
|
||||||
|
if (left + popupWidth + 12 > viewportWidth) {
|
||||||
|
left = Math.max(12, (event.clientX || 0) - popupWidth - offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (top + popupHeight + 12 > viewportHeight) {
|
||||||
|
top = Math.max(12, viewportHeight - popupHeight - 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
$popup.css({ left: left + 'px', top: top + 'px' });
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).off('.bannerThumbPopup');
|
||||||
|
|
||||||
|
$(document).on('mouseenter.bannerThumbPopup', '.js-banner-thumb-preview', function(event) {
|
||||||
|
var src = $(this).data('previewSrc');
|
||||||
|
if (!src) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$popupImage.attr('src', String(src));
|
||||||
|
$popup.addClass('is-visible');
|
||||||
|
positionPopup(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('mousemove.bannerThumbPopup', '.js-banner-thumb-preview', function(event) {
|
||||||
|
if ($popup.hasClass('is-visible')) {
|
||||||
|
positionPopup(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('mouseleave.bannerThumbPopup', '.js-banner-thumb-preview', function() {
|
||||||
|
$popup.removeClass('is-visible');
|
||||||
|
$popupImage.attr('src', '');
|
||||||
|
});
|
||||||
|
})(window.jQuery);
|
||||||
|
</script>
|
||||||
|
|
||||||
<?php if (!empty($this->viewModel->customScriptView)): ?>
|
<?php if (!empty($this->viewModel->customScriptView)): ?>
|
||||||
<?= \Tpl::view($this->viewModel->customScriptView, ['list' => $this->viewModel]); ?>
|
<?= \Tpl::view($this->viewModel->customScriptView, ['list' => $this->viewModel]); ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|||||||
278
admin/templates/components/form-edit.php
Normal file
278
admin/templates/components/form-edit.php
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Uniwersalny szablon formularza edycji
|
||||||
|
*
|
||||||
|
* @var FormEditViewModel $form
|
||||||
|
*/
|
||||||
|
use admin\Support\Forms\FormFieldRenderer;
|
||||||
|
use admin\ViewModels\Forms\FormFieldType;
|
||||||
|
|
||||||
|
$form = $this->form;
|
||||||
|
$renderer = new FormFieldRenderer($form);
|
||||||
|
|
||||||
|
// Przygotuj filemanager key
|
||||||
|
\S::set_session('admin', true);
|
||||||
|
if (
|
||||||
|
empty($_SESSION['rfm_akey']) ||
|
||||||
|
(($_SESSION['rfm_akey_expires'] ?? 0) < time())
|
||||||
|
) {
|
||||||
|
$_SESSION['rfm_akey'] = bin2hex(random_bytes(16));
|
||||||
|
}
|
||||||
|
$_SESSION['rfm_akey_expires'] = time() + 20 * 60;
|
||||||
|
$_SESSION['can_use_rfm'] = true;
|
||||||
|
?>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="/libraries/ckeditor/ckeditor.js"></script>
|
||||||
|
<script type="text/javascript" src="/libraries/ckeditor/adapters/jquery.js"></script>
|
||||||
|
|
||||||
|
<!-- iCheck -->
|
||||||
|
<link rel="stylesheet" type="text/css" href="/libraries/grid/plugins/icheck/skins/minimal/minimal.css" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="/libraries/grid/plugins/icheck/skins/minimal/blue.css" />
|
||||||
|
<script type="text/javascript" src="/libraries/grid/plugins/icheck/icheck.min.js"></script>
|
||||||
|
<style>
|
||||||
|
.icheckbox_minimal-blue { margin-top: 10px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-xs-12">
|
||||||
|
<div class="g-container" data="table:<?= $form->formId ?>">
|
||||||
|
<div class="panel panel-info panel-border top">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<span class="panel-title"><?= htmlspecialchars($form->title) ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="panel-heading p10 pl15" id="g-menu" style="height: auto;">
|
||||||
|
<?php foreach ($form->actions as $action): ?>
|
||||||
|
<?php if ($action->name === 'save'): ?>
|
||||||
|
<?php if ($form->persist): ?>
|
||||||
|
<a href="#" id="g-edit-save-close" class="btn btn-system btn-sm"
|
||||||
|
persist_edit="0"
|
||||||
|
back_url="<?= htmlspecialchars($action->backUrl ?? '') ?>"
|
||||||
|
url="<?= htmlspecialchars($action->url) ?>">
|
||||||
|
<i class="fa fa-check-circle mr5"></i>Zatwierdź i zamknij
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<a href="#" id="g-edit-save" class="btn btn-success btn-sm"
|
||||||
|
persist_edit="<?= $form->persist ? '1' : '0' ?>"
|
||||||
|
back_url="<?= htmlspecialchars($action->backUrl ?? '') ?>"
|
||||||
|
url="<?= htmlspecialchars($action->url) ?>">
|
||||||
|
<i class="fa fa-check-circle mr5"></i>Zatwierdź
|
||||||
|
</a>
|
||||||
|
<?php elseif ($action->name === 'cancel'): ?>
|
||||||
|
<a href="<?= htmlspecialchars($action->url) ?>" class="btn btn-dark btn-sm" id="g-edit-cancel">
|
||||||
|
<i class="fa fa-reply mr5"></i>Wstecz
|
||||||
|
</a>
|
||||||
|
<?php else: ?>
|
||||||
|
<a href="<?= htmlspecialchars($action->url) ?>" class="btn <?= htmlspecialchars($action->cssClass) ?> btn-sm">
|
||||||
|
<?= htmlspecialchars($action->label) ?>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel-body">
|
||||||
|
<form method="<?= $form->method ?>" id="fg-<?= $form->formId ?>" class="g-form form-horizontal"
|
||||||
|
action="<?= htmlspecialchars($form->action) ?>" enctype="multipart/form-data">
|
||||||
|
|
||||||
|
<input type="hidden" name="_form_id" value="<?= htmlspecialchars($form->formId) ?>">
|
||||||
|
|
||||||
|
<?php foreach ($form->hiddenFields as $name => $value): ?>
|
||||||
|
<input type="hidden" name="<?= htmlspecialchars($name) ?>" value="<?= htmlspecialchars($value ?? '') ?>">
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<?php if ($form->hasTabs()): ?>
|
||||||
|
<!-- Formularz z zakładkami -->
|
||||||
|
<div id="form-tabs-<?= $form->formId ?>">
|
||||||
|
<ul class="resp-tabs-list form-tabs">
|
||||||
|
<?php foreach ($form->tabs as $tab): ?>
|
||||||
|
<li><i class="fa <?= htmlspecialchars($tab->icon) ?>"></i><?= htmlspecialchars($tab->label) ?></li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ul>
|
||||||
|
<div class="resp-tabs-container form-tabs">
|
||||||
|
<?php foreach ($form->tabs as $tab): ?>
|
||||||
|
<div>
|
||||||
|
<?php
|
||||||
|
$tabFields = $form->getFieldsForTab($tab->id);
|
||||||
|
$langSections = $form->getLangSectionsForTab($tab->id);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php foreach ($tabFields as $field): ?>
|
||||||
|
<?= $renderer->renderField($field) ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<?php foreach ($langSections as $section): ?>
|
||||||
|
<?= $renderer->renderLangSection($section) ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php else: ?>
|
||||||
|
<!-- Formularz bez zakładek -->
|
||||||
|
<?php foreach ($form->fields as $field): ?>
|
||||||
|
<?php if ($field->type === FormFieldType::LANG_SECTION): ?>
|
||||||
|
<?= $renderer->renderLangSection($field) ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<?= $renderer->renderField($field) ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function() {
|
||||||
|
// Inicjalizacja datepickerów
|
||||||
|
$('input.date').datetimepicker({
|
||||||
|
format: "YYYY-MM-DD",
|
||||||
|
pickTime: false
|
||||||
|
});
|
||||||
|
|
||||||
|
$('input.datetime').datetimepicker({
|
||||||
|
format: "YYYY-MM-DD HH:mm"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inicjalizacja zakładek
|
||||||
|
<?php if ($form->hasTabs()): ?>
|
||||||
|
$('#form-tabs-<?= $form->formId ?>').easyResponsiveTabs({
|
||||||
|
width: 'auto',
|
||||||
|
fit: true,
|
||||||
|
tabidentify: 'form-tabs',
|
||||||
|
type: 'vertical'
|
||||||
|
});
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
// Inicjalizacja iCheck
|
||||||
|
$('.icheck').iCheck({
|
||||||
|
checkboxClass: 'icheckbox_minimal-blue',
|
||||||
|
radioClass: 'iradio_minimal-blue'
|
||||||
|
});
|
||||||
|
|
||||||
|
function showFormMessage(type, text) {
|
||||||
|
var safeText = $('<div/>').text(text || '').html();
|
||||||
|
var alertClass = type === 'error' ? 'alert-danger' : 'alert-primary';
|
||||||
|
var html = '' +
|
||||||
|
'<div class="row js-form-message">' +
|
||||||
|
'<div class="col col-xs-12">' +
|
||||||
|
'<div class="alert ' + alertClass + ' alert-dismissable">' +
|
||||||
|
'<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>' +
|
||||||
|
'<i class="fa fa-info pr10"></i>' + safeText +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
$('#content .js-form-message').remove();
|
||||||
|
$('#content').prepend(html);
|
||||||
|
}
|
||||||
|
// Obsługa przycisków zapisu
|
||||||
|
$('#g-edit-save, #g-edit-save-close').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
var $btn = $(this);
|
||||||
|
var url = $btn.attr('url');
|
||||||
|
var backUrl = $btn.attr('back_url');
|
||||||
|
var persist = $btn.attr('persist_edit');
|
||||||
|
var formId = 'fg-<?= $form->formId ?>';
|
||||||
|
|
||||||
|
// Synchronizuj zawartosc CKEditor do textarea przed serializacja
|
||||||
|
if (typeof CKEDITOR !== 'undefined' && CKEDITOR.instances) {
|
||||||
|
for (var instanceName in CKEDITOR.instances) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(CKEDITOR.instances, instanceName)) {
|
||||||
|
CKEDITOR.instances[instanceName].updateElement();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zbierz dane formularza
|
||||||
|
var formData = $('#' + formId).serialize();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: url,
|
||||||
|
type: 'POST',
|
||||||
|
data: formData,
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
var successMessage = response.message || 'Zmiany zostały zapisane.';
|
||||||
|
if (backUrl && persist === '0') {
|
||||||
|
window.location.href = backUrl;
|
||||||
|
} else {
|
||||||
|
showFormMessage('success', successMessage);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var errors = response.errors ? Object.values(response.errors).join(', ') : 'Błąd walidacji';
|
||||||
|
showFormMessage('error', 'Błąd: ' + errors);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function(xhr, status, error) {
|
||||||
|
// Fallback - tradycyjne submit formularza
|
||||||
|
$('#' + formId).attr('action', url).submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
disable_menu();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Renderowanie CKEditor dla pól edytora (zwykłych)
|
||||||
|
foreach ($form->fields as $field):
|
||||||
|
if ($field->type === FormFieldType::EDITOR):
|
||||||
|
?>
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function() {
|
||||||
|
$('#<?= $field->id ?>').ckeditor({
|
||||||
|
toolbar: '<?= $field->editorToolbar ?>',
|
||||||
|
height: '<?= $field->editorHeight ?>'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
endif;
|
||||||
|
endforeach;
|
||||||
|
?>
|
||||||
|
|
||||||
|
<?php if ($form->hasLangSections()): ?>
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function() {
|
||||||
|
// Inicjalizacja zakładek językowych
|
||||||
|
$('[id^="languages-"].languages-tabs').each(function() {
|
||||||
|
$(this).easyResponsiveTabs({
|
||||||
|
width: 'auto',
|
||||||
|
fit: true,
|
||||||
|
tabidentify: 'languages-tabs',
|
||||||
|
type: 'horizontal'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Inicjalizacja CKEditor dla pól w sekcjach językowych
|
||||||
|
<?php
|
||||||
|
foreach ($form->fields as $section):
|
||||||
|
if ($section->type === FormFieldType::LANG_SECTION && $section->langFields):
|
||||||
|
foreach ($section->langFields as $langField):
|
||||||
|
if ($langField->type === FormFieldType::EDITOR && $form->languages):
|
||||||
|
foreach ($form->languages as $lang):
|
||||||
|
if ($lang['status']):
|
||||||
|
?>
|
||||||
|
$('#<?= $langField->getLocalizedId($lang['id']) ?>').ckeditor({
|
||||||
|
toolbar: '<?= $langField->editorToolbar ?>',
|
||||||
|
height: '<?= $langField->editorHeight ?>'
|
||||||
|
});
|
||||||
|
<?php
|
||||||
|
endif;
|
||||||
|
endforeach;
|
||||||
|
endif;
|
||||||
|
endforeach;
|
||||||
|
endif;
|
||||||
|
endforeach;
|
||||||
|
?>
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<script>CKEDITOR.dtd.$removeEmpty['span'] = false;</script>
|
||||||
|
|
||||||
|
|
||||||
@@ -1 +1 @@
|
|||||||
<iframe src="/libraries/filemanager-9.14.2/dialog.php?akey=c3cb2537d25c0efc9e573d059d79c3b8" style="border: 0px; width: 100%; height: 800px; background: #FFF; padding: 5px;"></iframe>
|
<iframe src="/libraries/filemanager-9.14.2/dialog.php?akey=c3cb2537d25c0efc9e573d059d79c3b8" style="border: 0px; width: 100%; height: 800px; background: #FFF; padding: 5px;"></iframe>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
$out .= 'id="' . $this -> params['id'] . '" ';
|
$out .= 'id="' . $this -> params['id'] . '" ';
|
||||||
else
|
else
|
||||||
$out .= 'id="' . $this -> params['name'] . '" ';
|
$out .= 'id="' . $this -> params['name'] . '" ';
|
||||||
$out .= 'name="' . $this -> params['name'] . '" type="checkbox"';
|
$out .= 'name="' . $this -> params['name'] . '" type="checkbox" value="1"';
|
||||||
|
|
||||||
if ( $this -> params['checked'] )
|
if ( $this -> params['checked'] )
|
||||||
$out .= 'checked="checked" ';
|
$out .= 'checked="checked" ';
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ ob_start();
|
|||||||
'id' => 'img',
|
'id' => 'img',
|
||||||
'value' => $this -> producer['img'],
|
'value' => $this -> producer['img'],
|
||||||
'icon_content' => 'przeglądaj',
|
'icon_content' => 'przeglądaj',
|
||||||
'icon_js' => "window.open ( 'http://" . $_SERVER['SERVER_NAME'] . "/libraries/filemanager-9.14.2/dialog.php?type=1&popup=1&field_id=img&akey=" . $rfmAkeyJS . "', 'mywindow', 'location=1,status=1,scrollbars=1, width=1100,height=700');"
|
'icon_js' => "window.open ( '/libraries/filemanager-9.14.2/dialog.php?type=1&popup=1&field_id=img&akey=" . $rfmAkeyJS . "', 'mywindow', 'location=1,status=1,scrollbars=1, width=1100,height=700');"
|
||||||
] );
|
] );
|
||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
@@ -177,4 +177,4 @@ echo $grid -> draw();
|
|||||||
tabidentify: 'languages-main'
|
tabidentify: 'languages-main'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1343,4 +1343,4 @@ echo $grid->draw();
|
|||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
CKEDITOR.dtd.$removeEmpty['span'] = false;
|
CKEDITOR.dtd.$removeEmpty['span'] = false;
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -29,7 +29,10 @@ class BannerRepository
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$results = $this->db->select('pp_banners_langs', '*', ['id_banner' => $bannerId]);
|
$results = $this->db->select('pp_banners_langs', '*', [
|
||||||
|
'id_banner' => $bannerId,
|
||||||
|
'ORDER' => ['id' => 'ASC'],
|
||||||
|
]);
|
||||||
if (is_array($results)) {
|
if (is_array($results)) {
|
||||||
foreach ($results as $row) {
|
foreach ($results as $row) {
|
||||||
$banner['languages'][$row['id_lang']] = $row;
|
$banner['languages'][$row['id_lang']] = $row;
|
||||||
@@ -54,19 +57,30 @@ class BannerRepository
|
|||||||
/**
|
/**
|
||||||
* Zapisuje baner (insert lub update)
|
* Zapisuje baner (insert lub update)
|
||||||
*
|
*
|
||||||
* @param array $data Dane banera
|
* @param array $data Dane banera (obsługuje format z FormRequestHandler lub stary format)
|
||||||
* @return int|false ID banera lub false
|
* @return int|false ID banera lub false
|
||||||
*/
|
*/
|
||||||
public function save(array $data)
|
public function save(array $data)
|
||||||
{
|
{
|
||||||
$bannerId = $data['id'] ?? null;
|
$bannerId = $data['id'] ?? null;
|
||||||
|
|
||||||
|
// Obsługa obu formatów: nowy (int) i stary ('on'/string)
|
||||||
|
$status = $data['status'] ?? 0;
|
||||||
|
if ($status === 'on') {
|
||||||
|
$status = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$homePage = $data['home_page'] ?? 0;
|
||||||
|
if ($homePage === 'on') {
|
||||||
|
$homePage = 1;
|
||||||
|
}
|
||||||
|
|
||||||
$bannerData = [
|
$bannerData = [
|
||||||
'name' => $data['name'],
|
'name' => $data['name'],
|
||||||
'status' => $data['status'] == 'on' ? 1 : 0,
|
'status' => (int)$status,
|
||||||
'date_start' => $data['date_start'] != '' ? $data['date_start'] : null,
|
'date_start' => !empty($data['date_start']) ? $data['date_start'] : null,
|
||||||
'date_end' => $data['date_end'] != '' ? $data['date_end'] : null,
|
'date_end' => !empty($data['date_end']) ? $data['date_end'] : null,
|
||||||
'home_page' => $data['home_page'] == 'on' ? 1 : 0,
|
'home_page' => (int)$homePage,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!$bannerId) {
|
if (!$bannerId) {
|
||||||
@@ -79,7 +93,14 @@ class BannerRepository
|
|||||||
$this->db->update('pp_banners', $bannerData, ['id' => (int)$bannerId]);
|
$this->db->update('pp_banners', $bannerData, ['id' => (int)$bannerId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->saveTranslations($bannerId, $data['src'], $data['url'], $data['html'], $data['text']);
|
// Obsługa danych językowych - nowy format (translations) lub stary (src/url/html/text)
|
||||||
|
if (isset($data['translations']) && is_array($data['translations'])) {
|
||||||
|
// Nowy format z FormRequestHandler
|
||||||
|
$this->saveTranslationsFromArray($bannerId, $data['translations']);
|
||||||
|
} elseif (isset($data['src']) && is_array($data['src'])) {
|
||||||
|
// Stary format (backward compatibility)
|
||||||
|
$this->saveTranslations($bannerId, $data['src'], $data['url'], $data['html'], $data['text']);
|
||||||
|
}
|
||||||
|
|
||||||
return (int)$bannerId;
|
return (int)$bannerId;
|
||||||
}
|
}
|
||||||
@@ -159,35 +180,134 @@ class BannerRepository
|
|||||||
|
|
||||||
$stmt = $this->db->query($sql, $params);
|
$stmt = $this->db->query($sql, $params);
|
||||||
$items = $stmt ? $stmt->fetchAll() : [];
|
$items = $stmt ? $stmt->fetchAll() : [];
|
||||||
|
$items = is_array($items) ? $items : [];
|
||||||
|
|
||||||
|
if (!empty($items)) {
|
||||||
|
$bannerIds = array_map('intval', array_column($items, 'id'));
|
||||||
|
$thumbByBannerId = $this->fetchThumbnailsByBannerIds($bannerIds);
|
||||||
|
|
||||||
|
foreach ($items as &$item) {
|
||||||
|
$item['thumbnail_src'] = $thumbByBannerId[(int)($item['id'] ?? 0)] ?? '';
|
||||||
|
}
|
||||||
|
unset($item);
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'items' => is_array($items) ? $items : [],
|
'items' => $items,
|
||||||
'total' => $total,
|
'total' => $total,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Zapisuje tłumaczenia banera
|
* Pobiera pierwsza dostepna sciezke obrazka (src) dla kazdego banera.
|
||||||
|
*
|
||||||
|
* @param array<int, int> $bannerIds
|
||||||
|
* @return array<int, string> [id_banner => src]
|
||||||
|
*/
|
||||||
|
private function fetchThumbnailsByBannerIds(array $bannerIds): array
|
||||||
|
{
|
||||||
|
$bannerIds = array_values(array_unique(array_filter($bannerIds, static function ($id): bool {
|
||||||
|
return (int)$id > 0;
|
||||||
|
})));
|
||||||
|
|
||||||
|
if (empty($bannerIds)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$in = [];
|
||||||
|
$params = [];
|
||||||
|
foreach ($bannerIds as $index => $bannerId) {
|
||||||
|
$placeholder = ':id' . $index;
|
||||||
|
$in[] = $placeholder;
|
||||||
|
$params[$placeholder] = (int)$bannerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = '
|
||||||
|
SELECT id_banner, src
|
||||||
|
FROM pp_banners_langs
|
||||||
|
WHERE id_banner IN (' . implode(', ', $in) . ')
|
||||||
|
AND src IS NOT NULL
|
||||||
|
AND src <> \'\'
|
||||||
|
ORDER BY id_lang ASC, id ASC
|
||||||
|
';
|
||||||
|
|
||||||
|
$stmt = $this->db->query($sql, $params);
|
||||||
|
$rows = $stmt ? $stmt->fetchAll() : [];
|
||||||
|
if (!is_array($rows)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$thumbByBannerId = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$bannerId = (int)($row['id_banner'] ?? 0);
|
||||||
|
if ($bannerId <= 0 || isset($thumbByBannerId[$bannerId])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$src = trim((string)($row['src'] ?? ''));
|
||||||
|
if ($src !== '') {
|
||||||
|
$thumbByBannerId[$bannerId] = $src;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $thumbByBannerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zapisuje tłumaczenia banera (stary format - zachowano dla kompatybilności)
|
||||||
*/
|
*/
|
||||||
private function saveTranslations(int $bannerId, array $src, array $url, array $html, array $text): void
|
private function saveTranslations(int $bannerId, array $src, array $url, array $html, array $text): void
|
||||||
{
|
{
|
||||||
foreach ($src as $langId => $val) {
|
foreach ($src as $langId => $val) {
|
||||||
$translationData = [
|
$this->upsertTranslation($bannerId, $langId, [
|
||||||
'id_banner' => $bannerId,
|
'src' => $src[$langId] ?? '',
|
||||||
'id_lang' => $langId,
|
'url' => $url[$langId] ?? '',
|
||||||
'src' => $src[$langId],
|
'html' => $html[$langId] ?? '',
|
||||||
'url' => $url[$langId],
|
'text' => $text[$langId] ?? '',
|
||||||
'html' => $html[$langId],
|
]);
|
||||||
'text' => $text[$langId],
|
|
||||||
];
|
|
||||||
|
|
||||||
$existingId = $this->db->get('pp_banners_langs', 'id', ['AND' => ['banner_id' => $bannerId, 'lang_id' => $langId]]);
|
|
||||||
|
|
||||||
if ($existingId) {
|
|
||||||
$this->db->update('pp_banners_langs', $translationData, ['id' => $existingId]);
|
|
||||||
} else {
|
|
||||||
$this->db->insert('pp_banners_langs', $translationData);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zapisuje tłumaczenia banera z nowego formatu (z FormRequestHandler)
|
||||||
|
* Format: [lang_id => [field => value]]
|
||||||
|
*/
|
||||||
|
private function saveTranslationsFromArray(int $bannerId, array $translations): void
|
||||||
|
{
|
||||||
|
foreach ($translations as $langId => $fields) {
|
||||||
|
$this->upsertTranslation($bannerId, $langId, [
|
||||||
|
'src' => $fields['src'] ?? '',
|
||||||
|
'url' => $fields['url'] ?? '',
|
||||||
|
'html' => $fields['html'] ?? '',
|
||||||
|
'text' => $fields['text'] ?? '',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upsert tlumaczenia banera.
|
||||||
|
* Aktualizuje wszystkie rekordy dla pary id_banner + id_lang,
|
||||||
|
* co usuwa problem z historycznymi duplikatami.
|
||||||
|
*/
|
||||||
|
private function upsertTranslation(int $bannerId, $langId, array $fields): void
|
||||||
|
{
|
||||||
|
$where = ['AND' => ['id_banner' => $bannerId, 'id_lang' => $langId]];
|
||||||
|
$translationData = [
|
||||||
|
'id_banner' => $bannerId,
|
||||||
|
'id_lang' => $langId,
|
||||||
|
'src' => $fields['src'] ?? '',
|
||||||
|
'url' => $fields['url'] ?? '',
|
||||||
|
'html' => $fields['html'] ?? '',
|
||||||
|
'text' => $fields['text'] ?? '',
|
||||||
|
];
|
||||||
|
|
||||||
|
$hasExisting = (int)$this->db->count('pp_banners_langs', $where) > 0;
|
||||||
|
|
||||||
|
if ($hasExisting) {
|
||||||
|
$this->db->update('pp_banners_langs', $translationData, $where);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->db->insert('pp_banners_langs', $translationData);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,21 @@
|
|||||||
namespace admin\Controllers;
|
namespace admin\Controllers;
|
||||||
|
|
||||||
use Domain\Banner\BannerRepository;
|
use Domain\Banner\BannerRepository;
|
||||||
|
use admin\ViewModels\Forms\FormEditViewModel;
|
||||||
|
use admin\ViewModels\Forms\FormField;
|
||||||
|
use admin\ViewModels\Forms\FormTab;
|
||||||
|
use admin\ViewModels\Forms\FormAction;
|
||||||
|
use admin\Support\Forms\FormRequestHandler;
|
||||||
|
|
||||||
class BannerController
|
class BannerController
|
||||||
{
|
{
|
||||||
private BannerRepository $repository;
|
private BannerRepository $repository;
|
||||||
|
private FormRequestHandler $formHandler;
|
||||||
|
|
||||||
public function __construct(BannerRepository $repository)
|
public function __construct(BannerRepository $repository)
|
||||||
{
|
{
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
|
$this->formHandler = new FormRequestHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,9 +71,24 @@ class BannerController
|
|||||||
$name = (string)($item['name'] ?? '');
|
$name = (string)($item['name'] ?? '');
|
||||||
$homePage = (int)($item['home_page'] ?? 0);
|
$homePage = (int)($item['home_page'] ?? 0);
|
||||||
$isActive = (int)($item['status'] ?? 0) === 1;
|
$isActive = (int)($item['status'] ?? 0) === 1;
|
||||||
|
$thumbnailSrc = trim((string)($item['thumbnail_src'] ?? ''));
|
||||||
|
if ($thumbnailSrc !== '' && !preg_match('#^(https?:)?//#i', $thumbnailSrc) && strpos($thumbnailSrc, '/') !== 0) {
|
||||||
|
$thumbnailSrc = '/' . ltrim($thumbnailSrc, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
$thumbnail = '<span class="text-muted">-</span>';
|
||||||
|
if ($thumbnailSrc !== '') {
|
||||||
|
$thumbnail = '<div class="banner-thumb-wrap">'
|
||||||
|
. '<img src="' . htmlspecialchars($thumbnailSrc, ENT_QUOTES, 'UTF-8') . '" alt="" '
|
||||||
|
. 'data-preview-src="' . htmlspecialchars($thumbnailSrc, ENT_QUOTES, 'UTF-8') . '" '
|
||||||
|
. 'class="banner-thumb-image js-banner-thumb-preview" '
|
||||||
|
. 'loading="lazy">'
|
||||||
|
. '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
$rows[] = [
|
$rows[] = [
|
||||||
'lp' => $lp++ . '.',
|
'lp' => $lp++ . '.',
|
||||||
|
'thumbnail' => $thumbnail,
|
||||||
'name' => '<a href="/admin/banners/banner_edit/id=' . $id . '">' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '</a>',
|
'name' => '<a href="/admin/banners/banner_edit/id=' . $id . '">' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '</a>',
|
||||||
'status' => $isActive ? 'tak' : '<span style="color: #FF0000;">nie</span>',
|
'status' => $isActive ? 'tak' : '<span style="color: #FF0000;">nie</span>',
|
||||||
'home_page' => $homePage === 1 ? '<span class="text-system">tak</span>' : 'nie',
|
'home_page' => $homePage === 1 ? '<span class="text-system">tak</span>' : 'nie',
|
||||||
@@ -95,6 +117,7 @@ class BannerController
|
|||||||
$viewModel = new \admin\ViewModels\Common\PaginatedTableViewModel(
|
$viewModel = new \admin\ViewModels\Common\PaginatedTableViewModel(
|
||||||
[
|
[
|
||||||
['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false],
|
['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false],
|
||||||
|
['key' => 'thumbnail', 'label' => 'Miniatura', 'class' => 'text-center', 'sortable' => false, 'raw' => true],
|
||||||
['key' => 'name', 'sort_key' => 'name', 'label' => 'Nazwa', 'sortable' => true, 'raw' => true],
|
['key' => 'name', 'sort_key' => 'name', 'label' => 'Nazwa', 'sortable' => true, 'raw' => true],
|
||||||
['key' => 'status', 'sort_key' => 'status', 'label' => 'Aktywny', 'class' => 'text-center', 'sortable' => true, 'raw' => true],
|
['key' => 'status', 'sort_key' => 'status', 'label' => 'Aktywny', 'class' => 'text-center', 'sortable' => true, 'raw' => true],
|
||||||
['key' => 'home_page', 'sort_key' => 'home_page', 'label' => 'Strona glowna', 'class' => 'text-center', 'sortable' => true, 'raw' => true],
|
['key' => 'home_page', 'sort_key' => 'home_page', 'label' => 'Strona glowna', 'class' => 'text-center', 'sortable' => true, 'raw' => true],
|
||||||
@@ -141,7 +164,15 @@ class BannerController
|
|||||||
$banner = $this->repository->find($bannerId);
|
$banner = $this->repository->find($bannerId);
|
||||||
$languages = \admin\factory\Languages::languages_list();
|
$languages = \admin\factory\Languages::languages_list();
|
||||||
|
|
||||||
return \admin\view\Banners::banner_edit($banner, $languages);
|
// Sprawdź czy są błędy walidacji z poprzedniego requestu
|
||||||
|
$validationErrors = $_SESSION['form_errors'][$this->getFormId()] ?? null;
|
||||||
|
if ($validationErrors) {
|
||||||
|
unset($_SESSION['form_errors'][$this->getFormId()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$viewModel = $this->buildFormViewModel($banner, $languages, $validationErrors);
|
||||||
|
|
||||||
|
return \Tpl::view('components/form-edit', ['form' => $viewModel]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -149,13 +180,40 @@ class BannerController
|
|||||||
*/
|
*/
|
||||||
public function save(): void
|
public function save(): void
|
||||||
{
|
{
|
||||||
$response = ['status' => 'error', 'msg' => 'Podczas zapisywania baneru wystapil blad. Prosze sprobowac ponownie.'];
|
$response = ['success' => false, 'errors' => []];
|
||||||
|
|
||||||
$values = json_decode(\S::get('values'), true);
|
$bannerId = (int)\S::get('id');
|
||||||
$bannerId = $this->repository->save($values);
|
$banner = $this->repository->find($bannerId);
|
||||||
if ($bannerId) {
|
$languages = \admin\factory\Languages::languages_list();
|
||||||
|
|
||||||
|
$viewModel = $this->buildFormViewModel($banner, $languages);
|
||||||
|
|
||||||
|
// Przetwórz dane z POST
|
||||||
|
$result = $this->formHandler->handleSubmit($viewModel, $_POST);
|
||||||
|
|
||||||
|
if (!$result['success']) {
|
||||||
|
// Zapisz błędy w sesji i zwróć jako JSON
|
||||||
|
$_SESSION['form_errors'][$this->getFormId()] = $result['errors'];
|
||||||
|
$response['errors'] = $result['errors'];
|
||||||
|
echo json_encode($response);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zapisz dane
|
||||||
|
$data = $result['data'];
|
||||||
|
$data['id'] = $bannerId ?: null;
|
||||||
|
|
||||||
|
$savedId = $this->repository->save($data);
|
||||||
|
|
||||||
|
if ($savedId) {
|
||||||
\S::delete_dir('../temp/');
|
\S::delete_dir('../temp/');
|
||||||
$response = ['status' => 'ok', 'msg' => 'Baner zostal zapisany.', 'id' => $bannerId];
|
$response = [
|
||||||
|
'success' => true,
|
||||||
|
'id' => $savedId,
|
||||||
|
'message' => 'Baner został zapisany.'
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$response['errors'] = ['general' => 'Błąd podczas zapisywania do bazy.'];
|
||||||
}
|
}
|
||||||
|
|
||||||
echo json_encode($response);
|
echo json_encode($response);
|
||||||
@@ -176,4 +234,103 @@ class BannerController
|
|||||||
header('Location: /admin/banners/view_list/');
|
header('Location: /admin/banners/view_list/');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buduje model widoku formularza
|
||||||
|
*/
|
||||||
|
private function buildFormViewModel(array $banner, array $languages, ?array $errors = null): FormEditViewModel
|
||||||
|
{
|
||||||
|
$bannerId = $banner['id'] ?? 0;
|
||||||
|
$isNew = empty($bannerId);
|
||||||
|
|
||||||
|
// Domyślne wartości dla nowego banera
|
||||||
|
if ($isNew) {
|
||||||
|
$banner['status'] = 1;
|
||||||
|
$banner['home_page'] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tabs = [
|
||||||
|
new FormTab('settings', 'Ustawienia', 'fa-wrench'),
|
||||||
|
new FormTab('content', 'Zawartość', 'fa-file'),
|
||||||
|
];
|
||||||
|
|
||||||
|
$fields = [
|
||||||
|
// Zakładka Ustawienia
|
||||||
|
FormField::text('name', [
|
||||||
|
'label' => 'Nazwa',
|
||||||
|
'tab' => 'settings',
|
||||||
|
'required' => true,
|
||||||
|
]),
|
||||||
|
FormField::switch('status', [
|
||||||
|
'label' => 'Aktywny',
|
||||||
|
'tab' => 'settings',
|
||||||
|
'value' => ($banner['status'] ?? 1) == 1,
|
||||||
|
]),
|
||||||
|
FormField::date('date_start', [
|
||||||
|
'label' => 'Data rozpoczęcia',
|
||||||
|
'tab' => 'settings',
|
||||||
|
]),
|
||||||
|
FormField::date('date_end', [
|
||||||
|
'label' => 'Data zakończenia',
|
||||||
|
'tab' => 'settings',
|
||||||
|
]),
|
||||||
|
FormField::switch('home_page', [
|
||||||
|
'label' => 'Slajder / Strona główna',
|
||||||
|
'tab' => 'settings',
|
||||||
|
'value' => ($banner['home_page'] ?? 0) == 1,
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Sekcja językowa w zakładce Zawartość
|
||||||
|
FormField::langSection('translations', 'content', [
|
||||||
|
FormField::image('src', [
|
||||||
|
'label' => 'Obraz',
|
||||||
|
'filemanager' => true,
|
||||||
|
]),
|
||||||
|
FormField::text('url', [
|
||||||
|
'label' => 'Url',
|
||||||
|
]),
|
||||||
|
FormField::textarea('html', [
|
||||||
|
'label' => 'Kod HTML',
|
||||||
|
'rows' => 6,
|
||||||
|
]),
|
||||||
|
FormField::editor('text', [
|
||||||
|
'label' => 'Treść',
|
||||||
|
'toolbar' => 'MyTool',
|
||||||
|
'height' => 300,
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
$actions = [
|
||||||
|
FormAction::save(
|
||||||
|
'/admin/banners/banner_save/' . ($isNew ? '' : 'id=' . $bannerId),
|
||||||
|
'/admin/banners/view_list/'
|
||||||
|
),
|
||||||
|
FormAction::cancel('/admin/banners/view_list/'),
|
||||||
|
];
|
||||||
|
|
||||||
|
return new FormEditViewModel(
|
||||||
|
$this->getFormId(),
|
||||||
|
$isNew ? 'Nowy baner' : 'Edycja banera',
|
||||||
|
$banner,
|
||||||
|
$fields,
|
||||||
|
$tabs,
|
||||||
|
$actions,
|
||||||
|
'POST',
|
||||||
|
'/admin/banners/banner_save/' . ($isNew ? '' : 'id=' . $bannerId),
|
||||||
|
'/admin/banners/view_list/',
|
||||||
|
true,
|
||||||
|
['id' => $bannerId],
|
||||||
|
$languages,
|
||||||
|
$errors
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zwraca identyfikator formularza
|
||||||
|
*/
|
||||||
|
private function getFormId(): string
|
||||||
|
{
|
||||||
|
return 'banner-edit';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
430
autoload/admin/Support/Forms/FormFieldRenderer.php
Normal file
430
autoload/admin/Support/Forms/FormFieldRenderer.php
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
<?php
|
||||||
|
namespace admin\Support\Forms;
|
||||||
|
|
||||||
|
use admin\ViewModels\Forms\FormEditViewModel;
|
||||||
|
use admin\ViewModels\Forms\FormField;
|
||||||
|
use admin\ViewModels\Forms\FormFieldType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderer pól formularza
|
||||||
|
*/
|
||||||
|
class FormFieldRenderer
|
||||||
|
{
|
||||||
|
private FormEditViewModel $form;
|
||||||
|
|
||||||
|
public function __construct(FormEditViewModel $form)
|
||||||
|
{
|
||||||
|
$this->form = $form;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderuje pojedyncze pole
|
||||||
|
*/
|
||||||
|
public function renderField(FormField $field): string
|
||||||
|
{
|
||||||
|
$method = 'render' . ucfirst($field->type);
|
||||||
|
|
||||||
|
if (method_exists($this, $method)) {
|
||||||
|
return $this->$method($field);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback dla nieznanych typów - renderuj jako text
|
||||||
|
return $this->renderText($field);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderuje pole tekstowe
|
||||||
|
*/
|
||||||
|
public function renderText(FormField $field): string
|
||||||
|
{
|
||||||
|
$value = $this->form->getFieldValue($field);
|
||||||
|
$error = $this->form->getError($field->name);
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'label' => $field->label,
|
||||||
|
'name' => $field->name,
|
||||||
|
'id' => $field->id,
|
||||||
|
'value' => $value ?? '',
|
||||||
|
'type' => 'text',
|
||||||
|
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($field->placeholder) {
|
||||||
|
$params['placeholder'] = $field->placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$params['class'] .= ' error';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->wrapWithError(\Html::input($params), $error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderuje pole number
|
||||||
|
*/
|
||||||
|
public function renderNumber(FormField $field): string
|
||||||
|
{
|
||||||
|
$value = $this->form->getFieldValue($field);
|
||||||
|
$error = $this->form->getError($field->name);
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'label' => $field->label,
|
||||||
|
'name' => $field->name,
|
||||||
|
'id' => $field->id,
|
||||||
|
'value' => $value ?? '',
|
||||||
|
'type' => 'number',
|
||||||
|
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$params['class'] .= ' error';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->wrapWithError(\Html::input($params), $error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderuje pole email
|
||||||
|
*/
|
||||||
|
public function renderEmail(FormField $field): string
|
||||||
|
{
|
||||||
|
$value = $this->form->getFieldValue($field);
|
||||||
|
$error = $this->form->getError($field->name);
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'label' => $field->label,
|
||||||
|
'name' => $field->name,
|
||||||
|
'id' => $field->id,
|
||||||
|
'value' => $value ?? '',
|
||||||
|
'type' => 'email',
|
||||||
|
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$params['class'] .= ' error';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->wrapWithError(\Html::input($params), $error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderuje pole password
|
||||||
|
*/
|
||||||
|
public function renderPassword(FormField $field): string
|
||||||
|
{
|
||||||
|
$value = $this->form->getFieldValue($field);
|
||||||
|
|
||||||
|
return \Html::input([
|
||||||
|
'label' => $field->label,
|
||||||
|
'name' => $field->name,
|
||||||
|
'id' => $field->id,
|
||||||
|
'value' => $value ?? '',
|
||||||
|
'type' => 'password',
|
||||||
|
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderuje pole daty
|
||||||
|
*/
|
||||||
|
public function renderDate(FormField $field): string
|
||||||
|
{
|
||||||
|
$value = $this->form->getFieldValue($field);
|
||||||
|
$error = $this->form->getError($field->name);
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'label' => $field->label,
|
||||||
|
'name' => $field->name,
|
||||||
|
'id' => $field->id,
|
||||||
|
'value' => $value ?? '',
|
||||||
|
'type' => 'text',
|
||||||
|
'class' => 'date ' . ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$params['class'] .= ' error';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->wrapWithError(\Html::input($params), $error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderuje pole daty i czasu
|
||||||
|
*/
|
||||||
|
public function renderDatetime(FormField $field): string
|
||||||
|
{
|
||||||
|
$value = $this->form->getFieldValue($field);
|
||||||
|
|
||||||
|
return \Html::input([
|
||||||
|
'label' => $field->label,
|
||||||
|
'name' => $field->name,
|
||||||
|
'id' => $field->id,
|
||||||
|
'value' => $value ?? '',
|
||||||
|
'type' => 'text',
|
||||||
|
'class' => 'datetime ' . ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderuje przełącznik (switch)
|
||||||
|
*/
|
||||||
|
public function renderSwitch(FormField $field): string
|
||||||
|
{
|
||||||
|
$value = $this->form->getFieldValue($field);
|
||||||
|
|
||||||
|
// Domyślna wartość dla nowego rekordu
|
||||||
|
if ($value === null && $field->value === true) {
|
||||||
|
$checked = true;
|
||||||
|
} else {
|
||||||
|
$checked = (bool) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return \Html::input_switch([
|
||||||
|
'label' => $field->label,
|
||||||
|
'name' => $field->name,
|
||||||
|
'id' => $field->id,
|
||||||
|
'checked' => $checked,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderuje select
|
||||||
|
*/
|
||||||
|
public function renderSelect(FormField $field): string
|
||||||
|
{
|
||||||
|
$value = $this->form->getFieldValue($field);
|
||||||
|
$error = $this->form->getError($field->name);
|
||||||
|
|
||||||
|
$params = [
|
||||||
|
'label' => $field->label,
|
||||||
|
'name' => $field->name,
|
||||||
|
'id' => $field->id,
|
||||||
|
'value' => $value ?? '',
|
||||||
|
'options' => $field->options,
|
||||||
|
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($error) {
|
||||||
|
$params['class'] .= ' error';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->wrapWithError(\Html::select($params), $error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderuje textarea
|
||||||
|
*/
|
||||||
|
public function renderTextarea(FormField $field): string
|
||||||
|
{
|
||||||
|
$value = $this->form->getFieldValue($field);
|
||||||
|
|
||||||
|
return \Html::textarea([
|
||||||
|
'label' => $field->label,
|
||||||
|
'name' => $field->name,
|
||||||
|
'id' => $field->id,
|
||||||
|
'value' => $value ?? '',
|
||||||
|
'rows' => $field->attributes['rows'] ?? 4,
|
||||||
|
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderuje edytor (CKEditor)
|
||||||
|
*/
|
||||||
|
public function renderEditor(FormField $field): string
|
||||||
|
{
|
||||||
|
$value = $this->form->getFieldValue($field);
|
||||||
|
|
||||||
|
return \Html::textarea([
|
||||||
|
'label' => $field->label,
|
||||||
|
'name' => $field->name,
|
||||||
|
'id' => $field->id,
|
||||||
|
'value' => $value ?? '',
|
||||||
|
'rows' => max(10, ($field->attributes['rows'] ?? 10)),
|
||||||
|
'class' => 'editor ' . ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderuje pole obrazu z filemanagerem
|
||||||
|
*/
|
||||||
|
public function renderImage(FormField $field): string
|
||||||
|
{
|
||||||
|
$value = $this->form->getFieldValue($field);
|
||||||
|
|
||||||
|
$filemanagerUrl = $field->filemanagerUrl ?? $this->generateFilemanagerUrl($field->id);
|
||||||
|
|
||||||
|
return \Html::input_icon([
|
||||||
|
'label' => $field->label,
|
||||||
|
'name' => $field->name,
|
||||||
|
'id' => $field->id,
|
||||||
|
'value' => $value ?? '',
|
||||||
|
'type' => 'text',
|
||||||
|
'icon_content' => 'przeglądaj',
|
||||||
|
'icon_js' => "window.open('{$filemanagerUrl}', 'filemanager', 'location=1,status=1,scrollbars=1,width=1100,height=700')",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderuje pole pliku
|
||||||
|
*/
|
||||||
|
public function renderFile(FormField $field): string
|
||||||
|
{
|
||||||
|
$value = $this->form->getFieldValue($field);
|
||||||
|
|
||||||
|
if ($field->useFilemanager) {
|
||||||
|
$filemanagerUrl = $field->filemanagerUrl ?? $this->generateFilemanagerUrl($field->id);
|
||||||
|
|
||||||
|
return \Html::input_icon([
|
||||||
|
'label' => $field->label,
|
||||||
|
'name' => $field->name,
|
||||||
|
'id' => $field->id,
|
||||||
|
'value' => $value ?? '',
|
||||||
|
'type' => 'text',
|
||||||
|
'icon_content' => 'przeglądaj',
|
||||||
|
'icon_js' => "window.open('{$filemanagerUrl}', 'filemanager', 'location=1,status=1,scrollbars=1,width=1100,height=700')",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return \Html::input([
|
||||||
|
'label' => $field->label,
|
||||||
|
'name' => $field->name,
|
||||||
|
'id' => $field->id,
|
||||||
|
'type' => 'file',
|
||||||
|
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderuje ukryte pole
|
||||||
|
*/
|
||||||
|
public function renderHidden(FormField $field): string
|
||||||
|
{
|
||||||
|
$value = $this->form->getFieldValue($field);
|
||||||
|
|
||||||
|
return '<input type="hidden" name="' . htmlspecialchars($field->name) . '" ' .
|
||||||
|
'id="' . htmlspecialchars($field->id) . '" ' .
|
||||||
|
'value="' . htmlspecialchars($value ?? '') . '">';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderuje sekcję językową
|
||||||
|
*/
|
||||||
|
public function renderLangSection(FormField $section): string
|
||||||
|
{
|
||||||
|
if ($section->langFields === null || $this->form->languages === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$out = '<div id="languages-' . $section->name . '" class="languages-tabs">';
|
||||||
|
|
||||||
|
// Zakładki języków
|
||||||
|
$out .= '<ul class="resp-tabs-list languages-tabs htabs">';
|
||||||
|
foreach ($this->form->languages as $lang) {
|
||||||
|
if ($lang['status']) {
|
||||||
|
$out .= '<li>' . htmlspecialchars($lang['name']) . '</li>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$out .= '</ul>';
|
||||||
|
|
||||||
|
// Kontenery języków
|
||||||
|
$out .= '<div class="resp-tabs-container languages-tabs">';
|
||||||
|
foreach ($this->form->languages as $lang) {
|
||||||
|
if ($lang['status']) {
|
||||||
|
$out .= '<div>';
|
||||||
|
foreach ($section->langFields as $field) {
|
||||||
|
$out .= $this->renderLangField($field, $lang['id'], $section->name);
|
||||||
|
}
|
||||||
|
$out .= '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$out .= '</div>';
|
||||||
|
|
||||||
|
$out .= '</div>';
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renderuje pole w sekcji językowej
|
||||||
|
*/
|
||||||
|
private function renderLangField(FormField $field, $languageId, string $sectionName): string
|
||||||
|
{
|
||||||
|
$value = $this->form->getFieldValue($field, $languageId, $field->name);
|
||||||
|
$error = $this->form->getError($sectionName . '_' . $field->name, $languageId);
|
||||||
|
|
||||||
|
$name = $field->getLocalizedName($languageId);
|
||||||
|
$id = $field->getLocalizedId($languageId);
|
||||||
|
|
||||||
|
switch ($field->type) {
|
||||||
|
case FormFieldType::IMAGE:
|
||||||
|
$filemanagerUrl = $field->filemanagerUrl ?? $this->generateFilemanagerUrl($id);
|
||||||
|
return $this->wrapWithError(\Html::input_icon([
|
||||||
|
'label' => $field->label,
|
||||||
|
'name' => $name,
|
||||||
|
'id' => $id,
|
||||||
|
'value' => $value ?? '',
|
||||||
|
'type' => 'text',
|
||||||
|
'icon_content' => 'przeglądaj',
|
||||||
|
'icon_js' => "window.open('{$filemanagerUrl}', 'filemanager', 'location=1,status=1,scrollbars=1,width=1100,height=700')",
|
||||||
|
]), $error);
|
||||||
|
|
||||||
|
case FormFieldType::TEXTAREA:
|
||||||
|
case FormFieldType::EDITOR:
|
||||||
|
return $this->wrapWithError(\Html::textarea([
|
||||||
|
'label' => $field->label,
|
||||||
|
'name' => $name,
|
||||||
|
'id' => $id,
|
||||||
|
'value' => $value ?? '',
|
||||||
|
'rows' => $field->type === FormFieldType::EDITOR ? 10 : ($field->attributes['rows'] ?? 4),
|
||||||
|
'class' => $field->type === FormFieldType::EDITOR ? 'editor' : '',
|
||||||
|
]), $error);
|
||||||
|
|
||||||
|
case FormFieldType::SWITCH:
|
||||||
|
return \Html::input_switch([
|
||||||
|
'label' => $field->label,
|
||||||
|
'name' => $name,
|
||||||
|
'id' => $id,
|
||||||
|
'checked' => (bool) $value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
default: // TEXT, URL, etc.
|
||||||
|
return $this->wrapWithError(\Html::input([
|
||||||
|
'label' => $field->label,
|
||||||
|
'name' => $name,
|
||||||
|
'id' => $id,
|
||||||
|
'value' => $value ?? '',
|
||||||
|
'type' => $field->type === FormFieldType::EMAIL ? 'email' : 'text',
|
||||||
|
'class' => ($field->required ? 'require ' : '') . ($field->attributes['class'] ?? ''),
|
||||||
|
]), $error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generuje URL do filemanagera
|
||||||
|
*/
|
||||||
|
private function generateFilemanagerUrl(string $fieldId): string
|
||||||
|
{
|
||||||
|
$rfmAkey = $_SESSION['rfm_akey'] ?? bin2hex(random_bytes(16));
|
||||||
|
$_SESSION['rfm_akey'] = $rfmAkey;
|
||||||
|
$_SESSION['rfm_akey_expires'] = time() + 20 * 60;
|
||||||
|
$_SESSION['can_use_rfm'] = true;
|
||||||
|
|
||||||
|
$fieldIdParam = rawurlencode($fieldId);
|
||||||
|
$akeyParam = rawurlencode($rfmAkey);
|
||||||
|
return "/libraries/filemanager-9.14.2/dialog.php?type=1&popup=1&field_id={$fieldIdParam}&akey={$akeyParam}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opakowuje pole w kontener błędu
|
||||||
|
*/
|
||||||
|
private function wrapWithError(string $html, ?string $error): string
|
||||||
|
{
|
||||||
|
if ($error) {
|
||||||
|
return '<div class="field-with-error">' . $html .
|
||||||
|
'<span class="error-message">' . htmlspecialchars($error) . '</span></div>';
|
||||||
|
}
|
||||||
|
return $html;
|
||||||
|
}
|
||||||
|
}
|
||||||
152
autoload/admin/Support/Forms/FormRequestHandler.php
Normal file
152
autoload/admin/Support/Forms/FormRequestHandler.php
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<?php
|
||||||
|
namespace admin\Support\Forms;
|
||||||
|
|
||||||
|
use admin\ViewModels\Forms\FormEditViewModel;
|
||||||
|
use admin\ViewModels\Forms\FormFieldType;
|
||||||
|
use admin\Validation\FormValidator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obsługa żądań formularza (POST, persist, walidacja)
|
||||||
|
*/
|
||||||
|
class FormRequestHandler
|
||||||
|
{
|
||||||
|
private FormValidator $validator;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->validator = new FormValidator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Przetwarza żądanie POST formularza
|
||||||
|
*
|
||||||
|
* @param FormEditViewModel $formViewModel
|
||||||
|
* @param array $postData Dane z $_POST
|
||||||
|
* @return array Wynik przetwarzania ['success' => bool, 'errors' => array, 'data' => array]
|
||||||
|
*/
|
||||||
|
public function handleSubmit(FormEditViewModel $formViewModel, array $postData): array
|
||||||
|
{
|
||||||
|
$result = [
|
||||||
|
'success' => false,
|
||||||
|
'errors' => [],
|
||||||
|
'data' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
// Walidacja
|
||||||
|
$errors = $this->validator->validate($postData, $formViewModel->fields, $formViewModel->languages);
|
||||||
|
|
||||||
|
if (!empty($errors)) {
|
||||||
|
$result['errors'] = $errors;
|
||||||
|
// Zapisz dane do persist przy błędzie walidacji
|
||||||
|
if ($formViewModel->persist) {
|
||||||
|
$formViewModel->saveToPersist($postData);
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Przetwórz dane (np. konwersja typów)
|
||||||
|
$processedData = $this->processData($postData, $formViewModel->fields);
|
||||||
|
|
||||||
|
$result['success'] = true;
|
||||||
|
$result['data'] = $processedData;
|
||||||
|
|
||||||
|
// Wyczyść persist po sukcesie
|
||||||
|
if ($formViewModel->persist) {
|
||||||
|
$formViewModel->clearPersist();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Przetwarza dane z formularza (konwersja typów)
|
||||||
|
*/
|
||||||
|
private function processData(array $postData, array $fields): array
|
||||||
|
{
|
||||||
|
$processed = [];
|
||||||
|
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
$value = $postData[$field->name] ?? null;
|
||||||
|
|
||||||
|
// Konwersja typów
|
||||||
|
switch ($field->type) {
|
||||||
|
case FormFieldType::SWITCH:
|
||||||
|
$processed[$field->name] = $value ? 1 : 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FormFieldType::NUMBER:
|
||||||
|
$processed[$field->name] = $value !== null && $value !== '' ? (float)$value : null;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FormFieldType::LANG_SECTION:
|
||||||
|
if ($field->langFields !== null) {
|
||||||
|
$processed[$field->name] = $this->processLangSection($postData, $field);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$processed[$field->name] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $processed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Przetwarza sekcję językową
|
||||||
|
*/
|
||||||
|
private function processLangSection(array $postData, $section): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
|
||||||
|
if ($section->langFields === null) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($section->langFields as $field) {
|
||||||
|
$fieldName = $field->name;
|
||||||
|
$langData = $postData[$fieldName] ?? [];
|
||||||
|
|
||||||
|
foreach ($langData as $langId => $value) {
|
||||||
|
if (!isset($result[$langId])) {
|
||||||
|
$result[$langId] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Konwersja typów dla pól językowych
|
||||||
|
switch ($field->type) {
|
||||||
|
case FormFieldType::SWITCH:
|
||||||
|
$result[$langId][$fieldName] = $value ? 1 : 0;
|
||||||
|
break;
|
||||||
|
case FormFieldType::NUMBER:
|
||||||
|
$result[$langId][$fieldName] = $value !== null && $value !== '' ? (float)$value : null;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$result[$langId][$fieldName] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Przywraca dane z persist do POST (przy błędzie walidacji)
|
||||||
|
*/
|
||||||
|
public function restoreFromPersist(FormEditViewModel $formViewModel): ?array
|
||||||
|
{
|
||||||
|
if (!$formViewModel->persist) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $_SESSION['form_persist'][$formViewModel->formId] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sprawdza czy żądanie jest submitowaniem formularza
|
||||||
|
*/
|
||||||
|
public function isFormSubmit(string $formId): bool
|
||||||
|
{
|
||||||
|
return $_SERVER['REQUEST_METHOD'] === 'POST' &&
|
||||||
|
(isset($_POST['_form_id']) && $_POST['_form_id'] === $formId);
|
||||||
|
}
|
||||||
|
}
|
||||||
196
autoload/admin/Validation/FormValidator.php
Normal file
196
autoload/admin/Validation/FormValidator.php
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
<?php
|
||||||
|
namespace admin\Validation;
|
||||||
|
|
||||||
|
use admin\ViewModels\Forms\FormField;
|
||||||
|
use admin\ViewModels\Forms\FormFieldType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walidator formularzy
|
||||||
|
*/
|
||||||
|
class FormValidator
|
||||||
|
{
|
||||||
|
private array $errors = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waliduje dane na podstawie definicji pól
|
||||||
|
*
|
||||||
|
* @param array $data Dane z POST
|
||||||
|
* @param array $fields Definicje pól (FormField[])
|
||||||
|
* @param array|null $languages Języki (dla walidacji pól językowych)
|
||||||
|
* @return array Tablica błędów (pusta jeśli OK)
|
||||||
|
*/
|
||||||
|
public function validate(array $data, array $fields, ?array $languages = null): array
|
||||||
|
{
|
||||||
|
$this->errors = [];
|
||||||
|
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
if ($field->type === FormFieldType::LANG_SECTION) {
|
||||||
|
$this->validateLangSection($data, $field, $languages ?? []);
|
||||||
|
} else {
|
||||||
|
$this->validateField($data, $field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waliduje pojedyncze pole
|
||||||
|
*/
|
||||||
|
private function validateField(array $data, FormField $field): void
|
||||||
|
{
|
||||||
|
$value = $data[$field->name] ?? null;
|
||||||
|
|
||||||
|
// Walidacja wymagalności
|
||||||
|
if ($field->required && $this->isEmpty($value)) {
|
||||||
|
$this->errors[$field->name] = "Pole \"{$field->label}\" jest wymagane.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jeśli pole puste i nie jest wymagane - pomijamy dalszą walidację
|
||||||
|
if ($this->isEmpty($value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walidacja typu
|
||||||
|
switch ($field->type) {
|
||||||
|
case FormFieldType::EMAIL:
|
||||||
|
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$this->errors[$field->name] = "Pole \"{$field->label}\" musi być poprawnym adresem e-mail.";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FormFieldType::NUMBER:
|
||||||
|
if (!is_numeric($value)) {
|
||||||
|
$this->errors[$field->name] = "Pole \"{$field->label}\" musi być liczbą.";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FormFieldType::DATE:
|
||||||
|
if (!$this->isValidDate($value)) {
|
||||||
|
$this->errors[$field->name] = "Pole \"{$field->label}\" musi być poprawną datą (YYYY-MM-DD).";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FormFieldType::DATETIME:
|
||||||
|
if (!$this->isValidDateTime($value)) {
|
||||||
|
$this->errors[$field->name] = "Pole \"{$field->label}\" musi być poprawną datą i czasem.";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walidacja customowa (callback)
|
||||||
|
if (isset($field->attributes['validate_callback']) && is_callable($field->attributes['validate_callback'])) {
|
||||||
|
$result = call_user_func($field->attributes['validate_callback'], $value, $data);
|
||||||
|
if ($result !== true) {
|
||||||
|
$this->errors[$field->name] = is_string($result) ? $result : "Pole \"{$field->label}\" zawiera nieprawidłową wartość.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waliduje sekcję językową
|
||||||
|
*/
|
||||||
|
private function validateLangSection(array $data, FormField $section, array $languages): void
|
||||||
|
{
|
||||||
|
if ($section->langFields === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($languages as $language) {
|
||||||
|
if (!($language['status'] ?? false)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$langId = $language['id'];
|
||||||
|
|
||||||
|
foreach ($section->langFields as $field) {
|
||||||
|
$fieldName = $field->name;
|
||||||
|
$value = $data[$fieldName][$langId] ?? null;
|
||||||
|
|
||||||
|
// Walidacja wymagalności
|
||||||
|
if ($field->required && $this->isEmpty($value)) {
|
||||||
|
$errorKey = "{$section->name}_{$fieldName}";
|
||||||
|
$this->errors[$errorKey][$langId] = "Pole \"{$field->label}\" ({$language['name']}) jest wymagane.";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walidacja typu dla pól językowych
|
||||||
|
if (!$this->isEmpty($value)) {
|
||||||
|
switch ($field->type) {
|
||||||
|
case FormFieldType::EMAIL:
|
||||||
|
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||||
|
$errorKey = "{$section->name}_{$fieldName}";
|
||||||
|
$this->errors[$errorKey][$langId] = "Pole \"{$field->label}\" ({$language['name']}) musi być poprawnym e-mailem.";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sprawdza czy wartość jest pusta
|
||||||
|
*/
|
||||||
|
private function isEmpty($value): bool
|
||||||
|
{
|
||||||
|
return $value === null || $value === '' || (is_array($value) && empty($value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sprawdza czy data jest poprawna (YYYY-MM-DD)
|
||||||
|
*/
|
||||||
|
private function isValidDate(string $date): bool
|
||||||
|
{
|
||||||
|
$d = \DateTime::createFromFormat('Y-m-d', $date);
|
||||||
|
return $d && $d->format('Y-m-d') === $date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sprawdza czy data i czas są poprawne
|
||||||
|
*/
|
||||||
|
private function isValidDateTime(string $datetime): bool
|
||||||
|
{
|
||||||
|
$d = \DateTime::createFromFormat('Y-m-d H:i:s', $datetime);
|
||||||
|
if ($d && $d->format('Y-m-d H:i:s') === $datetime) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spróbuj bez sekund
|
||||||
|
$d = \DateTime::createFromFormat('Y-m-d H:i', $datetime);
|
||||||
|
return $d && $d->format('Y-m-d H:i') === $datetime;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sprawdza czy walidacja zakończyła się sukcesem
|
||||||
|
*/
|
||||||
|
public function isValid(): bool
|
||||||
|
{
|
||||||
|
return empty($this->errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zwraca wszystkie błędy
|
||||||
|
*/
|
||||||
|
public function getErrors(): array
|
||||||
|
{
|
||||||
|
return $this->errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zwraca pierwszy błąd
|
||||||
|
*/
|
||||||
|
public function getFirstError(): ?string
|
||||||
|
{
|
||||||
|
if (empty($this->errors)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$first = reset($this->errors);
|
||||||
|
if (is_array($first)) {
|
||||||
|
return reset($first);
|
||||||
|
}
|
||||||
|
return $first;
|
||||||
|
}
|
||||||
|
}
|
||||||
73
autoload/admin/ViewModels/Forms/FormAction.php
Normal file
73
autoload/admin/ViewModels/Forms/FormAction.php
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
namespace admin\ViewModels\Forms;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definicja akcji formularza (przycisku)
|
||||||
|
*/
|
||||||
|
class FormAction
|
||||||
|
{
|
||||||
|
public string $name;
|
||||||
|
public string $label;
|
||||||
|
public string $type;
|
||||||
|
public string $url;
|
||||||
|
public ?string $backUrl;
|
||||||
|
public string $cssClass;
|
||||||
|
public array $attributes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name Nazwa akcji (save, cancel, delete)
|
||||||
|
* @param string $label Etykieta przycisku
|
||||||
|
* @param string $url URL akcji (dla save)
|
||||||
|
* @param string|null $backUrl URL powrotu po zapisie
|
||||||
|
* @param string $cssClass Klasy CSS przycisku
|
||||||
|
* @param string $type Typ przycisku (submit, button, link)
|
||||||
|
* @param array $attributes Dodatkowe atrybuty HTML
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
string $name,
|
||||||
|
string $label,
|
||||||
|
string $url = '',
|
||||||
|
?string $backUrl = null,
|
||||||
|
string $cssClass = 'btn btn-primary',
|
||||||
|
string $type = 'submit',
|
||||||
|
array $attributes = []
|
||||||
|
) {
|
||||||
|
$this->name = $name;
|
||||||
|
$this->label = $label;
|
||||||
|
$this->url = $url;
|
||||||
|
$this->backUrl = $backUrl;
|
||||||
|
$this->cssClass = $cssClass;
|
||||||
|
$this->type = $type;
|
||||||
|
$this->attributes = $attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Predefiniowana akcja Zapisz
|
||||||
|
*/
|
||||||
|
public static function save(string $url, string $backUrl = '', string $label = 'Zapisz'): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
'save',
|
||||||
|
$label,
|
||||||
|
$url,
|
||||||
|
$backUrl,
|
||||||
|
'btn btn-primary',
|
||||||
|
'submit'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Predefiniowana akcja Anuluj
|
||||||
|
*/
|
||||||
|
public static function cancel(string $backUrl, string $label = 'Anuluj'): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
'cancel',
|
||||||
|
$label,
|
||||||
|
$backUrl,
|
||||||
|
null,
|
||||||
|
'btn btn-default',
|
||||||
|
'link'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
178
autoload/admin/ViewModels/Forms/FormEditViewModel.php
Normal file
178
autoload/admin/ViewModels/Forms/FormEditViewModel.php
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<?php
|
||||||
|
namespace admin\ViewModels\Forms;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Główny model widoku formularza edycji
|
||||||
|
*/
|
||||||
|
class FormEditViewModel
|
||||||
|
{
|
||||||
|
public string $formId;
|
||||||
|
public string $title;
|
||||||
|
public string $method;
|
||||||
|
public string $action;
|
||||||
|
public ?string $backUrl;
|
||||||
|
public array $tabs;
|
||||||
|
public array $fields;
|
||||||
|
public array $hiddenFields;
|
||||||
|
public array $actions;
|
||||||
|
public bool $persist;
|
||||||
|
public array $data;
|
||||||
|
public ?array $validationErrors;
|
||||||
|
public ?array $languages;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $formId Unikalny identyfikator formularza
|
||||||
|
* @param string $title Tytuł formularza
|
||||||
|
* @param array $data Dane obiektu (np. banner)
|
||||||
|
* @param array $fields Pola formularza
|
||||||
|
* @param array $tabs Zakładki formularza
|
||||||
|
* @param array $actions Akcje (przyciski)
|
||||||
|
* @param string $method Metoda HTTP (POST, GET)
|
||||||
|
* @param string $action URL akcji formularza
|
||||||
|
* @param string|null $backUrl URL powrotu
|
||||||
|
* @param bool $persist Czy zapamiętywać dane w sesji
|
||||||
|
* @param array $hiddenFields Dodatkowe ukryte pola
|
||||||
|
* @param array|null $languages Dostępne języki (dla sekcji językowych)
|
||||||
|
* @param array|null $validationErrors Błędy walidacji
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
string $formId,
|
||||||
|
string $title,
|
||||||
|
array $data = [],
|
||||||
|
array $fields = [],
|
||||||
|
array $tabs = [],
|
||||||
|
array $actions = [],
|
||||||
|
string $method = 'POST',
|
||||||
|
string $action = '',
|
||||||
|
?string $backUrl = null,
|
||||||
|
bool $persist = true,
|
||||||
|
array $hiddenFields = [],
|
||||||
|
?array $languages = null,
|
||||||
|
?array $validationErrors = null
|
||||||
|
) {
|
||||||
|
$this->formId = $formId;
|
||||||
|
$this->title = $title;
|
||||||
|
$this->data = $data;
|
||||||
|
$this->fields = $fields;
|
||||||
|
$this->tabs = $tabs;
|
||||||
|
$this->actions = $actions;
|
||||||
|
$this->method = $method;
|
||||||
|
$this->action = $action;
|
||||||
|
$this->backUrl = $backUrl;
|
||||||
|
$this->persist = $persist;
|
||||||
|
$this->hiddenFields = $hiddenFields;
|
||||||
|
$this->languages = $languages;
|
||||||
|
$this->validationErrors = $validationErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sprawdza czy formularz ma zakładki
|
||||||
|
*/
|
||||||
|
public function hasTabs(): bool
|
||||||
|
{
|
||||||
|
return count($this->tabs) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sprawdza czy formularz ma sekcje językowe
|
||||||
|
*/
|
||||||
|
public function hasLangSections(): bool
|
||||||
|
{
|
||||||
|
foreach ($this->fields as $field) {
|
||||||
|
if ($field->type === FormFieldType::LANG_SECTION) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zwraca pola dla konkretnej zakładki
|
||||||
|
*/
|
||||||
|
public function getFieldsForTab(string $tabId): array
|
||||||
|
{
|
||||||
|
return array_filter($this->fields, function (FormField $field) use ($tabId) {
|
||||||
|
return $field->tabId === $tabId && $field->type !== FormFieldType::LANG_SECTION;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zwraca sekcje językowe dla konkretnej zakładki
|
||||||
|
*/
|
||||||
|
public function getLangSectionsForTab(string $tabId): array
|
||||||
|
{
|
||||||
|
return array_filter($this->fields, function (FormField $field) use ($tabId) {
|
||||||
|
return $field->type === FormFieldType::LANG_SECTION &&
|
||||||
|
$field->langSectionParentTab === $tabId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pobiera wartość pola z danych lub sesji (persist)
|
||||||
|
*/
|
||||||
|
public function getFieldValue(FormField $field, $languageId = null, ?string $langFieldName = null)
|
||||||
|
{
|
||||||
|
$fieldName = $field->name;
|
||||||
|
|
||||||
|
// Dla sekcji językowych - pobierz wartość z data[lang_id][field_name]
|
||||||
|
if ($languageId !== null && $langFieldName !== null) {
|
||||||
|
$fieldName = $langFieldName;
|
||||||
|
return $this->data['languages'][$languageId][$fieldName] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zwykłe pole - najpierw sprawdź sesję (persist), potem dane
|
||||||
|
if ($this->persist && isset($_SESSION['form_persist'][$this->formId][$fieldName])) {
|
||||||
|
return $_SESSION['form_persist'][$this->formId][$fieldName];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->data[$fieldName] ?? $field->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sprawdza czy pole ma błąd walidacji
|
||||||
|
*/
|
||||||
|
public function hasError(string $fieldName, $languageId = null): bool
|
||||||
|
{
|
||||||
|
if ($this->validationErrors === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($languageId !== null) {
|
||||||
|
return isset($this->validationErrors[$fieldName][$languageId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isset($this->validationErrors[$fieldName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pobiera komunikat błędu dla pola
|
||||||
|
*/
|
||||||
|
public function getError(string $fieldName, $languageId = null): ?string
|
||||||
|
{
|
||||||
|
if ($languageId !== null) {
|
||||||
|
return $this->validationErrors[$fieldName][$languageId] ?? null;
|
||||||
|
}
|
||||||
|
return $this->validationErrors[$fieldName] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Czyści dane persist z sesji
|
||||||
|
*/
|
||||||
|
public function clearPersist(): void
|
||||||
|
{
|
||||||
|
if (isset($_SESSION['form_persist'][$this->formId])) {
|
||||||
|
unset($_SESSION['form_persist'][$this->formId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zapisuje dane do sesji (persist)
|
||||||
|
*/
|
||||||
|
public function saveToPersist(array $data): void
|
||||||
|
{
|
||||||
|
if (!isset($_SESSION['form_persist'])) {
|
||||||
|
$_SESSION['form_persist'] = [];
|
||||||
|
}
|
||||||
|
$_SESSION['form_persist'][$this->formId] = $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
323
autoload/admin/ViewModels/Forms/FormField.php
Normal file
323
autoload/admin/ViewModels/Forms/FormField.php
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
<?php
|
||||||
|
namespace admin\ViewModels\Forms;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definicja pojedynczego pola formularza
|
||||||
|
*/
|
||||||
|
class FormField
|
||||||
|
{
|
||||||
|
public string $name;
|
||||||
|
public string $type;
|
||||||
|
public string $label;
|
||||||
|
public $value;
|
||||||
|
public string $tabId;
|
||||||
|
public bool $required;
|
||||||
|
public array $attributes;
|
||||||
|
public array $options;
|
||||||
|
public ?string $helpText;
|
||||||
|
public ?string $placeholder;
|
||||||
|
public ?string $id;
|
||||||
|
|
||||||
|
// Specyficzne dla obrazów/plików
|
||||||
|
public bool $useFilemanager;
|
||||||
|
public ?string $filemanagerUrl;
|
||||||
|
|
||||||
|
// Specyficzne dla edytora
|
||||||
|
public string $editorToolbar;
|
||||||
|
public int $editorHeight;
|
||||||
|
|
||||||
|
// Specyficzne dla lang_section
|
||||||
|
public ?array $langFields;
|
||||||
|
public ?string $langSectionParentTab;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name Nazwa pola (name)
|
||||||
|
* @param string $type Typ pola (z FormFieldType)
|
||||||
|
* @param string $label Etykieta pola
|
||||||
|
* @param mixed $value Wartość domyślna
|
||||||
|
* @param string $tabId Identyfikator zakładki
|
||||||
|
* @param bool $required Czy pole wymagane
|
||||||
|
* @param array $attributes Atrybuty HTML
|
||||||
|
* @param array $options Opcje dla select
|
||||||
|
* @param string|null $helpText Tekst pomocniczy
|
||||||
|
* @param string|null $placeholder Placeholder
|
||||||
|
* @param bool $useFilemanager Czy używać filemanagera
|
||||||
|
* @param string|null $filemanagerUrl URL filemanagera
|
||||||
|
* @param string $editorToolbar Konfiguracja toolbar CKEditor
|
||||||
|
* @param int $editorHeight Wysokość edytora
|
||||||
|
* @param array|null $langFields Pola w sekcji językowej
|
||||||
|
* @param string|null $langSectionParentTab Zakładka nadrzędna dla sekcji językowej
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
string $name,
|
||||||
|
string $type = FormFieldType::TEXT,
|
||||||
|
string $label = '',
|
||||||
|
$value = null,
|
||||||
|
string $tabId = 'default',
|
||||||
|
bool $required = false,
|
||||||
|
array $attributes = [],
|
||||||
|
array $options = [],
|
||||||
|
?string $helpText = null,
|
||||||
|
?string $placeholder = null,
|
||||||
|
bool $useFilemanager = false,
|
||||||
|
?string $filemanagerUrl = null,
|
||||||
|
string $editorToolbar = 'MyTool',
|
||||||
|
int $editorHeight = 300,
|
||||||
|
?array $langFields = null,
|
||||||
|
?string $langSectionParentTab = null
|
||||||
|
) {
|
||||||
|
$this->name = $name;
|
||||||
|
$this->type = $type;
|
||||||
|
$this->label = $label;
|
||||||
|
$this->value = $value;
|
||||||
|
$this->tabId = $tabId;
|
||||||
|
$this->required = $required;
|
||||||
|
$this->attributes = $attributes;
|
||||||
|
$this->options = $options;
|
||||||
|
$this->helpText = $helpText;
|
||||||
|
$this->placeholder = $placeholder;
|
||||||
|
$this->useFilemanager = $useFilemanager;
|
||||||
|
$this->filemanagerUrl = $filemanagerUrl;
|
||||||
|
$this->editorToolbar = $editorToolbar;
|
||||||
|
$this->editorHeight = $editorHeight;
|
||||||
|
$this->langFields = $langFields;
|
||||||
|
$this->langSectionParentTab = $langSectionParentTab;
|
||||||
|
$this->id = $attributes['id'] ?? $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Factory methods dla różnych typów pól
|
||||||
|
|
||||||
|
public static function text(string $name, array $config = []): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
$name,
|
||||||
|
FormFieldType::TEXT,
|
||||||
|
$config['label'] ?? '',
|
||||||
|
$config['value'] ?? null,
|
||||||
|
$config['tab'] ?? 'default',
|
||||||
|
$config['required'] ?? false,
|
||||||
|
$config['attributes'] ?? [],
|
||||||
|
[],
|
||||||
|
$config['help'] ?? null,
|
||||||
|
$config['placeholder'] ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function number(string $name, array $config = []): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
$name,
|
||||||
|
FormFieldType::NUMBER,
|
||||||
|
$config['label'] ?? '',
|
||||||
|
$config['value'] ?? null,
|
||||||
|
$config['tab'] ?? 'default',
|
||||||
|
$config['required'] ?? false,
|
||||||
|
$config['attributes'] ?? [],
|
||||||
|
[],
|
||||||
|
$config['help'] ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function email(string $name, array $config = []): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
$name,
|
||||||
|
FormFieldType::EMAIL,
|
||||||
|
$config['label'] ?? '',
|
||||||
|
$config['value'] ?? null,
|
||||||
|
$config['tab'] ?? 'default',
|
||||||
|
$config['required'] ?? false,
|
||||||
|
$config['attributes'] ?? []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function password(string $name, array $config = []): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
$name,
|
||||||
|
FormFieldType::PASSWORD,
|
||||||
|
$config['label'] ?? '',
|
||||||
|
$config['value'] ?? null,
|
||||||
|
$config['tab'] ?? 'default',
|
||||||
|
$config['required'] ?? false,
|
||||||
|
$config['attributes'] ?? []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function date(string $name, array $config = []): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
$name,
|
||||||
|
FormFieldType::DATE,
|
||||||
|
$config['label'] ?? '',
|
||||||
|
$config['value'] ?? null,
|
||||||
|
$config['tab'] ?? 'default',
|
||||||
|
$config['required'] ?? false,
|
||||||
|
array_merge(['class' => 'date'], $config['attributes'] ?? [])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function datetime(string $name, array $config = []): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
$name,
|
||||||
|
FormFieldType::DATETIME,
|
||||||
|
$config['label'] ?? '',
|
||||||
|
$config['value'] ?? null,
|
||||||
|
$config['tab'] ?? 'default',
|
||||||
|
$config['required'] ?? false,
|
||||||
|
array_merge(['class' => 'datetime'], $config['attributes'] ?? [])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function switch(string $name, array $config = []): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
$name,
|
||||||
|
FormFieldType::SWITCH,
|
||||||
|
$config['label'] ?? '',
|
||||||
|
$config['value'] ?? false,
|
||||||
|
$config['tab'] ?? 'default',
|
||||||
|
false,
|
||||||
|
$config['attributes'] ?? []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function select(string $name, array $config = []): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
$name,
|
||||||
|
FormFieldType::SELECT,
|
||||||
|
$config['label'] ?? '',
|
||||||
|
$config['value'] ?? null,
|
||||||
|
$config['tab'] ?? 'default',
|
||||||
|
$config['required'] ?? false,
|
||||||
|
$config['attributes'] ?? [],
|
||||||
|
$config['options'] ?? []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function textarea(string $name, array $config = []): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
$name,
|
||||||
|
FormFieldType::TEXTAREA,
|
||||||
|
$config['label'] ?? '',
|
||||||
|
$config['value'] ?? null,
|
||||||
|
$config['tab'] ?? 'default',
|
||||||
|
$config['required'] ?? false,
|
||||||
|
array_merge(['rows' => $config['rows'] ?? 4], $config['attributes'] ?? [])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function editor(string $name, array $config = []): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
$name,
|
||||||
|
FormFieldType::EDITOR,
|
||||||
|
$config['label'] ?? '',
|
||||||
|
$config['value'] ?? null,
|
||||||
|
$config['tab'] ?? 'default',
|
||||||
|
$config['required'] ?? false,
|
||||||
|
$config['attributes'] ?? [],
|
||||||
|
[],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
$config['toolbar'] ?? 'MyTool',
|
||||||
|
$config['height'] ?? 300
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function image(string $name, array $config = []): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
$name,
|
||||||
|
FormFieldType::IMAGE,
|
||||||
|
$config['label'] ?? '',
|
||||||
|
$config['value'] ?? null,
|
||||||
|
$config['tab'] ?? 'default',
|
||||||
|
$config['required'] ?? false,
|
||||||
|
$config['attributes'] ?? [],
|
||||||
|
[],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
$config['filemanager'] ?? true,
|
||||||
|
$config['filemanager_url'] ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function file(string $name, array $config = []): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
$name,
|
||||||
|
FormFieldType::FILE,
|
||||||
|
$config['label'] ?? '',
|
||||||
|
$config['value'] ?? null,
|
||||||
|
$config['tab'] ?? 'default',
|
||||||
|
$config['required'] ?? false,
|
||||||
|
$config['attributes'] ?? [],
|
||||||
|
[],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
$config['filemanager'] ?? true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function hidden(string $name, $value = null): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
$name,
|
||||||
|
FormFieldType::HIDDEN,
|
||||||
|
'',
|
||||||
|
$value,
|
||||||
|
'default'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sekcja językowa - grupa pól powtarzana dla każdego języka
|
||||||
|
*
|
||||||
|
* @param string $name Nazwa sekcji (prefiks dla pól)
|
||||||
|
* @param string $parentTab Identyfikator zakładki nadrzędnej
|
||||||
|
* @param array $fields Pola w sekcji językowej (tablica FormField)
|
||||||
|
*/
|
||||||
|
public static function langSection(string $name, string $parentTab, array $fields): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
$name,
|
||||||
|
FormFieldType::LANG_SECTION,
|
||||||
|
'',
|
||||||
|
null,
|
||||||
|
$parentTab,
|
||||||
|
false,
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
'MyTool',
|
||||||
|
300,
|
||||||
|
$fields,
|
||||||
|
$parentTab
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zwraca nazwę pola z sufiksem dla konkretnego języka
|
||||||
|
*/
|
||||||
|
public function getLocalizedName($languageId): string
|
||||||
|
{
|
||||||
|
return "{$this->name}[{$languageId}]";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zwraca ID pola z sufiksem dla konkretnego języka
|
||||||
|
*/
|
||||||
|
public function getLocalizedId($languageId): string
|
||||||
|
{
|
||||||
|
return "{$this->id}_{$languageId}";
|
||||||
|
}
|
||||||
|
}
|
||||||
23
autoload/admin/ViewModels/Forms/FormFieldType.php
Normal file
23
autoload/admin/ViewModels/Forms/FormFieldType.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
namespace admin\ViewModels\Forms;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dostępne typy pól formularza
|
||||||
|
*/
|
||||||
|
class FormFieldType
|
||||||
|
{
|
||||||
|
public const TEXT = 'text';
|
||||||
|
public const NUMBER = 'number';
|
||||||
|
public const EMAIL = 'email';
|
||||||
|
public const PASSWORD = 'password';
|
||||||
|
public const DATE = 'date';
|
||||||
|
public const DATETIME = 'datetime';
|
||||||
|
public const SWITCH = 'switch';
|
||||||
|
public const SELECT = 'select';
|
||||||
|
public const TEXTAREA = 'textarea';
|
||||||
|
public const EDITOR = 'editor';
|
||||||
|
public const IMAGE = 'image';
|
||||||
|
public const FILE = 'file';
|
||||||
|
public const HIDDEN = 'hidden';
|
||||||
|
public const LANG_SECTION = 'lang_section';
|
||||||
|
}
|
||||||
31
autoload/admin/ViewModels/Forms/FormTab.php
Normal file
31
autoload/admin/ViewModels/Forms/FormTab.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
namespace admin\ViewModels\Forms;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definicja zakładki formularza
|
||||||
|
*/
|
||||||
|
class FormTab
|
||||||
|
{
|
||||||
|
public string $id;
|
||||||
|
public string $label;
|
||||||
|
public string $icon;
|
||||||
|
public ?string $parentTabId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $id Unikalny identyfikator zakładki
|
||||||
|
* @param string $label Etykieta wyświetlana
|
||||||
|
* @param string $icon Klasa FontAwesome (np. 'fa-wrench')
|
||||||
|
* @param string|null $parentTabId Identyfikator zakładki nadrzędnej (dla zagnieżdżenia)
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
string $id,
|
||||||
|
string $label,
|
||||||
|
string $icon = '',
|
||||||
|
?string $parentTabId = null
|
||||||
|
) {
|
||||||
|
$this->id = $id;
|
||||||
|
$this->label = $label;
|
||||||
|
$this->icon = $icon;
|
||||||
|
$this->parentTabId = $parentTabId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
# Wyłącz listowanie
|
# Wyłącz listowanie
|
||||||
Options -Indexes
|
Options -Indexes
|
||||||
|
|
||||||
|
# Zezwol na wykonywanie PHP tylko dla legacy filemanagera
|
||||||
|
SetEnvIf Request_URI "^/libraries/filemanager-9\.14\.[12]/.*\.(php|phtml|php[0-9]?|phar|pht)$" allow_legacy_filemanager_php=1
|
||||||
|
|
||||||
# Domyślnie blokujemy wszystko…
|
# Domyślnie blokujemy wszystko…
|
||||||
Require all denied
|
Require all denied
|
||||||
|
|
||||||
@@ -11,7 +14,10 @@ Require all denied
|
|||||||
|
|
||||||
# Twardo blokuj cokolwiek, co mogłoby się wykonać
|
# Twardo blokuj cokolwiek, co mogłoby się wykonać
|
||||||
<FilesMatch "\.(php|phtml|php[0-9]?|phar|pht|cgi|pl|py|sh)$">
|
<FilesMatch "\.(php|phtml|php[0-9]?|phar|pht|cgi|pl|py|sh)$">
|
||||||
Require all denied
|
<RequireAny>
|
||||||
|
Require env allow_legacy_filemanager_php
|
||||||
|
Require all denied
|
||||||
|
</RequireAny>
|
||||||
</FilesMatch>
|
</FilesMatch>
|
||||||
|
|
||||||
<Files "thumb.php">
|
<Files "thumb.php">
|
||||||
@@ -41,4 +47,4 @@ Require all denied
|
|||||||
# Nie serwuj plików ukrytych (.env itp.)
|
# Nie serwuj plików ukrytych (.env itp.)
|
||||||
<FilesMatch "^\.(.*)$">
|
<FilesMatch "^\.(.*)$">
|
||||||
Require all denied
|
Require all denied
|
||||||
</FilesMatch>
|
</FilesMatch>
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ class BannerRepositoryTest extends TestCase
|
|||||||
|
|
||||||
$mockDb->expects($this->once())
|
$mockDb->expects($this->once())
|
||||||
->method('select')
|
->method('select')
|
||||||
->with('pp_banners_langs', '*', ['id_banner' => 1])
|
->with('pp_banners_langs', '*', [
|
||||||
|
'id_banner' => 1,
|
||||||
|
'ORDER' => ['id' => 'ASC'],
|
||||||
|
])
|
||||||
->willReturn([
|
->willReturn([
|
||||||
['id_lang' => 'pl', 'src' => 'banner.jpg', 'url' => '/promo'],
|
['id_lang' => 'pl', 'src' => 'banner.jpg', 'url' => '/promo'],
|
||||||
['id_lang' => 'en', 'src' => 'banner-en.jpg', 'url' => '/promo-en'],
|
['id_lang' => 'en', 'src' => 'banner-en.jpg', 'url' => '/promo-en'],
|
||||||
@@ -80,7 +83,7 @@ class BannerRepositoryTest extends TestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test zapisywania nowego banera
|
* Test zapisywania nowego banera (stary format danych - zachowano kompatybilność)
|
||||||
*/
|
*/
|
||||||
public function testSaveInsertsNewBanner()
|
public function testSaveInsertsNewBanner()
|
||||||
{
|
{
|
||||||
@@ -100,9 +103,51 @@ class BannerRepositoryTest extends TestCase
|
|||||||
|
|
||||||
$repository = new BannerRepository($mockDb);
|
$repository = new BannerRepository($mockDb);
|
||||||
|
|
||||||
// Act
|
// Act - nowy format z FormRequestHandler (przetworzone dane)
|
||||||
$result = $repository->save([
|
$result = $repository->save([
|
||||||
'name' => 'Nowy baner',
|
'name' => 'Nowy baner',
|
||||||
|
'status' => 1, // już przetworzone na int
|
||||||
|
'date_start' => null,
|
||||||
|
'date_end' => null,
|
||||||
|
'home_page' => 1, // już przetworzone na int
|
||||||
|
'translations' => [
|
||||||
|
1 => [ // id języka jako klucz
|
||||||
|
'src' => 'banner.jpg',
|
||||||
|
'url' => '/promo',
|
||||||
|
'html' => '',
|
||||||
|
'text' => 'Tekst',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$this->assertEquals(10, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test zapisywania banera ze starym formatem danych (backward compatibility)
|
||||||
|
*/
|
||||||
|
public function testSaveWithLegacyFormat()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
|
||||||
|
// insert() wywoływane 2x: raz dla banera, raz dla tłumaczenia
|
||||||
|
$mockDb->expects($this->exactly(2))
|
||||||
|
->method('insert');
|
||||||
|
|
||||||
|
$mockDb->expects($this->once())
|
||||||
|
->method('id')
|
||||||
|
->willReturn(11);
|
||||||
|
|
||||||
|
// get() for checking existing translations - returns false (no existing)
|
||||||
|
$mockDb->method('get')->willReturn(false);
|
||||||
|
|
||||||
|
$repository = new BannerRepository($mockDb);
|
||||||
|
|
||||||
|
// Act - stary format (dla kompatybilności wstecznej)
|
||||||
|
$result = $repository->save([
|
||||||
|
'name' => 'Baner legacy',
|
||||||
'status' => 'on',
|
'status' => 'on',
|
||||||
'date_start' => '',
|
'date_start' => '',
|
||||||
'date_end' => '',
|
'date_end' => '',
|
||||||
@@ -114,6 +159,114 @@ class BannerRepositoryTest extends TestCase
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
$this->assertEquals(10, $result);
|
$this->assertEquals(11, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test zapisu istniejacego banera - aktualizacja tlumaczen po id_banner + id_lang
|
||||||
|
*/
|
||||||
|
public function testSaveUpdatesExistingTranslationsByBannerAndLang(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
|
||||||
|
$mockDb->expects($this->exactly(2))
|
||||||
|
->method('update')
|
||||||
|
->withConsecutive(
|
||||||
|
[
|
||||||
|
'pp_banners',
|
||||||
|
$this->arrayHasKey('name'),
|
||||||
|
['id' => 5],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'pp_banners_langs',
|
||||||
|
$this->callback(function (array $data): bool {
|
||||||
|
return $data['id_banner'] === 5
|
||||||
|
&& $data['id_lang'] === 'pl'
|
||||||
|
&& $data['src'] === 'banner-new.jpg';
|
||||||
|
}),
|
||||||
|
['AND' => ['id_banner' => 5, 'id_lang' => 'pl']],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$mockDb->expects($this->once())
|
||||||
|
->method('count')
|
||||||
|
->with('pp_banners_langs', ['AND' => ['id_banner' => 5, 'id_lang' => 'pl']])
|
||||||
|
->willReturn(2);
|
||||||
|
|
||||||
|
$mockDb->expects($this->never())
|
||||||
|
->method('insert');
|
||||||
|
|
||||||
|
$repository = new BannerRepository($mockDb);
|
||||||
|
|
||||||
|
$result = $repository->save([
|
||||||
|
'id' => 5,
|
||||||
|
'name' => 'Baner update',
|
||||||
|
'status' => 1,
|
||||||
|
'date_start' => null,
|
||||||
|
'date_end' => null,
|
||||||
|
'home_page' => 0,
|
||||||
|
'translations' => [
|
||||||
|
'pl' => [
|
||||||
|
'src' => 'banner-new.jpg',
|
||||||
|
'url' => '/promo-new',
|
||||||
|
'html' => '<b>promo</b>',
|
||||||
|
'text' => 'Nowa tresc',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertSame(5, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testListForAdminIncludesThumbnailSrc(): void
|
||||||
|
{
|
||||||
|
$mockDb = $this->createMock(\medoo::class);
|
||||||
|
|
||||||
|
$countStmt = $this->createMock(\PDOStatement::class);
|
||||||
|
$countStmt->expects($this->once())
|
||||||
|
->method('fetchAll')
|
||||||
|
->willReturn([[2]]);
|
||||||
|
|
||||||
|
$itemsStmt = $this->createMock(\PDOStatement::class);
|
||||||
|
$itemsStmt->expects($this->once())
|
||||||
|
->method('fetchAll')
|
||||||
|
->willReturn([
|
||||||
|
[
|
||||||
|
'id' => 10,
|
||||||
|
'name' => 'Baner A',
|
||||||
|
'status' => 1,
|
||||||
|
'home_page' => 0,
|
||||||
|
'date_start' => null,
|
||||||
|
'date_end' => null,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'id' => 11,
|
||||||
|
'name' => 'Baner B',
|
||||||
|
'status' => 1,
|
||||||
|
'home_page' => 1,
|
||||||
|
'date_start' => null,
|
||||||
|
'date_end' => null,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$thumbsStmt = $this->createMock(\PDOStatement::class);
|
||||||
|
$thumbsStmt->expects($this->once())
|
||||||
|
->method('fetchAll')
|
||||||
|
->willReturn([
|
||||||
|
['id_banner' => 10, 'src' => '/uploads/banner-a.jpg'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$mockDb->expects($this->exactly(3))
|
||||||
|
->method('query')
|
||||||
|
->willReturnOnConsecutiveCalls($countStmt, $itemsStmt, $thumbsStmt);
|
||||||
|
|
||||||
|
$repository = new BannerRepository($mockDb);
|
||||||
|
|
||||||
|
$result = $repository->listForAdmin([], 'name', 'ASC', 1, 15);
|
||||||
|
|
||||||
|
$this->assertSame(2, $result['total']);
|
||||||
|
$this->assertCount(2, $result['items']);
|
||||||
|
$this->assertSame('/uploads/banner-a.jpg', $result['items'][0]['thumbnail_src']);
|
||||||
|
$this->assertSame('', $result['items'][1]['thumbnail_src']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
updates/0.20/ver_0.249.zip
Normal file
BIN
updates/0.20/ver_0.249.zip
Normal file
Binary file not shown.
0
updates/0.20/ver_0.249_files.txt
Normal file
0
updates/0.20/ver_0.249_files.txt
Normal file
@@ -1,4 +1,10 @@
|
|||||||
<b>ver. 0.248</b><br />
|
<b>ver. 0.249</b><br />
|
||||||
|
- FIX - banner edit: poprawiono zapisywanie danych jezykowych i synchronizacje CKEditor przed zapisem
|
||||||
|
- FIX - banner edit: naprawiono hash zakladek (usunieto `undefined` w URL)
|
||||||
|
- FIX - filemanager: przywrocono dzialanie popupa wyboru obrazka z banera
|
||||||
|
- UPDATE - komunikaty zapisu w nowym formularzu sa wyswietlane w stylu panelu (bez natywnego alertu JS)
|
||||||
|
- UPDATE - lista banerow: dodano kolumne miniatury oraz podglad duzego obrazka w popup po najechaniu
|
||||||
|
<hr><b>ver. 0.248</b><br />
|
||||||
- UPDATE - filtry w nowych tabelach dzialaja automatycznie na `onchange`
|
- UPDATE - filtry w nowych tabelach dzialaja automatycznie na `onchange`
|
||||||
- UPDATE - `components/table-list`: auto-submit formularza filtrow po zmianie pola (select, date, text)
|
- UPDATE - `components/table-list`: auto-submit formularza filtrow po zmianie pola (select, date, text)
|
||||||
<hr><b>ver. 0.247</b><br />
|
<hr><b>ver. 0.247</b><br />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?
|
<?
|
||||||
$current_ver = 248;
|
$current_ver = 249;
|
||||||
|
|
||||||
for ($i = 1; $i <= $current_ver; $i++)
|
for ($i = 1; $i <= $current_ver; $i++)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user