diff --git a/Plugin.php b/Plugin.php index 519a3c6..48906fc 100644 --- a/Plugin.php +++ b/Plugin.php @@ -2,15 +2,19 @@ namespace Winter\Blocks; +use Backend\Classes\NavigationManager; use Backend\Classes\WidgetManager; +use Backend\Facades\Backend; +use Backend\Models\UserRole; use Cms\Classes\AutoDatasource; use Cms\Classes\Theme; -use Event; use System\Classes\PluginBase; +use Winter\Blocks\Classes\Block as BlockModel; use Winter\Blocks\Classes\BlockManager; use Winter\Blocks\Classes\BlocksDatasource; -use Winter\Blocks\Classes\Block as BlockModel; use Winter\Blocks\FormWidgets\Block; +use Winter\Storm\Support\Facades\Config; +use Winter\Storm\Support\Facades\Event; /** * Blocks Plugin Information File @@ -103,6 +107,27 @@ public function boot(): void { $this->extendThemeDatasource(); $this->extendControlLibraryBlocks(); + + if ($this->app->runningInBackend() && in_array('Cms', Config::get('cms.loadModules'))) { + $this->extendCms(); + } + } + + /** + * Registers any back-end permissions used by this plugin. + * + * @return array + */ + public function registerPermissions() + { + return [ + 'winter.blocks.manage_blocks' => [ + 'tab' => 'winter.blocks::lang.plugin.name', + 'order' => 200, + 'roles' => [UserRole::CODE_DEVELOPER, UserRole::CODE_PUBLISHER], + 'label' => 'winter.blocks::lang.blocks.manage_blocks' + ], + ]; } /** @@ -176,4 +201,21 @@ protected function extendControlLibraryBlocks(): void } }); } + + /** + * Extend the CMS to implement the BlocksController as a child of the CMS + */ + public function extendCms(): void + { + Event::listen('backend.menu.extendItems', function (NavigationManager $manager) { + $manager->addSideMenuItem('winter.cms', 'cms', 'blocks', [ + 'label' => 'winter.blocks::lang.plugin.name', + 'icon' => 'icon-cubes', + 'url' => Backend::url('winter/blocks/blockscontroller'), + // TODO: Make good + 'attributes' => 'onclick="window.location.href = this.querySelector(\'a\').href;"', + 'permissions' => ['winter.blocks.manage_blocks'] + ]); + }); + } } diff --git a/assets/dist/js/winter.cmspage.extension.js b/assets/dist/js/winter.cmspage.extension.js new file mode 100644 index 0000000..60195eb --- /dev/null +++ b/assets/dist/js/winter.cmspage.extension.js @@ -0,0 +1 @@ +(()=>{var t;(t=window.jQuery).wn.cmsPage.updateModifiedCounter=function(){var n={page:{menu:"pages",count:0},partial:{menu:"partials",count:0},layout:{menu:"layouts",count:0},content:{menu:"content",count:0},asset:{menu:"assets",count:0},block:{menu:"blocks",count:0}};t("> div.tab-content > div.tab-pane[data-modified]","#cms-master-tabs").each((function(){var e=t("> form > input[name=templateType]",this).val();n[e].count++})),t.each(n,(function(n,e){t.wn.sideNav.setCounter("cms/"+e.menu,e.count)}))}})(); \ No newline at end of file diff --git a/assets/src/js/winter.cmspage.extension.js b/assets/src/js/winter.cmspage.extension.js new file mode 100644 index 0000000..8318d4f --- /dev/null +++ b/assets/src/js/winter.cmspage.extension.js @@ -0,0 +1,21 @@ +(($) => { + $.wn.cmsPage.updateModifiedCounter = function () { + var counters = { + page: {menu: 'pages', count: 0}, + partial: {menu: 'partials', count: 0}, + layout: {menu: 'layouts', count: 0}, + content: {menu: 'content', count: 0}, + asset: {menu: 'assets', count: 0}, + block: {menu: 'blocks', count: 0}, + } + + $('> div.tab-content > div.tab-pane[data-modified]', '#cms-master-tabs').each(function () { + var inputType = $('> form > input[name=templateType]', this).val(); + counters[inputType].count++; + }); + + $.each(counters, function (type, data) { + $.wn.sideNav.setCounter('cms/' + data.menu, data.count); + }); + }; +})(window.jQuery); diff --git a/classes/Block.php b/classes/Block.php index 9962b2c..4f928eb 100644 --- a/classes/Block.php +++ b/classes/Block.php @@ -26,6 +26,16 @@ class Block extends CmsCompoundObject */ protected $allowedExtensions = ['block']; + /** + * @var array The attributes that are mass assignable. + */ + protected $fillable = [ + 'markup', + 'settings', + 'code', + 'yaml' + ]; + protected PartialStack $partialStack; public function __construct(array $attributes = []) diff --git a/classes/BlockParser.php b/classes/BlockParser.php index 6e7d9d0..a2cc42d 100644 --- a/classes/BlockParser.php +++ b/classes/BlockParser.php @@ -15,7 +15,9 @@ class BlockParser extends SectionParser */ public static function parseSettings(string $settings): array { - return Yaml::parse($settings); + $parsed = Yaml::parse($settings); + // Ensure that the parsed settings returns an array (errors return input string) + return is_array($parsed) ? $parsed : []; } /** @@ -23,6 +25,62 @@ public static function parseSettings(string $settings): array */ public static function renderSettings(array $data): string { - return Yaml::render($data); + return is_string($data['yaml']) ? $data['yaml'] : Yaml::render($data); + } + + /** + * Parses Halcyon section content. + * The expected file format is following: + * + * INI settings section + * == + * PHP code section + * == + * Twig markup section + * + * If the content has only 2 sections they are parsed as settings and markup. + * If there is only a single section, it is parsed as markup. + * + * Returns an array with the following elements: (array|null) 'settings', + * (string|null) 'markup', (string|null) 'code'. + */ + public static function parse(string $content, array $options = []): array + { + $sectionOptions = array_merge([ + 'isCompoundObject' => true + ], $options); + extract($sectionOptions); + + $result = [ + 'settings' => [], + 'code' => null, + 'markup' => null, + 'yaml' => null + ]; + + if (!isset($isCompoundObject) || $isCompoundObject === false || !strlen($content)) { + return $result; + } + + $sections = static::parseIntoSections($content); + $count = count($sections); + foreach ($sections as &$section) { + $section = trim($section); + } + + if ($count >= 3) { + $result['yaml'] = $sections[0]; + $result['settings'] = static::parseSettings($sections[0]); + $result['code'] = static::parseCode($sections[1]); + $result['markup'] = static::parseMarkup($sections[2]); + } elseif ($count == 2) { + $result['yaml'] = $sections[0]; + $result['settings'] = static::parseSettings($sections[0]); + $result['markup'] = static::parseMarkup($sections[1]); + } elseif ($count == 1) { + $result['markup'] = static::parseMarkup($sections[0]); + } + + return $result; } } diff --git a/classes/BlockProcessor.php b/classes/BlockProcessor.php index 7794ab4..f8efa97 100644 --- a/classes/BlockProcessor.php +++ b/classes/BlockProcessor.php @@ -32,7 +32,8 @@ protected function parseTemplateContent($query, $result, $fileName) 'content' => $content, 'mtime' => array_get($result, 'mtime'), 'markup' => $processed['markup'], - 'code' => $processed['code'] + 'code' => $processed['code'], + 'yaml' => $processed['yaml'], ] + $processed['settings']; } diff --git a/controllers/BlocksController.php b/controllers/BlocksController.php new file mode 100644 index 0000000..1d2b6db --- /dev/null +++ b/controllers/BlocksController.php @@ -0,0 +1,238 @@ +<?php + +namespace Winter\Blocks\Controllers; + +use Backend\Classes\Controller; +use Backend\Classes\NavigationManager; +use Backend\Facades\Backend; +use Backend\Facades\BackendMenu; +use Cms\Classes\Theme; +use Cms\Controllers\Index as CmsIndexController; +use Cms\Widgets\TemplateList; +use Illuminate\Support\Facades\Lang; +use Illuminate\Support\Facades\Request; +use Winter\Blocks\Classes\Block; +use Winter\Storm\Exception\ApplicationException; +use Winter\Storm\Support\Facades\Config; +use Winter\Storm\Support\Facades\Flash; + +/** + * Blocks Controller Backend Controller + */ +class BlocksController extends CmsIndexController +{ + protected $theme; + + /** + * @var array Permissions required to view this page. + */ + public $requiredPermissions = [ + 'blocks.manage_blocks', + ]; + + public function __construct() + { + Controller::__construct(); + + BackendMenu::setContext('Winter.Cms', 'cms', 'blocks'); + + try { + if (!($theme = Theme::getEditTheme())) { + throw new ApplicationException(Lang::get('cms::lang.theme.edit.not_found')); + } + + $this->theme = $theme; + + new TemplateList($this, 'blockList', function () use ($theme) { + return Block::listInTheme($theme, true); + }); + } catch (\Exception $ex) { + $this->handleError($ex); + } + + // Dynamically re-write the cms menu item urls to allow the user to return back to those pages + BackendMenu::registerCallback(function (NavigationManager $navigationManager) { + foreach ($navigationManager->getMainMenuItem('Winter.Cms', 'cms')->sideMenu as $menuItem) { + if ($menuItem->url === 'javascript:;') { + $menuItem->url = Backend::url('cms#' . $menuItem->code); + $menuItem->attributes = 'onclick="window.location.href = this.querySelector(\'a\').href;"'; + } + } + }); + } + + /** + * Index page action + * @return void + */ + public function index() + { + parent::index(); + $this->addJs('/plugins/winter/blocks/assets/dist/js/winter.cmspage.extension.js', 'core'); + } + + /** + * Resolves a template type to its class name + * @param string $type + * @return string + */ + protected function resolveTypeClassName($type) + { + if ($type !== 'block') { + throw new ApplicationException(Lang::get('cms::lang.template.invalid_type')); + } + + return Block::class; + } + + /** + * Returns the text for a template tab + * @param string $type + * @param string $template + * @return string + */ + protected function getTabTitle($type, $template) + { + if ($type !== 'block') { + throw new ApplicationException(Lang::get('cms::lang.template.invalid_type')); + } + + return $template->getFileName() ?? Lang::get('winter.blocks::lang.editor.new'); + } + + /** + * Returns a form widget for a specified template type. + * @param string $type + * @param string $template + * @param string $alias + * @return Backend\Widgets\Form + */ + protected function makeTemplateFormWidget($type, $template, $alias = null) + { + if ($type !== 'block') { + throw new ApplicationException(Lang::get('cms::lang.template.not_found')); + } + + $formConfig = '~/plugins/winter/blocks/controllers/blockscontroller/block_fields.yaml'; + + $widgetConfig = $this->makeConfig($formConfig); + + $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); + } + + $lang = 'php'; + if (array_get($widgetConfig->secondaryTabs, 'fields.markup.type') === 'codeeditor') { + switch ($ext) { + case 'htm': + $lang = 'twig'; + break; + case 'html': + $lang = 'html'; + break; + case 'css': + $lang = 'css'; + break; + case 'js': + case 'json': + $lang = 'javascript'; + break; + } + } + + $widgetConfig->model = $template; + $widgetConfig->alias = $alias ?: 'form'.studly_case($type).md5($template->exists ? $template->getFileName() : uniqid()); + + return $this->makeWidget('Backend\Widgets\Form', $widgetConfig); + } + + /** + * Saves the template currently open + * @return array + */ + public function onSave() + { + $this->validateRequestTheme(); + $type = Request::input('templateType'); + $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', 'yaml']; + + 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); + } +} diff --git a/controllers/blockscontroller/_button_commit.php b/controllers/blockscontroller/_button_commit.php new file mode 100644 index 0000000..90a4abd --- /dev/null +++ b/controllers/blockscontroller/_button_commit.php @@ -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> diff --git a/controllers/blockscontroller/_button_lastmodified.php b/controllers/blockscontroller/_button_lastmodified.php new file mode 100644 index 0000000..daff6f3 --- /dev/null +++ b/controllers/blockscontroller/_button_lastmodified.php @@ -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; ?> \ No newline at end of file diff --git a/controllers/blockscontroller/_button_reset.php b/controllers/blockscontroller/_button_reset.php new file mode 100644 index 0000000..a5a8c87 --- /dev/null +++ b/controllers/blockscontroller/_button_reset.php @@ -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> diff --git a/controllers/blockscontroller/_common_toolbar_actions.php b/controllers/blockscontroller/_common_toolbar_actions.php new file mode 100644 index 0000000..2e923a1 --- /dev/null +++ b/controllers/blockscontroller/_common_toolbar_actions.php @@ -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'); ?> diff --git a/controllers/blockscontroller/_concurrency_resolve_form.php b/controllers/blockscontroller/_concurrency_resolve_form.php new file mode 100644 index 0000000..03ec6ca --- /dev/null +++ b/controllers/blockscontroller/_concurrency_resolve_form.php @@ -0,0 +1,29 @@ +<?= Form::open(['onsubmit'=>'return false']) ?> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="popup">×</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() ?> diff --git a/controllers/blockscontroller/_form_page.php b/controllers/blockscontroller/_form_page.php new file mode 100644 index 0000000..74d1b2f --- /dev/null +++ b/controllers/blockscontroller/_form_page.php @@ -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() ?> \ No newline at end of file diff --git a/controllers/blockscontroller/_partial_toolbar.php b/controllers/blockscontroller/_partial_toolbar.php new file mode 100644 index 0000000..094da3b --- /dev/null +++ b/controllers/blockscontroller/_partial_toolbar.php @@ -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' => 'block']); ?> +</div> diff --git a/controllers/blockscontroller/_safemode_notice.php b/controllers/blockscontroller/_safemode_notice.php new file mode 100644 index 0000000..b9e320c --- /dev/null +++ b/controllers/blockscontroller/_safemode_notice.php @@ -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> diff --git a/controllers/blockscontroller/_sidepanel.php b/controllers/blockscontroller/_sidepanel.php new file mode 100644 index 0000000..0223688 --- /dev/null +++ b/controllers/blockscontroller/_sidepanel.php @@ -0,0 +1,21 @@ +<?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('winter.blocks.manage_blocks')): ?> + <!-- Partials --> + <form + role="form" + class="layout <?= ++$visibleCount == 1 ? '' : 'hide' ?>" + data-content-id="blocks" + data-template-type="block" + data-type-icon="wn-icon-tags" + onsubmit="return false"> + <?= $this->widget->blockList->render() ?> + </form> + <?php endif ?> + </div> + </div> +</div> diff --git a/controllers/blockscontroller/block_fields.yaml b/controllers/blockscontroller/block_fields.yaml new file mode 100644 index 0000000..8223579 --- /dev/null +++ b/controllers/blockscontroller/block_fields.yaml @@ -0,0 +1,45 @@ +# =================================== +# Form Field Definitions +# =================================== + +fields: + fileName: + span: left + label: cms::lang.editor.filename + attributes: + default-focus: 1 + + toolbar: + type: partial + path: partial_toolbar + cssClass: collapse-visible + +tabs: + cssClass: master-area + +secondaryTabs: + stretch: true + fields: + yaml: + tab: winter.blocks::lang.editor.settings + stretch: true + type: codeeditor + language: yaml + + markup: + tab: cms::lang.editor.markup + stretch: true + type: codeeditor + language: twig + + safemode_notice: + tab: cms::lang.editor.code + type: partial + hidden: true + cssClass: p-b-0 + + code: + tab: cms::lang.editor.code + stretch: true + type: codeeditor + language: php diff --git a/controllers/blockscontroller/config_block_list.yaml b/controllers/blockscontroller/config_block_list.yaml new file mode 100644 index 0000000..be56179 --- /dev/null +++ b/controllers/blockscontroller/config_block_list.yaml @@ -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: block diff --git a/controllers/blockscontroller/index.php b/controllers/blockscontroller/index.php new file mode 100644 index 0000000..138c663 --- /dev/null +++ b/controllers/blockscontroller/index.php @@ -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() ?> diff --git a/lang/en/lang.php b/lang/en/lang.php index 724a88a..abfce55 100644 --- a/lang/en/lang.php +++ b/lang/en/lang.php @@ -23,6 +23,10 @@ 'tabs' => [ 'display' => 'Display', ], + 'editor' => [ + 'settings' => 'Settings', + 'new' => 'New Block', + ], 'blocks' => [ 'button' => [ 'name' => 'Button', @@ -101,6 +105,7 @@ 'description' => 'Embed a YouTube video', 'youtube_id' => 'YouTube Video ID', ], + 'manage_blocks' => 'Manage Blocks', ], 'fields' => [ 'actions_prompt' => 'Add action', @@ -120,4 +125,10 @@ 'right' => 'Right', ], ], + 'models' => [ + 'blockscontroller' => [ + 'label' => 'Blocks Controller', + 'label_plural' => 'Blocks Controllers', + ], + ], ]; diff --git a/winter.mix.js b/winter.mix.js index 082f4b8..ac70e1d 100644 --- a/winter.mix.js +++ b/winter.mix.js @@ -3,4 +3,5 @@ const mix = require('laravel-mix'); mix .setPublicPath(__dirname) .js('assets/src/js/blocks.js', 'assets/dist/js/blocks.js') + .js('assets/src/js/winter.cmspage.extension.js', 'assets/dist/js/winter.cmspage.extension.js') .less('formwidgets/blocks/assets/less/blocks.less', 'formwidgets/blocks/assets/css/blocks.css');