feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
Some checks are pending
Module sub-split / Sub-split (push) Waiting to run
- Base: wintercms/winter branch 1.2 (full framework) - Theme vivespos: Canvas 7 + Bootstrap 5 CDN, custom CSS - Layout: deferred GTM/GA4 tracking, JSON-LD SoftwareApplication - Partials: hero (offline-first), features, modes (offline/nube toggle), screenshots, pricing (3 planes), comparison, FAQ, CTA - Plugin VivesPOS.Site with ContactForm - Dockerfile: PHP 8.2 Apache, port 80, healthcheck - Added winter/wn-pages, blog, sitemap, seo plugins - Active theme set to vivespos
This commit is contained in:
998
modules/backend/behaviors/FormController.php
Normal file
998
modules/backend/behaviors/FormController.php
Normal file
@@ -0,0 +1,998 @@
|
||||
<?php namespace Backend\Behaviors;
|
||||
|
||||
use Db;
|
||||
use Str;
|
||||
use Lang;
|
||||
use Flash;
|
||||
use Event;
|
||||
use Redirect;
|
||||
use Backend;
|
||||
use Backend\Classes\ControllerBehavior;
|
||||
use Winter\Storm\Router\Helper as RouterHelper;
|
||||
use ApplicationException;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Adds features for working with backend forms. This behavior
|
||||
* will inject CRUD actions to the controller -- including create,
|
||||
* update and preview -- along with some relevant AJAX handlers.
|
||||
*
|
||||
* Each action supports a custom context code, allowing fields
|
||||
* to be displayed or hidden on a contextual basis, as specified
|
||||
* by the form field definitions or some other custom logic.
|
||||
*
|
||||
* This behavior is implemented in the controller like so:
|
||||
*
|
||||
* public $implement = [
|
||||
* \Backend\Behaviors\FormController::class,
|
||||
* ];
|
||||
*
|
||||
* public $formConfig = 'config_form.yaml';
|
||||
*
|
||||
* The `$formConfig` property makes reference to the form configuration
|
||||
* values as either a YAML file, located in the controller view directory,
|
||||
* or directly as a PHP array.
|
||||
*
|
||||
* @see https://wintercms.com/docs/backend/forms Back-end form documentation
|
||||
* @package winter\wn-backend-module
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
*/
|
||||
class FormController extends ControllerBehavior
|
||||
{
|
||||
use \Backend\Traits\FormModelSaver;
|
||||
|
||||
/**
|
||||
* @var string Default context for "create" pages.
|
||||
*/
|
||||
const CONTEXT_CREATE = 'create';
|
||||
|
||||
/**
|
||||
* @var string Default context for "update" pages.
|
||||
*/
|
||||
const CONTEXT_UPDATE = 'update';
|
||||
|
||||
/**
|
||||
* @var string Default context for "preview" pages.
|
||||
*/
|
||||
const CONTEXT_PREVIEW = 'preview';
|
||||
|
||||
/**
|
||||
* @var \Backend\Classes\Controller|FormController Reference to the back end controller.
|
||||
*/
|
||||
protected $controller;
|
||||
|
||||
/**
|
||||
* @var \Backend\Widgets\Form Reference to the widget object.
|
||||
*/
|
||||
protected $formWidget;
|
||||
|
||||
/**
|
||||
* @var array Configuration values that must exist when applying the primary config file.
|
||||
* - modelClass: Class name for the model
|
||||
* - form: Form field definitions
|
||||
*/
|
||||
protected $requiredConfig = ['modelClass', 'form'];
|
||||
|
||||
/**
|
||||
* @var array Visible actions in context of the controller
|
||||
*/
|
||||
protected $actions = ['create', 'update', 'preview'];
|
||||
|
||||
/**
|
||||
* @var string The context to pass to the form widget.
|
||||
*/
|
||||
protected $context;
|
||||
|
||||
/**
|
||||
* @var \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model The initialized model used by the form.
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* @var mixed Configuration for this behaviour
|
||||
*/
|
||||
public $formConfig = 'config_form.yaml';
|
||||
|
||||
/**
|
||||
* Behavior constructor
|
||||
* @param \Backend\Classes\Controller $controller
|
||||
*/
|
||||
public function __construct($controller)
|
||||
{
|
||||
parent::__construct($controller);
|
||||
|
||||
/*
|
||||
* Build configuration
|
||||
*/
|
||||
$this->config = $this->makeConfig($controller->formConfig ?: $this->formConfig, $this->requiredConfig);
|
||||
$this->config->modelClass = Str::normalizeClassName($this->config->modelClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the form configuration against a model and context value.
|
||||
* This will process the configuration found in the `$formConfig` property
|
||||
* and prepare the Form widget, which is the underlying tool used for
|
||||
* actually rendering the form. The model used by this form is passed
|
||||
* to this behavior via this method as the first argument.
|
||||
*
|
||||
* @see \Backend\Widgets\Form
|
||||
* @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model $model
|
||||
* @param string $context Form context
|
||||
* @return void
|
||||
*/
|
||||
public function initForm($model, $context = null)
|
||||
{
|
||||
$context = $this->context = $context ?? $this->formGetContext();
|
||||
|
||||
/*
|
||||
* Each page can supply a unique form definition, if desired
|
||||
*/
|
||||
$formFields = $this->getConfig("{$context}[form]", $this->config->form);
|
||||
|
||||
$config = $this->makeConfig($formFields);
|
||||
$config->model = $model;
|
||||
$config->arrayName = class_basename($model);
|
||||
$config->context = $context;
|
||||
|
||||
/*
|
||||
* Form Widget with extensibility
|
||||
*/
|
||||
$this->formWidget = $this->makeWidget('Backend\Widgets\Form', $config);
|
||||
|
||||
// Setup the default preview mode on form initialization if the context is preview
|
||||
if ($config->context === 'preview') {
|
||||
$this->formWidget->previewMode = true;
|
||||
}
|
||||
|
||||
$this->formWidget->bindEvent('form.extendFieldsBefore', function () {
|
||||
$this->controller->formExtendFieldsBefore($this->formWidget);
|
||||
});
|
||||
|
||||
$this->formWidget->bindEvent('form.extendFields', function ($fields) {
|
||||
$this->controller->formExtendFields($this->formWidget, $fields);
|
||||
});
|
||||
|
||||
$this->formWidget->bindEvent('form.beforeRefresh', function ($holder) {
|
||||
$result = $this->controller->formExtendRefreshData($this->formWidget, $holder->data);
|
||||
if (is_array($result)) {
|
||||
$holder->data = $result;
|
||||
}
|
||||
});
|
||||
|
||||
$this->formWidget->bindEvent('form.refreshFields', function ($fields) {
|
||||
return $this->controller->formExtendRefreshFields($this->formWidget, $fields);
|
||||
});
|
||||
|
||||
$this->formWidget->bindEvent('form.refresh', function ($result) {
|
||||
return $this->controller->formExtendRefreshResults($this->formWidget, $result);
|
||||
});
|
||||
|
||||
$this->formWidget->bindToController();
|
||||
|
||||
/*
|
||||
* Detected Relation controller behavior
|
||||
*/
|
||||
if ($this->controller->isClassExtendedWith(\Backend\Behaviors\RelationController::class)) {
|
||||
$this->controller->initRelation(clone $model);
|
||||
}
|
||||
|
||||
$this->prepareVars($model);
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares commonly used view data.
|
||||
* @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model $model
|
||||
*/
|
||||
protected function prepareVars($model)
|
||||
{
|
||||
$this->controller->vars['formModel'] = $model;
|
||||
$this->controller->vars['formConfig'] = $this->getConfig();
|
||||
$this->controller->vars['formContext'] = $this->formGetContext();
|
||||
$this->controller->vars['formController'] = $this;
|
||||
$this->controller->vars['formRecordName'] = Lang::get($this->getConfig('name', 'backend::lang.model.name'));
|
||||
}
|
||||
|
||||
//
|
||||
// Create
|
||||
//
|
||||
|
||||
/**
|
||||
* Controller "create" action used for creating new model records.
|
||||
*
|
||||
* @param string $context Form context
|
||||
* @return void
|
||||
*/
|
||||
public function create($context = null)
|
||||
{
|
||||
try {
|
||||
$this->context = strlen($context) ? $context : $this->getConfig('create[context]', self::CONTEXT_CREATE);
|
||||
$this->controller->pageTitle = $this->controller->pageTitle ?: $this->getLang(
|
||||
"{$this->context}[title]",
|
||||
'backend::lang.form.create_title'
|
||||
);
|
||||
|
||||
$model = $this->controller->formCreateModelObject();
|
||||
$model = $this->controller->formExtendModel($model) ?: $model;
|
||||
|
||||
$this->initForm($model);
|
||||
}
|
||||
catch (Exception $ex) {
|
||||
$this->controller->handleError($ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler "onSave" called from the create action and
|
||||
* primarily used for creating new records.
|
||||
*
|
||||
* This handler will invoke the unique controller overrides
|
||||
* `formBeforeCreate` and `formAfterCreate`.
|
||||
*
|
||||
* @param string $context Form context
|
||||
* @return \Illuminate\Http\RedirectResponse|void
|
||||
*/
|
||||
public function create_onSave($context = null)
|
||||
{
|
||||
$this->context = strlen($context) ? $context : $this->getConfig('create[context]', self::CONTEXT_CREATE);
|
||||
|
||||
$model = $this->controller->formCreateModelObject();
|
||||
$model = $this->controller->formExtendModel($model) ?: $model;
|
||||
|
||||
$this->initForm($model);
|
||||
|
||||
$this->controller->formBeforeSave($model);
|
||||
$this->controller->formBeforeCreate($model);
|
||||
|
||||
$modelsToSave = $this->prepareModelsToSave($model, $this->formWidget->getSaveData());
|
||||
Db::transaction(function () use ($modelsToSave) {
|
||||
foreach ($modelsToSave as $modelToSave) {
|
||||
$modelToSave->save(null, $this->formWidget->getSessionKey());
|
||||
}
|
||||
});
|
||||
|
||||
$this->controller->formAfterSave($model);
|
||||
$this->controller->formAfterCreate($model);
|
||||
|
||||
Flash::success($this->getLang("{$this->context}[flashSave]", 'backend::lang.form.create_success'));
|
||||
|
||||
if ($redirect = $this->makeRedirect($this->context, $model)) {
|
||||
return $redirect;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Update
|
||||
//
|
||||
|
||||
/**
|
||||
* Controller "update" action used for updating existing model records.
|
||||
* This action takes a record identifier (primary key of the model)
|
||||
* to locate the record used for sourcing the existing form values.
|
||||
*
|
||||
* @param int $recordId Record identifier
|
||||
* @param string $context Form context
|
||||
* @return void
|
||||
*/
|
||||
public function update($recordId = null, $context = null)
|
||||
{
|
||||
try {
|
||||
$this->context = strlen($context) ? $context : $this->getConfig('update[context]', self::CONTEXT_UPDATE);
|
||||
$this->controller->pageTitle = $this->controller->pageTitle ?: $this->getLang(
|
||||
"{$this->context}[title]",
|
||||
'backend::lang.form.update_title'
|
||||
);
|
||||
|
||||
$model = $this->controller->formFindModelObject($recordId);
|
||||
$this->initForm($model);
|
||||
}
|
||||
catch (Exception $ex) {
|
||||
$this->controller->handleError($ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler "onSave" called from the update action and
|
||||
* primarily used for updating existing records.
|
||||
*
|
||||
* This handler will invoke the unique controller overrides
|
||||
* `formBeforeUpdate` and `formAfterUpdate`.
|
||||
*
|
||||
* @param int $recordId Record identifier
|
||||
* @param string $context Form context
|
||||
* @return \Illuminate\Http\RedirectResponse|void
|
||||
* @throws \Winter\Storm\Exception\ApplicationException if the provided recordId is not found
|
||||
*/
|
||||
public function update_onSave($recordId = null, $context = null)
|
||||
{
|
||||
$this->context = strlen($context) ? $context : $this->getConfig('update[context]', self::CONTEXT_UPDATE);
|
||||
$model = $this->controller->formFindModelObject($recordId);
|
||||
$this->initForm($model);
|
||||
|
||||
$this->controller->formBeforeSave($model);
|
||||
$this->controller->formBeforeUpdate($model);
|
||||
|
||||
$modelsToSave = $this->prepareModelsToSave($model, $this->formWidget->getSaveData());
|
||||
Db::transaction(function () use ($modelsToSave) {
|
||||
foreach ($modelsToSave as $modelToSave) {
|
||||
$modelToSave->save(null, $this->formWidget->getSessionKey());
|
||||
}
|
||||
});
|
||||
|
||||
$this->controller->formAfterSave($model);
|
||||
$this->controller->formAfterUpdate($model);
|
||||
|
||||
Flash::success($this->getLang("{$this->context}[flashSave]", 'backend::lang.form.update_success'));
|
||||
|
||||
if ($redirect = $this->makeRedirect($this->context, $model)) {
|
||||
return $redirect;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler "onDelete" called from the update action and
|
||||
* used for deleting existing records.
|
||||
*
|
||||
* This handler will invoke the unique controller override
|
||||
* `formAfterDelete`.
|
||||
*
|
||||
* @param int $recordId Record identifier
|
||||
* @return \Illuminate\Http\RedirectResponse|void
|
||||
* @throws \Winter\Storm\Exception\ApplicationException if the provided recordId is not found
|
||||
* @throws Exception if there is no primary key on the model
|
||||
*/
|
||||
public function update_onDelete($recordId = null)
|
||||
{
|
||||
$this->context = $this->getConfig('update[context]', self::CONTEXT_UPDATE);
|
||||
$model = $this->controller->formFindModelObject($recordId);
|
||||
$this->initForm($model);
|
||||
|
||||
$model->delete();
|
||||
|
||||
$this->controller->formAfterDelete($model);
|
||||
|
||||
Flash::success($this->getLang("{$this->context}[flashDelete]", 'backend::lang.form.delete_success'));
|
||||
|
||||
if ($redirect = $this->makeRedirect('delete', $model)) {
|
||||
return $redirect;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Preview
|
||||
//
|
||||
|
||||
/**
|
||||
* Controller "preview" action used for viewing existing model records.
|
||||
* This action takes a record identifier (primary key of the model)
|
||||
* to locate the record used for sourcing the existing preview data.
|
||||
*
|
||||
* @param int $recordId Record identifier
|
||||
* @param string $context Form context
|
||||
* @return void
|
||||
*/
|
||||
public function preview($recordId = null, $context = null)
|
||||
{
|
||||
try {
|
||||
$this->context = strlen($context) ? $context : $this->getConfig('preview[context]', self::CONTEXT_PREVIEW);
|
||||
$this->controller->pageTitle = $this->controller->pageTitle ?: $this->getLang(
|
||||
"{$this->context}[title]",
|
||||
'backend::lang.form.preview_title'
|
||||
);
|
||||
|
||||
$model = $this->controller->formFindModelObject($recordId);
|
||||
$this->initForm($model);
|
||||
}
|
||||
catch (Exception $ex) {
|
||||
$this->controller->handleError($ex);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Utils
|
||||
//
|
||||
|
||||
/**
|
||||
* Method to render the prepared form markup. This method is usually
|
||||
* called from a view file.
|
||||
*
|
||||
* <?= $this->formRender() ?>
|
||||
*
|
||||
* The first argument supports an array of render options. The supported
|
||||
* options can be found via the `render` method of the Form widget class.
|
||||
*
|
||||
* <?= $this->formRender(['preview' => true, section' => 'primary']) ?>
|
||||
*
|
||||
* @see \Backend\Widgets\Form
|
||||
* @param array $options Render options
|
||||
* @return string Rendered HTML for the form.
|
||||
* @throws \Winter\Storm\Exception\ApplicationException if the Form Widget isn't set
|
||||
*/
|
||||
public function formRender($options = [])
|
||||
{
|
||||
if (!$this->formWidget) {
|
||||
throw new ApplicationException(Lang::get('backend::lang.form.behavior_not_ready'));
|
||||
}
|
||||
|
||||
return $this->formWidget->render($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the model initialized by this form behavior.
|
||||
* The model will be provided by one of the page actions or AJAX
|
||||
* handlers via the `initForm` method.
|
||||
*
|
||||
* @return \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model
|
||||
*/
|
||||
public function formGetModel()
|
||||
{
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the active form context, either obtained from the postback
|
||||
* variable called `form_context` or detected from the configuration,
|
||||
* or routing parameters.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function formGetContext()
|
||||
{
|
||||
return $this->context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method used to prepare the form model object.
|
||||
*
|
||||
* @return \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model
|
||||
*/
|
||||
protected function createModel()
|
||||
{
|
||||
$class = $this->config->modelClass;
|
||||
return new $class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Redirect object based on supplied context and parses
|
||||
* the model primary key.
|
||||
*
|
||||
* @param string $context Redirect context, eg: create, update, delete
|
||||
* @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model $model The active model to parse in it's ID and attributes.
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function makeRedirect($context = null, $model = null)
|
||||
{
|
||||
$redirectUrl = null;
|
||||
if (post('close') && !ends_with($context, '-close')) {
|
||||
$context .= '-close';
|
||||
}
|
||||
|
||||
if (post('refresh', false)) {
|
||||
return Redirect::refresh();
|
||||
}
|
||||
|
||||
if (post('new', false)) {
|
||||
return Redirect::to($this->controller->actionUrl('create'));
|
||||
}
|
||||
|
||||
if (post('redirect', true)) {
|
||||
$redirectUrl = $this->controller->formGetRedirectUrl($context, $model);
|
||||
}
|
||||
|
||||
if ($model && $redirectUrl) {
|
||||
$redirectUrl = RouterHelper::replaceParameters($model, $redirectUrl);
|
||||
}
|
||||
|
||||
if (starts_with($redirectUrl, 'http://') || starts_with($redirectUrl, 'https://')) {
|
||||
// Process absolute redirects
|
||||
$redirect = Redirect::to($redirectUrl);
|
||||
} else {
|
||||
// Process relative redirects
|
||||
$redirect = $redirectUrl ? Backend::redirect($redirectUrl) : null;
|
||||
}
|
||||
|
||||
return $redirect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a redirect URL from the config based on supplied context.
|
||||
* Otherwise the default redirect is used. Relative URLs are treated as
|
||||
* backend URLs.
|
||||
*
|
||||
* @param string $context Redirect context, eg: create, update, delete.
|
||||
* @param Model $model The active model.
|
||||
* @return string
|
||||
*/
|
||||
public function formGetRedirectUrl($context = null, $model = null)
|
||||
{
|
||||
$redirectContext = explode('-', $context, 2)[0];
|
||||
$redirectSource = ends_with($context, '-close') ? 'redirectClose' : 'redirect';
|
||||
|
||||
// Get the redirect for the provided context
|
||||
$redirects = [$context => $this->getConfig("{$redirectContext}[{$redirectSource}]", '')];
|
||||
|
||||
// Assign the default redirect afterwards to prevent the
|
||||
// source for the default redirect being default[redirect]
|
||||
$redirects['default'] = $this->getConfig('defaultRedirect', '');
|
||||
|
||||
if (empty($redirects[$context])) {
|
||||
return $redirects['default'];
|
||||
}
|
||||
|
||||
return $redirects[$context];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses in some default variables to a language string defined in config.
|
||||
*
|
||||
* @param string $name Configuration property containing the language string
|
||||
* @param string $default A default language string to use if the config is not found
|
||||
* @param array $extras Any extra params to include in the language string variables
|
||||
* @return string The translated string.
|
||||
*/
|
||||
protected function getLang($name, $default = null, $extras = [])
|
||||
{
|
||||
$name = $this->getConfig($name, $default);
|
||||
$vars = [
|
||||
'name' => Lang::get($this->getConfig('name', 'backend::lang.model.name'))
|
||||
];
|
||||
$vars = array_merge($vars, $extras);
|
||||
return Lang::get($name, $vars);
|
||||
}
|
||||
|
||||
//
|
||||
// Pass-through Helpers
|
||||
//
|
||||
|
||||
/**
|
||||
* View helper to render a single form field.
|
||||
*
|
||||
* <?= $this->formRenderField('field_name') ?>
|
||||
*
|
||||
* @param string $name Field name
|
||||
* @param array $options (e.g. ['useContainer'=>false])
|
||||
* @return string HTML markup
|
||||
*/
|
||||
public function formRenderField($name, $options = [])
|
||||
{
|
||||
return $this->formWidget->renderField($name, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* View helper to render the form in preview mode.
|
||||
*
|
||||
* <?= $this->formRenderPreview() ?>
|
||||
*
|
||||
* @return string The form HTML markup.
|
||||
* @throws \Winter\Storm\Exception\ApplicationException if the Form Widget isn't set
|
||||
*/
|
||||
public function formRenderPreview()
|
||||
{
|
||||
return $this->formRender(['preview' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* View helper to check if a form tab has fields in the
|
||||
* non-tabbed section (outside fields).
|
||||
*
|
||||
* <?php if ($this->formHasOutsideFields()): ?>
|
||||
* <!-- Do something -->
|
||||
* <?php endif ?>
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function formHasOutsideFields()
|
||||
{
|
||||
return $this->formWidget->getTab('outside')->hasFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* View helper to render the form fields belonging to the
|
||||
* non-tabbed section (outside form fields).
|
||||
*
|
||||
* <?= $this->formRenderOutsideFields() ?>
|
||||
*
|
||||
* @return string HTML markup
|
||||
* @throws \Winter\Storm\Exception\ApplicationException if the Form Widget isn't set
|
||||
*/
|
||||
public function formRenderOutsideFields()
|
||||
{
|
||||
return $this->formRender(['section' => 'outside']);
|
||||
}
|
||||
|
||||
/**
|
||||
* View helper to check if a form tab has fields in the
|
||||
* primary tab section.
|
||||
*
|
||||
* <?php if ($this->formHasPrimaryTabs()): ?>
|
||||
* <!-- Do something -->
|
||||
* <?php endif ?>
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function formHasPrimaryTabs()
|
||||
{
|
||||
return $this->formWidget->getTab('primary')->hasFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* View helper to render the form fields belonging to the
|
||||
* primary tabs section.
|
||||
*
|
||||
* <?= $this->formRenderPrimaryTabs() ?>
|
||||
*
|
||||
* @return string HTML markup
|
||||
* @throws \Winter\Storm\Exception\ApplicationException if the Form Widget isn't set
|
||||
*/
|
||||
public function formRenderPrimaryTabs()
|
||||
{
|
||||
return $this->formRender(['section' => 'primary']);
|
||||
}
|
||||
|
||||
/**
|
||||
* View helper to check if a form tab has fields in the
|
||||
* secondary tab section.
|
||||
*
|
||||
* <?php if ($this->formHasSecondaryTabs()): ?>
|
||||
* <!-- Do something -->
|
||||
* <?php endif ?>
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function formHasSecondaryTabs()
|
||||
{
|
||||
return $this->formWidget->getTab('secondary')->hasFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* View helper to render the form fields belonging to the
|
||||
* secondary tabs section.
|
||||
*
|
||||
* <?= $this->formRenderSecondaryTabs() ?>
|
||||
*
|
||||
* @return string HTML markup
|
||||
* @throws \Winter\Storm\Exception\ApplicationException if the Form Widget isn't set
|
||||
*/
|
||||
public function formRenderSecondaryTabs()
|
||||
{
|
||||
return $this->formRender(['section' => 'secondary']);
|
||||
}
|
||||
|
||||
/**
|
||||
* View helper to render the previous/next record navigation for the record
|
||||
* being edited, relative to its sibling records in the controller's list.
|
||||
*
|
||||
* <?= $this->formRenderRecordNavigation() ?>
|
||||
*
|
||||
* Renders nothing unless the `recordNavigation` config is enabled (the
|
||||
* default), the controller also implements the ListController behavior, and
|
||||
* an existing record is being viewed.
|
||||
*
|
||||
* @return string HTML markup (empty string when navigation is unavailable)
|
||||
*/
|
||||
public function formRenderRecordNavigation(): string
|
||||
{
|
||||
$navigation = $this->formGetRecordNavigation();
|
||||
if ($navigation === null || $navigation['current'] === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->formMakePartial('record_navigation', [
|
||||
'navigation' => $navigation,
|
||||
'navigationContext' => $this->context,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the position of the current record within the controller's list
|
||||
* and the neighboring record keys used for previous/next navigation.
|
||||
*
|
||||
* The sibling set comes from the ListController's prepared query, so it
|
||||
* reflects the active filters, search and sorting exactly as the user left
|
||||
* the list. Ordered keys are read with a single portable `pluck` and the
|
||||
* position is resolved in PHP — no driver-specific SQL — so it behaves
|
||||
* identically across every database Winter supports.
|
||||
*
|
||||
* @param \Winter\Storm\Database\Model|null $model
|
||||
* @return array{previous: mixed, next: mixed, current: int|null, total: int}|null
|
||||
*/
|
||||
public function formGetRecordNavigation($model = null): ?array
|
||||
{
|
||||
if (!$this->getConfig('recordNavigation', true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$model = $model ?: $this->model;
|
||||
if (!$model || !$model->exists) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$this->controller->isClassExtendedWith(\Backend\Behaviors\ListController::class)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->controller->makeLists();
|
||||
$listWidget = $this->controller->listGetWidget();
|
||||
if (!$listWidget) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$keys = $listWidget->prepareQuery()->pluck($model->getQualifiedKeyName())->all();
|
||||
|
||||
return static::resolveRecordPosition($keys, $model->getKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure position math for record navigation: given an ordered list of record
|
||||
* keys and the current key, returns the navigation descriptor. Performs no
|
||||
* database access, so it is trivially unit-testable and identical on every
|
||||
* driver.
|
||||
*
|
||||
* @param array<int, mixed> $keys Ordered list of record keys.
|
||||
* @param mixed $currentKey The key of the record being viewed.
|
||||
* @return array{previous: mixed, next: mixed, current: int|null, total: int}
|
||||
*/
|
||||
public static function resolveRecordPosition(array $keys, $currentKey): array
|
||||
{
|
||||
$keys = array_values($keys);
|
||||
$total = count($keys);
|
||||
|
||||
$position = null;
|
||||
foreach ($keys as $index => $key) {
|
||||
if ((string) $key === (string) $currentKey) {
|
||||
$position = $index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'previous' => ($position !== null && $position > 0) ? $keys[$position - 1] : null,
|
||||
'next' => ($position !== null && $position < $total - 1) ? $keys[$position + 1] : null,
|
||||
'current' => $position === null ? null : $position + 1,
|
||||
'total' => $total,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the form widget used by this behavior.
|
||||
*
|
||||
* @return \Backend\Widgets\Form
|
||||
*/
|
||||
public function formGetWidget()
|
||||
{
|
||||
return $this->formWidget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a unique ID for the form widget used by this behavior.
|
||||
* This is useful for dealing with identifiers in the markup.
|
||||
*
|
||||
* <div id="<?= $this->formGetId()">...</div>
|
||||
*
|
||||
* A suffix may be used passed as the first argument to reuse
|
||||
* the identifier in other areas.
|
||||
*
|
||||
* <button id="<?= $this->formGetId('button')">...</button>
|
||||
*
|
||||
* @param string $suffix
|
||||
* @return string
|
||||
*/
|
||||
public function formGetId($suffix = null)
|
||||
{
|
||||
return $this->formWidget->getId($suffix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get the form session key.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function formGetSessionKey()
|
||||
{
|
||||
return $this->formWidget->getSessionKey();
|
||||
}
|
||||
|
||||
//
|
||||
// Overrides
|
||||
//
|
||||
|
||||
/**
|
||||
* Called before the creation or updating form is saved.
|
||||
* @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model
|
||||
*/
|
||||
public function formBeforeSave($model)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after the creation or updating form is saved.
|
||||
* @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model
|
||||
*/
|
||||
public function formAfterSave($model)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before the creation form is saved.
|
||||
* @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model
|
||||
*/
|
||||
public function formBeforeCreate($model)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after the creation form is saved.
|
||||
* @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model
|
||||
*/
|
||||
public function formAfterCreate($model)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before the updating form is saved.
|
||||
* @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model
|
||||
*/
|
||||
public function formBeforeUpdate($model)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after the updating form is saved.
|
||||
* @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model
|
||||
*/
|
||||
public function formAfterUpdate($model)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after the form model is deleted.
|
||||
* @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model
|
||||
*/
|
||||
public function formAfterDelete($model)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a Model record by its primary identifier, used by update actions. This logic
|
||||
* can be changed by overriding it in the controller.
|
||||
* @param string $recordId
|
||||
* @return \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model
|
||||
* @throws \Winter\Storm\Exception\ApplicationException if the provided recordId is not found
|
||||
*/
|
||||
public function formFindModelObject($recordId)
|
||||
{
|
||||
if (!strlen($recordId)) {
|
||||
throw new ApplicationException($this->getLang('not-found-message', 'backend::lang.form.missing_id'));
|
||||
}
|
||||
|
||||
$model = $this->controller->formCreateModelObject();
|
||||
|
||||
/*
|
||||
* Prepare query and find model record
|
||||
*/
|
||||
$query = $model->newQuery();
|
||||
$this->controller->formExtendQuery($query);
|
||||
$result = $query->find($recordId);
|
||||
|
||||
if (!$result) {
|
||||
throw new ApplicationException($this->getLang('not-found-message', 'backend::lang.form.not_found', [
|
||||
'class' => get_class($model), 'id' => $recordId
|
||||
]));
|
||||
}
|
||||
|
||||
$result = $this->controller->formExtendModel($result) ?: $result;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance of a form model. This logic can be changed
|
||||
* by overriding it in the controller.
|
||||
* @return \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model
|
||||
*/
|
||||
public function formCreateModelObject()
|
||||
{
|
||||
return $this->createModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before the form fields are defined.
|
||||
* @param \Backend\Widgets\Form $host The hosting form widget
|
||||
* @return void
|
||||
*/
|
||||
public function formExtendFieldsBefore($host)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after the form fields are defined.
|
||||
* @param \Backend\Widgets\Form $host The hosting form widget
|
||||
* @param array $fields Array of all defined form field objects (\Backend\Classes\FormField)
|
||||
* @return void
|
||||
*/
|
||||
public function formExtendFields($host, $fields)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before the form is refreshed, should return an array of additional save data.
|
||||
* @param \Backend\Widgets\Form $host The hosting form widget
|
||||
* @param array $saveData Current save data
|
||||
* @return array|void
|
||||
*/
|
||||
public function formExtendRefreshData($host, $saveData)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the form is refreshed, giving the opportunity to modify the form fields.
|
||||
* @param \Backend\Widgets\Form $host The hosting form widget
|
||||
* @param array $fields Current form fields
|
||||
* @return array|void
|
||||
*/
|
||||
public function formExtendRefreshFields($host, $fields)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after the form is refreshed, should return an array of additional result parameters.
|
||||
* @param \Backend\Widgets\Form $host The hosting form widget
|
||||
* @param array $result Current result parameters.
|
||||
* @return array|void
|
||||
*/
|
||||
public function formExtendRefreshResults($host, $result)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend supplied model used by create and update actions, the model can
|
||||
* be altered by overriding it in the controller.
|
||||
* @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model $model
|
||||
* @return \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model|void
|
||||
*/
|
||||
public function formExtendModel($model)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the query used for finding the form model. Extra conditions
|
||||
* can be applied to the query, for example, $query->withTrashed();
|
||||
* @param \Winter\Storm\Database\Builder|\Winter\Storm\Halcyon\Builder $query
|
||||
* @return void
|
||||
*/
|
||||
public function formExtendQuery($query)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper for extending form fields.
|
||||
* @param callable $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function extendFormFields($callback)
|
||||
{
|
||||
$calledClass = self::getCalledExtensionClass();
|
||||
Event::listen('backend.form.extendFields', function ($widget) use ($calledClass, $callback) {
|
||||
if (!is_a($widget->getController(), $calledClass)) {
|
||||
return;
|
||||
}
|
||||
call_user_func_array($callback, [$widget, $widget->model, $widget->getContext()]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller accessor for making partials within this behavior.
|
||||
*/
|
||||
public function formMakePartial(string $partial, array $params = []): string
|
||||
{
|
||||
$contents = $this->controller->makePartial('form_' . $this->context . '_' . $partial, $params + $this->vars, false);
|
||||
if (!$contents) {
|
||||
$contents = $this->controller->makePartial('form_' . $partial, $params + $this->vars, false);
|
||||
}
|
||||
if (!$contents) {
|
||||
$contents = $this->makePartial($partial, $params);
|
||||
}
|
||||
|
||||
return $contents;
|
||||
}
|
||||
}
|
||||
873
modules/backend/behaviors/ImportExportController.php
Normal file
873
modules/backend/behaviors/ImportExportController.php
Normal file
@@ -0,0 +1,873 @@
|
||||
<?php
|
||||
|
||||
namespace Backend\Behaviors;
|
||||
|
||||
use Backend\Behaviors\ImportExportController\TranscodeFilter;
|
||||
use Backend\Classes\Controller;
|
||||
use Backend\Classes\ControllerBehavior;
|
||||
use Backend\Classes\WidgetBase;
|
||||
use Backend\Facades\Backend;
|
||||
use Backend\Facades\BackendAuth;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\MassAssignmentException;
|
||||
use Illuminate\Support\Facades\Lang;
|
||||
use Illuminate\Support\Facades\Response;
|
||||
use League\Csv\EscapeFormula as CsvEscapeFormula;
|
||||
use League\Csv\Reader as CsvReader;
|
||||
use League\Csv\Statement as CsvStatement;
|
||||
use League\Csv\Writer as CsvWriter;
|
||||
use SplTempFileObject;
|
||||
use Winter\Storm\Database\Model;
|
||||
use Winter\Storm\Exception\ApplicationException;
|
||||
use Winter\Storm\Support\Str;
|
||||
|
||||
/**
|
||||
* Adds features for importing and exporting data.
|
||||
*
|
||||
* This behavior is implemented in the controller like so:
|
||||
*
|
||||
* public $implement = [
|
||||
* \Backend\Behaviors\ImportExportController::class,
|
||||
* ];
|
||||
*
|
||||
* public $importExportConfig = 'config_import_export.yaml';
|
||||
*
|
||||
* The `$importExportConfig` property makes reference to the configuration
|
||||
* values as either a YAML file, located in the controller view directory,
|
||||
* or directly as a PHP array.
|
||||
*
|
||||
* @package winter\wn-backend-module
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
*/
|
||||
class ImportExportController extends ControllerBehavior
|
||||
{
|
||||
/**
|
||||
* @var array Configuration values that must exist when applying the primary config file.
|
||||
*/
|
||||
protected $requiredConfig = [];
|
||||
|
||||
/**
|
||||
* @var array Visible actions in context of the controller
|
||||
*/
|
||||
protected $actions = ['import', 'export', 'download'];
|
||||
|
||||
/**
|
||||
* @var Model Import model
|
||||
*/
|
||||
public $importModel;
|
||||
|
||||
/**
|
||||
* @var array Import column configuration.
|
||||
*/
|
||||
public $importColumns;
|
||||
|
||||
/**
|
||||
* @var WidgetBase Reference to the widget used for uploading import file.
|
||||
*/
|
||||
protected $importUploadFormWidget;
|
||||
|
||||
/**
|
||||
* @var WidgetBase Reference to the widget used for specifying import options.
|
||||
*/
|
||||
protected $importOptionsFormWidget;
|
||||
|
||||
/**
|
||||
* @var Model Export model
|
||||
*/
|
||||
public $exportModel;
|
||||
|
||||
/**
|
||||
* @var array Export column configuration.
|
||||
*/
|
||||
public $exportColumns;
|
||||
|
||||
/**
|
||||
* @var string File name used for export output.
|
||||
*/
|
||||
protected $exportFileName = 'export.csv';
|
||||
|
||||
/**
|
||||
* @var WidgetBase Reference to the widget used for standard export options.
|
||||
*/
|
||||
protected $exportFormatFormWidget;
|
||||
|
||||
/**
|
||||
* @var WidgetBase Reference to the widget used for custom export options.
|
||||
*/
|
||||
protected $exportOptionsFormWidget;
|
||||
|
||||
/**
|
||||
* @var mixed Configuration for this behaviour
|
||||
*/
|
||||
public $importExportConfig = 'config_import_export.yaml';
|
||||
|
||||
/**
|
||||
* Behavior constructor
|
||||
* @param Controller $controller
|
||||
*/
|
||||
public function __construct($controller)
|
||||
{
|
||||
parent::__construct($controller);
|
||||
|
||||
/*
|
||||
* Build configuration
|
||||
*/
|
||||
$this->config = $this->makeConfig($controller->importExportConfig ?: $this->importExportConfig, $this->requiredConfig);
|
||||
|
||||
/*
|
||||
* Process config
|
||||
*/
|
||||
if ($exportFileName = $this->getConfig('export[fileName]')) {
|
||||
$this->exportFileName = $exportFileName;
|
||||
}
|
||||
|
||||
/*
|
||||
* Import form widgets
|
||||
*/
|
||||
if ($this->importUploadFormWidget = $this->makeImportUploadFormWidget()) {
|
||||
$this->importUploadFormWidget->bindToController();
|
||||
}
|
||||
|
||||
if ($this->importOptionsFormWidget = $this->makeImportOptionsFormWidget()) {
|
||||
$this->importOptionsFormWidget->bindToController();
|
||||
}
|
||||
|
||||
/*
|
||||
* Export form widgets
|
||||
*/
|
||||
if ($this->exportFormatFormWidget = $this->makeExportFormatFormWidget()) {
|
||||
$this->exportFormatFormWidget->bindToController();
|
||||
}
|
||||
|
||||
if ($this->exportOptionsFormWidget = $this->makeExportOptionsFormWidget()) {
|
||||
$this->exportOptionsFormWidget->bindToController();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Controller actions
|
||||
//
|
||||
|
||||
public function import()
|
||||
{
|
||||
if (!$this->userHasAccess('import')) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$this->addJs('js/winter.import.js', 'core');
|
||||
$this->addCss('css/import.css', 'core');
|
||||
|
||||
$this->controller->pageTitle = $this->controller->pageTitle
|
||||
?: Lang::get($this->getConfig('import[title]', 'Import records'));
|
||||
|
||||
$this->prepareImportVars();
|
||||
}
|
||||
|
||||
public function export()
|
||||
{
|
||||
if (!$this->userHasAccess('export')) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
if ($response = $this->checkUseListExportMode()) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$this->addJs('js/winter.export.js', 'core');
|
||||
$this->addCss('css/export.css', 'core');
|
||||
|
||||
$this->controller->pageTitle = $this->controller->pageTitle
|
||||
?: Lang::get($this->getConfig('export[title]', 'Export records'));
|
||||
|
||||
$this->prepareExportVars();
|
||||
}
|
||||
|
||||
public function download($name, $outputName = null)
|
||||
{
|
||||
if (!$this->userHasAccess('export')) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$this->controller->pageTitle = $this->controller->pageTitle
|
||||
?: Lang::get($this->getConfig('export[title]', 'Export records'));
|
||||
|
||||
return $this->exportGetModel()->download($name, $outputName);
|
||||
}
|
||||
|
||||
//
|
||||
// Importing AJAX
|
||||
//
|
||||
|
||||
public function onImport()
|
||||
{
|
||||
if (!$this->userHasAccess('import')) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
try {
|
||||
$model = $this->importGetModel();
|
||||
$matches = post('column_match', []);
|
||||
|
||||
if ($optionData = post('ImportOptions')) {
|
||||
$model->fill($optionData);
|
||||
}
|
||||
|
||||
$importOptions = $this->getFormatOptionsFromPost();
|
||||
$importOptions['sessionKey'] = $this->importUploadFormWidget->getSessionKey();
|
||||
$importOptions['firstRowTitles'] = post('first_row_titles', false);
|
||||
|
||||
$model->import($matches, $importOptions);
|
||||
|
||||
$this->vars['importResults'] = $model->getResultStats();
|
||||
$this->vars['returnUrl'] = $this->getRedirectUrlForType('import');
|
||||
}
|
||||
catch (MassAssignmentException $ex) {
|
||||
$this->controller->handleError(new ApplicationException(Lang::get(
|
||||
'backend::lang.model.mass_assignment_failed',
|
||||
['attribute' => $ex->getMessage()]
|
||||
)));
|
||||
}
|
||||
catch (Exception $ex) {
|
||||
$this->controller->handleError($ex);
|
||||
}
|
||||
|
||||
$this->vars['sourceIndexOffset'] = $this->getImportSourceIndexOffset($importOptions['firstRowTitles']);
|
||||
|
||||
return $this->importExportMakePartial('import_result_form');
|
||||
}
|
||||
|
||||
public function onImportLoadForm()
|
||||
{
|
||||
if (!$this->userHasAccess('import')) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->checkRequiredImportColumns();
|
||||
}
|
||||
catch (Exception $ex) {
|
||||
$this->controller->handleError($ex);
|
||||
}
|
||||
|
||||
return $this->importExportMakePartial('import_form');
|
||||
}
|
||||
|
||||
public function onImportLoadColumnSampleForm()
|
||||
{
|
||||
if (!$this->userHasAccess('import')) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
if (($columnId = post('file_column_id', false)) === false) {
|
||||
throw new ApplicationException(Lang::get('backend::lang.import_export.missing_column_id_error'));
|
||||
}
|
||||
|
||||
$columns = $this->getImportFileColumns();
|
||||
if (!array_key_exists($columnId, $columns)) {
|
||||
throw new ApplicationException(Lang::get('backend::lang.import_export.unknown_column_error'));
|
||||
}
|
||||
|
||||
$path = $this->getImportFilePath();
|
||||
$reader = $this->createCsvReader($path);
|
||||
|
||||
if (post('first_row_titles')) {
|
||||
$reader->setHeaderOffset(1);
|
||||
}
|
||||
|
||||
$result = (new CsvStatement())->limit(50)->process($reader)->fetchColumn((int) $columnId);
|
||||
$data = iterator_to_array($result, false);
|
||||
|
||||
/*
|
||||
* Clean up data
|
||||
*/
|
||||
foreach ($data as $index => $sample) {
|
||||
$data[$index] = Str::limit($sample, 100);
|
||||
if (!strlen($data[$index])) {
|
||||
unset($data[$index]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->vars['columnName'] = array_get($columns, $columnId);
|
||||
$this->vars['columnData'] = $data;
|
||||
|
||||
return $this->importExportMakePartial('column_sample_form');
|
||||
}
|
||||
|
||||
//
|
||||
// Importing
|
||||
//
|
||||
|
||||
/**
|
||||
* Prepares the view data.
|
||||
* @return void
|
||||
*/
|
||||
public function prepareImportVars()
|
||||
{
|
||||
$this->vars['importUploadFormWidget'] = $this->importUploadFormWidget;
|
||||
$this->vars['importOptionsFormWidget'] = $this->importOptionsFormWidget;
|
||||
$this->vars['importDbColumns'] = $this->getImportDbColumns();
|
||||
$this->vars['importFileColumns'] = $this->getImportFileColumns();
|
||||
|
||||
// Make these variables available to widgets
|
||||
$this->controller->vars += $this->vars;
|
||||
}
|
||||
|
||||
public function importRender()
|
||||
{
|
||||
return $this->importExportMakePartial('container_import');
|
||||
}
|
||||
|
||||
public function importGetModel()
|
||||
{
|
||||
return $this->getModelForType('import');
|
||||
}
|
||||
|
||||
protected function getImportDbColumns()
|
||||
{
|
||||
if ($this->importColumns !== null) {
|
||||
return $this->importColumns;
|
||||
}
|
||||
|
||||
$columnConfig = $this->getConfig('import[list]');
|
||||
$columns = $this->makeListColumns($columnConfig);
|
||||
|
||||
if (empty($columns)) {
|
||||
throw new ApplicationException(Lang::get('backend::lang.import_export.empty_import_columns_error'));
|
||||
}
|
||||
|
||||
return $this->importColumns = $columns;
|
||||
}
|
||||
|
||||
protected function getImportFileColumns()
|
||||
{
|
||||
if (!$path = $this->getImportFilePath()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$reader = $this->createCsvReader($path);
|
||||
$firstRow = $reader->fetchOne(0);
|
||||
|
||||
if (!post('first_row_titles')) {
|
||||
array_walk($firstRow, function (&$value, $key) {
|
||||
$value = 'Column #'.($key + 1);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Prevents unfriendly error to be thrown due to bad encoding at response time.
|
||||
*/
|
||||
if (json_encode($firstRow) === false) {
|
||||
throw new ApplicationException(Lang::get('backend::lang.import_export.encoding_not_supported_error'));
|
||||
}
|
||||
|
||||
return $firstRow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the index offset to add to the reported row number in status messages
|
||||
*
|
||||
* @param bool $firstRowTitles Whether or not the first row contains column titles
|
||||
* @return int $offset
|
||||
*/
|
||||
protected function getImportSourceIndexOffset($firstRowTitles)
|
||||
{
|
||||
return $firstRowTitles ? 2 : 1;
|
||||
}
|
||||
|
||||
protected function makeImportUploadFormWidget()
|
||||
{
|
||||
if (!$this->getConfig('import')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$widgetConfig = $this->makeConfig('~/modules/backend/behaviors/importexportcontroller/partials/fields_import.yaml');
|
||||
$widgetConfig->model = $this->importGetModel();
|
||||
$widgetConfig->alias = 'importUploadForm';
|
||||
|
||||
$widget = $this->makeWidget('Backend\Widgets\Form', $widgetConfig);
|
||||
|
||||
$widget->bindEvent('form.beforeRefresh', function ($holder) {
|
||||
$holder->data = [];
|
||||
});
|
||||
|
||||
return $widget;
|
||||
}
|
||||
|
||||
protected function makeImportOptionsFormWidget()
|
||||
{
|
||||
$widget = $this->makeOptionsFormWidgetForType('import');
|
||||
|
||||
if (!$widget && $this->importUploadFormWidget) {
|
||||
$stepSection = $this->importUploadFormWidget->getField('step3_section');
|
||||
$stepSection->hidden = true;
|
||||
}
|
||||
|
||||
return $widget;
|
||||
}
|
||||
|
||||
protected function getImportFilePath()
|
||||
{
|
||||
return $this
|
||||
->importGetModel()
|
||||
->getImportFilePath($this->importUploadFormWidget->getSessionKey());
|
||||
}
|
||||
|
||||
public function importIsColumnRequired($columnName)
|
||||
{
|
||||
$model = $this->importGetModel();
|
||||
|
||||
return $model->isAttributeRequired($columnName);
|
||||
}
|
||||
|
||||
protected function checkRequiredImportColumns()
|
||||
{
|
||||
if (!$matches = post('column_match', [])) {
|
||||
throw new ApplicationException(Lang::get('backend::lang.import_export.match_some_column_error'));
|
||||
}
|
||||
|
||||
$dbColumns = $this->getImportDbColumns();
|
||||
foreach ($dbColumns as $column => $label) {
|
||||
if (!$this->importIsColumnRequired($column)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$found = false;
|
||||
foreach ($matches as $matchedColumns) {
|
||||
if (in_array($column, $matchedColumns)) {
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$found) {
|
||||
throw new ApplicationException(Lang::get('backend::lang.import_export.required_match_column_error', [
|
||||
'label' => Lang::get($label)
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Exporting AJAX
|
||||
//
|
||||
|
||||
public function onExport()
|
||||
{
|
||||
if (!$this->userHasAccess('export')) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
try {
|
||||
$model = $this->exportGetModel();
|
||||
$columns = $this->processExportColumnsFromPost();
|
||||
|
||||
if ($optionData = post('ExportOptions')) {
|
||||
$model->fill($optionData);
|
||||
}
|
||||
|
||||
$exportOptions = $this->getFormatOptionsFromPost();
|
||||
$exportOptions['sessionKey'] = $this->exportFormatFormWidget->getSessionKey();
|
||||
|
||||
$reference = $model->export($columns, $exportOptions);
|
||||
$fileUrl = $this->controller->actionUrl(
|
||||
'download',
|
||||
$reference.'/'.$this->exportFileName
|
||||
);
|
||||
|
||||
$this->vars['fileUrl'] = $fileUrl;
|
||||
$this->vars['returnUrl'] = $this->getRedirectUrlForType('export');
|
||||
}
|
||||
catch (MassAssignmentException $ex) {
|
||||
$this->controller->handleError(new ApplicationException(Lang::get(
|
||||
'backend::lang.model.mass_assignment_failed',
|
||||
['attribute' => $ex->getMessage()]
|
||||
)));
|
||||
}
|
||||
catch (Exception $ex) {
|
||||
$this->controller->handleError($ex);
|
||||
}
|
||||
|
||||
return $this->importExportMakePartial('export_result_form');
|
||||
}
|
||||
|
||||
public function onExportLoadForm()
|
||||
{
|
||||
if (!$this->userHasAccess('export')) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
return $this->importExportMakePartial('export_form');
|
||||
}
|
||||
|
||||
//
|
||||
// Exporting
|
||||
//
|
||||
|
||||
/**
|
||||
* Prepares the view data.
|
||||
* @return void
|
||||
*/
|
||||
public function prepareExportVars()
|
||||
{
|
||||
$this->vars['exportFormatFormWidget'] = $this->exportFormatFormWidget;
|
||||
$this->vars['exportOptionsFormWidget'] = $this->exportOptionsFormWidget;
|
||||
$this->vars['exportColumns'] = $this->getExportColumns();
|
||||
|
||||
// Make these variables available to widgets
|
||||
$this->controller->vars += $this->vars;
|
||||
}
|
||||
|
||||
public function exportRender()
|
||||
{
|
||||
return $this->importExportMakePartial('container_export');
|
||||
}
|
||||
|
||||
public function exportGetModel()
|
||||
{
|
||||
return $this->getModelForType('export');
|
||||
}
|
||||
|
||||
protected function getExportColumns()
|
||||
{
|
||||
if ($this->exportColumns !== null) {
|
||||
return $this->exportColumns;
|
||||
}
|
||||
|
||||
$columnConfig = $this->getConfig('export[list]');
|
||||
$columns = $this->makeListColumns($columnConfig);
|
||||
|
||||
if (empty($columns)) {
|
||||
throw new ApplicationException(Lang::get('backend::lang.import_export.empty_export_columns_error'));
|
||||
}
|
||||
|
||||
return $this->exportColumns = $columns;
|
||||
}
|
||||
|
||||
protected function makeExportFormatFormWidget()
|
||||
{
|
||||
if (!$this->getConfig('export') || $this->getConfig('export[useList]')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$widgetConfig = $this->makeConfig('~/modules/backend/behaviors/importexportcontroller/partials/fields_export.yaml');
|
||||
$widgetConfig->model = $this->exportGetModel();
|
||||
$widgetConfig->alias = 'exportUploadForm';
|
||||
|
||||
$widget = $this->makeWidget('Backend\Widgets\Form', $widgetConfig);
|
||||
|
||||
$widget->bindEvent('form.beforeRefresh', function ($holder) {
|
||||
$holder->data = [];
|
||||
});
|
||||
|
||||
return $widget;
|
||||
}
|
||||
|
||||
protected function makeExportOptionsFormWidget()
|
||||
{
|
||||
$widget = $this->makeOptionsFormWidgetForType('export');
|
||||
|
||||
if (!$widget && $this->exportFormatFormWidget) {
|
||||
$stepSection = $this->exportFormatFormWidget->getField('step3_section');
|
||||
$stepSection->hidden = true;
|
||||
}
|
||||
|
||||
return $widget;
|
||||
}
|
||||
|
||||
protected function processExportColumnsFromPost()
|
||||
{
|
||||
$visibleColumns = post('visible_columns', []);
|
||||
$columns = post('export_columns', []);
|
||||
|
||||
foreach ($columns as $key => $columnName) {
|
||||
if (!isset($visibleColumns[$columnName])) {
|
||||
unset($columns[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$result = [];
|
||||
$definitions = $this->getExportColumns();
|
||||
|
||||
foreach ($columns as $column) {
|
||||
$result[$column] = array_get($definitions, $column, '???');
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
//
|
||||
// ListController integration
|
||||
//
|
||||
|
||||
protected function checkUseListExportMode()
|
||||
{
|
||||
if (!$useList = $this->getConfig('export[useList]')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->controller->isClassExtendedWith(\Backend\Behaviors\ListController::class)) {
|
||||
throw new ApplicationException(Lang::get('backend::lang.import_export.behavior_missing_uselist_error'));
|
||||
}
|
||||
|
||||
if (is_array($useList)) {
|
||||
$listDefinition = array_get($useList, 'definition');
|
||||
}
|
||||
else {
|
||||
$listDefinition = $useList;
|
||||
}
|
||||
|
||||
return $this->exportFromList($listDefinition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs the list results as a CSV export.
|
||||
* @param string $definition
|
||||
* @param array $options
|
||||
* @return void
|
||||
*/
|
||||
public function exportFromList($definition = null, $options = [])
|
||||
{
|
||||
$lists = $this->controller->makeLists();
|
||||
|
||||
$widget = $lists[$definition] ?? reset($lists);
|
||||
|
||||
/*
|
||||
* Parse options
|
||||
*/
|
||||
$defaultOptions = [
|
||||
'fileName' => $this->exportFileName,
|
||||
'delimiter' => $this->getConfig('defaultFormatOptions[delimiter]', ','),
|
||||
'enclosure' => $this->getConfig('defaultFormatOptions[enclosure]', '"'),
|
||||
'escape' => $this->getConfig('defaultFormatOptions[escape]', '\\'),
|
||||
];
|
||||
|
||||
$options = array_merge($defaultOptions, $options);
|
||||
|
||||
$filename = filter_var($options['fileName'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);
|
||||
|
||||
/*
|
||||
* Prepare CSV
|
||||
*/
|
||||
$csv = CsvWriter::createFromFileObject(new SplTempFileObject);
|
||||
$csv->setOutputBOM(CsvWriter::BOM_UTF8);
|
||||
$csv->setDelimiter($options['delimiter']);
|
||||
$csv->setEnclosure($options['enclosure']);
|
||||
$csv->setEscape($options['escape']);
|
||||
$csv->addFormatter(new CsvEscapeFormula());
|
||||
|
||||
/*
|
||||
* Add headers
|
||||
*/
|
||||
$headers = [];
|
||||
$columns = $widget->getVisibleColumns();
|
||||
foreach ($columns as $column) {
|
||||
$headers[] = $widget->getHeaderValue($column);
|
||||
}
|
||||
$csv->insertOne($headers);
|
||||
|
||||
/*
|
||||
* Add records
|
||||
*/
|
||||
$getter = $this->getConfig('export[useList][raw]', false)
|
||||
? 'getColumnValueRaw'
|
||||
: 'getColumnValue';
|
||||
|
||||
$query = $widget->prepareQuery();
|
||||
$results = $query->get();
|
||||
|
||||
if ($event = $widget->fireSystemEvent('backend.list.extendRecords', [&$results])) {
|
||||
$results = $event;
|
||||
}
|
||||
|
||||
foreach ($results as $result) {
|
||||
$record = [];
|
||||
foreach ($columns as $column) {
|
||||
$value = $widget->$getter($result, $column);
|
||||
if (is_array($value)) {
|
||||
$value = implode('|', $value);
|
||||
}
|
||||
$record[] = $value;
|
||||
}
|
||||
|
||||
$csv->insertOne($record);
|
||||
}
|
||||
|
||||
/*
|
||||
* Response
|
||||
*/
|
||||
$response = Response::make();
|
||||
$response->header('Content-Type', 'text/csv');
|
||||
$response->header('Content-Transfer-Encoding', 'binary');
|
||||
$response->header('Content-Disposition', sprintf('%s; filename="%s"', 'attachment', $filename));
|
||||
$response->setContent((string) $csv);
|
||||
return $response;
|
||||
}
|
||||
|
||||
//
|
||||
// Helpers
|
||||
//
|
||||
|
||||
/**
|
||||
* Controller accessor for making partials within this behavior.
|
||||
* @param string $partial
|
||||
* @param array $params
|
||||
* @return string Partial contents
|
||||
*/
|
||||
public function importExportMakePartial($partial, $params = [])
|
||||
{
|
||||
$contents = $this->controller->makePartial('import_export_'.$partial, $params + $this->vars, false);
|
||||
|
||||
if (!$contents) {
|
||||
$contents = $this->makePartial($partial, $params);
|
||||
}
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user has access to the provided import/export action
|
||||
*/
|
||||
public function userHasAccess(string $type): bool
|
||||
{
|
||||
if (
|
||||
($permissions = $this->getConfig($type.'[permissions]')) &&
|
||||
(!BackendAuth::getUser()->hasAnyAccess((array) $permissions))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function makeOptionsFormWidgetForType($type)
|
||||
{
|
||||
if (!$this->getConfig($type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($fieldConfig = $this->getConfig($type.'[form]')) {
|
||||
$widgetConfig = $this->makeConfig($fieldConfig);
|
||||
$widgetConfig->model = $this->getModelForType($type);
|
||||
$widgetConfig->alias = $type.'OptionsForm';
|
||||
$widgetConfig->arrayName = ucfirst($type).'Options';
|
||||
|
||||
return $this->makeWidget('Backend\Widgets\Form', $widgetConfig);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getModelForType($type)
|
||||
{
|
||||
$cacheProperty = $type.'Model';
|
||||
|
||||
if ($this->{$cacheProperty} !== null) {
|
||||
return $this->{$cacheProperty};
|
||||
}
|
||||
|
||||
$modelClass = $this->getConfig($type.'[modelClass]');
|
||||
if (!$modelClass) {
|
||||
throw new ApplicationException(Lang::get('backend::lang.import_export.missing_model_class_error', [
|
||||
'type' => $type
|
||||
]));
|
||||
}
|
||||
|
||||
return $this->{$cacheProperty} = new $modelClass;
|
||||
}
|
||||
|
||||
protected function makeListColumns($config)
|
||||
{
|
||||
$config = $this->makeConfig($config);
|
||||
|
||||
if (!isset($config->columns) || !is_array($config->columns)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($config->columns as $attribute => $column) {
|
||||
if (is_array($column)) {
|
||||
$result[$attribute] = array_get($column, 'label', $attribute);
|
||||
}
|
||||
else {
|
||||
$result[$attribute] = $column ?: $attribute;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function getRedirectUrlForType($type)
|
||||
{
|
||||
$redirect = $this->getConfig($type.'[redirect]');
|
||||
|
||||
if ($redirect !== null) {
|
||||
return $redirect ? Backend::url($redirect) : 'javascript:;';
|
||||
}
|
||||
|
||||
return $this->controller->actionUrl($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new CSV reader with options selected by the user
|
||||
* @param string $path
|
||||
*
|
||||
* @return CsvReader
|
||||
*/
|
||||
protected function createCsvReader($path)
|
||||
{
|
||||
$reader = CsvReader::createFromPath($path);
|
||||
$options = $this->getFormatOptionsFromPost();
|
||||
|
||||
if ($options['delimiter'] !== null) {
|
||||
$reader->setDelimiter($options['delimiter']);
|
||||
}
|
||||
|
||||
if ($options['enclosure'] !== null) {
|
||||
$reader->setEnclosure($options['enclosure']);
|
||||
}
|
||||
|
||||
if ($options['escape'] !== null) {
|
||||
$reader->setEscape($options['escape']);
|
||||
}
|
||||
|
||||
if (
|
||||
$options['encoding'] !== null &&
|
||||
$reader->supportsStreamFilter()
|
||||
) {
|
||||
$reader->addStreamFilter(sprintf(
|
||||
'%s%s:%s',
|
||||
TranscodeFilter::FILTER_NAME,
|
||||
strtolower($options['encoding']),
|
||||
'utf-8'
|
||||
));
|
||||
}
|
||||
|
||||
return $reader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the file format options from postback. This method
|
||||
* can be used to define presets.
|
||||
* @return array
|
||||
*/
|
||||
protected function getFormatOptionsFromPost()
|
||||
{
|
||||
$presetMode = post('format_preset');
|
||||
|
||||
$options = [
|
||||
'delimiter' => $this->getConfig('defaultFormatOptions[delimiter]'),
|
||||
'enclosure' => $this->getConfig('defaultFormatOptions[enclosure]'),
|
||||
'escape' => $this->getConfig('defaultFormatOptions[escape]'),
|
||||
'encoding' => $this->getConfig('defaultFormatOptions[encoding]'),
|
||||
];
|
||||
|
||||
if ($presetMode == 'custom') {
|
||||
$options['delimiter'] = post('format_delimiter');
|
||||
$options['enclosure'] = post('format_enclosure');
|
||||
$options['escape'] = post('format_escape');
|
||||
$options['encoding'] = post('format_encoding');
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
652
modules/backend/behaviors/ListController.php
Normal file
652
modules/backend/behaviors/ListController.php
Normal file
@@ -0,0 +1,652 @@
|
||||
<?php
|
||||
|
||||
namespace Backend\Behaviors;
|
||||
|
||||
use Backend\Classes\ControllerBehavior;
|
||||
use Illuminate\Support\Facades\Lang;
|
||||
use Winter\Storm\Exception\ApplicationException;
|
||||
use Winter\Storm\Support\Facades\Event;
|
||||
use Winter\Storm\Support\Facades\Flash;
|
||||
|
||||
/**
|
||||
* Adds features for working with backend lists.
|
||||
*
|
||||
* This behavior is implemented in the controller like so:
|
||||
*
|
||||
* public $implement = [
|
||||
* \Backend\Behaviors\ListController::class,
|
||||
* ];
|
||||
*
|
||||
* public $listConfig = 'config_list.yaml';
|
||||
*
|
||||
* The `$listConfig` property makes reference to the list configuration
|
||||
* values as either a YAML file, located in the controller view directory,
|
||||
* or directly as a PHP array.
|
||||
*
|
||||
* @package winter\wn-backend-module
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
*/
|
||||
class ListController extends ControllerBehavior
|
||||
{
|
||||
/**
|
||||
* @var array List definitions, keys for alias and value for configuration.
|
||||
*/
|
||||
protected $listDefinitions;
|
||||
|
||||
/**
|
||||
* @var string The primary list alias to use. Default: list
|
||||
*/
|
||||
protected $primaryDefinition;
|
||||
|
||||
/**
|
||||
* @var \Backend\Classes\WidgetBase[] Reference to the list widget object.
|
||||
*/
|
||||
protected $listWidgets = [];
|
||||
|
||||
/**
|
||||
* @var \Backend\Classes\WidgetBase[] Reference to the toolbar widget objects.
|
||||
*/
|
||||
protected $toolbarWidgets = [];
|
||||
|
||||
/**
|
||||
* @var \Backend\Classes\WidgetBase[] Reference to the filter widget objects.
|
||||
*/
|
||||
protected $filterWidgets = [];
|
||||
|
||||
/**
|
||||
* @var array Configuration values that must exist when applying the primary config file.
|
||||
* - modelClass: Class name for the model
|
||||
* - list: List column definitions
|
||||
*/
|
||||
protected $requiredConfig = ['modelClass', 'list'];
|
||||
|
||||
/**
|
||||
* @var array Visible actions in context of the controller
|
||||
*/
|
||||
protected $actions = ['index'];
|
||||
|
||||
/**
|
||||
* @var mixed Configuration for this behaviour
|
||||
*/
|
||||
public $listConfig = 'config_list.yaml';
|
||||
|
||||
/**
|
||||
* Behavior constructor
|
||||
* @param \Backend\Classes\Controller $controller
|
||||
*/
|
||||
public function __construct($controller)
|
||||
{
|
||||
parent::__construct($controller);
|
||||
|
||||
/*
|
||||
* Extract list definitions
|
||||
*/
|
||||
$config = $controller->listConfig ?: $this->listConfig;
|
||||
if (is_array($config)) {
|
||||
$this->listDefinitions = $config;
|
||||
$this->primaryDefinition = key($this->listDefinitions);
|
||||
}
|
||||
else {
|
||||
$this->listDefinitions = ['list' => $config];
|
||||
$this->primaryDefinition = 'list';
|
||||
}
|
||||
|
||||
/*
|
||||
* Build configuration
|
||||
*/
|
||||
$this->setConfig($this->listDefinitions[$this->primaryDefinition], $this->requiredConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates all the list widgets based on the definitions.
|
||||
* @return array
|
||||
*/
|
||||
public function makeLists()
|
||||
{
|
||||
foreach ($this->listDefinitions as $definition => $config) {
|
||||
$this->listWidgets[$definition] = $this->makeList($definition);
|
||||
}
|
||||
|
||||
return $this->listWidgets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the widgets used by this action
|
||||
* @return \Backend\Widgets\Lists
|
||||
*/
|
||||
public function makeList($definition = null)
|
||||
{
|
||||
if (!$definition || !isset($this->listDefinitions[$definition])) {
|
||||
$definition = $this->primaryDefinition;
|
||||
}
|
||||
|
||||
$listConfig = $this->controller->listGetConfig($definition);
|
||||
|
||||
/*
|
||||
* Create the model
|
||||
*/
|
||||
$class = $listConfig->modelClass;
|
||||
$model = new $class;
|
||||
$model = $this->controller->listExtendModel($model, $definition);
|
||||
|
||||
/*
|
||||
* Prepare the list widget
|
||||
*/
|
||||
$columnConfig = $this->makeConfig($listConfig->list);
|
||||
$columnConfig->model = $model;
|
||||
$columnConfig->alias = $definition;
|
||||
|
||||
/*
|
||||
* Prepare the columns configuration
|
||||
*/
|
||||
$configFieldsToTransfer = [
|
||||
'recordUrl',
|
||||
'recordOnClick',
|
||||
'recordsPerPage',
|
||||
'perPageOptions',
|
||||
'showPageNumbers',
|
||||
'noRecordsMessage',
|
||||
'defaultSort',
|
||||
'showSorting',
|
||||
'showSetup',
|
||||
'showCheckboxes',
|
||||
'showTree',
|
||||
'treeExpanded',
|
||||
'customViewPath',
|
||||
'sortable',
|
||||
];
|
||||
|
||||
foreach ($configFieldsToTransfer as $field) {
|
||||
if (isset($listConfig->{$field})) {
|
||||
$columnConfig->{$field} = $listConfig->{$field};
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* List Widget with extensibility
|
||||
*/
|
||||
$widget = $this->makeWidget(\Backend\Widgets\Lists::class, $columnConfig);
|
||||
|
||||
/*
|
||||
* Drag-and-drop reordering - requires the model to use the Sortable trait.
|
||||
*/
|
||||
if (!empty($listConfig->sortable)) {
|
||||
if (!in_array(\Winter\Storm\Database\Traits\Sortable::class, class_uses_recursive($model))) {
|
||||
throw new ApplicationException(sprintf(
|
||||
'To use "sortable" on a list, the model "%s" must use the %s trait.',
|
||||
get_class($model),
|
||||
\Winter\Storm\Database\Traits\Sortable::class
|
||||
));
|
||||
}
|
||||
|
||||
/*
|
||||
* Drag-and-drop reordering presents every record in a single fixed order, so it
|
||||
* cannot coexist with features that show a partial or re-ordered view. Reject those
|
||||
* combinations up front rather than silently producing a wrong order.
|
||||
*/
|
||||
$toolbar = $listConfig->toolbar ?? null;
|
||||
$conflicts = array_keys(array_filter([
|
||||
'toolbar search' => is_array($toolbar) && !empty($toolbar['search']),
|
||||
'filter' => $listConfig->filter ?? null,
|
||||
'recordsPerPage' => $listConfig->recordsPerPage ?? null,
|
||||
'defaultSort' => $listConfig->defaultSort ?? null,
|
||||
]));
|
||||
if ($conflicts) {
|
||||
throw new ApplicationException(sprintf(
|
||||
'A "sortable" list cannot also use: %s. Drag-and-drop reordering requires the whole list in a fixed order. Remove these options, or use the ReorderController for a dedicated reordering page.',
|
||||
implode(', ', $conflicts)
|
||||
));
|
||||
}
|
||||
|
||||
$widget->bindEvent('list.reorder', function ($ids, $orders) use ($model) {
|
||||
$model->setSortableOrder($ids, $orders);
|
||||
});
|
||||
}
|
||||
|
||||
$widget->bindEvent('list.extendColumnsBefore', function () use ($widget) {
|
||||
$this->controller->listExtendColumnsBefore($widget);
|
||||
});
|
||||
|
||||
$widget->bindEvent('list.extendColumns', function () use ($widget) {
|
||||
$this->controller->listExtendColumns($widget);
|
||||
});
|
||||
|
||||
$widget->bindEvent('list.extendQueryBefore', function ($query) use ($definition) {
|
||||
$this->controller->listExtendQueryBefore($query, $definition);
|
||||
});
|
||||
|
||||
$widget->bindEvent('list.extendQuery', function ($query) use ($definition) {
|
||||
$this->controller->listExtendQuery($query, $definition);
|
||||
});
|
||||
|
||||
$widget->bindEvent('list.extendRecords', function ($records) use ($definition) {
|
||||
return $this->controller->listExtendRecords($records, $definition);
|
||||
});
|
||||
|
||||
$widget->bindEvent('list.injectRowClass', function ($record) use ($definition) {
|
||||
return $this->controller->listInjectRowClass($record, $definition);
|
||||
});
|
||||
|
||||
$widget->bindEvent('list.overrideColumnValue', function ($record, $column, $value) use ($definition) {
|
||||
return $this->controller->listOverrideColumnValue($record, $column->columnName, $definition);
|
||||
});
|
||||
|
||||
$widget->bindEvent('list.overrideHeaderValue', function ($column, $value) use ($definition) {
|
||||
return $this->controller->listOverrideHeaderValue($column->columnName, $definition);
|
||||
});
|
||||
|
||||
$widget->bindToController();
|
||||
|
||||
/*
|
||||
* Prepare the toolbar widget (optional)
|
||||
*/
|
||||
if (isset($listConfig->toolbar)) {
|
||||
$toolbarConfig = $this->makeConfig($listConfig->toolbar);
|
||||
$toolbarConfig->alias = $widget->alias . 'Toolbar';
|
||||
$toolbarWidget = $this->makeWidget(\Backend\Widgets\Toolbar::class, $toolbarConfig);
|
||||
$toolbarWidget->bindToController();
|
||||
$toolbarWidget->cssClasses[] = 'list-header';
|
||||
|
||||
/*
|
||||
* Link the Search Widget to the List Widget
|
||||
*/
|
||||
if ($searchWidget = $toolbarWidget->getSearchWidget()) {
|
||||
$searchWidget->bindEvent('search.submit', function () use ($widget, $searchWidget) {
|
||||
$widget->setSearchTerm($searchWidget->getActiveTerm(), true);
|
||||
return $widget->onRefresh();
|
||||
});
|
||||
|
||||
$widget->setSearchOptions([
|
||||
'mode' => $searchWidget->mode,
|
||||
'scope' => $searchWidget->scope,
|
||||
]);
|
||||
|
||||
// Find predefined search term
|
||||
$widget->setSearchTerm($searchWidget->getActiveTerm());
|
||||
}
|
||||
|
||||
$this->toolbarWidgets[$definition] = $toolbarWidget;
|
||||
}
|
||||
|
||||
/*
|
||||
* Prepare the filter widget (optional)
|
||||
*/
|
||||
if (isset($listConfig->filter)) {
|
||||
$filterConfig = $this->makeConfig($listConfig->filter);
|
||||
|
||||
$widget->cssClasses[] = 'list-flush';
|
||||
|
||||
$filterConfig->alias = $widget->alias . 'Filter';
|
||||
$filterWidget = $this->makeWidget(\Backend\Widgets\Filter::class, $filterConfig);
|
||||
$filterWidget->bindToController();
|
||||
|
||||
/*
|
||||
* Filter the list when the scopes are changed
|
||||
*/
|
||||
$filterWidget->bindEvent('filter.update', function () use ($widget, $filterWidget) {
|
||||
return $widget->onFilter();
|
||||
});
|
||||
|
||||
/*
|
||||
* Filter Widget with extensibility
|
||||
*/
|
||||
$filterWidget->bindEvent('filter.extendScopes', function () use ($filterWidget) {
|
||||
$this->controller->listFilterExtendScopes($filterWidget);
|
||||
});
|
||||
|
||||
/*
|
||||
* Extend the query of the list of options
|
||||
*/
|
||||
$filterWidget->bindEvent('filter.extendQuery', function ($query, $scope) {
|
||||
$this->controller->listFilterExtendQuery($query, $scope);
|
||||
});
|
||||
|
||||
// Apply predefined filter values
|
||||
$widget->addFilter([$filterWidget, 'applyAllScopesToQuery']);
|
||||
|
||||
$this->filterWidgets[$definition] = $filterWidget;
|
||||
}
|
||||
|
||||
return $widget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Index Controller action.
|
||||
* @return void
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$this->controller->pageTitle = $this->controller->pageTitle ?: Lang::get($this->getConfig(
|
||||
'title',
|
||||
'backend::lang.list.default_title'
|
||||
));
|
||||
$this->controller->bodyClass = 'slim-container';
|
||||
$this->makeLists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk delete records.
|
||||
* @return void
|
||||
* @throws \Winter\Storm\Exception\ApplicationException when the parent definition is missing.
|
||||
*/
|
||||
public function index_onDelete()
|
||||
{
|
||||
if (method_exists($this->controller, 'onDelete')) {
|
||||
return call_user_func_array([$this->controller, 'onDelete'], func_get_args());
|
||||
}
|
||||
|
||||
/*
|
||||
* Establish the list definition
|
||||
*/
|
||||
$definition = post('definition', $this->primaryDefinition);
|
||||
|
||||
if (!isset($this->listDefinitions[$definition])) {
|
||||
throw new ApplicationException(Lang::get('backend::lang.list.missing_parent_definition', compact('definition')));
|
||||
}
|
||||
|
||||
$listConfig = $this->controller->listGetConfig($definition);
|
||||
|
||||
/*
|
||||
* Validate checked identifiers
|
||||
*/
|
||||
$checkedIds = post('checked');
|
||||
|
||||
if (!$checkedIds || !is_array($checkedIds) || !count($checkedIds)) {
|
||||
Flash::error(Lang::get(
|
||||
(!empty($listConfig->noRecordsDeletedMessage))
|
||||
? $listConfig->noRecordsDeletedMessage
|
||||
: 'backend::lang.list.delete_selected_empty'
|
||||
));
|
||||
return $this->controller->listRefresh();
|
||||
}
|
||||
|
||||
/*
|
||||
* Create the model
|
||||
*/
|
||||
$class = $listConfig->modelClass;
|
||||
$model = new $class;
|
||||
$model = $this->controller->listExtendModel($model, $definition);
|
||||
|
||||
/*
|
||||
* Create the query
|
||||
*/
|
||||
$query = $model->newQuery();
|
||||
$this->controller->listExtendQueryBefore($query, $definition);
|
||||
|
||||
$query->whereIn($model->getKeyName(), $checkedIds);
|
||||
$this->controller->listExtendQuery($query, $definition);
|
||||
|
||||
/*
|
||||
* Delete records
|
||||
*/
|
||||
$records = $query->get();
|
||||
|
||||
if ($records->count()) {
|
||||
foreach ($records as $record) {
|
||||
$record->delete();
|
||||
}
|
||||
|
||||
Flash::success(Lang::get(
|
||||
(!empty($listConfig->deleteMessage))
|
||||
? $listConfig->deleteMessage
|
||||
: 'backend::lang.list.delete_selected_success'
|
||||
));
|
||||
}
|
||||
else {
|
||||
Flash::error(Lang::get(
|
||||
(!empty($listConfig->noRecordsDeletedMessage))
|
||||
? $listConfig->noRecordsDeletedMessage
|
||||
: 'backend::lang.list.delete_selected_empty'
|
||||
));
|
||||
}
|
||||
|
||||
return $this->controller->listRefresh($definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the widget collection.
|
||||
* @param string $definition Optional list definition.
|
||||
* @return string Rendered HTML for the list.
|
||||
* @throws \Winter\Storm\Exception\ApplicationException when there are no list widgets set.
|
||||
*/
|
||||
public function listRender($definition = null)
|
||||
{
|
||||
if (!count($this->listWidgets)) {
|
||||
throw new ApplicationException(Lang::get('backend::lang.list.behavior_not_ready'));
|
||||
}
|
||||
|
||||
if (!$definition || !isset($this->listDefinitions[$definition])) {
|
||||
$definition = $this->primaryDefinition;
|
||||
}
|
||||
|
||||
$vars = [
|
||||
'toolbar' => null,
|
||||
'filter' => null,
|
||||
'list' => null,
|
||||
];
|
||||
|
||||
if (isset($this->toolbarWidgets[$definition])) {
|
||||
$vars['toolbar'] = $this->toolbarWidgets[$definition];
|
||||
}
|
||||
|
||||
if (isset($this->filterWidgets[$definition])) {
|
||||
$vars['filter'] = $this->filterWidgets[$definition];
|
||||
}
|
||||
|
||||
$vars['list'] = $this->listWidgets[$definition];
|
||||
|
||||
return $this->listMakePartial('container', $vars);
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller accessor for making partials within this behavior.
|
||||
* @param string $partial
|
||||
* @param array $params
|
||||
* @return string Partial contents
|
||||
*/
|
||||
public function listMakePartial($partial, $params = [])
|
||||
{
|
||||
$contents = $this->controller->makePartial('list_'.$partial, $params + $this->vars, false);
|
||||
if (!$contents) {
|
||||
$contents = $this->makePartial($partial, $params);
|
||||
}
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the list container only, useful for returning in custom AJAX requests.
|
||||
*
|
||||
* @return array The list element selector as the key, and the list contents are the value.
|
||||
*/
|
||||
public function listRefresh(?string $definition = null)
|
||||
{
|
||||
if (!count($this->listWidgets)) {
|
||||
$this->makeLists();
|
||||
}
|
||||
|
||||
if (!$definition || !isset($this->listDefinitions[$definition])) {
|
||||
$definition = $this->primaryDefinition;
|
||||
}
|
||||
|
||||
return $this->listWidgets[$definition]->onRefresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the widget used by this behavior.
|
||||
* @return \Backend\Classes\WidgetBase
|
||||
*/
|
||||
public function listGetWidget(?string $definition = null)
|
||||
{
|
||||
if (!$definition) {
|
||||
$definition = $this->primaryDefinition;
|
||||
}
|
||||
|
||||
return array_get($this->listWidgets, $definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configuration used by this behavior.
|
||||
* @return stdClass
|
||||
*/
|
||||
public function listGetConfig(?string $definition = null)
|
||||
{
|
||||
if (!$definition) {
|
||||
$definition = $this->primaryDefinition;
|
||||
}
|
||||
|
||||
if (
|
||||
!($config = array_get($this->listDefinitions, $definition))
|
||||
|| !is_object($config)
|
||||
) {
|
||||
$config = $this->listDefinitions[$definition] = $this->makeConfig($this->listDefinitions[$definition], $this->requiredConfig);
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
//
|
||||
// Overrides
|
||||
//
|
||||
|
||||
/**
|
||||
* Called before the list columns are defined.
|
||||
* @param \Backend\Widgets\Lists $host The hosting list widget
|
||||
* @return void
|
||||
*/
|
||||
public function listExtendColumnsBefore($host)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after the list columns are defined.
|
||||
* @param \Backend\Widgets\Lists $host The hosting list widget
|
||||
* @return void
|
||||
*/
|
||||
public function listExtendColumns($host)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after the filter scopes are defined.
|
||||
* @param \Backend\Widgets\Filter $host The hosting filter widget
|
||||
* @return void
|
||||
*/
|
||||
public function listFilterExtendScopes($host)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller override: Extend supplied model
|
||||
* @param \Winter\Storm\Database\Model $model
|
||||
* @param string|null $definition
|
||||
* @return \Winter\Storm\Database\Model
|
||||
*/
|
||||
public function listExtendModel($model, $definition = null)
|
||||
{
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller override: Extend the query used for populating the list
|
||||
* before the default query is processed.
|
||||
* @param \Winter\Storm\Database\Builder $query
|
||||
* @param string|null $definition
|
||||
*/
|
||||
public function listExtendQueryBefore($query, $definition = null)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller override: Extend the query used for populating the list
|
||||
* after the default query is processed.
|
||||
* @param \Winter\Storm\Database\Builder $query
|
||||
* @param string|null $definition
|
||||
*/
|
||||
public function listExtendQuery($query, $definition = null)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller override: Extend the records used for populating the list
|
||||
* after the query is processed.
|
||||
* @param \Illuminate\Contracts\Pagination\LengthAwarePaginator|\Illuminate\Database\Eloquent\Collection $records
|
||||
* @param string|null $definition
|
||||
*/
|
||||
public function listExtendRecords($records, $definition = null)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller override: Extend the query used for populating the filter
|
||||
* options before the default query is processed.
|
||||
* @param \Winter\Storm\Database\Builder $query
|
||||
* @param array $scope
|
||||
*/
|
||||
public function listFilterExtendQuery($query, $scope)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a CSS class name for a list row (<tr class="...">).
|
||||
* @param \Winter\Storm\Database\Model $record The populated model used for the column
|
||||
* @param string|null $definition List definition (optional)
|
||||
* @return string|void CSS class name
|
||||
*/
|
||||
public function listInjectRowClass($record, $definition = null)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a table column value (<td>...</td>)
|
||||
* @param \Winter\Storm\Database\Model $record The populated model used for the column
|
||||
* @param string $columnName The column name to override
|
||||
* @param string|null $definition List definition (optional)
|
||||
* @return string|void HTML view
|
||||
*/
|
||||
public function listOverrideColumnValue($record, $columnName, $definition = null)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the entire table header contents (<th>...</th>) with custom HTML
|
||||
* @param string $columnName The column name to override
|
||||
* @param string|null $definition List definition (optional)
|
||||
* @return string|void HTML view
|
||||
*/
|
||||
public function listOverrideHeaderValue($columnName, $definition = null)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper for extending list columns.
|
||||
* @param callable $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function extendListColumns($callback)
|
||||
{
|
||||
$calledClass = self::getCalledExtensionClass();
|
||||
Event::listen('backend.list.extendColumns', function (\Backend\Widgets\Lists $widget) use ($calledClass, $callback) {
|
||||
if (!is_a($widget->getController(), $calledClass)) {
|
||||
return;
|
||||
}
|
||||
call_user_func_array($callback, [$widget, $widget->model]);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper for extending filter scopes.
|
||||
* @param callable $callback
|
||||
* @return void
|
||||
*/
|
||||
public static function extendListFilterScopes($callback)
|
||||
{
|
||||
$calledClass = self::getCalledExtensionClass();
|
||||
Event::listen('backend.filter.extendScopes', function (\Backend\Widgets\Filter $widget) use ($calledClass, $callback) {
|
||||
if (!is_a($widget->getController(), $calledClass)) {
|
||||
return;
|
||||
}
|
||||
call_user_func_array($callback, [$widget]);
|
||||
});
|
||||
}
|
||||
}
|
||||
2018
modules/backend/behaviors/RelationController.php
Normal file
2018
modules/backend/behaviors/RelationController.php
Normal file
File diff suppressed because it is too large
Load Diff
315
modules/backend/behaviors/ReorderController.php
Normal file
315
modules/backend/behaviors/ReorderController.php
Normal file
@@ -0,0 +1,315 @@
|
||||
<?php namespace Backend\Behaviors;
|
||||
|
||||
use Lang;
|
||||
use Backend;
|
||||
use ApplicationException;
|
||||
use Backend\Classes\ControllerBehavior;
|
||||
|
||||
/**
|
||||
* Used for reordering and sorting records.
|
||||
*
|
||||
* This behavior is implemented in the controller like so:
|
||||
*
|
||||
* public $implement = [
|
||||
* \Backend\Behaviors\ReorderController::class,
|
||||
* ];
|
||||
*
|
||||
* public $reorderConfig = 'config_reorder.yaml';
|
||||
*
|
||||
* The `$reorderConfig` property makes reference to the configuration
|
||||
* values as either a YAML file, located in the controller view directory,
|
||||
* or directly as a PHP array.
|
||||
*
|
||||
* @package winter\wn-backend-module
|
||||
* @author Alexey Bobkov, Samuel Georges
|
||||
*/
|
||||
class ReorderController extends ControllerBehavior
|
||||
{
|
||||
/**
|
||||
* @var array Configuration values that must exist when applying the primary config file.
|
||||
*/
|
||||
protected $requiredConfig = ['modelClass'];
|
||||
|
||||
/**
|
||||
* @var array Visible actions in context of the controller
|
||||
*/
|
||||
protected $actions = ['reorder'];
|
||||
|
||||
/**
|
||||
* @var Model Import model
|
||||
*/
|
||||
public $model;
|
||||
|
||||
/**
|
||||
* @var string Model attribute to use for the display name
|
||||
*/
|
||||
public $nameFrom = 'name';
|
||||
|
||||
/**
|
||||
* @var bool Display parent/child relationships in the list.
|
||||
*/
|
||||
protected $showTree = false;
|
||||
|
||||
/**
|
||||
* @var string Reordering mode:
|
||||
* - simple: Winter\Storm\Database\Traits\Sortable
|
||||
* - nested: Winter\Storm\Database\Traits\NestedTree
|
||||
*/
|
||||
protected $sortMode;
|
||||
|
||||
/**
|
||||
* @var Backend\Classes\WidgetBase Reference to the widget used for the toolbar.
|
||||
*/
|
||||
protected $toolbarWidget;
|
||||
|
||||
/**
|
||||
* @var mixed Configuration for this behaviour
|
||||
*/
|
||||
public $reorderConfig = 'config_reorder.yaml';
|
||||
|
||||
/**
|
||||
* Behavior constructor
|
||||
* @param Backend\Classes\Controller $controller
|
||||
*/
|
||||
public function __construct($controller)
|
||||
{
|
||||
parent::__construct($controller);
|
||||
|
||||
/*
|
||||
* Build configuration
|
||||
*/
|
||||
$this->config = $this->makeConfig($controller->reorderConfig ?: $this->reorderConfig, $this->requiredConfig);
|
||||
|
||||
/*
|
||||
* Widgets
|
||||
*/
|
||||
if ($this->toolbarWidget = $this->makeToolbarWidget()) {
|
||||
$this->toolbarWidget->bindToController();
|
||||
}
|
||||
|
||||
/*
|
||||
* Populate from config
|
||||
*/
|
||||
$this->nameFrom = $this->getConfig('nameFrom', $this->nameFrom);
|
||||
}
|
||||
|
||||
//
|
||||
// Controller actions
|
||||
//
|
||||
|
||||
public function reorder()
|
||||
{
|
||||
$this->addJs('js/winter.reorder.js', 'core');
|
||||
|
||||
$this->controller->pageTitle = $this->controller->pageTitle
|
||||
?: Lang::get($this->getConfig('title', 'backend::lang.reorder.default_title'));
|
||||
|
||||
$this->validateModel();
|
||||
$this->prepareVars();
|
||||
}
|
||||
|
||||
//
|
||||
// AJAX
|
||||
//
|
||||
|
||||
public function onReorder()
|
||||
{
|
||||
$model = $this->validateModel();
|
||||
|
||||
/*
|
||||
* Simple
|
||||
*/
|
||||
if ($this->sortMode == 'simple') {
|
||||
if (
|
||||
(!$ids = post('record_ids')) ||
|
||||
(!$orders = post('sort_orders'))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$model->setSortableOrder($ids, $orders);
|
||||
}
|
||||
/*
|
||||
* Nested set
|
||||
*/
|
||||
elseif ($this->sortMode == 'nested') {
|
||||
$sourceNode = $model->find(post('sourceNode'));
|
||||
$targetNode = post('targetNode') ? $model->find(post('targetNode')) : null;
|
||||
|
||||
if ($sourceNode == $targetNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (post('position')) {
|
||||
case 'before':
|
||||
$sourceNode->moveBefore($targetNode);
|
||||
break;
|
||||
|
||||
case 'after':
|
||||
$sourceNode->moveAfter($targetNode);
|
||||
break;
|
||||
|
||||
case 'child':
|
||||
$sourceNode->makeChildOf($targetNode);
|
||||
break;
|
||||
|
||||
default:
|
||||
$sourceNode->makeRoot();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Reordering
|
||||
//
|
||||
|
||||
/**
|
||||
* Prepares common form data
|
||||
*/
|
||||
protected function prepareVars()
|
||||
{
|
||||
$this->vars['reorderRecords'] = $this->getRecords();
|
||||
$this->vars['reorderModel'] = $this->model;
|
||||
$this->vars['reorderSortMode'] = $this->sortMode;
|
||||
$this->vars['reorderShowTree'] = $this->showTree;
|
||||
$this->vars['reorderToolbarWidget'] = $this->toolbarWidget;
|
||||
}
|
||||
|
||||
public function reorderRender()
|
||||
{
|
||||
return $this->reorderMakePartial('container');
|
||||
}
|
||||
|
||||
public function reorderGetModel()
|
||||
{
|
||||
if ($this->model !== null) {
|
||||
return $this->model;
|
||||
}
|
||||
|
||||
$modelClass = $this->getConfig('modelClass');
|
||||
|
||||
if (!$modelClass) {
|
||||
throw new ApplicationException('Please specify the modelClass property for reordering');
|
||||
}
|
||||
|
||||
return $this->model = new $modelClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the display name for a record.
|
||||
* @return string
|
||||
*/
|
||||
public function reorderGetRecordName($record)
|
||||
{
|
||||
return $record->{$this->nameFrom};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the supplied form model.
|
||||
* @return void
|
||||
*/
|
||||
protected function validateModel()
|
||||
{
|
||||
$model = $this->controller->reorderGetModel();
|
||||
$modelTraits = class_uses($model);
|
||||
|
||||
if (
|
||||
isset($modelTraits[\Winter\Storm\Database\Traits\Sortable::class]) ||
|
||||
$model->isClassExtendedWith(\Winter\Storm\Database\Behaviors\Sortable::class) ||
|
||||
isset($modelTraits[\October\Rain\Database\Traits\Sortable::class]) ||
|
||||
$model->isClassExtendedWith(\October\Rain\Database\Behaviors\Sortable::class)
|
||||
) {
|
||||
$this->sortMode = 'simple';
|
||||
}
|
||||
elseif (
|
||||
isset($modelTraits[\Winter\Storm\Database\Traits\NestedTree::class]) ||
|
||||
isset($modelTraits[\October\Rain\Database\Traits\NestedTree::class])
|
||||
) {
|
||||
$this->sortMode = 'nested';
|
||||
$this->showTree = true;
|
||||
}
|
||||
else {
|
||||
throw new ApplicationException('The model must implement the Sortable trait/behavior or the NestedTree trait.');
|
||||
}
|
||||
|
||||
return $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the records from the supplied model.
|
||||
* @return Collection
|
||||
*/
|
||||
protected function getRecords()
|
||||
{
|
||||
$records = null;
|
||||
$model = $this->controller->reorderGetModel();
|
||||
$query = $model->newQuery();
|
||||
|
||||
$this->controller->reorderExtendQuery($query);
|
||||
|
||||
if ($this->sortMode == 'simple') {
|
||||
$records = $query
|
||||
->orderBy($model->getSortOrderColumn())
|
||||
->get()
|
||||
;
|
||||
}
|
||||
elseif ($this->sortMode == 'nested') {
|
||||
$records = $query->getNested();
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the query used for finding reorder records. Extra conditions
|
||||
* can be applied to the query, for example, $query->withTrashed();
|
||||
* @param Winter\Storm\Database\Builder $query
|
||||
* @return void
|
||||
*/
|
||||
public function reorderExtendQuery($query)
|
||||
{
|
||||
}
|
||||
|
||||
//
|
||||
// Widgets
|
||||
//
|
||||
|
||||
protected function makeToolbarWidget()
|
||||
{
|
||||
if ($toolbarConfig = $this->getConfig('toolbar')) {
|
||||
$toolbarConfig = $this->makeConfig($toolbarConfig);
|
||||
$toolbarWidget = $this->makeWidget('Backend\Widgets\Toolbar', $toolbarConfig);
|
||||
}
|
||||
else {
|
||||
$toolbarWidget = null;
|
||||
}
|
||||
|
||||
return $toolbarWidget;
|
||||
}
|
||||
|
||||
//
|
||||
// Helpers
|
||||
//
|
||||
|
||||
/**
|
||||
* Controller accessor for making partials within this behavior.
|
||||
* @param string $partial
|
||||
* @param array $params
|
||||
* @return string Partial contents
|
||||
*/
|
||||
public function reorderMakePartial($partial, $params = [])
|
||||
{
|
||||
$contents = $this->controller->makePartial(
|
||||
'reorder_' . $partial,
|
||||
$params + $this->vars,
|
||||
false
|
||||
);
|
||||
|
||||
if (!$contents) {
|
||||
$contents = $this->makePartial($partial, $params);
|
||||
}
|
||||
|
||||
return $contents;
|
||||
}
|
||||
}
|
||||
117
modules/backend/behaviors/UserPreferencesModel.php
Normal file
117
modules/backend/behaviors/UserPreferencesModel.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php namespace Backend\Behaviors;
|
||||
|
||||
use System\Behaviors\SettingsModel;
|
||||
use Backend\Models\UserPreference;
|
||||
use Winter\Storm\Database\Model;
|
||||
|
||||
/**
|
||||
* User Preferences model extension, identical to System\Behaviors\SettingsModel
|
||||
* except values are set against the logged in user's preferences via Backend\Models\UserPreference
|
||||
*
|
||||
* Add this the model class definition:
|
||||
*
|
||||
* public $implement = ['Backend.Behaviors.UserPreferencesModel'];
|
||||
* public $settingsCode = 'author.plugin::code';
|
||||
* public $settingsFields = 'fields.yaml';
|
||||
*
|
||||
*/
|
||||
class UserPreferencesModel extends SettingsModel
|
||||
{
|
||||
/**
|
||||
* @var array Internal cache of model objects.
|
||||
*/
|
||||
private static $instances = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct($model)
|
||||
{
|
||||
parent::__construct($model);
|
||||
|
||||
$this->model->setTable('backend_user_preferences');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of the settings model, intended as a static method
|
||||
*/
|
||||
public function instance()
|
||||
{
|
||||
if (isset(self::$instances[$this->recordCode])) {
|
||||
return self::$instances[$this->recordCode];
|
||||
}
|
||||
|
||||
if (!$item = $this->getSettingsRecord()) {
|
||||
$this->model->initSettingsData();
|
||||
$item = $this->model;
|
||||
}
|
||||
|
||||
return self::$instances[$this->recordCode] = $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the model has been set up previously, intended as a static method
|
||||
*/
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return $this->getSettingsRecord() !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw Model record that stores the settings.
|
||||
*/
|
||||
public function getSettingsRecord(): ?Model
|
||||
{
|
||||
$item = UserPreference::forUser();
|
||||
$record = $item
|
||||
->scopeApplyKeyAndUser($this->model, $this->recordCode, $item->userContext)
|
||||
->remember(1440, $this->getCacheKey())
|
||||
->first();
|
||||
|
||||
return $record ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Before the model is saved, ensure the record code is set
|
||||
* and the jsonable field values
|
||||
*/
|
||||
public function beforeModelSave()
|
||||
{
|
||||
$preferences = UserPreference::forUser();
|
||||
list($namespace, $group, $item) = $preferences->parseKey($this->recordCode);
|
||||
$this->model->item = $item;
|
||||
$this->model->group = $group;
|
||||
$this->model->namespace = $namespace;
|
||||
$this->model->user_id = $preferences->userContext->id;
|
||||
|
||||
if ($this->fieldValues) {
|
||||
$this->model->value = $this->fieldValues;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a key is legitimate or should be added to
|
||||
* the field value collection
|
||||
*/
|
||||
protected function isKeyAllowed($key)
|
||||
{
|
||||
/*
|
||||
* Let the core columns through
|
||||
*/
|
||||
if ($key == 'namespace' || $key == 'group') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return parent::isKeyAllowed($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a cache key for this record.
|
||||
*/
|
||||
protected function getCacheKey()
|
||||
{
|
||||
$item = UserPreference::forUser();
|
||||
$userId = $item->userContext ? $item->userContext->id : 0;
|
||||
return $this->recordCode.'-userpreference-'.$userId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/** @var array{previous: mixed, next: mixed, current: int, total: int} $navigation */
|
||||
$previousUrl = $navigation['previous'] !== null
|
||||
? $this->actionUrl($navigationContext, $navigation['previous'])
|
||||
: null;
|
||||
$nextUrl = $navigation['next'] !== null
|
||||
? $this->actionUrl($navigationContext, $navigation['next'])
|
||||
: null;
|
||||
$chevronUp = '<svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path d="M4 10l4-4 4 4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||||
$chevronDown = '<svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
||||
?>
|
||||
<div class="form-record-nav" role="navigation" aria-label="<?= e(trans('backend::lang.form.record_navigation')) ?>">
|
||||
<span class="form-record-nav-position">
|
||||
<?= e($navigation['current']) ?> / <?= e($navigation['total']) ?>
|
||||
</span>
|
||||
<span class="form-record-nav-group">
|
||||
<?php if ($previousUrl): ?>
|
||||
<a href="<?= e($previousUrl) ?>" class="form-record-nav-btn" title="<?= e(trans('backend::lang.form.previous_record')) ?>" data-hotkey="ctrl+up, cmd+up"><?= $chevronUp ?></a>
|
||||
<?php else: ?>
|
||||
<span class="form-record-nav-btn is-disabled" aria-disabled="true"><?= $chevronUp ?></span>
|
||||
<?php endif ?>
|
||||
<?php if ($nextUrl): ?>
|
||||
<a href="<?= e($nextUrl) ?>" class="form-record-nav-btn" title="<?= e(trans('backend::lang.form.next_record')) ?>" data-hotkey="ctrl+down, cmd+down"><?= $chevronDown ?></a>
|
||||
<?php else: ?>
|
||||
<span class="form-record-nav-btn is-disabled" aria-disabled="true"><?= $chevronDown ?></span>
|
||||
<?php endif ?>
|
||||
</span>
|
||||
</div>
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
$modelName = $formConfig->name ?? '';
|
||||
?>
|
||||
<?php if ($formContext === 'create'): ?>
|
||||
<div class="loading-indicator-container">
|
||||
<button
|
||||
type="submit"
|
||||
data-request="onSave"
|
||||
data-request-data="new:1"
|
||||
data-browser-validate
|
||||
data-hotkey="ctrl+shift+s, cmd+shift+s"
|
||||
data-load-indicator="<?= e(trans('backend::lang.form.creating_name', ['name' => trans($modelName)])); ?>"
|
||||
data-request-before-update="$el.trigger('unchange.oc.changeMonitor')"
|
||||
class="btn btn-primary wn-icon-plus">
|
||||
<?= e(trans('backend::lang.form.create_and_new')); ?>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-request="onSave"
|
||||
data-browser-validate
|
||||
data-hotkey="ctrl+s, cmd+s"
|
||||
data-load-indicator="<?= e(trans('backend::lang.form.creating_name', ['name' => trans($modelName)])); ?>"
|
||||
data-request-before-update="$el.trigger('unchange.oc.changeMonitor')"
|
||||
class="btn btn-primary wn-icon-save">
|
||||
<?= e(trans('backend::lang.form.create')); ?>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-request="onSave"
|
||||
data-browser-validate
|
||||
data-request-data="close:1"
|
||||
data-hotkey="ctrl+enter, cmd+enter"
|
||||
data-load-indicator="<?= e(trans('backend::lang.form.creating_name', ['name' => trans($modelName)])); ?>"
|
||||
data-request-before-update="$el.trigger('unchange.oc.changeMonitor')"
|
||||
class="btn btn-default wn-icon-check">
|
||||
<?= e(trans('backend::lang.form.create_and_close')); ?>
|
||||
</button>
|
||||
<span class="btn-text">
|
||||
<?= e(trans('backend::lang.form.or')) ?> <a href="<?= Backend::url($formConfig->defaultRedirect) ?>"><?= e(trans('backend::lang.form.cancel')); ?></a>
|
||||
</span>
|
||||
</div>
|
||||
<?php elseif ($formContext === 'update'): ?>
|
||||
<div class="loading-indicator-container">
|
||||
<button
|
||||
type="button"
|
||||
data-request="onSave"
|
||||
data-browser-validate
|
||||
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
|
||||
data-request-before-update="$el.trigger('unchange.oc.changeMonitor')"
|
||||
data-request-data="redirect:0"
|
||||
data-hotkey="ctrl+s, cmd+s"
|
||||
class="btn btn-primary wn-icon-save"
|
||||
>
|
||||
<?= e(trans('backend::lang.form.save')); ?>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-request="onSave"
|
||||
data-browser-validate
|
||||
data-request-data="close:1"
|
||||
data-hotkey="ctrl+enter, cmd+enter"
|
||||
data-load-indicator="<?= e(trans('backend::lang.form.saving')); ?>"
|
||||
data-request-before-update="$el.trigger('unchange.oc.changeMonitor')"
|
||||
class="btn btn-default wn-icon-check"
|
||||
>
|
||||
<?= e(trans('backend::lang.form.save_and_close')); ?>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-request="onDelete"
|
||||
data-load-indicator="<?= e(trans('backend::lang.form.deleting_name', ['name' => trans($modelName)])); ?>"
|
||||
data-request-before-update="$el.trigger('unchange.oc.changeMonitor')"
|
||||
data-request-confirm="<?= e(trans('backend::lang.form.confirm_delete')); ?>"
|
||||
class="wn-icon-trash-o btn-icon danger pull-right"
|
||||
>
|
||||
</button>
|
||||
<span class="btn-text">
|
||||
<?= e(trans('backend::lang.form.or')) ?> <a href="<?= Backend::url($formConfig->defaultRedirect) ?>"><?= e(trans('backend::lang.form.cancel')); ?></a>
|
||||
</span>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
20
modules/backend/behaviors/formcontroller/views/create.php
Normal file
20
modules/backend/behaviors/formcontroller/views/create.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
// Decide which layout we should be rendering
|
||||
$layout = $this->formLayout ?? $formConfig->formLayout ?? null;
|
||||
if (!in_array($layout, ['standard', 'sidebar', 'fancy'])) {
|
||||
$layout = 'standard';
|
||||
}
|
||||
|
||||
// If required, set the appropriate body classes
|
||||
$this->bodyClass = match ($layout) {
|
||||
'fancy' => 'fancy-layout compact-container breadcrumb-flush breadcrumb-fancy',
|
||||
'sidebar' => 'compact-container',
|
||||
default => '',
|
||||
};
|
||||
|
||||
// Define layout mode view path for inclusion
|
||||
$this->appendViewPath(sprintf('%s/create/%s', __DIR__, $layout));
|
||||
|
||||
// Render the form layout
|
||||
echo $this->makePartial(sprintf('create/%s.php', $layout));
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php Block::put('breadcrumb') ?>
|
||||
<?= $this->makeLayoutPartial('breadcrumb') ?>
|
||||
<?php Block::endPut() ?>
|
||||
|
||||
<?php if (!$this->fatalError): ?>
|
||||
<div class="layout fancy-layout">
|
||||
<?= Form::open([
|
||||
'id' => $this->formGetId(),
|
||||
'class' => 'layout',
|
||||
'data-change-monitor' => 'true',
|
||||
'data-window-close-confirm' => 'true',
|
||||
]) ?>
|
||||
<div class="layout-row">
|
||||
<?= $this->formRender() ?>
|
||||
</div>
|
||||
<?= Form::close() ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p class="flash-message static error"><?= e($this->fatalError) ?></p>
|
||||
<p><a href="<?= isset($formConfig) ? Backend::url($formConfig->defaultRedirect) : 'javascript:history.back()' ?>" class="btn btn-default"><?= e(trans('backend::lang.form.return_to_list')); ?></a></p>
|
||||
<?php endif ?>
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php Block::put('breadcrumb') ?>
|
||||
<?= $this->makeLayoutPartial('breadcrumb') ?>
|
||||
<?php Block::endPut() ?>
|
||||
|
||||
<?php if (!$this->fatalError): ?>
|
||||
<?php Block::put('form-contents') ?>
|
||||
<div class="layout">
|
||||
<div class="layout-row">
|
||||
<?= $this->formRenderOutsideFields() ?>
|
||||
<?= $this->formRenderPrimaryTabs() ?>
|
||||
</div>
|
||||
|
||||
<div class="form-buttons p-t">
|
||||
<?= $this->formMakePartial('toolbar') ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php Block::endPut() ?>
|
||||
|
||||
<?php Block::put('form-sidebar') ?>
|
||||
<div class="hide-tabs"><?= $this->formRenderSecondaryTabs() ?></div>
|
||||
<?php Block::endPut() ?>
|
||||
|
||||
<?php Block::put('body') ?>
|
||||
<?= Form::open([
|
||||
'id' => $this->formGetId(),
|
||||
'class' => 'layout stretch',
|
||||
'data-change-monitor' => 'true',
|
||||
'data-window-close-confirm' => 'true',
|
||||
]) ?>
|
||||
<?= $this->makeLayout('form-with-sidebar') ?>
|
||||
<?= Form::close() ?>
|
||||
<?php Block::endPut() ?>
|
||||
<?php else: ?>
|
||||
<div class="control-breadcrumb">
|
||||
<?= Block::placeholder('breadcrumb') ?>
|
||||
</div>
|
||||
<div class="padded-container">
|
||||
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
|
||||
<p><a href="<?= isset($formConfig) ? Backend::url($formConfig->defaultRedirect) : 'javascript:history.back()' ?>" class="btn btn-default"><?= e(trans('backend::lang.form.return_to_list')); ?></a></p>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php Block::put('breadcrumb') ?>
|
||||
<?= $this->makeLayoutPartial('breadcrumb') ?>
|
||||
<?php Block::endPut() ?>
|
||||
|
||||
<?php if (!$this->fatalError): ?>
|
||||
<?= Form::open([
|
||||
'id' => $this->formGetId(),
|
||||
'class' => 'layout',
|
||||
'data-change-monitor' => 'true',
|
||||
'data-window-close-confirm' => 'true',
|
||||
]) ?>
|
||||
<div class="layout-row">
|
||||
<?= $this->formRender() ?>
|
||||
</div>
|
||||
|
||||
<div class="form-buttons p-t">
|
||||
<?= $this->formMakePartial('toolbar') ?>
|
||||
</div>
|
||||
<?= Form::close() ?>
|
||||
<?php else: ?>
|
||||
<p class="flash-message static error"><?= e($this->fatalError) ?></p>
|
||||
<p><a href="<?= isset($formConfig) ? Backend::url($formConfig->defaultRedirect) : 'javascript:history.back()' ?>" class="btn btn-default"><?= e(trans('backend::lang.form.return_to_list')); ?></a></p>
|
||||
<?php endif ?>
|
||||
@@ -0,0 +1,36 @@
|
||||
<div class="form-buttons loading-indicator-container">
|
||||
<button
|
||||
type="submit"
|
||||
data-request="onSave"
|
||||
data-request-data="new:1"
|
||||
data-browser-validate
|
||||
data-hotkey="ctrl+shift+s, cmd+shift+s"
|
||||
data-load-indicator="<?= e(trans('backend::lang.form.creating')); ?>"
|
||||
data-request-before-update="$el.trigger('unchange.oc.changeMonitor')"
|
||||
class="btn btn-primary wn-icon-plus">
|
||||
<?= e(trans('backend::lang.form.create_and_new')); ?>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-request="onSave"
|
||||
data-browser-validate
|
||||
data-hotkey="ctrl+s, cmd+s"
|
||||
data-load-indicator="<?= e(trans('backend::lang.form.creating')); ?>"
|
||||
data-request-before-update="$el.trigger('unchange.oc.changeMonitor')"
|
||||
class="btn btn-primary wn-icon-save">
|
||||
<?= e(trans('backend::lang.form.create')); ?>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-request="onSave"
|
||||
data-browser-validate
|
||||
data-request-data="close:1"
|
||||
data-hotkey="ctrl+enter, cmd+enter"
|
||||
data-load-indicator="<?= e(trans('backend::lang.form.creating')); ?>"
|
||||
data-request-before-update="$el.trigger('unchange.oc.changeMonitor')"
|
||||
class="btn btn-default wn-icon-check">
|
||||
<?= e(trans('backend::lang.form.create_and_close')); ?>
|
||||
</button>
|
||||
|
||||
<a class="btn btn-default wn-icon-ban" href="<?= $this->actionUrl('') ?>"><?= e(trans('backend::lang.form.cancel')); ?></a>
|
||||
</div>
|
||||
21
modules/backend/behaviors/formcontroller/views/preview.php
Normal file
21
modules/backend/behaviors/formcontroller/views/preview.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
|
||||
// Decide which layout we should be rendering
|
||||
$layout = $this->formLayout ?? $formConfig->formLayout ?? null;
|
||||
if (!in_array($layout, ['standard', 'sidebar', 'fancy'])) {
|
||||
$layout = 'standard';
|
||||
}
|
||||
|
||||
// If required, set the appropriate body classes
|
||||
$this->bodyClass = match ($layout) {
|
||||
'fancy' => 'fancy-layout compact-container breadcrumb-flush breadcrumb-fancy',
|
||||
'sidebar' => 'compact-container',
|
||||
default => '',
|
||||
};
|
||||
|
||||
// Define layout mode view path for inclusion
|
||||
$this->appendViewPath(sprintf('%s/preview/%s', __DIR__, $layout));
|
||||
|
||||
// Render the form layout
|
||||
echo $this->makePartial(sprintf('preview/%s.php', $layout));
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php Block::put('breadcrumb') ?>
|
||||
<?= $this->makeLayoutPartial('breadcrumb') ?>
|
||||
<?= $this->formRenderRecordNavigation() ?>
|
||||
<?php Block::endPut() ?>
|
||||
|
||||
<?php if (!$this->fatalError): ?>
|
||||
<?= Form::open([
|
||||
'id' => $this->formGetId(),
|
||||
'class' => 'layout',
|
||||
]) ?>
|
||||
<div class="layout-row form-preview">
|
||||
<?= $this->formRenderPreview() ?>
|
||||
</div>
|
||||
<?= Form::close() ?>
|
||||
<?php else: ?>
|
||||
<p class="flash-message static error"><?= e($this->fatalError) ?></p>
|
||||
<p><a href="<?= isset($formConfig) ? Backend::url($formConfig->defaultRedirect) : 'javascript:history.back()' ?>" class="btn btn-default"><?= e(trans('backend::lang.form.return_to_list')); ?></a></p>
|
||||
<?php endif ?>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php Block::put('breadcrumb') ?>
|
||||
<?= $this->makeLayoutPartial('breadcrumb') ?>
|
||||
<?= $this->formRenderRecordNavigation() ?>
|
||||
<?php Block::endPut() ?>
|
||||
|
||||
<?php if (!$this->fatalError): ?>
|
||||
<?php Block::put('form-contents') ?>
|
||||
<div class="layout">
|
||||
<div class="layout-row">
|
||||
<?= $this->formRenderOutsideFields() ?>
|
||||
<?= $this->formRenderPrimaryTabs() ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php Block::endPut() ?>
|
||||
|
||||
<?php Block::put('form-sidebar') ?>
|
||||
<div class="hide-tabs"><?= $this->formRenderSecondaryTabs() ?></div>
|
||||
<?php Block::endPut() ?>
|
||||
|
||||
<?php Block::put('body') ?>
|
||||
<?= Form::open([
|
||||
'id' => $this->formGetId(),
|
||||
'class'=>'layout stretch',
|
||||
]) ?>
|
||||
<?= $this->makeLayout('form-with-sidebar') ?>
|
||||
<?= Form::close() ?>
|
||||
<?php Block::endPut() ?>
|
||||
<?php else: ?>
|
||||
<div class="control-breadcrumb">
|
||||
<?= Block::placeholder('breadcrumb') ?>
|
||||
</div>
|
||||
<div class="padded-container">
|
||||
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
|
||||
<p><a href="<?= isset($formConfig) ? Backend::url($formConfig->defaultRedirect) : 'javascript:history.back()' ?>" class="btn btn-default"><?= e(trans('backend::lang.form.return_to_list')); ?></a></p>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php Block::put('breadcrumb') ?>
|
||||
<?= $this->makeLayoutPartial('breadcrumb') ?>
|
||||
<?= $this->formRenderRecordNavigation() ?>
|
||||
<?php Block::endPut() ?>
|
||||
|
||||
<?php if (!$this->fatalError): ?>
|
||||
<div class="form-preview">
|
||||
<?= $this->formRenderPreview() ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p class="flash-message static error"><?= e($this->fatalError) ?></p>
|
||||
<p><a href="<?= isset($formConfig) ? Backend::url($formConfig->defaultRedirect) : 'javascript:history.back()' ?>" class="btn btn-default"><?= e(trans('backend::lang.form.return_to_list')); ?></a></p>
|
||||
<?php endif ?>
|
||||
@@ -0,0 +1 @@
|
||||
<div class="form-buttons loading-indicator-container"></div>
|
||||
20
modules/backend/behaviors/formcontroller/views/update.php
Normal file
20
modules/backend/behaviors/formcontroller/views/update.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
// Decide which layout we should be rendering
|
||||
$layout = $this->formLayout ?? $formConfig->formLayout ?? null;
|
||||
if (!in_array($layout, ['standard', 'sidebar', 'fancy'])) {
|
||||
$layout = 'standard';
|
||||
}
|
||||
|
||||
// If required, set the appropriate body classes
|
||||
$this->bodyClass .= match ($layout) {
|
||||
'fancy' => ' fancy-layout compact-container breadcrumb-flush breadcrumb-fancy',
|
||||
'sidebar' => ' compact-container',
|
||||
default => '',
|
||||
};
|
||||
|
||||
// Define layout mode view path for inclusion
|
||||
$this->appendViewPath(sprintf('%s/update/%s', __DIR__, $layout));
|
||||
|
||||
// Render the form layout
|
||||
echo $this->makePartial(sprintf('update/%s.php', $layout));
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php Block::put('breadcrumb') ?>
|
||||
<?= $this->makeLayoutPartial('breadcrumb') ?>
|
||||
<?= $this->formRenderRecordNavigation() ?>
|
||||
<?php Block::endPut() ?>
|
||||
|
||||
<?php if (!$this->fatalError): ?>
|
||||
<div class="layout fancy-layout">
|
||||
<?= Form::open([
|
||||
'id' => $this->formGetId(),
|
||||
'class' => 'layout',
|
||||
'data-change-monitor' => 'true',
|
||||
'data-window-close-confirm' => 'true',
|
||||
]) ?>
|
||||
<div class="layout-row">
|
||||
<?= $this->formRender() ?>
|
||||
</div>
|
||||
<?= Form::close() ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<p class="flash-message static error"><?= e($this->fatalError) ?></p>
|
||||
<p><a href="<?= isset($formConfig) ? Backend::url($formConfig->defaultRedirect) : 'javascript:history.back()' ?>" class="btn btn-default"><?= e(trans('backend::lang.form.return_to_list')); ?></a></p>
|
||||
<?php endif ?>
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php Block::put('breadcrumb') ?>
|
||||
<?= $this->makeLayoutPartial('breadcrumb') ?>
|
||||
<?= $this->formRenderRecordNavigation() ?>
|
||||
<?php Block::endPut() ?>
|
||||
|
||||
<?php if (!$this->fatalError): ?>
|
||||
<?php Block::put('form-contents') ?>
|
||||
<div class="layout">
|
||||
<div class="layout-row">
|
||||
<?= $this->formRenderOutsideFields() ?>
|
||||
<?= $this->formRenderPrimaryTabs() ?>
|
||||
</div>
|
||||
|
||||
<div class="form-buttons p-t">
|
||||
<?= $this->formMakePartial('toolbar') ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php Block::endPut() ?>
|
||||
|
||||
<?php Block::put('form-sidebar') ?>
|
||||
<div class="hide-tabs"><?= $this->formRenderSecondaryTabs() ?></div>
|
||||
<?php Block::endPut() ?>
|
||||
|
||||
<?php Block::put('body') ?>
|
||||
<?= Form::open([
|
||||
'id' => $this->formGetId(),
|
||||
'class' => 'layout stretch',
|
||||
'data-change-monitor' => 'true',
|
||||
'data-window-close-confirm' => 'true',
|
||||
]) ?>
|
||||
<?= $this->makeLayout('form-with-sidebar') ?>
|
||||
<?= Form::close() ?>
|
||||
<?php Block::endPut() ?>
|
||||
<?php else: ?>
|
||||
<div class="control-breadcrumb">
|
||||
<?= Block::placeholder('breadcrumb') ?>
|
||||
</div>
|
||||
<div class="padded-container">
|
||||
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
|
||||
<p><a href="<?= isset($formConfig) ? Backend::url($formConfig->defaultRedirect) : 'javascript:history.back()' ?>" class="btn btn-default"><?= e(trans('backend::lang.form.return_to_list')); ?></a></p>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php Block::put('breadcrumb') ?>
|
||||
<?= $this->makeLayoutPartial('breadcrumb') ?>
|
||||
<?= $this->formRenderRecordNavigation() ?>
|
||||
<?php Block::endPut() ?>
|
||||
|
||||
<?php if (!$this->fatalError): ?>
|
||||
<?= Form::open([
|
||||
'id' => $this->formGetId(),
|
||||
'class' => 'layout',
|
||||
'data-change-monitor' => 'true',
|
||||
'data-window-close-confirm' => 'true',
|
||||
]) ?>
|
||||
<div class="layout-row">
|
||||
<?= $this->formRender() ?>
|
||||
</div>
|
||||
|
||||
<div class="form-buttons p-t">
|
||||
<?= $this->formMakePartial('toolbar') ?>
|
||||
</div>
|
||||
<?= Form::close() ?>
|
||||
<?php else: ?>
|
||||
<p class="flash-message static error"><?= e($this->fatalError) ?></p>
|
||||
<p><a href="<?= isset($formConfig) ? Backend::url($formConfig->defaultRedirect) : 'javascript:history.back()' ?>" class="btn btn-default"><?= e(trans('backend::lang.form.return_to_list')); ?></a></p>
|
||||
<?php endif ?>
|
||||
@@ -0,0 +1,53 @@
|
||||
<div class="form-buttons loading-indicator-container">
|
||||
<!-- Save -->
|
||||
<a
|
||||
href="javascript:;"
|
||||
class="btn btn-primary wn-icon-save save"
|
||||
data-request="onSave"
|
||||
data-browser-validate
|
||||
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
|
||||
data-request-before-update="$el.trigger('unchange.oc.changeMonitor')"
|
||||
data-request-data="redirect:0"
|
||||
data-hotkey="ctrl+s, cmd+s"
|
||||
>
|
||||
<?= e(trans('backend::lang.form.save')) ?>
|
||||
</a>
|
||||
|
||||
<!-- Save and Close -->
|
||||
<a
|
||||
href="javascript:;"
|
||||
class="btn btn-primary wn-icon-check save"
|
||||
data-request-before-update="$el.trigger('unchange.oc.changeMonitor')"
|
||||
data-request="onSave"
|
||||
data-browser-validate
|
||||
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
|
||||
>
|
||||
<?= e(trans('backend::lang.form.save_and_close')) ?>
|
||||
</a>
|
||||
|
||||
<?php if ($formModel->url): ?>
|
||||
<!-- Preview -->
|
||||
<a
|
||||
href="<?= e($formModel->url) ?>"
|
||||
target="_blank"
|
||||
class="btn btn-primary wn-icon-crosshairs"
|
||||
data-control="preview-button"
|
||||
>
|
||||
<?= e(trans('backend::lang.form.preview')) ?>
|
||||
</a>
|
||||
<?php endif ?>
|
||||
|
||||
<a class="btn btn-default wn-icon-ban" href="<?= $this->actionUrl('') ?>"><?= e(trans('backend::lang.form.cancel')); ?></a>
|
||||
|
||||
<!-- Delete -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default empty wn-icon-trash-o"
|
||||
data-request="onDelete"
|
||||
title="<?= e(trans('backend::lang.form.delete')); ?>"
|
||||
data-load-indicator="<?= e(trans('backend::lang.form.deleting')); ?>"
|
||||
data-request-before-update="$el.trigger('unchange.oc.changeMonitor')"
|
||||
data-request-confirm="<?= e(trans('backend::lang.form.confirm_delete')); ?>"
|
||||
data-control="delete-button"
|
||||
></button>
|
||||
</div>
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php namespace Backend\Behaviors\ImportExportController;
|
||||
|
||||
use php_user_filter;
|
||||
|
||||
stream_filter_register(TranscodeFilter::FILTER_NAME . "*", TranscodeFilter::class);
|
||||
|
||||
/**
|
||||
* Transcode stream filter.
|
||||
*
|
||||
* Convert CSV source files from one encoding to another.
|
||||
*/
|
||||
class TranscodeFilter extends php_user_filter
|
||||
{
|
||||
const FILTER_NAME = 'winter.csv.transcode.';
|
||||
|
||||
protected $encodingFrom = 'auto';
|
||||
|
||||
protected $encodingTo;
|
||||
|
||||
public function filter($in, $out, &$consumed, $closing)
|
||||
{
|
||||
while ($resource = stream_bucket_make_writeable($in)) {
|
||||
if (in_array($this->encodingFrom, mb_list_encodings())) {
|
||||
$resource->data = @mb_convert_encoding(
|
||||
$resource->data,
|
||||
$this->encodingTo,
|
||||
$this->encodingFrom
|
||||
);
|
||||
} else {
|
||||
$resource->data = @iconv(
|
||||
$this->encodingFrom,
|
||||
$this->encodingTo,
|
||||
$resource->data
|
||||
);
|
||||
}
|
||||
|
||||
$consumed += $resource->datalen;
|
||||
|
||||
stream_bucket_append($out, $resource);
|
||||
}
|
||||
|
||||
return PSFS_PASS_ON;
|
||||
}
|
||||
|
||||
public function onCreate()
|
||||
{
|
||||
if (strpos($this->filtername, self::FILTER_NAME) !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$params = substr($this->filtername, strlen(self::FILTER_NAME));
|
||||
if (!preg_match('/^([-\w]+)(:([-\w]+))?$/', $params, $matches)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($matches[1])) {
|
||||
$this->encodingFrom = $matches[1];
|
||||
}
|
||||
|
||||
$this->encodingTo = mb_internal_encoding();
|
||||
if (isset($matches[3])) {
|
||||
$this->encodingTo = $matches[3];
|
||||
}
|
||||
|
||||
$this->params['locale'] = setlocale(LC_CTYPE, '0');
|
||||
if (stripos($this->params['locale'], 'UTF-8') === false) {
|
||||
setlocale(LC_CTYPE, 'en_US.UTF-8');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function onClose()
|
||||
{
|
||||
setlocale(LC_CTYPE, $this->params['locale']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.export-behavior .export-columns {
|
||||
max-height: 400px;
|
||||
background: #f0f0f0;
|
||||
padding: 20px;
|
||||
padding-bottom: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
164
modules/backend/behaviors/importexportcontroller/assets/css/import.css
vendored
Normal file
164
modules/backend/behaviors/importexportcontroller/assets/css/import.css
vendored
Normal file
@@ -0,0 +1,164 @@
|
||||
.import-behavior ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.import-behavior ul li {
|
||||
font-size: 13px;
|
||||
}
|
||||
.import-behavior ul li.placeholder {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.import-behavior ul li.dragged {
|
||||
position: absolute;
|
||||
z-index: 2000;
|
||||
-webkit-box-shadow: 0 3px 6px rgba(0, 0, 0, 0.075);
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
.import-behavior .import-file-columns,
|
||||
.import-behavior .import-db-columns {
|
||||
height: 400px;
|
||||
background: #f0f0f0;
|
||||
padding: 5px;
|
||||
overflow: auto;
|
||||
}
|
||||
.import-behavior .import-file-columns .upload-prompt {
|
||||
display: block;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: -10px;
|
||||
}
|
||||
.import-behavior .import-column-bindings > ul > li,
|
||||
.import-behavior .import-db-columns > ul > li {
|
||||
cursor: pointer;
|
||||
}
|
||||
.import-behavior ul li.dragged,
|
||||
.import-behavior .import-file-columns > ul > li,
|
||||
.import-behavior .import-db-columns > ul > li {
|
||||
background: #ffffff;
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.import-behavior ul li.dragged div.import-column-name > span,
|
||||
.import-behavior .import-file-columns > ul > li div.import-column-name > span,
|
||||
.import-behavior .import-db-columns > ul > li div.import-column-name > span,
|
||||
.import-behavior ul li.dragged > span,
|
||||
.import-behavior .import-file-columns > ul > li > span,
|
||||
.import-behavior .import-db-columns > ul > li > span {
|
||||
display: block;
|
||||
padding: 8px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
.import-behavior .import-db-columns > ul > li .column-icon {
|
||||
color: #ccc;
|
||||
position: relative;
|
||||
left: -3px;
|
||||
}
|
||||
.import-behavior .import-db-columns > ul > li:hover .column-icon {
|
||||
color: #4da7e8;
|
||||
}
|
||||
.import-behavior .import-db-columns > ul > li.is-required .column-icon {
|
||||
color: #ab2a1c;
|
||||
}
|
||||
.import-behavior .import-file-columns > ul > li:before,
|
||||
.import-behavior .import-file-columns > ul > li:after {
|
||||
content: " ";
|
||||
display: table;
|
||||
}
|
||||
.import-behavior .import-file-columns > ul > li:after {
|
||||
clear: both;
|
||||
}
|
||||
.import-behavior .import-file-columns > ul > li.is-ignored {
|
||||
display: none;
|
||||
}
|
||||
.import-behavior .import-file-columns > ul > li .column-success-icon {
|
||||
display: none;
|
||||
position: relative;
|
||||
left: -2px;
|
||||
width: 15px;
|
||||
}
|
||||
.import-behavior .import-file-columns > ul > li.is-matched .column-success-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
.import-behavior .import-file-columns > ul > li.is-matched .column-ignore-button {
|
||||
display: none !important;
|
||||
}
|
||||
.import-behavior .import-file-columns > ul div.import-column-name {
|
||||
float: left;
|
||||
width: 45%;
|
||||
}
|
||||
.import-behavior .import-file-columns > ul div.import-column-name > span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.import-behavior .import-file-columns > ul div.import-column-name a.column-label {
|
||||
color: #333;
|
||||
}
|
||||
.import-behavior .import-file-columns > ul div.import-column-name a.column-ignore-button {
|
||||
color: #fff;
|
||||
background: #ccc;
|
||||
font-size: 10px;
|
||||
border-radius: 15px;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
left: -3px;
|
||||
}
|
||||
.import-behavior .import-file-columns > ul div.import-column-name a.column-ignore-button:hover {
|
||||
background: #ab2a1c;
|
||||
}
|
||||
.import-behavior .import-file-columns > ul .import-column-bindings > ul {
|
||||
float: right;
|
||||
width: 55%;
|
||||
}
|
||||
.import-behavior .import-column-bindings > ul {
|
||||
background: #dadedf;
|
||||
position: relative;
|
||||
min-height: 34px;
|
||||
}
|
||||
.import-behavior .import-column-bindings > ul:after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 17px solid transparent;
|
||||
border-bottom: 17px solid transparent;
|
||||
border-left: 18px solid #ffffff;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.import-behavior .import-column-bindings > ul:before {
|
||||
position: absolute;
|
||||
padding: 8px;
|
||||
padding-left: 28px;
|
||||
content: attr(data-empty-text);
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.import-behavior .import-column-bindings > ul > li .column-icon {
|
||||
color: #595959;
|
||||
float: right;
|
||||
margin: 3px;
|
||||
}
|
||||
.import-behavior .import-column-bindings > ul > li:hover .column-icon {
|
||||
color: #333333;
|
||||
}
|
||||
.import-behavior .import-column-bindings > ul > li:not(.dragged) {
|
||||
background: #e8eaeb;
|
||||
position: relative;
|
||||
}
|
||||
.import-behavior .import-column-bindings > ul > li:not(.dragged) > span {
|
||||
display: block;
|
||||
padding: 8px;
|
||||
padding-left: 28px;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Scripts for the Export controller behavior.
|
||||
*/
|
||||
+function ($) { "use strict";
|
||||
|
||||
var ExportBehavior = function() {
|
||||
|
||||
this.processExport = function () {
|
||||
var $form = $('#exportColumns').closest('form')
|
||||
|
||||
$form.request('onExport', {
|
||||
success: function(data) {
|
||||
$('#exportContainer').html(data.result)
|
||||
$(document).trigger('render')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$.wn.exportBehavior = new ExportBehavior;
|
||||
}(window.jQuery);
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Scripts for the Import controller behavior.
|
||||
*/
|
||||
+function ($) { "use strict";
|
||||
|
||||
var ImportBehavior = function() {
|
||||
|
||||
this.processImport = function () {
|
||||
var $form = $('#importFileColumns').closest('form')
|
||||
|
||||
$form.request('onImport', {
|
||||
success: function(data) {
|
||||
$('#importContainer').html(data.result)
|
||||
$(document).trigger('render')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.loadFileColumnSample = function(el) {
|
||||
var $el = $(el),
|
||||
$column = $el.closest('[data-column-id]'),
|
||||
columnId = $column.data('column-id')
|
||||
|
||||
$el.popup({
|
||||
handler: 'onImportLoadColumnSampleForm',
|
||||
extraData: {
|
||||
file_column_id: columnId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.bindColumnSorting = function() {
|
||||
/*
|
||||
* Unbind existing
|
||||
*/
|
||||
$('#importDbColumns > ul, .import-column-bindings > ul').each(function(){
|
||||
var $this = $(this)
|
||||
if ($this.data('oc.sortable')) {
|
||||
$this.sortable('destroyGroup')
|
||||
$this.sortable('destroy')
|
||||
}
|
||||
})
|
||||
|
||||
var sortableOptions = {
|
||||
group: 'import-fields',
|
||||
usePlaceholderClone: true,
|
||||
nested: false,
|
||||
onDrop: $.proxy(this.onDropColumn, this)
|
||||
}
|
||||
|
||||
$('#importDbColumns > ul, .import-column-bindings > ul').sortable(sortableOptions)
|
||||
}
|
||||
|
||||
this.onDropColumn = function ($dbItem, container, _super, event) {
|
||||
var
|
||||
$fileColumns = $('#importFileColumns'),
|
||||
$fileItem,
|
||||
isMatch = $.contains($fileColumns.get(0), $dbItem.get(0)),
|
||||
matchColumnId
|
||||
|
||||
/*
|
||||
* Has a previous match?
|
||||
*/
|
||||
matchColumnId = $dbItem.data('column-matched-id')
|
||||
if (matchColumnId !== null) {
|
||||
$fileItem = $('[data-column-id='+matchColumnId+']', $fileColumns)
|
||||
this.toggleMatchState($fileItem)
|
||||
}
|
||||
|
||||
/*
|
||||
* Is a new match?
|
||||
*/
|
||||
if (isMatch) {
|
||||
$fileItem = $dbItem.closest('[data-column-id]'),
|
||||
this.matchColumn($dbItem, $fileItem)
|
||||
}
|
||||
else {
|
||||
this.unmatchColumn($dbItem)
|
||||
}
|
||||
|
||||
if (_super) {
|
||||
_super($dbItem, container)
|
||||
}
|
||||
}
|
||||
|
||||
this.toggleMatchState = function ($container) {
|
||||
var hasItems = !!$('.import-column-bindings li', $container).length
|
||||
$container.toggleClass('is-matched', hasItems)
|
||||
}
|
||||
|
||||
this.ignoreFileColumn = function(el) {
|
||||
var $el = $(el),
|
||||
$column = $el.closest('[data-column-id]')
|
||||
|
||||
$column.addClass('is-ignored')
|
||||
$('#showIgnoredColumnsButton').removeClass('disabled')
|
||||
}
|
||||
|
||||
this.showIgnoredColumns = function() {
|
||||
$('#importFileColumns li.is-ignored').removeClass('is-ignored')
|
||||
$('#showIgnoredColumnsButton').addClass('disabled')
|
||||
}
|
||||
|
||||
this.autoMatchColumns = function() {
|
||||
var self = this,
|
||||
fileColumns = {},
|
||||
$this,
|
||||
name
|
||||
|
||||
$('#importFileColumns li').each(function() {
|
||||
$this = $(this)
|
||||
name = $.trim($('.column-label', $this).text())
|
||||
fileColumns[name] = $this
|
||||
})
|
||||
|
||||
$('#importDbColumns li').each(function() {
|
||||
$this = $(this)
|
||||
name = $.trim($('> span', $this).text())
|
||||
if (fileColumns[name]) {
|
||||
|
||||
$this.appendTo($('.import-column-bindings > ul', fileColumns[name]))
|
||||
self.matchColumn($this, fileColumns[name])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.matchColumn = function($dbItem, $fileItem) {
|
||||
var matchColumnId = $fileItem.data('column-id'),
|
||||
dbColumnName = $dbItem.data('column-name'),
|
||||
$dbItemMatchInput = $('[data-column-match-input]', $dbItem)
|
||||
|
||||
this.toggleMatchState($fileItem)
|
||||
|
||||
$dbItem.data('column-matched-id', matchColumnId)
|
||||
$dbItemMatchInput.attr('name', 'column_match['+matchColumnId+'][]')
|
||||
$dbItemMatchInput.attr('value', dbColumnName)
|
||||
}
|
||||
|
||||
this.unmatchColumn = function($dbItem) {
|
||||
var $dbItemMatchInput = $('[data-column-match-input]', $dbItem)
|
||||
|
||||
$dbItem.removeData('column-matched-id')
|
||||
$dbItemMatchInput.attr('name', '');
|
||||
$dbItemMatchInput.attr('value', '');
|
||||
}
|
||||
}
|
||||
|
||||
$.wn.importBehavior = new ImportBehavior;
|
||||
}(window.jQuery);
|
||||
@@ -0,0 +1,13 @@
|
||||
@import "../../../../assets/less/core/boot.less";
|
||||
|
||||
.export-behavior {
|
||||
|
||||
.export-columns {
|
||||
max-height: 400px;
|
||||
background: #f0f0f0;
|
||||
padding: @padding-standard;
|
||||
padding-bottom: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
}
|
||||
201
modules/backend/behaviors/importexportcontroller/assets/less/import.less
vendored
Normal file
201
modules/backend/behaviors/importexportcontroller/assets/less/import.less
vendored
Normal file
@@ -0,0 +1,201 @@
|
||||
@import "../../../../assets/less/core/boot.less";
|
||||
|
||||
@color-import-column-bg: #fff;
|
||||
@color-import-column-border: #ccc;
|
||||
@color-import-bound-bg: #e8eaeb;
|
||||
@import-column-padding: 8px;
|
||||
@import-column-font-size: 13px;
|
||||
|
||||
.import-behavior {
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
font-size: @import-column-font-size;
|
||||
}
|
||||
|
||||
li.placeholder {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
li.dragged {
|
||||
position: absolute;
|
||||
z-index: 2000;
|
||||
.box-shadow(0 3px 6px rgba(0,0,0,.075));
|
||||
}
|
||||
}
|
||||
|
||||
.import-file-columns,
|
||||
.import-db-columns {
|
||||
height: 400px;
|
||||
background: #f0f0f0;
|
||||
padding: 5px;
|
||||
overflow: auto;
|
||||
}
|
||||
.import-file-columns {
|
||||
.upload-prompt {
|
||||
display: block;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: -10px;
|
||||
}
|
||||
}
|
||||
|
||||
.import-column-bindings > ul > li,
|
||||
.import-db-columns > ul > li {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ul li.dragged,
|
||||
.import-file-columns > ul > li,
|
||||
.import-db-columns > ul > li {
|
||||
background: @color-import-column-bg;
|
||||
border: 1px solid @color-import-column-border;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 5px;
|
||||
|
||||
div.import-column-name > span,
|
||||
> span {
|
||||
display: block;
|
||||
padding: @import-column-padding;
|
||||
padding-left: (@import-column-padding * 1.5);
|
||||
}
|
||||
}
|
||||
|
||||
.import-db-columns > ul {
|
||||
> li {
|
||||
.column-icon {
|
||||
color: #ccc;
|
||||
position: relative;
|
||||
left: -3px;
|
||||
}
|
||||
&:hover .column-icon {
|
||||
color: #4da7e8;
|
||||
}
|
||||
|
||||
&.is-required {
|
||||
.column-icon {
|
||||
color: @brand-danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.import-file-columns > ul {
|
||||
> li {
|
||||
.clearfix;
|
||||
|
||||
&.is-ignored {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.column-success-icon {
|
||||
display: none;
|
||||
position: relative;
|
||||
left: -2px;
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
&.is-matched {
|
||||
.column-success-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
.column-ignore-button {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.import-column-name {
|
||||
float: left;
|
||||
width: 45%;
|
||||
|
||||
> span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
a.column-label {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
a.column-ignore-button {
|
||||
color: #fff;
|
||||
background: #ccc;
|
||||
font-size: 10px;
|
||||
border-radius: 15px;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
left: -3px;
|
||||
&:hover {
|
||||
background: @brand-danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.import-column-bindings > ul {
|
||||
float: right;
|
||||
width: 55%;
|
||||
}
|
||||
}
|
||||
|
||||
.import-column-bindings > ul {
|
||||
background: darken(@color-import-bound-bg, 5%);
|
||||
position: relative;
|
||||
min-height: (@import-column-padding * 2) + 18px;
|
||||
|
||||
&:after {
|
||||
.triangle(right, 18px, (@import-column-padding * 2) + 18px, @color-import-column-bg);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
padding: @import-column-padding;
|
||||
padding-left: 28px;
|
||||
content: attr(data-empty-text);
|
||||
color: rgba(0,0,0,.5);
|
||||
}
|
||||
|
||||
> li {
|
||||
.column-icon {
|
||||
color: #595959;
|
||||
float: right;
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.column-icon {
|
||||
color: #333333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> li:not(.dragged) {
|
||||
background: @color-import-bound-bg;
|
||||
position: relative;
|
||||
|
||||
> span {
|
||||
display: block;
|
||||
padding: @import-column-padding;
|
||||
padding-left: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="popup">×</button>
|
||||
<h4 class="modal-title"><?= e(trans('backend::lang.import_export.column_preview')) ?></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
<?= e(trans('backend::lang.import_export.column')) ?>:
|
||||
<strong><?= $columnName ?></strong>
|
||||
</p>
|
||||
<div class="list-preview">
|
||||
<div class="control-simplelist is-divided is-scrollable size-small" data-control="simplelist">
|
||||
<ul>
|
||||
<?php foreach ($columnData as $sample): ?>
|
||||
<li class="wn-icon-file-o">
|
||||
<?= e($sample) ?>
|
||||
</li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
data-dismiss="popup">
|
||||
<?= e(trans('backend::lang.form.close')) ?>
|
||||
</button>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="export-behavior">
|
||||
|
||||
<?= $exportFormatFormWidget->render() ?>
|
||||
|
||||
<?php if ($exportOptionsFormWidget): ?>
|
||||
<?= $exportOptionsFormWidget->render() ?>
|
||||
<?php endif ?>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="import-behavior">
|
||||
|
||||
<?= $importUploadFormWidget->render() ?>
|
||||
|
||||
<?php if ($importOptionsFormWidget): ?>
|
||||
<?= $importOptionsFormWidget->render() ?>
|
||||
<?php endif ?>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,27 @@
|
||||
<div class="export-columns" id="exportColumns">
|
||||
<div class="control-simplelist with-checkboxes is-sortable" data-control="simplelist">
|
||||
<ul>
|
||||
<?php foreach ($exportColumns as $key => $column): ?>
|
||||
<li>
|
||||
<div class="checkbox custom-checkbox">
|
||||
<input
|
||||
type="hidden"
|
||||
name="export_columns[]"
|
||||
value="<?= $key ?>" />
|
||||
<input
|
||||
id="<?= $this->getId('exportCheckbox-'.$key) ?>"
|
||||
name="visible_columns[<?= $key ?>]"
|
||||
value="1"
|
||||
checked="checked"
|
||||
type="checkbox" />
|
||||
<label
|
||||
class="choice"
|
||||
for="<?= $this->getId('exportCheckbox-'.$key) ?>">
|
||||
<?= e(trans($column)) ?>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,50 @@
|
||||
<div id="exportFormPopup">
|
||||
<?php if (!$this->fatalError): ?>
|
||||
|
||||
<?= Form::open(['id' => 'exportForm']) ?>
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title"><?= e(trans('backend::lang.import_export.export_progress')) ?></h4>
|
||||
</div>
|
||||
|
||||
<div id="exportContainer">
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="loading-indicator-container">
|
||||
<p> </p>
|
||||
<div class="loading-indicator transparent">
|
||||
<div><?= e(trans('backend::lang.import_export.processing')) ?></div>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
<p> </p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?= Form::close() ?>
|
||||
|
||||
<script>
|
||||
$('#exportFormPopup').on('popupComplete', function() {
|
||||
$.wn.exportBehavior.processExport()
|
||||
})
|
||||
</script>
|
||||
|
||||
<?php else: ?>
|
||||
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="popup">×</button>
|
||||
<h4 class="modal-title"><?= e(trans('backend::lang.import_export.export_error')) ?></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="flash-message static error"><?= e($this->fatalError) ?></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
data-dismiss="popup">
|
||||
<?= e(trans('backend::lang.form.close')) ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php endif ?>
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php if (!$this->fatalError): ?>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
<?= e(trans('backend::lang.import_export.processing_successful_line1')) ?>
|
||||
<?= e(trans('backend::lang.import_export.processing_successful_line2')) ?>
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a
|
||||
href="<?= $returnUrl ?>"
|
||||
class="btn btn-success"
|
||||
data-dismiss="popup">
|
||||
<?= e(trans('backend::lang.form.complete')) ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<script> window.location = '<?= $fileUrl ?>' </script>
|
||||
|
||||
<?php else: ?>
|
||||
|
||||
<div class="modal-body">
|
||||
<p class="flash-message static error"><?= e($this->fatalError) ?></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
data-dismiss="popup">
|
||||
<?= e(trans('backend::lang.form.close')) ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php endif ?>
|
||||
@@ -0,0 +1,23 @@
|
||||
<div class="import-db-columns" id="importDbColumns">
|
||||
<ul>
|
||||
<?php foreach ($importDbColumns as $column => $label): ?>
|
||||
<?php
|
||||
$isRequired = $this->importIsColumnRequired($column);
|
||||
$iconName = $isRequired ? 'icon-asterisk' : 'icon-link';
|
||||
?>
|
||||
<li
|
||||
class="<?= $isRequired ? 'is-required' : '' ?>"
|
||||
data-column-name="<?= e($column) ?>">
|
||||
<span>
|
||||
<i class="column-icon <?= $iconName ?>"></i>
|
||||
<?= e(trans($label)) ?>
|
||||
</span>
|
||||
<input type="hidden" data-column-match-input />
|
||||
</li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$.wn.importBehavior.bindColumnSorting()
|
||||
</script>
|
||||
@@ -0,0 +1,44 @@
|
||||
<div class="import-file-columns" id="importFileColumns">
|
||||
<?php if ($importFileColumns): ?>
|
||||
<ul>
|
||||
<?php foreach ($importFileColumns as $index => $column): ?>
|
||||
<li data-column-id="<?= $index ?>">
|
||||
<div class="import-column-name">
|
||||
<span>
|
||||
<i class="column-success-icon text-success icon-check"></i>
|
||||
<a
|
||||
href="javascript:;"
|
||||
class="column-ignore-button"
|
||||
data-toggle="tooltip"
|
||||
data-delay="300"
|
||||
data-placement="right"
|
||||
title="<?= e(trans('backend::lang.import_export.ignore_this_column')) ?>"
|
||||
onclick="$.wn.importBehavior.ignoreFileColumn(this)"
|
||||
>
|
||||
<i class="icon-close"></i>
|
||||
</a>
|
||||
<a
|
||||
href="javascript:;"
|
||||
class="column-label"
|
||||
onclick="$.wn.importBehavior.loadFileColumnSample(this)"
|
||||
>
|
||||
<?= e($column) ?>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="import-column-bindings">
|
||||
<ul data-empty-text="<?= e(trans('backend::lang.import_export.drop_column_here')) ?>"></ul>
|
||||
</div>
|
||||
</li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
<?php else: ?>
|
||||
<p class="upload-prompt">
|
||||
<?= e(trans('backend::lang.import_export.upload_valid_csv')) ?>
|
||||
</p>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$.wn.importBehavior.bindColumnSorting()
|
||||
</script>
|
||||
@@ -0,0 +1,50 @@
|
||||
<div id="importFormPopup">
|
||||
<?php if (!$this->fatalError): ?>
|
||||
|
||||
<?= Form::open(['id' => 'importForm']) ?>
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title"><?= e(trans('backend::lang.import_export.import_progress')) ?></h4>
|
||||
</div>
|
||||
|
||||
<div id="importContainer">
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="loading-indicator-container">
|
||||
<p> </p>
|
||||
<div class="loading-indicator transparent">
|
||||
<div><?= e(trans('backend::lang.import_export.processing')) ?></div>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
<p> </p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<?= Form::close() ?>
|
||||
|
||||
<script>
|
||||
$('#importFormPopup').on('popupComplete', function() {
|
||||
$.wn.importBehavior.processImport()
|
||||
})
|
||||
</script>
|
||||
|
||||
<?php else: ?>
|
||||
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="popup">×</button>
|
||||
<h4 class="modal-title"><?= e(trans('backend::lang.import_export.import_error')) ?></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="flash-message static error"><?= e($this->fatalError) ?></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
data-dismiss="popup">
|
||||
<?= e(trans('backend::lang.form.close')) ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php endif ?>
|
||||
</div>
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php if (!$this->fatalError): ?>
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="scoreboard">
|
||||
<div data-control="toolbar">
|
||||
<div class="scoreboard-item title-value">
|
||||
<h4><?= e(trans('backend::lang.import_export.created')) ?></h4>
|
||||
<p><?= $importResults->created ?></p>
|
||||
</div>
|
||||
<div class="scoreboard-item title-value">
|
||||
<h4><?= e(trans('backend::lang.import_export.updated')) ?></h4>
|
||||
<p><?= $importResults->updated ?></p>
|
||||
</div>
|
||||
<?php if ($importResults->skippedCount): ?>
|
||||
<div class="scoreboard-item title-value">
|
||||
<h4><?= e(trans('backend::lang.import_export.skipped')) ?></h4>
|
||||
<p><?= $importResults->skippedCount ?></p>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<?php if ($importResults->warningCount): ?>
|
||||
<div class="scoreboard-item title-value">
|
||||
<h4><?= e(trans('backend::lang.import_export.warnings')) ?></h4>
|
||||
<p><?= $importResults->warningCount ?></p>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<div class="scoreboard-item title-value">
|
||||
<h4><?= e(trans('backend::lang.import_export.errors')) ?></h4>
|
||||
<p><?= $importResults->errorCount ?></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if ($importResults->hasMessages): ?>
|
||||
<?php
|
||||
$tabs = [
|
||||
'skipped' => trans('backend::lang.import_export.skipped_rows'),
|
||||
'warnings' => trans('backend::lang.import_export.warnings'),
|
||||
'errors' => trans('backend::lang.import_export.errors'),
|
||||
];
|
||||
|
||||
if (!$importResults->skippedCount) {
|
||||
unset($tabs['skipped']);
|
||||
}
|
||||
if (!$importResults->warningCount) {
|
||||
unset($tabs['warnings']);
|
||||
}
|
||||
if (!$importResults->errorCount) {
|
||||
unset($tabs['errors']);
|
||||
}
|
||||
?>
|
||||
<div class="control-tabs secondary-tabs" data-control="tab">
|
||||
<ul class="nav nav-tabs">
|
||||
<?php $count = 0; foreach ($tabs as $code => $tab): ?>
|
||||
<li class="<?= $count++ == 0 ? 'active' : '' ?>">
|
||||
<a href="#importTab<?= $code ?>">
|
||||
<?= $tab ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<?php $count = 0; foreach ($tabs as $code => $tab): ?>
|
||||
<div class="tab-pane <?= $count++ == 0 ? 'active' : '' ?>">
|
||||
<div class="list-preview">
|
||||
<div class="control-simplelist is-divided is-scrollable size-small" data-control="simplelist">
|
||||
<ul>
|
||||
<?php foreach ($importResults->{$code} as $row => $message): ?>
|
||||
<li>
|
||||
<strong><?= e(trans('backend::lang.import_export.row', ['row' => $row + $sourceIndexOffset])) ?></strong>
|
||||
- <?= e($message) ?>
|
||||
</li>
|
||||
<?php endforeach ?>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a
|
||||
href="<?= $returnUrl ?>"
|
||||
class="btn btn-success"
|
||||
data-dismiss="popup">
|
||||
<?= e(trans('backend::lang.form.complete')) ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php else: ?>
|
||||
|
||||
<div class="modal-body">
|
||||
<p class="flash-message static error"><?= e($this->fatalError) ?></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
data-dismiss="popup">
|
||||
<?= e(trans('backend::lang.form.close')) ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php endif ?>
|
||||
@@ -0,0 +1,16 @@
|
||||
<div data-control="toolbar">
|
||||
<a
|
||||
href="javascript:;"
|
||||
id="showIgnoredColumnsButton"
|
||||
class="btn btn-sm btn-secondary wn-icon-eye disabled"
|
||||
onclick="$.wn.importBehavior.showIgnoredColumns()">
|
||||
<?= e(trans('backend::lang.import_export.show_ignored_columns')) ?>
|
||||
</a>
|
||||
<a
|
||||
href="javascript:;"
|
||||
id="autoMatchColumnsButton"
|
||||
class="btn btn-sm btn-secondary wn-icon-bullseye"
|
||||
onclick="$.wn.importBehavior.autoMatchColumns()">
|
||||
<?= e(trans('backend::lang.import_export.auto_match_columns')) ?>
|
||||
</a>
|
||||
</div>
|
||||
@@ -0,0 +1,58 @@
|
||||
# ===================================
|
||||
# Field Definitions
|
||||
# ===================================
|
||||
|
||||
fields:
|
||||
step1_section:
|
||||
label: backend::lang.import_export.export_output_format
|
||||
type: section
|
||||
|
||||
format_preset:
|
||||
label: backend::lang.import_export.file_format
|
||||
type: dropdown
|
||||
default: standard
|
||||
options:
|
||||
standard: backend::lang.import_export.standard_format
|
||||
custom: backend::lang.import_export.custom_format
|
||||
span: left
|
||||
|
||||
format_delimiter:
|
||||
label: backend::lang.import_export.delimiter_char
|
||||
default: ','
|
||||
span: left
|
||||
trigger:
|
||||
action: show
|
||||
condition: value[custom]
|
||||
field: format_preset
|
||||
|
||||
format_enclosure:
|
||||
label: backend::lang.import_export.enclosure_char
|
||||
span: auto
|
||||
default: '"'
|
||||
trigger:
|
||||
action: show
|
||||
condition: value[custom]
|
||||
field: format_preset
|
||||
|
||||
format_escape:
|
||||
label: backend::lang.import_export.escape_char
|
||||
span: auto
|
||||
default: '\'
|
||||
trigger:
|
||||
action: show
|
||||
condition: value[custom]
|
||||
field: format_preset
|
||||
|
||||
step2_section:
|
||||
label: backend::lang.import_export.select_columns
|
||||
type: section
|
||||
|
||||
export_columns:
|
||||
label: backend::lang.import_export.columns
|
||||
type: partial
|
||||
path: ~/modules/backend/behaviors/importexportcontroller/partials/_export_columns.php
|
||||
span: left
|
||||
|
||||
step3_section:
|
||||
label: backend::lang.import_export.set_export_options
|
||||
type: section
|
||||
@@ -0,0 +1,95 @@
|
||||
# ===================================
|
||||
# Field Definitions
|
||||
# ===================================
|
||||
|
||||
fields:
|
||||
step1_section:
|
||||
label: backend::lang.import_export.upload_csv_file
|
||||
type: section
|
||||
|
||||
import_file:
|
||||
label: backend::lang.import_export.import_file
|
||||
type: fileupload
|
||||
mode: file
|
||||
span: left
|
||||
fileTypes: csv
|
||||
useCaption: false
|
||||
|
||||
format_preset:
|
||||
label: backend::lang.import_export.file_format
|
||||
type: dropdown
|
||||
default: standard
|
||||
options:
|
||||
standard: backend::lang.import_export.standard_format
|
||||
custom: backend::lang.import_export.custom_format
|
||||
span: right
|
||||
|
||||
format_delimiter:
|
||||
label: backend::lang.import_export.delimiter_char
|
||||
default: ','
|
||||
span: left
|
||||
trigger:
|
||||
action: show
|
||||
condition: value[custom]
|
||||
field: format_preset
|
||||
|
||||
format_enclosure:
|
||||
label: backend::lang.import_export.enclosure_char
|
||||
span: auto
|
||||
default: '"'
|
||||
trigger:
|
||||
action: show
|
||||
condition: value[custom]
|
||||
field: format_preset
|
||||
|
||||
format_escape:
|
||||
label: backend::lang.import_export.escape_char
|
||||
span: auto
|
||||
default: '\'
|
||||
trigger:
|
||||
action: show
|
||||
condition: value[custom]
|
||||
field: format_preset
|
||||
|
||||
format_encoding:
|
||||
label: backend::lang.import_export.encoding_format
|
||||
span: auto
|
||||
default: UTF-8
|
||||
type: dropdown
|
||||
trigger:
|
||||
action: show
|
||||
condition: value[custom]
|
||||
field: format_preset
|
||||
|
||||
first_row_titles:
|
||||
label: backend::lang.import_export.first_row_contains_titles
|
||||
comment: backend::lang.import_export.first_row_contains_titles_desc
|
||||
type: checkbox
|
||||
default: true
|
||||
span: left
|
||||
|
||||
step2_section:
|
||||
label: backend::lang.import_export.match_columns
|
||||
type: section
|
||||
|
||||
column_control_panel:
|
||||
type: partial
|
||||
path: ~/modules/backend/behaviors/importexportcontroller/partials/_import_toolbar.php
|
||||
|
||||
import_file_columns:
|
||||
label: backend::lang.import_export.file_columns
|
||||
type: partial
|
||||
path: ~/modules/backend/behaviors/importexportcontroller/partials/_import_file_columns.php
|
||||
dependsOn: [import_file, first_row_titles, format_delimiter, format_enclosure, format_escape, format_encoding]
|
||||
span: left
|
||||
|
||||
import_db_columns:
|
||||
label: backend::lang.import_export.database_fields
|
||||
type: partial
|
||||
path: ~/modules/backend/behaviors/importexportcontroller/partials/_import_db_columns.php
|
||||
dependsOn: [import_file, first_row_titles, format_delimiter, format_enclosure, format_escape, format_encoding]
|
||||
span: right
|
||||
|
||||
step3_section:
|
||||
label: backend::lang.import_export.set_import_options
|
||||
type: section
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php Block::put('breadcrumb') ?>
|
||||
<?= $this->makeLayoutPartial('breadcrumb') ?>
|
||||
<?php Block::endPut() ?>
|
||||
|
||||
<?= Form::open(['class' => 'layout']) ?>
|
||||
|
||||
<div class="layout-row">
|
||||
<?= $this->exportRender() ?>
|
||||
</div>
|
||||
|
||||
<div class="form-buttons">
|
||||
<div class="loading-indicator-container">
|
||||
<button
|
||||
type="submit"
|
||||
data-control="popup"
|
||||
data-handler="onExportLoadForm"
|
||||
data-keyboard="false"
|
||||
class="btn btn-primary">
|
||||
<?= e(trans('backend::lang.import_export.export')) ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= Form::close() ?>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php Block::put('breadcrumb') ?>
|
||||
<?= $this->makeLayoutPartial('breadcrumb') ?>
|
||||
<?php Block::endPut() ?>
|
||||
|
||||
<?= Form::open(['class' => 'layout']) ?>
|
||||
|
||||
<div class="layout-row">
|
||||
<?= $this->importRender() ?>
|
||||
</div>
|
||||
|
||||
<div class="form-buttons">
|
||||
<div class="loading-indicator-container">
|
||||
<button
|
||||
type="submit"
|
||||
data-control="popup"
|
||||
data-handler="onImportLoadForm"
|
||||
data-keyboard="false"
|
||||
class="btn btn-primary">
|
||||
<?= e(trans('backend::lang.import_export.import')) ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?= Form::close() ?>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php if ($toolbar): ?>
|
||||
<?= $toolbar->render() ?>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if ($filter): ?>
|
||||
<?= $filter->render() ?>
|
||||
<?php endif ?>
|
||||
|
||||
<?= $list->render() ?>
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
$listController = $this->getClassExtension(\Backend\Behaviors\ListController::class);
|
||||
$listConfig = $listController->getConfig();
|
||||
?>
|
||||
|
||||
<div data-control="toolbar">
|
||||
<?php if ($this->isClassExtendedWith(\Backend\Behaviors\FormController::class)): ?>
|
||||
<a
|
||||
href="<?= $this->actionUrl('create') ?>"
|
||||
class="btn btn-primary wn-icon-plus">
|
||||
<?= e(trans('backend::lang.form.create_title', ['name' => trans(\Winter\Storm\Support\Str::before($listConfig->title, '_plural'))])); ?>
|
||||
</a>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if (isset($listConfig->showCheckboxes) && $listConfig->showCheckboxes != false): ?>
|
||||
<button
|
||||
class="btn btn-danger wn-icon-trash-o"
|
||||
disabled="disabled"
|
||||
onclick="$(this).data('request-data', { checked: $('.control-list').listWidget('getChecked') })"
|
||||
data-request="onDelete"
|
||||
data-request-confirm="<?= e(trans('backend::lang.list.delete_selected_confirm')); ?>"
|
||||
data-trigger-action="enable"
|
||||
data-trigger=".control-list input[type=checkbox]"
|
||||
data-trigger-condition="checked"
|
||||
data-request-success="$(this).prop('disabled', 'disabled')"
|
||||
data-stripe-load-indicator
|
||||
>
|
||||
<?= e(trans('backend::lang.list.delete_selected')); ?>
|
||||
</button>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if ($this->isClassExtendedWith(\Backend\Behaviors\ReorderController::class)): ?>
|
||||
<a
|
||||
href="<?= $this->actionUrl('reorder') ?>"
|
||||
class="btn btn-default wn-icon-arrows-up-down">
|
||||
<?= e(trans('backend::lang.reorder.reorder_title', ['name' => trans($listConfig->title)])); ?>
|
||||
</a>
|
||||
<?php endif ?>
|
||||
|
||||
<?php if ($this->isClassExtendedWith(\Backend\Behaviors\ImportExportController::class)): ?>
|
||||
<div class="btn-group">
|
||||
<?php $importExport = $this->asExtension(\Backend\Behaviors\ImportExportController::class); ?>
|
||||
<?php if ($importExport->userHasAccess('export')): ?>
|
||||
<a
|
||||
href="<?= $this->actionUrl('export') ?>"
|
||||
class="btn btn-default wn-icon-download">
|
||||
<?= e(trans('backend::lang.import_export.export')) ?>
|
||||
</a>
|
||||
<?php endif ?>
|
||||
<?php if ($importExport->userHasAccess('import')): ?>
|
||||
<a
|
||||
href="<?= $this->actionUrl('import') ?>"
|
||||
class="btn btn-default wn-icon-upload">
|
||||
<?= e(trans('backend::lang.import_export.import')) ?>
|
||||
</a>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
1
modules/backend/behaviors/listcontroller/views/index.php
Normal file
1
modules/backend/behaviors/listcontroller/views/index.php
Normal file
@@ -0,0 +1 @@
|
||||
<?= $this->listRender() ?>
|
||||
@@ -0,0 +1,39 @@
|
||||
.relation-behavior {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.relation-behavior .control-list {
|
||||
border: 1px solid #eeeeee;
|
||||
}
|
||||
.relation-behavior .control-list thead > tr > th {
|
||||
border-top: none !important;
|
||||
border-color: #eeeeee;
|
||||
}
|
||||
.relation-behavior .control-toolbar {
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
.relation-behavior .control-toolbar .toolbar-item .form-control.search {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.relation-behavior .control-toolbar .loading-indicator-container.size-input-text {
|
||||
min-height: 0;
|
||||
}
|
||||
.relation-behavior .control-toolbar .loading-indicator-container.size-input-text .loading-indicator > span {
|
||||
top: 4px;
|
||||
}
|
||||
.relation-behavior .list-header {
|
||||
padding: 0;
|
||||
}
|
||||
.relation-behavior .control-list:last-child > table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.relation-flush .control-list {
|
||||
border-top: none;
|
||||
}
|
||||
.relation-inset {
|
||||
margin-left: -20px;
|
||||
margin-right: -20px;
|
||||
}
|
||||
.form-group > .relation-behavior .control-toolbar {
|
||||
padding: 0 0 10px 0;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Scripts for the Relation controller behavior.
|
||||
*/
|
||||
+function ($) { "use strict";
|
||||
|
||||
var RelationBehavior = function() {
|
||||
|
||||
this.toggleListCheckbox = function(el) {
|
||||
$(el).closest('.control-list').listWidget('toggleChecked', [el])
|
||||
}
|
||||
|
||||
this.clickViewListRecord = function(recordId, relationId, sessionKey) {
|
||||
var newPopup = $('<a />'),
|
||||
$container = $('#'+relationId),
|
||||
requestData = paramToObj('data-request-data', $container.data('request-data'))
|
||||
|
||||
newPopup.popup({
|
||||
handler: 'onRelationClickViewList',
|
||||
size: 'huge',
|
||||
extraData: $.extend({}, requestData, {
|
||||
'manage_id': recordId,
|
||||
'_session_key': sessionKey
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
this.clickManageListRecord = function(recordId, relationId, sessionKey) {
|
||||
var oldPopup = $('#relationManagePopup'),
|
||||
$container = $('#'+relationId),
|
||||
requestData = paramToObj('data-request-data', $container.data('request-data'))
|
||||
|
||||
$.request('onRelationClickManageList', {
|
||||
data: $.extend({}, requestData, {
|
||||
'record_id': recordId,
|
||||
'_session_key': sessionKey
|
||||
})
|
||||
})
|
||||
|
||||
oldPopup.popup('hide')
|
||||
}
|
||||
|
||||
this.clickManagePivotListRecord = function(foreignId, relationId, sessionKey) {
|
||||
var oldPopup = $('#relationManagePivotPopup'),
|
||||
newPopup = $('<a />'),
|
||||
$container = $('#'+relationId),
|
||||
requestData = paramToObj('data-request-data', $container.data('request-data'))
|
||||
|
||||
if (oldPopup.length) {
|
||||
oldPopup.popup('hide')
|
||||
}
|
||||
|
||||
newPopup.popup({
|
||||
handler: 'onRelationClickManageListPivot',
|
||||
size: 'huge',
|
||||
extraData: $.extend({}, requestData, {
|
||||
'foreign_id': foreignId,
|
||||
'_session_key': sessionKey
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* This function is called every time a record is created, added, removed
|
||||
* or deleted using the relation widget. It triggers the change.oc.formwidget
|
||||
* event to notify other elements on the page about the changed form state.
|
||||
*/
|
||||
this.changed = function(relationId, event) {
|
||||
$('[data-field-name="' + relationId + '"]').trigger('change.oc.formwidget', {event: event});
|
||||
}
|
||||
|
||||
/*
|
||||
* This function transfers the supplied variables as hidden form inputs,
|
||||
* to any popup that is spawned within the supplied container. The spawned
|
||||
* popup must contain a form element.
|
||||
*/
|
||||
this.bindToPopups = function(container, vars) {
|
||||
$(container).on('show.oc.popup', function(event, $trigger, $modal){
|
||||
var $form = $('form', $modal)
|
||||
$.each(vars, function(name, value){
|
||||
$form.prepend($('<input />').attr({ type: 'hidden', name: name, value: value }))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function paramToObj(name, value) {
|
||||
if (value === undefined) value = ''
|
||||
if (typeof value == 'object') return value
|
||||
|
||||
try {
|
||||
return ocJSON("{" + value + "}")
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error('Error parsing the '+name+' attribute value. '+e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$.wn.relationBehavior = new RelationBehavior;
|
||||
}(window.jQuery);
|
||||
@@ -0,0 +1,60 @@
|
||||
@import "../../../../assets/less/core/boot.less";
|
||||
|
||||
@color-relation-border: #eeeeee;
|
||||
|
||||
.relation-behavior {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.control-list {
|
||||
border: 1px solid @color-relation-border;
|
||||
|
||||
thead > tr > th {
|
||||
border-top: none !important;
|
||||
border-color: @color-relation-border;
|
||||
}
|
||||
}
|
||||
|
||||
.control-toolbar {
|
||||
padding: 0 20px 20px 20px;
|
||||
|
||||
.toolbar-item .form-control.search {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.loading-indicator-container.size-input-text {
|
||||
min-height: 0;
|
||||
.loading-indicator > span {
|
||||
top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-header {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.control-list:last-child > table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Relation manager to sit flush to the element above
|
||||
.relation-flush {
|
||||
.control-list {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Relation manager to sit inset the standard padding (20px)
|
||||
.relation-inset {
|
||||
margin-left: -20px;
|
||||
margin-right: -20px;
|
||||
}
|
||||
|
||||
// Displayed in a form field
|
||||
.form-group > .relation-behavior {
|
||||
.control-toolbar {
|
||||
padding: 0 0 10px 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<a
|
||||
data-control="popup"
|
||||
data-size="huge"
|
||||
data-handler="onRelationButtonAdd"
|
||||
href="javascript:;"
|
||||
class="btn btn-sm btn-secondary wn-icon-plus">
|
||||
<?= e(trans($text, ['name' => trans($relationLabel)])) ?>
|
||||
</a>
|
||||
@@ -0,0 +1,8 @@
|
||||
<a
|
||||
data-control="popup"
|
||||
data-size="huge"
|
||||
data-handler="onRelationButtonCreate"
|
||||
href="javascript:;"
|
||||
class="btn btn-sm btn-secondary wn-icon-file">
|
||||
<?= e(trans($text, ['name' => trans($relationLabel)])) ?>
|
||||
</a>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php if ($relationViewMode == 'single'): ?>
|
||||
<button
|
||||
class="btn btn-sm btn-secondary wn-icon-trash-o"
|
||||
data-request="onRelationButtonDelete"
|
||||
data-request-confirm="<?= e(trans('backend::lang.relation.delete_confirm')) ?>"
|
||||
data-request-success="$.wn.relationBehavior.changed('<?= e($relationField) ?>', 'deleted')"
|
||||
data-stripe-load-indicator>
|
||||
<?= e(trans($text)) ?>
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<button
|
||||
class="btn btn-sm btn-secondary wn-icon-trash-o"
|
||||
onclick="$(this).data('request-data', {
|
||||
checked: $('#<?= $this->relationGetId('view') ?> .control-list').listWidget('getChecked')
|
||||
})"
|
||||
disabled="disabled"
|
||||
data-request="onRelationButtonDelete"
|
||||
data-request-confirm="<?= e(trans('backend::lang.relation.delete_confirm')) ?>"
|
||||
data-request-success="$.wn.relationBehavior.changed('<?= e($relationField) ?>', 'deleted')"
|
||||
data-trigger-action="enable"
|
||||
data-trigger="#<?= $this->relationGetId('view') ?> .control-list input[type=checkbox]"
|
||||
data-trigger-condition="checked"
|
||||
data-stripe-load-indicator>
|
||||
<?= e(trans($text)) ?>
|
||||
</button>
|
||||
<?php endif ?>
|
||||
@@ -0,0 +1,8 @@
|
||||
<a
|
||||
data-control="popup"
|
||||
data-size="huge"
|
||||
data-handler="onRelationButtonLink"
|
||||
href="javascript:;"
|
||||
class="btn btn-sm btn-secondary wn-icon-link">
|
||||
<?= e(trans($text, ['name' => trans($relationLabel)])) ?>
|
||||
</a>
|
||||
@@ -0,0 +1,6 @@
|
||||
<button
|
||||
class="btn btn-sm btn-secondary wn-icon-arrows-rotate"
|
||||
data-request="onRelationButtonRefresh"
|
||||
data-stripe-load-indicator>
|
||||
<?= e(trans($text)) ?>
|
||||
</button>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php if ($relationViewMode == 'single'): ?>
|
||||
<button
|
||||
class="btn btn-sm btn-secondary wn-icon-minus"
|
||||
data-request="onRelationButtonRemove"
|
||||
data-request-success="$.wn.relationBehavior.changed('<?= e($relationField) ?>', 'removed')"
|
||||
data-stripe-load-indicator>
|
||||
<?= e(trans($text)) ?>
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<button
|
||||
class="btn btn-sm btn-secondary wn-icon-minus"
|
||||
onclick="$(this).data('request-data', {
|
||||
checked: $('#<?= $this->relationGetId('view') ?> .control-list').listWidget('getChecked')
|
||||
})"
|
||||
disabled="disabled"
|
||||
data-request="onRelationButtonRemove"
|
||||
data-request-success="$.wn.relationBehavior.changed('<?= e($relationField) ?>', 'removed')"
|
||||
data-trigger-action="enable"
|
||||
data-trigger="#<?= $this->relationGetId('view') ?> .control-list input[type=checkbox]"
|
||||
data-trigger-condition="checked"
|
||||
data-stripe-load-indicator>
|
||||
<?= e(trans($text)) ?>
|
||||
</button>
|
||||
<?php endif ?>
|
||||
@@ -0,0 +1,9 @@
|
||||
<a
|
||||
href="javascript:;"
|
||||
class="btn btn-sm btn-secondary wn-icon-unlink"
|
||||
data-request="onRelationButtonUnlink"
|
||||
data-request-success="$.wn.relationBehavior.changed('<?= e($relationField) ?>', 'removed')"
|
||||
data-request-confirm="<?= e(trans('backend::lang.relation.unlink_confirm')) ?>"
|
||||
data-stripe-load-indicator>
|
||||
<?= e(trans($text)) ?>
|
||||
</a>
|
||||
@@ -0,0 +1,9 @@
|
||||
<a
|
||||
data-control="popup"
|
||||
data-size="huge"
|
||||
data-handler="onRelationButtonUpdate"
|
||||
data-request-data="manage_id: '<?= $relationManageId ?>'"
|
||||
href="javascript:;"
|
||||
class="btn btn-sm btn-secondary wn-icon-pencil">
|
||||
<?= e(trans($text, ['name' => trans($relationLabel)])) ?>
|
||||
</a>
|
||||
@@ -0,0 +1,18 @@
|
||||
<div
|
||||
id="<?= $this->relationGetId() ?>"
|
||||
data-request-data="_relation_field: '<?= $relationField ?>', _relation_extra_config: '<?= e(base64_encode(json_encode($relationExtraConfig))) ?>'"
|
||||
class="relation-behavior relation-view-<?= $relationViewMode ?>">
|
||||
|
||||
<?php if ($toolbar = $this->relationRenderToolbar()): ?>
|
||||
<!-- Relation Toolbar -->
|
||||
<div id="<?= $this->relationGetId('toolbar') ?>" class="relation-toolbar">
|
||||
<?= $toolbar ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<!-- Relation View -->
|
||||
<div id="<?= $this->relationGetId('view') ?>" class="relation-manager">
|
||||
<?= $this->relationRenderView() ?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,71 @@
|
||||
<div id="<?= $relationManageWidget->getId('managePopup') ?>">
|
||||
<?php if ($relationManageId): ?>
|
||||
|
||||
<?= Form::ajax('onRelationManageUpdate', [
|
||||
'data-popup-load-indicator' => true,
|
||||
'sessionKey' => $newSessionKey,
|
||||
'data-request-success' => "$.wn.relationBehavior.changed('" . e($relationField) . "', 'updated')",
|
||||
]) ?>
|
||||
|
||||
<!-- Passable fields -->
|
||||
<input type="hidden" name="manage_id" value="<?= $relationManageId ?>" />
|
||||
<input type="hidden" name="_relation_field" value="<?= $relationField ?>" />
|
||||
<input type="hidden" name="_relation_mode" value="form" />
|
||||
<input type="hidden" name="_relation_session_key" value="<?= $relationSessionKey ?>" />
|
||||
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="popup">×</button>
|
||||
<h4 class="modal-title">
|
||||
<?= e(trans($relationManageTitle, ['name' => trans($relationLabel)])) ?>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<?= $relationManageWidget->render(['preview' => $this->readOnly]) ?>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<?= $this->relationMakePartial('manage_form_footer_update') ?>
|
||||
</div>
|
||||
|
||||
<?= Form::close() ?>
|
||||
|
||||
<?php else: ?>
|
||||
|
||||
<?= Form::ajax('onRelationManageCreate', [
|
||||
'data-popup-load-indicator' => true,
|
||||
'data-request-success' => "$.wn.relationBehavior.changed('" . e($relationField) . "', 'created')",
|
||||
'sessionKey' => $newSessionKey
|
||||
]) ?>
|
||||
|
||||
<!-- Passable fields -->
|
||||
<input type="hidden" name="_relation_field" value="<?= $relationField ?>" />
|
||||
<input type="hidden" name="_relation_mode" value="form" />
|
||||
<input type="hidden" name="_relation_session_key" value="<?= $relationSessionKey ?>" />
|
||||
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="popup">×</button>
|
||||
<h4 class="modal-title">
|
||||
<?= e(trans($relationManageTitle, ['name' => trans($relationLabel)])) ?>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<?= $relationManageWidget->render() ?>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<?= $this->relationMakePartial('manage_form_footer_create') ?>
|
||||
</div>
|
||||
<?= Form::close() ?>
|
||||
|
||||
<?php endif ?>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$.wn.relationBehavior.bindToPopups('#<?= $relationManageWidget->getId("managePopup") ?>', {
|
||||
_relation_field: '<?= $relationField ?>',
|
||||
_relation_mode: 'form'
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,11 @@
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary">
|
||||
<?= e(trans('backend::lang.relation.create')) ?>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
data-dismiss="popup">
|
||||
<?= e(trans('backend::lang.relation.cancel')) ?>
|
||||
</button>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php if ($this->readOnly): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
data-dismiss="popup">
|
||||
<?= e(trans('backend::lang.relation.close')) ?>
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary">
|
||||
<?= e(trans('backend::lang.relation.update')) ?>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
data-dismiss="popup">
|
||||
<?= e(trans('backend::lang.relation.cancel')) ?>
|
||||
</button>
|
||||
<?php endif ?>
|
||||
@@ -0,0 +1,24 @@
|
||||
<div id="relationManagePopup" data-request-data="_relation_field: '<?= $relationField ?>', _relation_mode: 'list'">
|
||||
<?= Form::open() ?>
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="popup">×</button>
|
||||
<h4 class="modal-title"><?= e(trans($relationManageTitle, [
|
||||
'name' => trans($relationLabel)
|
||||
])) ?></h4>
|
||||
</div>
|
||||
|
||||
<div class="list-flush">
|
||||
<?php if ($relationSearchWidget): ?>
|
||||
<?= $relationSearchWidget->render() ?>
|
||||
<?php endif ?>
|
||||
<?php if ($relationManageFilterWidget): ?>
|
||||
<?= $relationManageFilterWidget->render() ?>
|
||||
<?php endif ?>
|
||||
<?= $relationManageWidget->render() ?>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<?= $this->relationMakePartial('manage_list_footer') ?>
|
||||
</div>
|
||||
<?= Form::close() ?>
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php if ($relationManageWidget->showCheckboxes): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
data-request="onRelationManageAdd"
|
||||
data-dismiss="popup"
|
||||
data-request-success="$.wn.relationBehavior.changed('<?= e($relationField) ?>', 'added')"
|
||||
data-stripe-load-indicator>
|
||||
<?= e(trans('backend::lang.relation.add_selected')) ?>
|
||||
</button>
|
||||
<?php endif ?>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
data-dismiss="popup">
|
||||
<?= e(trans('backend::lang.relation.cancel')) ?>
|
||||
</button>
|
||||
@@ -0,0 +1,32 @@
|
||||
<div id="relationManagePivotPopup" data-request-data="_relation_field: '<?= $relationField ?>'">
|
||||
<?= Form::open() ?>
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="popup">×</button>
|
||||
<h4 class="modal-title"><?= e(trans($relationManageTitle, ['name'=>trans($relationLabel)])) ?></h4>
|
||||
</div>
|
||||
<?php if (!$relationSearchWidget): ?>
|
||||
<div class="modal-body">
|
||||
<p><?= e(trans('backend::lang.relation.help')) ?></p>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
<div class="list-flush">
|
||||
<?php if ($relationSearchWidget): ?>
|
||||
<?= $relationSearchWidget->render() ?>
|
||||
<?php endif ?>
|
||||
<?php if ($relationManageFilterWidget): ?>
|
||||
<?= $relationManageFilterWidget->render() ?>
|
||||
<?php endif ?>
|
||||
<?= $relationManageWidget->render() ?>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<?= $this->relationMakePartial('manage_pivot_footer') ?>
|
||||
</div>
|
||||
<?= Form::close() ?>
|
||||
</div>
|
||||
<script>
|
||||
setTimeout(
|
||||
function(){ $('#relationManagePivotPopup input.form-control:first').focus() },
|
||||
310
|
||||
)
|
||||
</script>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php if ($relationManageWidget->showCheckboxes): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
data-control="popup"
|
||||
data-handler="onRelationManageAddPivot"
|
||||
data-size="huge"
|
||||
data-dismiss="popup"
|
||||
data-stripe-load-indicator>
|
||||
<?= e(trans('backend::lang.relation.add_selected')) ?>
|
||||
</button>
|
||||
<?php endif ?>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
data-dismiss="popup">
|
||||
<?= e(trans('backend::lang.relation.cancel')) ?>
|
||||
</button>
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php if ($relationManageId): ?>
|
||||
|
||||
<?= Form::ajax('onRelationManagePivotUpdate', [
|
||||
'data' => ['_relation_field' => $relationField, 'manage_id' => $relationManageId],
|
||||
'data-request-success' => "$.wn.relationBehavior.changed('" . e($relationField) . "', 'updated')",
|
||||
'data-popup-load-indicator' => true
|
||||
]) ?>
|
||||
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="popup">×</button>
|
||||
<h4 class="modal-title"><?= e(trans('backend::lang.relation.related_data', ['name'=>trans($relationLabel)])) ?></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<?= $relationPivotWidget->render(['preview' => $this->readOnly]) ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<?= $this->relationMakePartial('pivot_form_footer') ?>
|
||||
</div>
|
||||
|
||||
<?= Form::close() ?>
|
||||
|
||||
<?php else: ?>
|
||||
|
||||
<?= Form::ajax('onRelationManagePivotCreate', [
|
||||
'data' => ['_relation_field' => $relationField, 'foreign_id' => $foreignId],
|
||||
'data-request-success' => "$.wn.relationBehavior.changed('" . e($relationField) . "', 'created')",
|
||||
'data-popup-load-indicator' => true
|
||||
]) ?>
|
||||
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="popup">×</button>
|
||||
<h4 class="modal-title"><?= e(trans('backend::lang.relation.related_data', ['name'=>trans($relationLabel)])) ?></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<?= $relationPivotWidget->render() ?>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary">
|
||||
<?= e(trans('backend::lang.relation.add')) ?>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
data-dismiss="popup">
|
||||
<?= e(trans('backend::lang.relation.cancel')) ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?= Form::close() ?>
|
||||
|
||||
<?php endif ?>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php if ($this->readOnly): ?>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
data-dismiss="popup">
|
||||
<?= e(trans('backend::lang.relation.close')) ?>
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-primary">
|
||||
<?= e(trans('backend::lang.relation.update')) ?>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-default"
|
||||
data-dismiss="popup">
|
||||
<?= e(trans('backend::lang.relation.cancel')) ?>
|
||||
</button>
|
||||
<?php endif ?>
|
||||
@@ -0,0 +1,18 @@
|
||||
<div data-control="toolbar">
|
||||
|
||||
<?php foreach ($relationToolbarButtons as $type => $text): ?>
|
||||
|
||||
<?php if ($type === 'update'): ?>
|
||||
<?= $this->relationMakePartial('button_update', [
|
||||
'relationManageId' => $relationViewModel->getKey(),
|
||||
'text' => $text
|
||||
]) ?>
|
||||
<?php else: ?>
|
||||
<?= $this->relationMakePartial('button_' . $type, [
|
||||
'text' => $text
|
||||
]) ?>
|
||||
<?php endif ?>
|
||||
|
||||
<?php endforeach ?>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php if ($relationViewFilterWidget): ?>
|
||||
<?= $relationViewFilterWidget->render() ?>
|
||||
<?php endif ?>
|
||||
|
||||
<?= $relationViewWidget->render() ?>
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Scripts for the Reorder controller behavior.
|
||||
*
|
||||
* The following functions are observed:
|
||||
* - Simple sorting: Post back the original sort orders and the new ordered identifiers.
|
||||
* - Nested sorting: Post back source and target nodes IDs and the move positioning.
|
||||
*/
|
||||
+function ($) { "use strict";
|
||||
|
||||
var ReorderBehavior = function() {
|
||||
|
||||
this.sortMode = null
|
||||
|
||||
this.simpleSortOrders = []
|
||||
|
||||
this.initSorting = function (mode) {
|
||||
this.sortMode = mode
|
||||
|
||||
if (mode == 'simple') {
|
||||
this.initSortingSimple()
|
||||
}
|
||||
|
||||
$('#reorderTreeList').on('move.oc.treelist', $.proxy(this.processReorder, this))
|
||||
}
|
||||
|
||||
|
||||
this.processReorder = function(ev, sortData){
|
||||
var postData
|
||||
|
||||
if (this.sortMode == 'simple') {
|
||||
postData = { sort_orders: this.simpleSortOrders }
|
||||
}
|
||||
else if (this.sortMode == 'nested') {
|
||||
postData = this.getNestedMoveData(sortData)
|
||||
}
|
||||
|
||||
$('#reorderTreeList').request('onReorder', {
|
||||
data: postData
|
||||
})
|
||||
}
|
||||
|
||||
this.getNestedMoveData = function (sortData) {
|
||||
var
|
||||
$el,
|
||||
$item = sortData.item,
|
||||
moveData = {
|
||||
targetNode: 0,
|
||||
sourceNode: $item.data('recordId'),
|
||||
position: 'root'
|
||||
}
|
||||
|
||||
if (($el = $item.next()) && $el.length) {
|
||||
moveData.position = 'before'
|
||||
}
|
||||
else if (($el = $item.prev()) && $el.length) {
|
||||
moveData.position = 'after'
|
||||
}
|
||||
else if (($el = $item.parents('li:first')) && $el.length) {
|
||||
moveData.position = 'child'
|
||||
}
|
||||
|
||||
if ($el.length) {
|
||||
moveData.targetNode = $el.data('recordId')
|
||||
}
|
||||
|
||||
return moveData
|
||||
}
|
||||
|
||||
this.initSortingSimple = function () {
|
||||
var sortOrders = []
|
||||
|
||||
$('#reorderTreeList li').each(function(i) {
|
||||
sortOrders.push(i);
|
||||
})
|
||||
|
||||
this.simpleSortOrders = sortOrders
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$.wn.reorderBehavior = new ReorderBehavior;
|
||||
}(window.jQuery);
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php if ($reorderToolbarWidget): ?>
|
||||
<!-- Reorder Toolbar -->
|
||||
<div id="<?= $this->getId('reorderToolbar') ?>" class="reorder-toolbar">
|
||||
<?= $reorderToolbarWidget->render() ?>
|
||||
</div>
|
||||
<?php endif ?>
|
||||
|
||||
<!-- Reorder List -->
|
||||
<?= Form::open() ?>
|
||||
<div
|
||||
id="reorderTreeList"
|
||||
class="control-treelist"
|
||||
data-control="treelist"
|
||||
<?= $reorderShowTree ? '' : 'data-nested="0"' ?>
|
||||
data-handle="<?= $reorderShowTree ? 'a.move' : '> li > .record > a.move' ?>"
|
||||
data-stripe-load-indicator>
|
||||
<?php if ($reorderRecords): ?>
|
||||
<ol id="reorderRecords">
|
||||
<?= $this->reorderMakePartial('records', ['records' => $reorderRecords]) ?>
|
||||
</ol>
|
||||
<?php else: ?>
|
||||
<p><?= Lang::get('backend::lang.reorder.no_records') ?></p>
|
||||
<?php endif ?>
|
||||
</div>
|
||||
<?= Form::close() ?>
|
||||
|
||||
<script>
|
||||
$.wn.reorderBehavior.initSorting('<?= $reorderSortMode ?>')
|
||||
</script>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php foreach ($records as $record): ?>
|
||||
|
||||
<li data-record-id="<?= $record->getKey() ?>"
|
||||
<?php if ($reorderSortMode === 'simple'): ?>
|
||||
data-record-sort-order="<?= $record->{$record->getSortOrderColumn()} ?>"
|
||||
<?php endif ?>
|
||||
>
|
||||
<div class="record">
|
||||
<a href="javascript:;" class="move"></a>
|
||||
<span><?= e($this->reorderGetRecordName($record)) ?></span>
|
||||
<input name="record_ids[]" type="hidden" value="<?= $record->getKey() ?>" />
|
||||
</div>
|
||||
|
||||
<?php if ($reorderShowTree): ?>
|
||||
<ol>
|
||||
<?php if ($record->children): ?>
|
||||
<?= $this->reorderMakePartial('records', ['records' => $record->children]) ?>
|
||||
<?php endif ?>
|
||||
</ol>
|
||||
<?php endif ?>
|
||||
</li>
|
||||
|
||||
<?php endforeach ?>
|
||||
@@ -0,0 +1,5 @@
|
||||
<div data-control="toolbar">
|
||||
<a href="<?= $this->actionUrl('/') ?>" class="btn btn-primary oc-icon-caret-left">
|
||||
<?= e(trans('backend::lang.form.return_to_list')) ?>
|
||||
</a>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php Block::put('breadcrumb') ?>
|
||||
<?= $this->makeLayoutPartial('breadcrumb') ?>
|
||||
<?php Block::endPut() ?>
|
||||
<!-- Reorder Controller Widget -->
|
||||
<?= $this->reorderRender() ?>
|
||||
Reference in New Issue
Block a user