feat: VivesPOS landing on Winter CMS 1.2 — theme + plugin + Dockerfile
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:
2026-08-21 19:29:00 -06:00
commit 1f72193a64
3266 changed files with 531480 additions and 0 deletions

View File

@@ -0,0 +1,922 @@
<?php
namespace Cms\Controllers;
use Backend\Classes\Controller;
use Backend\Facades\BackendMenu;
use Backend\Widgets\Form;
use Cms\Classes\Asset;
use Cms\Classes\CmsCompoundObject;
use Cms\Classes\CmsObject;
use Cms\Classes\ComponentManager;
use Cms\Classes\ComponentPartial;
use Cms\Classes\Content;
use Cms\Classes\Layout;
use Cms\Classes\Page;
use Cms\Classes\Partial;
use Cms\Classes\Router;
use Cms\Classes\Theme;
use Cms\Helpers\Cms as CmsHelpers;
use Cms\Widgets\AssetList;
use Cms\Widgets\ComponentList;
use Cms\Widgets\TemplateList;
use Exception;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Request;
use System\Helpers\DateTime;
use Winter\Storm\Auth\AuthorizationException;
use Winter\Storm\Exception\ApplicationException;
use Winter\Storm\Halcyon\Datasource\DatasourceInterface;
use Winter\Storm\Router\Router as StormRouter;
use Winter\Storm\Support\Facades\Config;
use Winter\Storm\Support\Facades\Event;
use Winter\Storm\Support\Facades\Flash;
use Winter\Storm\Support\Facades\Url;
/**
* CMS index
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
*/
class Index extends Controller
{
use \Backend\Traits\InspectableContainer;
/**
* @var Theme
*/
protected $theme;
/**
* @var array Permissions required to view this page.
*/
public $requiredPermissions = [
'cms.manage_content',
'cms.manage_assets',
'cms.manage_pages',
'cms.manage_layouts',
'cms.manage_partials'
];
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
Event::listen('backend.form.extendFieldsBefore', function ($widget) {
if (!$widget->getController() instanceof Index) {
return;
}
if (!$widget->model instanceof CmsCompoundObject) {
return;
}
if (empty($widget->secondaryTabs['fields'])) {
return;
}
if (array_key_exists('code', $widget->secondaryTabs['fields']) && CmsHelpers::safeModeEnabled()) {
$widget->secondaryTabs['fields']['safemode_notice']['hidden'] = false;
$widget->secondaryTabs['fields']['code']['readOnly'] = true;
};
});
BackendMenu::setContext('Winter.Cms', 'cms', true);
try {
if (!($theme = Theme::getEditTheme())) {
throw new ApplicationException(Lang::get('cms::lang.theme.edit.not_found'));
}
$this->theme = $theme;
if ($this->user?->hasAccess('cms.manage_pages')) {
new TemplateList($this, 'pageList', function () use ($theme) {
return Page::listInTheme($theme, true);
});
}
if ($this->user?->hasAccess('cms.manage_partials')) {
new TemplateList($this, 'partialList', function () use ($theme) {
return Partial::listInTheme($theme, true);
});
}
if ($this->user?->hasAccess('cms.manage_layouts')) {
new TemplateList($this, 'layoutList', function () use ($theme) {
return Layout::listInTheme($theme, true);
});
}
if ($this->user?->hasAccess('cms.manage_content')) {
new TemplateList($this, 'contentList', function () use ($theme) {
return Content::listInTheme($theme, true);
});
}
if (
$this->user?->hasAccess([
'cms.manage_pages',
'cms.manage_partials',
'cms.manage_layouts',
], false)
) {
new ComponentList($this, 'componentList');
}
if ($this->user?->hasAccess('cms.manage_assets')) {
new AssetList($this, 'assetList');
}
}
catch (Exception $ex) {
$this->handleError($ex);
}
}
//
// Pages
//
/**
* Index page action
*/
public function index(): void
{
$this->addJs('/modules/cms/assets/js/winter.cmspage.js', 'core');
$this->addJs('/modules/cms/assets/js/winter.dragcomponents.js', 'core');
$this->addJs('/modules/cms/assets/js/winter.tokenexpander.js', 'core');
$this->addCss('/modules/cms/assets/css/winter.components.css', 'core');
$this->bodyClass = 'compact-container';
$this->pageTitle = 'cms::lang.cms.menu_label';
$this->pageTitleTemplate = '%s '.Lang::get($this->pageTitle);
if (Request::ajax() && Request::input('formWidgetAlias')) {
$this->bindFormWidgetToController();
}
}
/**
* Opens an existing template from the index page
*/
public function index_onOpenTemplate(): array
{
$this->validateRequestTheme();
$type = Request::input('type');
$this->validateRequestType($type);
$template = $this->loadTemplate($type, Request::input('path'));
$widget = $this->makeTemplateFormWidget($type, $template);
$this->vars['templatePath'] = Request::input('path');
$this->vars['lastModified'] = DateTime::makeCarbon($template->mtime);
$this->vars['canCommit'] = $this->canCommitTemplate($template);
$this->vars['canReset'] = $this->canResetTemplate($template);
if ($type === 'page') {
$router = new StormRouter;
$this->vars['pageUrl'] = $router->urlFromPattern($template->url);
}
return [
'tabTitle' => $this->getTabTitle($type, $template),
'tab' => $this->makePartial('form_page', [
'form' => $widget,
'templateType' => $type,
'templateTheme' => $this->theme->getDirName(),
'templateMtime' => $template->mtime
])
];
}
/**
* Saves the template currently open
* @throws ApplicationException if the file in the datasource has been modified since the file was loaded in the browser
*/
public function onSave(): array
{
$this->validateRequestTheme();
$type = Request::input('templateType');
$this->validateRequestType($type);
$templatePath = trim(Request::input('templatePath'));
$template = $templatePath ? $this->loadTemplate($type, $templatePath) : $this->createTemplate($type);
$formWidget = $this->makeTemplateFormWidget($type, $template);
$saveData = $formWidget->getSaveData();
$postData = post();
$templateData = [];
$settings = array_get($saveData, 'settings', []) + Request::input('settings', []);
$settings = $this->upgradeSettings($settings, $template->settings);
if ($settings) {
$templateData['settings'] = $settings;
}
$fields = ['markup', 'code', 'fileName', 'content'];
foreach ($fields as $field) {
if (array_key_exists($field, $saveData)) {
$templateData[$field] = $saveData[$field];
}
elseif (array_key_exists($field, $postData)) {
$templateData[$field] = $postData[$field];
}
}
if (!empty($templateData['markup']) && Config::get('cms.convertLineEndings', false) === true) {
$templateData['markup'] = $this->convertLineEndings($templateData['markup']);
}
if (!empty($templateData['code']) && Config::get('cms.convertLineEndings', false) === true) {
$templateData['code'] = $this->convertLineEndings($templateData['code']);
}
if (
!Request::input('templateForceSave') && $template->mtime
&& Request::input('templateMtime') != $template->mtime
) {
throw new ApplicationException('mtime-mismatch');
}
$template->attributes = [];
$template->fill($templateData);
$template->save();
/**
* @event cms.template.save
* Fires after a CMS template (page|partial|layout|content|asset) has been saved.
*
* Example usage:
*
* Event::listen('cms.template.save', function ((\Cms\Controllers\Index) $controller, (mixed) $templateObject, (string) $type) {
* \Log::info("A $type has been saved");
* });
*
* Or
*
* $CmsIndexController->bindEvent('template.save', function ((mixed) $templateObject, (string) $type) {
* \Log::info("A $type has been saved");
* });
*
*/
$this->fireSystemEvent('cms.template.save', [$template, $type]);
Flash::success(Lang::get('cms::lang.template.saved'));
return $this->getUpdateResponse($template, $type);
}
/**
* Displays a form that suggests the template has been edited elsewhere
*/
public function onOpenConcurrencyResolveForm(): string
{
return $this->makePartial('concurrency_resolve_form');
}
/**
* Create a new template
*/
public function onCreateTemplate(): array
{
$type = Request::input('type');
$this->validateRequestType($type);
$template = $this->createTemplate($type);
if ($type === 'asset') {
$template->fileName = $this->widget->assetList->getCurrentRelativePath();
}
$widget = $this->makeTemplateFormWidget($type, $template);
$this->vars['templatePath'] = '';
$this->vars['canCommit'] = $this->canCommitTemplate($template);
$this->vars['canReset'] = $this->canResetTemplate($template);
return [
'tabTitle' => $this->getTabTitle($type, $template),
'tab' => $this->makePartial('form_page', [
'form' => $widget,
'templateType' => $type,
'templateTheme' => $this->theme->getDirName(),
'templateMtime' => null
])
];
}
/**
* Deletes multiple templates at the same time
*/
public function onDeleteTemplates(): array
{
$this->validateRequestTheme();
$type = Request::input('type');
$this->validateRequestType($type);
$templates = Request::input('template');
$error = null;
$deleted = [];
try {
foreach ($templates as $path => $selected) {
if ($selected) {
$this->loadTemplate($type, $path)->delete();
$deleted[] = $path;
}
}
}
catch (Exception $ex) {
$error = $ex->getMessage();
}
/**
* @event cms.template.delete
* Fires after a CMS template (page|partial|layout|content|asset) has been deleted.
*
* Example usage:
*
* Event::listen('cms.template.delete', function ((\Cms\Controllers\Index) $controller, (string) $type) {
* \Log::info("A $type has been deleted");
* });
*
* Or
*
* $CmsIndexController->bindEvent('template.delete', function ((string) $type) {
* \Log::info("A $type has been deleted");
* });
*
*/
$this->fireSystemEvent('cms.template.delete', [$type]);
return [
'deleted' => $deleted,
'error' => $error,
'theme' => Request::input('theme')
];
}
/**
* Deletes a template
*/
public function onDelete(): void
{
$this->validateRequestTheme();
$type = Request::input('templateType');
$this->validateRequestType($type);
$this->loadTemplate($type, trim(Request::input('templatePath')))->delete();
/*
* Extensibility - documented above
*/
$this->fireSystemEvent('cms.template.delete', [$type]);
}
/**
* Returns list of available templates
*/
public function onGetTemplateList(): array
{
$this->validateRequestTheme();
$page = Page::inTheme($this->theme);
return [
'layouts' => $page->getLayoutOptions()
];
}
/**
* Remembers an open or closed state for a supplied token, for example, component folders.
*/
public function onExpandMarkupToken(): string
{
if (!$alias = post('tokenName')) {
throw new ApplicationException(Lang::get('cms::lang.component.no_records'));
}
// Can only expand components at this stage
if ((!$type = post('tokenType')) && $type !== 'component') {
throw new ApplicationException("Unsupported token type: $type");
}
if (!($names = (array) post('component_names')) || !($aliases = (array) post('component_aliases'))) {
throw new ApplicationException(Lang::get('cms::lang.component.not_found', ['name' => $alias]));
}
if (($index = array_get(array_flip($aliases), $alias, false)) === false) {
throw new ApplicationException(Lang::get('cms::lang.component.not_found', ['name' => $alias]));
}
if (!$componentName = array_get($names, $index)) {
throw new ApplicationException(Lang::get('cms::lang.component.not_found', ['name' => $alias]));
}
$manager = ComponentManager::instance();
$componentObj = $manager->makeComponent($componentName);
/**
* @var ?ComponentPartial
*/
$partial = ComponentPartial::load($componentObj, 'default');
if (!$partial) {
throw new ApplicationException(Lang::get('cms::lang.component.no_default_partial'));
}
$content = $partial->getContent();
$content = str_replace('__SELF__', $alias, $content);
return $content;
}
/**
* Commits the DB changes of a template to the filesystem
*/
public function onCommit(): array
{
$this->validateRequestTheme();
$type = Request::input('templateType');
$this->validateRequestType($type);
$template = $this->loadTemplate($type, trim(Request::input('templatePath')));
if ($this->canCommitTemplate($template)) {
// Populate the filesystem with the template and then remove it from the db
/**
* @var AutoDatasource
*/
$datasource = $this->getThemeDatasource();
$datasource->pushToSource($template, 'filesystem');
$datasource->removeFromSource($template, 'database');
Flash::success(Lang::get('cms::lang.editor.commit_success', ['type' => $type]));
}
return array_merge($this->getUpdateResponse($template, $type), ['forceReload' => true]);
}
/**
* Resets a template to the version on the filesystem
*/
public function onReset(): array
{
$this->validateRequestTheme();
$type = Request::input('templateType');
$this->validateRequestType($type);
$template = $this->loadTemplate($type, trim(Request::input('templatePath')));
if ($this->canResetTemplate($template)) {
// Remove the template from the DB
/**
* @var AutoDatasource
*/
$datasource = $this->getThemeDatasource();
$datasource->removeFromSource($template, 'database');
Flash::success(Lang::get('cms::lang.editor.reset_success', ['type' => $type]));
}
return array_merge($this->getUpdateResponse($template, $type), ['forceReload' => true]);
}
//
// Methods for internal use
//
/**
* Get the response to return in an AJAX request that updates a template
*
* @param object $template The template that has been affected
* @param string $type The type of template being affected
*/
protected function getUpdateResponse($template, string $type): array
{
$result = [
'templatePath' => $template->fileName,
'templateMtime' => $template->mtime,
'tabTitle' => $this->getTabTitle($type, $template)
];
if ($type === 'page') {
$result['pageUrl'] = Url::to($template->url);
$router = new Router($this->theme);
$router->clearCache();
CmsCompoundObject::clearCache($this->theme);
}
$result['canCommit'] = $this->canCommitTemplate($template);
$result['canReset'] = $this->canResetTemplate($template);
return $result;
}
/**
* Get the active theme's datasource
*/
protected function getThemeDatasource(): DatasourceInterface
{
return $this->theme->getDatasource();
}
/**
* Check to see if the provided template can be committed
* Only available in debug mode, the DB layer must be enabled, and the template must exist in the database
*/
protected function canCommitTemplate(CmsObject|Asset $template): bool
{
if (
$template instanceof CmsObject === false
|| Config::get('app.debug', false)
) {
return false;
}
$result = false;
if (Theme::databaseLayerEnabled()) {
/**
* @var AutoDatasource
*/
$datasource = $this->getThemeDatasource();
$result = $datasource->sourceHasModel('database', $template);
}
return $result;
}
/**
* Check to see if the provided template can be reset
* Only available when the DB layer is enabled and the template exists in both the DB & Filesystem
*/
protected function canResetTemplate(CmsObject|Asset $template): bool
{
if ($template instanceof CmsObject === false) {
return false;
}
$result = false;
if (Theme::databaseLayerEnabled()) {
/**
* @var AutoDatasource
*/
$datasource = $this->getThemeDatasource();
$result = $datasource->sourceHasModel('database', $template) && $datasource->sourceHasModel('filesystem', $template);
}
return $result;
}
/**
* Validate that the current request is within the active theme
* @throws ApplicationException if the requested theme does not match the currently loaded theme
*/
protected function validateRequestTheme(): void
{
if ($this->theme->getDirName() != Request::input('theme')) {
throw new ApplicationException(Lang::get('cms::lang.theme.edit.not_match'));
}
}
/**
* Validates that the given request type is a valid type, and that the user has the relevant
* permission to access it.
*
* @throws AuthorizationException if the user doesn't have permission to access the type
*/
protected function validateRequestType(string $type): void
{
$this->resolveTypeClassName($type);
if (!$this->user?->hasAccess($this->getRelevantPermissionForType($type))) {
throw new AuthorizationException(Lang::get('cms::lang.template.type_not_permitted', [
'type' => str_plural($type),
'permission' => $this->getRelevantPermissionForType($type),
]));
}
}
/**
* Gets the relevant permission required for a specific template type.
*/
protected function getRelevantPermissionForType(string $type): string
{
return match ($type) {
'page' => 'cms.manage_pages',
'partial' => 'cms.manage_partials',
'layout' => 'cms.manage_layouts',
'content' => 'cms.manage_content',
'asset' => 'cms.manage_assets',
};
}
/**
* Resolves a template type to its class name
*/
protected function resolveTypeClassName(string $type): string
{
$types = [
'page' => Page::class,
'partial' => Partial::class,
'layout' => Layout::class,
'content' => Content::class,
'asset' => Asset::class
];
if (!array_key_exists($type, $types)) {
throw new ApplicationException(Lang::get('cms::lang.template.invalid_type'));
}
return $types[$type];
}
/**
* Returns an existing template of a given type
*/
protected function loadTemplate(string $type, string $path): CmsObject|Asset
{
$class = $this->resolveTypeClassName($type);
if (!($template = call_user_func([$class, 'load'], $this->theme, $path))) {
throw new ApplicationException(Lang::get('cms::lang.template.not_found'));
}
/**
* @event cms.template.processSettingsAfterLoad
* Fires immediately after a CMS template (page|partial|layout|content|asset) has been loaded and provides an opportunity to interact with it.
*
* Example usage:
*
* Event::listen('cms.template.processSettingsAfterLoad', function ((\Cms\Controllers\Index) $controller, (mixed) $templateObject) {
* // Make some modifications to the $template object
* });
*
* Or
*
* $CmsIndexController->bindEvent('template.processSettingsAfterLoad', function ((mixed) $templateObject) {
* // Make some modifications to the $template object
* });
*
*/
$this->fireSystemEvent('cms.template.processSettingsAfterLoad', [$template]);
return $template;
}
/**
* Creates a new template of a given type
* @throws ApplicationException if the requested type can't be initialized with the current theme
*/
protected function createTemplate(string $type): CmsObject|Asset
{
$class = $this->resolveTypeClassName($type);
if (!($template = $class::inTheme($this->theme))) {
throw new ApplicationException(Lang::get('cms::lang.template.not_found'));
}
return $template;
}
/**
* Returns the text for a template tab
*/
protected function getTabTitle(string $type, CmsObject|Asset $template): string
{
if ($type === 'page') {
$result = $template->title ?: $template->getFileName();
if (!$result) {
$result = Lang::get('cms::lang.page.new');
}
return $result;
}
if ($type === 'partial' || $type === 'layout' || $type === 'content' || $type === 'asset') {
$result = in_array($type, ['asset', 'content']) ? $template->getFileName() : $template->getBaseFileName();
if (!$result) {
$result = Lang::get('cms::lang.'.$type.'.new');
}
return $result;
}
return $template->getFileName();
}
/**
* Returns a form widget for a specified template type.
*/
protected function makeTemplateFormWidget(string $type, CmsObject|Asset $template, ?string $alias = null): Form
{
$this->validateRequestType($type);
$formConfigs = [
'page' => '~/modules/cms/classes/page/fields.yaml',
'partial' => '~/modules/cms/classes/partial/fields.yaml',
'layout' => '~/modules/cms/classes/layout/fields.yaml',
'content' => '~/modules/cms/classes/content/fields.yaml',
'asset' => '~/modules/cms/classes/asset/fields.yaml'
];
if (!array_key_exists($type, $formConfigs)) {
throw new ApplicationException(Lang::get('cms::lang.template.not_found'));
}
$widgetConfig = $this->makeConfig($formConfigs[$type]);
$ext = pathinfo($template->fileName, PATHINFO_EXTENSION);
if ($type === 'content') {
switch ($ext) {
case 'htm':
$type = 'richeditor';
break;
case 'md':
$type = 'markdown';
break;
default:
$type = 'codeeditor';
break;
}
array_set($widgetConfig->secondaryTabs, 'fields.markup.type', $type);
}
$codeField = ($template instanceof Asset) ? 'content' : 'markup';
$lang = match ($ext) {
'', 'htm' => 'twig',
'html' => 'html',
'css' => 'css',
'js', 'json' => 'javascript',
'less' => 'less',
'sass', 'scss' => 'scss',
'txt' => 'txt',
default => 'php',
};
if (array_get($widgetConfig->secondaryTabs, "fields.$codeField.type") === 'codeeditor') {
array_set($widgetConfig->secondaryTabs, "fields.$codeField.language", $lang);
}
$widgetConfig->model = $template;
$widgetConfig->alias = $alias ?: 'form'.studly_case($type).md5($template->exists ? $template->getFileName() : uniqid());
return $this->makeWidget(Form::class, $widgetConfig);
}
/**
* Processes the component settings so they are ready to be saved.
* @param array $settings The new settings for this template.
* @param array $prevSettings The previous settings for this template.
*/
protected function upgradeSettings($settings, $prevSettings): array
{
/*
* Handle component usage
*/
$componentProperties = post('component_properties');
$componentNames = post('component_names');
$componentAliases = post('component_aliases');
if ($componentProperties !== null) {
if ($componentNames === null || $componentAliases === null) {
throw new ApplicationException(Lang::get('cms::lang.component.invalid_request'));
}
$count = count($componentProperties);
if (count($componentNames) != $count || count($componentAliases) != $count) {
throw new ApplicationException(Lang::get('cms::lang.component.invalid_request'));
}
for ($index = 0; $index < $count; $index++) {
$componentName = $componentNames[$index];
$componentAlias = $componentAliases[$index];
$isSoftComponent = (substr($componentAlias, 0, 1) === '@');
$componentName = ltrim($componentName, '@');
$componentAlias = ltrim($componentAlias, '@');
if ($componentAlias !== $componentName) {
$section = $componentName . ' ' . $componentAlias;
} else {
$section = $componentName;
}
if ($isSoftComponent) {
$section = '@' . $section;
}
$properties = json_decode($componentProperties[$index], true);
unset($properties['oc.alias'], $properties['inspectorProperty'], $properties['inspectorClassName']);
if (!$properties) {
$oldComponentSettings = array_key_exists($section, $prevSettings['components'])
? $prevSettings['components'][$section]
: null;
if ($isSoftComponent && $oldComponentSettings) {
$settings[$section] = $oldComponentSettings;
} else {
$settings[$section] = $properties;
}
} else {
$settings[$section] = $properties;
}
}
}
/*
* Handle view bag
*/
$viewBag = post('viewBag');
if ($viewBag !== null) {
$settings['viewBag'] = $viewBag;
}
/**
* @event cms.template.processSettingsBeforeSave
* Fires before a CMS template (page|partial|layout|content|asset) is saved and provides an opportunity to interact with the settings data. `$dataHolder` = {settings: []}
*
* Example usage:
*
* Event::listen('cms.template.processSettingsBeforeSave', function ((\Cms\Controllers\Index) $controller, (object) $dataHolder) {
* // Make some modifications to the $dataHolder object
* });
*
* Or
*
* $CmsIndexController->bindEvent('template.processSettingsBeforeSave', function ((object) $dataHolder) {
* // Make some modifications to the $dataHolder object
* });
*
*/
$dataHolder = (object) ['settings' => $settings];
$this->fireSystemEvent('cms.template.processSettingsBeforeSave', [$dataHolder]);
return $dataHolder->settings;
}
/**
* Finds a given component by its alias.
*
* If found, this will return the component's name, alias and properties.
*
* @param string $aliasQuery The alias to search for
* @param array $components The array of components to look within.
*/
protected function findComponentByAlias(string $aliasQuery, array $components = []): ?array
{
$found = null;
foreach ($components as $name => $properties) {
list($name, $alias) = strpos($name, ' ') ? explode(' ', $name) : [$name, $name];
if (ltrim($alias, '@') === ltrim($aliasQuery, '@')) {
$found = [
'name' => ltrim($name, '@'),
'alias' => $alias,
'properties' => $properties
];
break;
}
}
return $found;
}
/**
* Binds the active form widget to the controller
*/
protected function bindFormWidgetToController(): void
{
$alias = Request::input('formWidgetAlias');
$type = Request::input('templateType');
if (!empty(Request::input('templatePath'))) {
$object = $this->loadTemplate($type, Request::input('templatePath'));
} else {
$object = $this->createTemplate($type);
}
$widget = $this->makeTemplateFormWidget($type, $object, $alias);
$widget->bindToController();
}
/**
* Replaces Windows style (/r/n) line endings with unix style (/n)
* line endings.
*/
protected function convertLineEndings(string $markup): string
{
return str_replace(["\r\n", "\r"], "\n", $markup);
}
}

View File

@@ -0,0 +1,22 @@
<?php namespace Cms\Controllers;
use Backend\Controllers\Media as MediaController;
/**
* CMS Media Manager
*
* @package winter\wn-cms-module
* @author Alexey Bobkov, Samuel Georges
* @deprecated Use Backend\Controllers\Media. Remove if year >= 2020.
*/
class Media extends MediaController
{
/**
* Constructor.
*/
public function __construct()
{
traceLog('Controller Cms\Controllers\Media has been deprecated, use ' . MediaController::class . ' instead.');
parent::__construct();
}
}

View File

@@ -0,0 +1,81 @@
<?php namespace Cms\Controllers;
use Lang;
use Flash;
use BackendMenu;
use Backend\Classes\Controller;
use System\Classes\SettingsManager;
use Cms\Models\ThemeLog;
/**
* Request Logs controller
*
* @package winter\wn-system-module
* @author Alexey Bobkov, Samuel Georges
*/
class ThemeLogs extends Controller
{
/**
* @var array Extensions implemented by this controller.
*/
public $implement = [
\Backend\Behaviors\FormController::class,
\Backend\Behaviors\ListController::class,
];
/**
* @var array Permissions required to view this page.
*/
public $requiredPermissions = ['system.access_logs'];
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
BackendMenu::setContext('Winter.System', 'system', 'settings');
SettingsManager::setContext('Winter.Cms', 'theme_logs');
}
public function index_onRefresh()
{
return $this->listRefresh();
}
public function index_onEmptyLog()
{
ThemeLog::truncate();
Flash::success(Lang::get('cms::lang.theme_log.empty_success'));
return $this->listRefresh();
}
public function index_onDelete()
{
if (($checkedIds = post('checked')) && is_array($checkedIds) && count($checkedIds)) {
foreach ($checkedIds as $recordId) {
if (!$record = ThemeLog::find($recordId)) {
continue;
}
$record->delete();
}
Flash::success(Lang::get('backend::lang.list.delete_selected_success'));
}
else {
Flash::error(Lang::get('backend::lang.list.delete_selected_empty'));
}
return $this->listRefresh();
}
public function preview($id)
{
$this->addCss('/modules/cms/assets/css/themelogs/template-diff.css', 'core');
$this->addJs('/modules/cms/assets/vendor/jsdiff/diff.js', 'core');
$this->addJs('/modules/cms/assets/js/themelogs/template-diff.js', 'core');
return $this->asExtension('FormController')->preview($id);
}
}

View File

@@ -0,0 +1,144 @@
<?php namespace Cms\Controllers;
use Backend;
use BackendMenu;
use ApplicationException;
use Cms\Models\ThemeData;
use Cms\Classes\Theme as CmsTheme;
use System\Classes\SettingsManager;
use Backend\Classes\Controller;
use Exception;
/**
* Theme customization controller
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*
*/
class ThemeOptions extends Controller
{
/**
* @var array Extensions implemented by this controller.
*/
public $implement = [
\Backend\Behaviors\FormController::class,
];
/**
* @var array Permissions required to view this page.
*/
public $requiredPermissions = ['cms.manage_themes', 'cms.manage_theme_options'];
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
$this->pageTitle = 'cms::lang.theme.settings_menu';
BackendMenu::setContext('Winter.System', 'system', 'settings');
SettingsManager::setContext('Winter.Cms', 'theme');
}
public function update($dirName = null)
{
$dirName = $this->getDirName($dirName);
try {
$model = $this->getThemeData($dirName);
$this->asExtension('FormController')->update($model->id);
$this->vars['hasCustomData'] = $this->hasThemeData($dirName);
}
catch (Exception $ex) {
$this->handleError($ex);
}
}
public function update_onSave($dirName = null)
{
$model = $this->getThemeData($this->getDirName($dirName));
$result = $this->asExtension('FormController')->update_onSave($model->id);
// Redirect close requests to the settings index when user doesn't have access
// to go back to the theme selection page
if (!$this->user->hasAccess('cms.manage_themes') && input('close')) {
$result = Backend::redirect('system/settings');
}
return $result;
}
public function update_onResetDefault($dirName = null)
{
$model = $this->getThemeData($this->getDirName($dirName));
$model->delete();
return Backend::redirect('cms/themeoptions/update/'.$dirName);
}
/**
* Add form fields defined in theme.yaml
*/
public function formExtendFieldsBefore($form)
{
$model = $form->model;
$theme = $this->findThemeObject($model->theme);
$form->config = $this->mergeConfig($form->config, $theme->getFormConfig());
$form->init();
}
//
// Helpers
//
/**
* Default to the active theme if user doesn't have access to manage all themes
*
* @param string $dirName
* @return string
*/
protected function getDirName(?string $dirName = null)
{
/*
* Only the active theme can be managed without this permission
*/
if ($dirName && !$this->user->hasAccess('cms.manage_themes')) {
$dirName = null;
}
if ($dirName === null) {
$dirName = CmsTheme::getActiveThemeCode();
}
return $dirName;
}
protected function hasThemeData($dirName)
{
return $this->findThemeObject($dirName)->hasCustomData();
}
protected function getThemeData($dirName)
{
$theme = $this->findThemeObject($dirName);
return ThemeData::forTheme($theme);
}
protected function findThemeObject($name = null)
{
if ($name === null) {
$name = post('theme');
}
if (!$name || (!$theme = CmsTheme::load($name))) {
throw new ApplicationException(trans('cms::lang.theme.not_found_name', ['name' => $name]));
}
return $theme;
}
}

View File

@@ -0,0 +1,359 @@
<?php namespace Cms\Controllers;
use ApplicationException;
use Artisan;
use Backend;
use Backend\Classes\Controller;
use Backend\Models\BrandSetting;
use Backend\Widgets\Form;
use BackendMenu;
use Cms\Classes\Theme as CmsTheme;
use Cms\Classes\ThemeManager;
use Cms\Helpers\Cms as CmsHelper;
use Cms\Models\ThemeExport;
use Cms\Models\ThemeImport;
use Exception;
use File;
use Flash;
use Lang;
use Redirect;
use System\Classes\SettingsManager;
use Url;
use ValidationException;
use Winter\Storm\Support\Str;
/**
* Theme selector controller
*
* @package winter\wn-backend-module
* @author Alexey Bobkov, Samuel Georges
*
*/
class Themes extends Controller
{
/**
* @var array Permissions required to view this page.
*/
public $requiredPermissions = [
'cms.manage_themes',
'cms.manage_theme_options',
];
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
$this->addCss('/modules/cms/assets/css/winter.theme-selector.css', 'core');
$this->pageTitle = 'cms::lang.theme.settings_menu';
BackendMenu::setContext('Winter.System', 'system', 'settings');
SettingsManager::setContext('Winter.Cms', 'theme');
/*
* Custom redirect for unauthorized request
*/
$this->bindEvent('page.beforeDisplay', function () {
if (!$this->user->hasAccess('cms.manage_themes')) {
return Backend::redirect('cms/themeoptions/update');
}
});
/*
* Enable AJAX for Form widgets
*/
if (post('mode') === 'import') {
$this->makeImportFormWidget($this->findThemeObject())->bindToController();
}
}
public function index()
{
$this->bodyClass = 'compact-container';
}
public function index_onSetActiveTheme()
{
CmsTheme::setActiveTheme(post('theme'));
return [
'#theme-list' => $this->makePartial('theme_list')
];
}
public function index_onDelete()
{
ThemeManager::instance()->deleteTheme(post('theme'));
Flash::success(Lang::get('cms::lang.theme.delete_theme_success'));
return Redirect::refresh();
}
//
// Theme properties
//
public function index_onLoadFieldsForm()
{
$theme = $this->findThemeObject();
$this->vars['widget'] = $this->makeFieldsFormWidget($theme);
$this->vars['themeDir'] = $theme->getDirName();
return $this->makePartial('theme_fields_form');
}
public function index_onSaveFields()
{
$theme = $this->findThemeObject();
$widget = $this->makeFieldsFormWidget($theme);
$theme->writeConfig($widget->getSaveData());
return ['#themeListItem-'.$theme->getId() => $this->makePartial('theme_list_item', ['theme' => $theme])];
}
protected function makeFieldsFormWidget($theme)
{
$widgetConfig = $this->makeConfig('~/modules/cms/classes/theme/fields.yaml');
$widgetConfig->alias = 'form'.studly_case($theme->getDirName());
$widgetConfig->model = $theme;
$widgetConfig->data = $theme->getConfig();
$widgetConfig->data['dir_name'] = $theme->getDirName();
$widgetConfig->arrayName = 'Theme';
$widgetConfig->context = 'update';
return $this->makeWidget(Form::class, $widgetConfig);
}
//
// Create theme
//
public function index_onLoadCreateForm()
{
$this->vars['widget'] = $this->makeCreateFormWidget();
return $this->makePartial('theme_create_form');
}
public function index_onCreate()
{
$widget = $this->makeCreateFormWidget();
$data = $widget->getSaveData();
$newDirName = trim(array_get($data, 'dir_name'));
$destinationPath = themes_path($newDirName);
$data = array_except($data, 'dir_name');
if (!strlen(trim(array_get($data, 'name')))) {
throw new ValidationException(['name' => Lang::get('cms::lang.theme.create_theme_required_name')]);
}
if (!CmsTheme::isValidDirName($newDirName)) {
throw new ValidationException(['dir_name' => Lang::get('cms::lang.theme.dir_name_invalid')]);
}
if (File::isDirectory($destinationPath)) {
throw new ValidationException(['dir_name' => Lang::get('cms::lang.theme.dir_name_taken')]);
}
switch ($data['scaffold']) {
case 'empty':
File::makeDirectory($destinationPath);
File::makeDirectory($destinationPath.'/assets');
File::makeDirectory($destinationPath.'/content');
File::makeDirectory($destinationPath.'/layouts');
File::makeDirectory($destinationPath.'/pages');
File::makeDirectory($destinationPath.'/partials');
File::put($destinationPath.'/theme.yaml', '');
break;
default:
try {
Artisan::call('create:theme', [
'theme' => $newDirName,
'scaffold' => $data['scaffold']
]);
} catch (\Exception $ex) {
throw new ApplicationException($ex->getMessage());
}
break;
}
unset($data['scaffold']);
$theme = CmsTheme::load($newDirName);
$theme->writeConfig($data);
Flash::success(Lang::get('cms::lang.theme.create_theme_success'));
return Redirect::refresh();
}
protected function makeCreateFormWidget()
{
$theme = new CmsTheme;
$theme->setDirName('newtheme');
$widgetConfig = $this->makeConfig('~/modules/cms/classes/theme/fields.yaml');
$widgetConfig->alias = 'formCreateTheme';
$widgetConfig->model = $theme;
$widgetConfig->arrayName = 'Theme';
$widgetConfig->context = 'create';
// Setup default values
$name = BrandSetting::get('app_name');
$slug = Str::slug($name);
$url = Url::to('/');
$widgetConfig->data = [
'name' => $name,
'dir_name' => $slug,
'description' => Lang::get('cms::lang.theme.default_description', ['url' => $url]),
'code' => $slug,
'author' => $this->user->full_name,
'homepage' => $url,
];
return $this->makeWidget('Backend\Widgets\Form', $widgetConfig);
}
//
// Duplicate
//
public function index_onLoadDuplicateForm()
{
$theme = $this->findThemeObject();
$this->vars['themeDir'] = $theme->getDirName();
return $this->makePartial('theme_duplicate_form');
}
public function index_onDuplicateTheme()
{
$theme = $this->findThemeObject();
$newDirName = trim(post('new_dir_name'));
$sourcePath = $theme->getPath();
$destinationPath = themes_path().'/'.$newDirName;
if (!CmsTheme::isValidDirName($newDirName)) {
throw new ValidationException(['new_dir_name' => Lang::get('cms::lang.theme.dir_name_invalid')]);
}
if (File::isDirectory($destinationPath)) {
throw new ValidationException(['new_dir_name' => Lang::get('cms::lang.theme.dir_name_taken')]);
}
File::copyDirectory($sourcePath, $destinationPath);
$newTheme = CmsTheme::load($newDirName);
$newName = $newTheme->getConfigValue('name') . ' - Copy';
$newTheme->writeConfig(['name' => $newName]);
Flash::success(Lang::get('cms::lang.theme.duplicate_theme_success'));
return Redirect::refresh();
}
//
// Theme export
//
public function index_onLoadExportForm()
{
$theme = $this->findThemeObject();
$this->vars['widget'] = $this->makeExportFormWidget($theme);
$this->vars['themeDir'] = $theme->getDirName();
return $this->makePartial('theme_export_form');
}
public function index_onExport()
{
$theme = $this->findThemeObject();
$widget = $this->makeExportFormWidget($theme);
$model = new ThemeExport;
$file = $model->export($theme, $widget->getSaveData());
return Backend::redirect('cms/themes/download/'.$file.'/'.$theme->getDirName().'.zip');
}
public function download($name, $outputName = null)
{
try {
$this->pageTitle = 'Download theme export archive';
return ThemeExport::download($name, $outputName);
}
catch (Exception $ex) {
$this->handleError($ex);
}
}
protected function makeExportFormWidget($theme)
{
$widgetConfig = $this->makeConfig('~/modules/cms/models/themeexport/fields.yaml');
$widgetConfig->alias = 'form'.studly_case($theme->getDirName());
$widgetConfig->model = new ThemeExport;
$widgetConfig->model->theme = $theme;
$widgetConfig->arrayName = 'ThemeExport';
return $this->makeWidget('Backend\Widgets\Form', $widgetConfig);
}
//
// Theme import
//
public function index_onLoadImportForm()
{
if (CmsHelper::safeModeEnabled()) {
throw new ApplicationException(Lang::get('cms::lang.cms_object.safe_mode_enabled'));
}
$theme = $this->findThemeObject();
$this->vars['widget'] = $this->makeImportFormWidget($theme);
$this->vars['themeDir'] = $theme->getDirName();
return $this->makePartial('theme_import_form');
}
public function index_onImport()
{
if (CmsHelper::safeModeEnabled()) {
throw new ApplicationException(Lang::get('cms::lang.cms_object.safe_mode_enabled'));
}
$theme = $this->findThemeObject();
$widget = $this->makeImportFormWidget($theme);
$model = new ThemeImport;
$model->import($theme, $widget->getSaveData(), $widget->getSessionKey());
Flash::success(Lang::get('cms::lang.theme.import_theme_success'));
return Redirect::refresh();
}
protected function makeImportFormWidget($theme)
{
$widgetConfig = $this->makeConfig('~/modules/cms/models/themeimport/fields.yaml');
$widgetConfig->alias = 'form'.studly_case($theme->getDirName());
$widgetConfig->model = new ThemeImport;
$widgetConfig->model->theme = $theme;
$widgetConfig->arrayName = 'ThemeImport';
return $this->makeWidget('Backend\Widgets\Form', $widgetConfig);
}
//
// Helpers
//
protected function findThemeObject($name = null)
{
if ($name === null) {
$name = post('theme');
}
if (!$name || (!$theme = CmsTheme::load($name))) {
throw new ApplicationException(Lang::get('cms::lang.theme.not_found_name', ['name' => $name]));
}
return $theme;
}
}

View File

@@ -0,0 +1,14 @@
<button
type="button"
class="
btn btn-danger wn-icon-download
<?php if (!$canCommit): ?>
hide
<?php endif ?>
"
data-request="onCommit"
data-request-confirm="<?= e(trans('cms::lang.editor.commit_confirm')) ?>"
data-load-indicator="<?= e(trans('cms::lang.editor.committing')) ?>"
data-control="commit-button">
<?= e(trans('cms::lang.editor.commit')) ?>
</button>

View File

@@ -0,0 +1,8 @@
<?php if (isset($lastModified)): ?>
<span
class="btn empty wn-icon-calendar"
title="<?= e(trans('backend::lang.media.last_modified')) ?>: <?= $lastModified ?>"
data-toggle="tooltip"
data-placement="right">
</span>
<?php endif; ?>

View File

@@ -0,0 +1,14 @@
<button
type="button"
class="
btn btn-danger wn-icon-bomb
<?php if (!$canReset): ?>
hide
<?php endif ?>
"
data-request="onReset"
data-request-confirm="<?= e(trans('cms::lang.editor.reset_confirm')) ?>"
data-load-indicator="<?= e(trans('cms::lang.editor.resetting')) ?>"
data-control="reset-button">
<?= e(trans('cms::lang.editor.reset')) ?>
</button>

View File

@@ -0,0 +1,19 @@
<?= $this->makePartial('button_commit'); ?>
<?= $this->makePartial('button_reset'); ?>
<button
type="button"
class="
btn btn-danger empty wn-icon-trash-o
<?php if (!$templatePath): ?>
hide
<?php endif ?>
"
data-request="onDelete"
data-request-confirm="<?= e(trans('cms::lang.' . $toolbarSource . '.delete_confirm_single')) ?>"
data-request-success="$.wn.cmsPage.updateTemplateList('<?= $toolbarSource ?>'); $(this).trigger('close.oc.tab', [{force: true}])"
data-control="delete-button">
</button>
<?= $this->makePartial('button_lastmodified'); ?>

View File

@@ -0,0 +1,29 @@
<?= Form::open(['onsubmit'=>'return false']) ?>
<div class="modal-header">
<button type="button" class="close" data-dismiss="popup">&times;</button>
<h4 class="modal-title"><?= e(trans('backend::lang.form.concurrency_file_changed_title')) ?></h4>
</div>
<div class="modal-body">
<p><?= e(trans('backend::lang.form.concurrency_file_changed_description')) ?></p>
</div>
<div class="modal-footer">
<button
type="submit"
data-action="reload"
class="btn btn-primary">
<?= e(trans('backend::lang.form.reload')) ?>
</button>
<button
type="submit"
data-action="save"
class="btn btn-primary">
<?= e(trans('backend::lang.form.save')) ?>
</button>
<button
type="button"
class="btn btn-default"
data-dismiss="popup">
<?= e(trans('backend::lang.form.cancel')) ?>
</button>
</div>
<?= Form::close() ?>

View File

@@ -0,0 +1,12 @@
<div class="form-buttons loading-indicator-container" data-toolbar-type="content">
<a
href="javascript:;"
class="btn btn-primary wn-icon-check save"
data-request="onSave"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
data-hotkey="ctrl+s, cmd+s">
<?= e(trans('backend::lang.form.save')) ?>
</a>
<?= $this->makePartial('common_toolbar_actions', ['toolbarSource' => 'content']); ?>
</div>

View File

@@ -0,0 +1,16 @@
<?= Form::open([
'class' => 'layout',
'data-change-monitor' => 'true',
'data-window-close-confirm' => e(trans('backend::lang.form.confirm_tab_close')),
'data-inspector-external-parameters' => true
]) ?>
<?= $form->render() ?>
<input type="hidden" value="<?= e($form->alias) ?>" name="formWidgetAlias" />
<input type="hidden" value="<?= ($templateType) ?>" name="templateType" />
<input type="hidden" value="<?= ($templatePath) ?>" name="templatePath" />
<input type="hidden" value="<?= ($templateTheme) ?>" name="theme" />
<input type="hidden" value="<?= ($templateMtime) ?>" name="templateMtime" />
<input type="hidden" value="0" name="templateForceSave" />
<?= Form::close() ?>

View File

@@ -0,0 +1,12 @@
<div class="form-buttons loading-indicator-container">
<a
href="javascript:;"
class="btn btn-primary wn-icon-check save"
data-request="onSave"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
data-hotkey="ctrl+s, cmd+s">
<?= e(trans('backend::lang.form.save')) ?>
</a>
<?= $this->makePartial('common_toolbar_actions', ['toolbarSource' => 'layout']); ?>
</div>

View File

@@ -0,0 +1,28 @@
<div class="form-buttons loading-indicator-container">
<a
href="javascript:;"
class="btn btn-primary wn-icon-check save"
data-request="onSave"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
data-hotkey="ctrl+s, cmd+s">
<?= e(trans('backend::lang.form.save')) ?>
</a>
<?php
$pageUrl = isset($pageUrl) ? $pageUrl : null;
?>
<a
href="<?= Url::to($pageUrl) ?>"
target="_blank"
class="
btn btn-primary wn-icon-crosshairs
<?php if (!$templatePath): ?>
hide
<?php endif ?>
"
data-control="preview-button">
<?= e(trans('cms::lang.editor.preview')) ?>
</a>
<?= $this->makePartial('common_toolbar_actions', ['toolbarSource' => 'page']); ?>
</div>

View File

@@ -0,0 +1,12 @@
<div class="form-buttons loading-indicator-container">
<a
href="javascript:;"
class="btn btn-primary wn-icon-check save"
data-request="onSave"
data-load-indicator="<?= e(trans('backend::lang.form.saving')) ?>"
data-hotkey="ctrl+s, cmd+s">
<?= e(trans('backend::lang.form.save')) ?>
</a>
<?= $this->makePartial('common_toolbar_actions', ['toolbarSource' => 'partial']); ?>
</div>

View File

@@ -0,0 +1,6 @@
<div class="callout callout-warning no-subheader">
<div class="header" style="border-radius: 0">
<i class="icon-warning"></i>
<h3><?= e(trans('cms::lang.cms_object.safe_mode_enabled')) ?></h3>
</div>
</div>

View File

@@ -0,0 +1,80 @@
<?php
$visibleCount = 0;
?>
<div class="layout control-scrollpanel" id="cms-side-panel">
<div class="layout-cell">
<div class="layout-relative fix-button-container">
<?php if ($this->user->hasAccess('cms.manage_pages')): ?>
<!-- Pages -->
<form
role="form"
class="layout <?= ++$visibleCount == 1 ? '' : 'hide' ?>"
data-content-id="pages"
data-template-type="page"
data-type-icon="wn-icon-copy"
onsubmit="return false">
<?= $this->widget->pageList->render() ?>
</form>
<?php endif ?>
<?php if ($this->user->hasAccess('cms.manage_partials')): ?>
<!-- Partials -->
<form
role="form"
class="layout <?= ++$visibleCount == 1 ? '' : 'hide' ?>"
data-content-id="partials"
data-template-type="partial"
data-type-icon="wn-icon-tags"
onsubmit="return false">
<?= $this->widget->partialList->render() ?>
</form>
<?php endif ?>
<?php if ($this->user->hasAccess('cms.manage_layouts')): ?>
<!-- Layouts -->
<form
role="form"
class="layout <?= ++$visibleCount == 1 ? '' : 'hide' ?>"
data-content-id="layouts"
data-template-type="layout"
data-type-icon="wn-icon-th-large"
onsubmit="return false">
<?= $this->widget->layoutList->render() ?>
</form>
<?php endif ?>
<?php if ($this->user->hasAccess('cms.manage_content')): ?>
<!-- Content -->
<form
role="form"
class="layout <?= ++$visibleCount == 1 ? '' : 'hide' ?>"
data-content-id="content"
data-template-type="content"
data-type-icon="wn-icon-file-text-o"
onsubmit="return false">
<?= $this->widget->contentList->render() ?>
</form>
<?php endif ?>
<?php if ($this->user->hasAccess('cms.manage_assets')): ?>
<!-- Assets -->
<form
role="form"
class="layout <?= ++$visibleCount == 1 ? '' : 'hide' ?>"
data-content-id="assets"
data-template-type="asset"
data-type-icon="wn-icon-file-text-o"
onsubmit="return false">
<?= $this->widget->assetList->render() ?>
</form>
<?php endif ?>
<?php if ($this->user->hasAccess(['cms.manage_pages', 'cms.manage_layouts', 'cms.manage_partials'], false)): ?>
<!-- Components -->
<form
role="form"
class="layout <?= ++$visibleCount == 1 ? '' : 'hide' ?>"
data-content-id="components"
onsubmit="return false"
id="cms-component-list">
<?= $this->widget->componentList->render() ?>
</form>
<?php endif ?>
</div>
</div>
</div>

View File

@@ -0,0 +1,7 @@
# ===================================
# Configures the layout list widget
# ===================================
noRecordsMessage: 'cms::lang.content.no_list_records'
deleteConfirmation: 'cms::lang.content.delete_confirm_multiple'
itemType: content

View File

@@ -0,0 +1,8 @@
# ===================================
# Configures the layout list widget
# ===================================
descriptionProperty: description
noRecordsMessage: 'cms::lang.layout.no_list_records'
deleteConfirmation: 'cms::lang.layout.delete_confirm_multiple'
itemType: layout

View File

@@ -0,0 +1,15 @@
# ===================================
# Configures the page list widget
# ===================================
titleProperty: 'title'
descriptionProperty: description
descriptionProperties:
url: URL
noRecordsMessage: 'cms::lang.page.no_list_records'
deleteConfirmation: 'cms::lang.page.delete_confirm_multiple'
itemType: page
sortingProperties:
url: 'cms::lang.page.url'
title: 'cms::lang.page.title'
fileName: 'cms::lang.page.file_name'

View File

@@ -0,0 +1,8 @@
# ===================================
# Configures the partial list widget
# ===================================
descriptionProperty: description
noRecordsMessage: 'cms::lang.partial.no_list_records'
deleteConfirmation: 'cms::lang.partial.delete_confirm_multiple'
itemType: partial

View File

@@ -0,0 +1,31 @@
<?= Block::put('sidepanel') ?>
<?php if (!$this->fatalError): ?>
<?= $this->makePartial('sidepanel') ?>
<?php endif ?>
<?= Block::endPut() ?>
<?= Block::put('body') ?>
<?php if (!$this->fatalError): ?>
<div
data-control="tab"
data-closable
data-close-confirmation="<?= e(trans('backend::lang.form.confirm_tab_close')) ?>"
data-pane-classes="layout-cell"
data-max-title-symbols="15"
data-title-as-file-names="true"
class="layout control-tabs master-tabs fancy-layout wn-logo-transparent"
id="cms-master-tabs">
<div class="layout-row min-size">
<div class="tabs-container">
<ul class="nav nav-tabs"></ul>
</div>
</div>
<div class="tab-content layout-row">
</div>
</div>
<?php else: ?>
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
<?php endif ?>
<?= Block::endPut() ?>

View File

@@ -0,0 +1,7 @@
<?= Block::put('head') ?><?= Block::endPut() ?>
<?= Block::put('body') ?>
<?= Form::open(['class'=>'layout', 'onsubmit'=>'return false']) ?>
<?= $this->widget->manager->render() ?>
<?= Form::close() ?>
<?= Block::endPut() ?>

View File

@@ -0,0 +1,3 @@
<div class="form-control">
<pre><?= e($value.PHP_EOL) ?></pre>
</div>

View File

@@ -0,0 +1,7 @@
<div class="form-control">
<pre
data-plugin="template-diff"
data-old-field-name="old_content"
data-new-field-name="content"
data-content-tag="pre"></pre>
</div>

View File

@@ -0,0 +1,7 @@
<div
class="form-control"
data-plugin="template-diff"
data-old-field-name="old_template"
data-diff-type="words"
data-new-field-name="template">
</div>

View File

@@ -0,0 +1 @@
<div class="form-control"><?= e($value) ?></div>

View File

@@ -0,0 +1,4 @@
<p>
<?= e(trans('cms::lang.theme_log.hint')) ?>
</p>

View File

@@ -0,0 +1,22 @@
<?php if ($formModel->type == $formModel::TYPE_DELETE): ?>
<div class="callout fade in callout-danger no-subheader m-b">
<div class="header">
<i class="icon-minus"></i>
<h3><?= e(trans('cms::lang.theme_log.template_deleted')) ?></h3>
</div>
</div>
<?php elseif ($formModel->type == $formModel::TYPE_CREATE): ?>
<div class="callout fade in callout-success no-subheader m-b">
<div class="header">
<i class="icon-plus"></i>
<h3><?= e(trans('cms::lang.theme_log.template_created')) ?></h3>
</div>
</div>
<?php else: ?>
<div class="callout fade in callout-info no-subheader">
<div class="header">
<i class="icon-pencil"></i>
<h3><?= e(trans('cms::lang.theme_log.template_updated')) ?></h3>
</div>
</div>
<?php endif ?>

View File

@@ -0,0 +1,31 @@
<div data-control="toolbar" class="loading-indicator-container">
<a
href="javascript:;"
data-request="onRefresh"
data-load-indicator="<?= e(trans('backend::lang.list.updating')) ?>"
class="btn btn-primary wn-icon-refresh">
<?= e(trans('backend::lang.list.refresh')) ?>
</a>
<a
href="javascript:;"
data-request="onEmptyLog"
data-request-confirm="<?= e(trans('backend::lang.list.delete_selected_confirm')) ?>"
data-load-indicator="<?= e(trans('cms::lang.theme_log.empty_loading')) ?>"
class="btn btn-default wn-icon-eraser">
<?= e(trans('cms::lang.theme_log.empty_link')) ?>
</a>
<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-trigger-action="enable"
data-trigger=".control-list input[type=checkbox]"
data-trigger-condition="checked"
data-request-success="$(this).prop('disabled', true)"
data-stripe-load-indicator>
<?= e(trans('backend::lang.list.delete_selected')) ?>
</button>
</div>

View File

@@ -0,0 +1,18 @@
<div class="scoreboard-item title-value">
<h4><?= e(trans('cms::lang.theme_log.id_label')) ?></h4>
<p>#<?= e($formModel->id) ?></p>
</div>
<?php if ($formModel->user): ?>
<div class="scoreboard-item title-value">
<h4><?= e(trans('cms::lang.theme_log.user')) ?></h4>
<p><?= e($formModel->user->full_name) ?></p>
</div>
<?php endif ?>
<div class="scoreboard-item title-value">
<h4><?= e(trans('cms::lang.theme_log.created_at')) ?></h4>
<p><?= Backend::dateTime($formModel->created_at) ?></p>
</div>
<div class="scoreboard-item title-value">
<h4><?= e(trans('cms::lang.theme_log.theme_name')) ?></h4>
<p><?= e($formModel->theme_name) ?></p>
</div>

View File

@@ -0,0 +1,16 @@
# ===================================
# Filter Scope Definitions
# ===================================
scopes:
created_at:
label: backend::lang.access_log.created_at
type: daterange
conditions: created_at >= ':after' AND created_at <= ':before'
user:
label: backend::lang.access_log.login
modelClass: Backend\Models\User
conditions: user_id in (:filtered)
nameFrom: login

View File

@@ -0,0 +1,19 @@
# ===================================
# Form Behavior Config
# ===================================
# Record name
name: system::lang.event_log.menu_label
# Model Form Field configuration
form: ~/modules/cms/models/themelog/fields.yaml
# Model Class name
modelClass: Cms\Models\ThemeLog
# Default redirect location
defaultRedirect: cms/themelogs
# Preview page
preview:
title: cms::lang.theme_log.preview_title

View File

@@ -0,0 +1,22 @@
# ===================================
# List Behavior Config
# ===================================
title: cms::lang.theme_log.menu_label
list: ~/modules/cms/models/themelog/columns.yaml
modelClass: Cms\Models\ThemeLog
recordUrl: cms/themelogs/preview/:id
noRecordsMessage: backend::lang.list.no_records
recordsPerPage: 30
showSetup: true
showCheckboxes: true
defaultSort:
column: count
direction: desc
toolbar:
buttons: list_toolbar
search:
prompt: backend::lang.list.search_prompt
filter: config_filter.yaml

View File

@@ -0,0 +1,5 @@
<div class="padded-container container-flush">
<?= $this->makeHintPartial('system_requestlogs_hint', 'hint') ?>
</div>
<?= $this->listRender() ?>

View File

@@ -0,0 +1,34 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('cms/themelogs') ?>"><?= e(trans('cms::lang.theme_log.menu_label')) ?></a></li>
<li><?= e(trans($this->pageTitle)) ?></li>
</ul>
<?php Block::endPut() ?>
<?php if (!$this->fatalError): ?>
<div class="scoreboard">
<div data-control="toolbar">
<?= $this->makePartial('preview_scoreboard') ?>
</div>
</div>
<div>
<?= $this->makePartial('hint_preview') ?>
</div>
<div class="layout-item stretch layout-column">
<?= $this->formRenderPreview() ?>
</div>
<?php else: ?>
<p class="flash-message static error"><?= e(trans($this->fatalError)) ?></p>
<?php endif ?>
<p>
<a href="<?= Backend::url('cms/themelogs') ?>" class="btn btn-default wn-icon-chevron-left">
<?= e(trans('cms::lang.theme_log.return_link')) ?>
</a>
</p>

View File

@@ -0,0 +1,21 @@
# ===================================
# Form Behavior Config
# ===================================
# Record name
name: cms::lang.theme.theme_label
# Fields are defined by extension
form: []
# Model Class name
modelClass: Cms\Models\ThemeData
# Default redirect location
defaultRedirect: cms/themes
# Update page
update:
title: cms::lang.theme.customize_theme
redirect: cms/themes
redirectClose: cms/themes

View File

@@ -0,0 +1,70 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('cms/themes') ?>"><?= e(trans('cms::lang.theme.theme_title')) ?></a></li>
<li><?= e(trans($this->pageTitle)) ?></li>
</ul>
<?php Block::endPut() ?>
<?php if (!$this->fatalError): ?>
<?php if ($hasCustomData): ?>
<?= Form::open(['class'=>'layout']) ?>
<div class="layout-row">
<?= $this->formRender() ?>
</div>
<div class="form-buttons">
<div class="loading-indicator-container">
<button
type="submit"
data-request="onSave"
data-browser-validate
data-request-data="redirect:0"
data-hotkey="ctrl+s, cmd+s"
data-load-indicator="<?= e(trans('cms::lang.theme.saving')) ?>"
class="btn btn-primary">
<?= 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('cms::lang.theme.saving')) ?>"
class="btn btn-default">
<?= e(trans('backend::lang.form.save_and_close')) ?>
</button>
<span class="btn-text">
<?= e(trans('backend::lang.form.or')) ?> <a href="<?= Backend::url('cms/themes') ?>"><?= e(trans('backend::lang.form.cancel')) ?></a>
</span>
<button
type="button"
class="btn btn-danger pull-right"
data-request="onResetDefault"
data-load-indicator="<?= e(trans('backend::lang.form.resetting')) ?>"
data-request-confirm="<?= e(trans('backend::lang.form.action_confirm')) ?>">
<?= e(trans('backend::lang.form.reset_default')) ?>
</button>
</div>
</div>
<?= Form::close() ?>
<?php else: ?>
<div class="callout callout-info">
<div class="content">
<p>There are no theme options available to customize.</p>
</div>
</div>
<?php endif ?>
<?php else: ?>
<p class="flash-message static error"><?= e($this->fatalError) ?></p>
<p><a href="<?= Backend::url('cms/themes') ?>" class="btn btn-default"><?= e(trans('cms::lang.theme.return')) ?></a></p>
<?php endif ?>

View File

@@ -0,0 +1,54 @@
<?= Form::ajax('onCreate', [
'id' => 'themeCreateForm',
'data-popup-load-indicator' => true
]) ?>
<div class="modal-header">
<button type="button" class="close" data-dismiss="popup">&times;</button>
<h4 class="modal-title"><?= e(trans('cms::lang.theme.create_title')) ?></h4>
</div>
<?php if (!$this->fatalError): ?>
<div class="modal-body">
<?= $widget->render() ?>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary">
<?= e(trans('cms::lang.theme.create_button')) ?>
</button>
<button
type="button"
class="btn btn-default"
data-dismiss="popup">
<?= e(trans('backend::lang.form.cancel')) ?>
</button>
</div>
<?php else: ?>
<div class="modal-body">
<p class="flash-message static error"><?= e(trans($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 ?>
<script>
setTimeout(
function(){ $('#themeCreateForm input.form-control:first').focus() },
310
)
</script>
<?= Form::close() ?>

View File

@@ -0,0 +1,75 @@
<?= Form::ajax('onDuplicateTheme', [
'id' => 'themeDuplicateForm',
'data-popup-load-indicator' => true,
]) ?>
<input type="hidden" name="theme" value="<?= $themeDir ?>" />
<div class="modal-header">
<button type="button" class="close" data-dismiss="popup">&times;</button>
<h4 class="modal-title"><?= e(trans('cms::lang.theme.duplicate_title')) ?>: <?= $themeDir ?></h4>
</div>
<?php if (!$this->fatalError): ?>
<div class="modal-body">
<div class="form-group text-field span-full">
<label for="Form-ThemeDuplicate-newDirName">
<?= e(trans('cms::lang.theme.new_directory_name_label')) ?>
</label>
<input
type="text"
name="new_dir_name"
id="Form-ThemeDuplicate-newDirName"
value="<?= $themeDir ?>"
placeholder=""
class="form-control"
autocomplete="off"
maxlength="255" />
<p class="help-block">
<?= e(trans('cms::lang.theme.new_directory_name_comment')) ?>
</p>
</div>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-success">
<?= e(trans('cms::lang.theme.duplicate_button')) ?>
</button>
<button
type="button"
class="btn btn-default"
data-dismiss="popup">
<?= e(trans('backend::lang.form.cancel')) ?>
</button>
</div>
<?php else: ?>
<div class="modal-body">
<p class="flash-message static error"><?= e(trans($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 ?>
<script>
setTimeout(
function(){ $('#themeDuplicateForm input.form-control:first').focus() },
310
)
</script>
<?= Form::close() ?>

View File

@@ -0,0 +1,63 @@
<?= Form::ajax('onExport', [
'id' => 'themeExportForm',
'data-popup-load-indicator' => true,
'data-request-success' => 'closeExportThemePopup()'
]) ?>
<input type="hidden" name="theme" value="<?= $themeDir ?>" />
<div class="modal-header">
<button type="button" class="close" data-dismiss="popup">&times;</button>
<h4 class="modal-title"><?= e(trans('cms::lang.theme.export_title')) ?></h4>
</div>
<?php if (!$this->fatalError): ?>
<div class="modal-body">
<?= $widget->render() ?>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-success">
<?= e(trans('cms::lang.theme.export_button')) ?>
</button>
<button
type="button"
class="btn btn-default"
data-dismiss="popup">
<?= e(trans('backend::lang.form.cancel')) ?>
</button>
</div>
<?php else: ?>
<div class="modal-body">
<p class="flash-message static error"><?= e(trans($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 ?>
<script>
setTimeout(
function(){ $('#themeExportForm input.form-control:first').focus() },
310
)
function closeExportThemePopup() {
$('#themeExportForm')
.closest('.control-popup')
.popup('hideLoading')
}
</script>
<?= Form::close() ?>

View File

@@ -0,0 +1,56 @@
<?= Form::ajax('onSaveFields', [
'id' => 'themeFieldsForm',
'data-popup-load-indicator' => true
]) ?>
<input type="hidden" name="theme" value="<?= $themeDir ?>" />
<div class="modal-header">
<button type="button" class="close" data-dismiss="popup">&times;</button>
<h4 class="modal-title"><?= e(trans('cms::lang.theme.edit_properties_title')) ?>: <?= $themeDir ?></h4>
</div>
<?php if (!$this->fatalError): ?>
<div class="modal-body">
<?= $widget->render() ?>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-primary">
<?= e(trans('cms::lang.theme.save_properties')) ?>
</button>
<button
type="button"
class="btn btn-default"
data-dismiss="popup">
<?= e(trans('backend::lang.form.cancel')) ?>
</button>
</div>
<?php else: ?>
<div class="modal-body">
<p class="flash-message static error"><?= e(trans($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 ?>
<script>
setTimeout(
function(){ $('#themeFieldsForm input.form-control:first').focus() },
310
)
</script>
<?= Form::close() ?>

View File

@@ -0,0 +1,57 @@
<?= Form::ajax('onImport', [
'id' => 'themeImportForm',
'data-popup-load-indicator' => true,
]) ?>
<input type="hidden" name="theme" value="<?= $themeDir ?>" />
<input type="hidden" name="mode" value="import" />
<div class="modal-header">
<button type="button" class="close" data-dismiss="popup">&times;</button>
<h4 class="modal-title"><?= e(trans('cms::lang.theme.import_title')) ?></h4>
</div>
<?php if (!$this->fatalError): ?>
<div class="modal-body">
<?= $widget->render() ?>
</div>
<div class="modal-footer">
<button
type="submit"
class="btn btn-success">
<?= e(trans('cms::lang.theme.import_button')) ?>
</button>
<button
type="button"
class="btn btn-default"
data-dismiss="popup">
<?= e(trans('backend::lang.form.cancel')) ?>
</button>
</div>
<?php else: ?>
<div class="modal-body">
<p class="flash-message static error"><?= e(trans($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 ?>
<script>
setTimeout(
function(){ $('#themeImportForm input.form-control:first').focus() },
310
)
</script>
<?= Form::close() ?>

View File

@@ -0,0 +1,31 @@
<?php
$themes = Cms\Classes\Theme::all();
?>
<?php foreach ($themes as $index => $theme): ?>
<div id="themeListItem-<?= $theme->getId() ?>" class="layout-row min-size <?= $theme->isActiveTheme() ? 'active' : null ?>">
<?= $this->makePartial('theme_list_item', ['theme' => $theme]) ?>
</div>
<?php endforeach ?>
<div class="layout-row links">
<div class="layout-cell theme-thumbnail">
<!-- Spacer -->
</div>
<div class="layout-cell theme-description">
<a
class="create-new-theme"
data-control="popup"
data-handler="onLoadCreateForm"
data-size="huge"
href="javascript:;">
<?= e(trans('cms::lang.theme.create_new_blank_theme')) ?>
</a>
<a
class="find-more-themes"
href="<?= Backend::url('system/updates/install/themes') ?>">
<?= e(trans('cms::lang.theme.find_more_themes')) ?>
</a>
</div>
</div>

View File

@@ -0,0 +1,121 @@
<?php
$author = $theme->getConfigValue('author');
?>
<div class="layout-cell min-height theme-thumbnail">
<div class="thumbnail-container"><img src="<?= $theme->getPreviewImageUrl() ?>" alt="" /></div>
</div>
<div class="layout-cell min-height theme-description">
<h3><?= e($theme->getConfigValue('name', $theme->getDirName())) ?></h3>
<?php if (strlen($author)): ?>
<p class="author"><?= trans('cms::lang.theme.by_author', ['name' => '<a href="'.e($theme->getConfigValue('homepage', '#')).'">'.e($author).'</a>']) ?></p>
<?php endif ?>
<p class="description">
<?= e($theme->getConfigValue('description', 'The theme description is not provided.')) ?>
</p>
<div class="controls">
<?php if ($theme->isActiveTheme()): ?>
<button
type="submit"
disabled
class="btn btn-secondary btn-disabled">
<i class="icon-star"></i>
<?= e(trans('cms::lang.theme.active_button')) ?>
</button>
<?php else: ?>
<button
type="submit"
data-request="onSetActiveTheme"
data-request-data="theme: '<?= e($theme->getDirName()) ?>'"
data-stripe-load-indicator
class="btn btn-primary">
<i class="icon-check"></i>
<?= e(trans('cms::lang.theme.activate_button')) ?>
</button>
<?php endif ?>
<?php if ($theme->hasCustomData()): ?>
<a
href="<?= Backend::url('cms/themeoptions/update/'.$theme->getDirName()) ?>"
class="btn btn-secondary<?= $theme->isActiveTheme() === false ? ' disabled' : '' ?>">
<i class="icon-paint-brush"></i>
<?= e(trans('cms::lang.theme.customize_button')) ?>
</a>
<?php endif ?>
<div class="dropdown">
<button
data-toggle="dropdown"
class="btn btn-secondary">
<i class="icon-wrench"></i>
<?= e(trans('cms::lang.theme.manage_button')) ?>
</button>
<ul class="dropdown-menu" role="menu" data-dropdown-title="<?= e(trans('cms::lang.theme.manage_title')) ?>">
<li role="presentation">
<a
role="menuitem"
tabindex="-1"
data-control="popup"
data-size="huge"
data-handler="onLoadFieldsForm"
data-request-data="theme: '<?= e($theme->getDirName()) ?>'"
href="javascript:;"
class="wn-icon-pencil">
<?= e(trans('cms::lang.theme.edit_properties_button')) ?>
</a>
</li>
<li role="presentation">
<a
role="menuitem"
tabindex="-1"
data-control="popup"
data-handler="onLoadDuplicateForm"
data-request-data="theme: '<?= e($theme->getDirName()) ?>'"
href="javascript:;"
class="wn-icon-copy">
<?= e(trans('cms::lang.theme.duplicate_button')) ?>
</a>
</li>
<li role="presentation">
<a
role="menuitem"
tabindex="-1"
data-control="popup"
data-handler="onLoadImportForm"
data-request-data="theme: '<?= e($theme->getDirName()) ?>'"
href="javascript:;"
class="wn-icon-upload">
<?= e(trans('cms::lang.theme.import_button')) ?>
</a>
</li>
<li role="presentation">
<a
role="menuitem"
tabindex="-1"
data-control="popup"
data-handler="onLoadExportForm"
data-request-data="theme: '<?= e($theme->getDirName()) ?>'"
href="javascript:;"
class="wn-icon-download">
<?= e(trans('cms::lang.theme.export_button')) ?>
</a>
</li>
<?php if (!$theme->isActiveTheme()): ?>
<li role="presentation" class="divider"></li>
<li role="presentation">
<a
role="menuitem"
tabindex="-1"
data-request="onDelete"
data-request-confirm="<?= e(trans('cms::lang.theme.delete_confirm')) ?>"
data-request-data="theme: '<?= e($theme->getDirName()) ?>'"
href="javascript:;"
class="wn-icon-trash">
<?= e(trans('cms::lang.theme.delete_button')) ?>
</a>
</li>
<?php endif ?>
</ul>
</div>
</div>
</div>

View File

@@ -0,0 +1,13 @@
<?php Block::put('breadcrumb') ?>
<ul>
<li><a href="<?= Backend::url('cms/themes') ?>"><?= e(trans('cms::lang.theme.theme_title')) ?></a></li>
<li><?= e(trans($this->pageTitle)) ?></li>
</ul>
<?php Block::endPut() ?>
<?php if ($this->fatalError): ?>
<p class="flash-message static error"><?= e($this->fatalError) ?></p>
<p><a href="<?= Backend::url('cms/themes') ?>" class="btn btn-default"><?= e(trans('cms::lang.theme.return')) ?></a></p>
<?php endif ?>

View File

@@ -0,0 +1,11 @@
<?= Block::put('body') ?>
<div class="layout">
<div class="layout-row">
<?= Form::open(['onsubmit'=>'return false']) ?>
<div class="layout theme-selector-layout" id="theme-list">
<?= $this->makePartial('theme_list') ?>
</div>
<?= Form::close() ?>
</div>
</div>
<?= Block::endPut() ?>