From d52bd4675926d3d3385c4a3a129f831610923cbf Mon Sep 17 00:00:00 2001 From: butschster Date: Wed, 25 Mar 2015 16:30:02 +0300 Subject: [PATCH] init --- classes/Controller/API/Page/Parts.php | 3 + classes/Filter/Decorator.php | 3 + classes/Filter/Default.php | 3 + classes/KodiCMS/Controller/API/Page/Parts.php | 73 +++++ classes/KodiCMS/Filter/Decorator.php | 10 + classes/KodiCMS/Filter/Default.php | 13 + classes/KodiCMS/Model/API/Page/Part.php | 42 +++ classes/KodiCMS/Model/Page/Part.php | 107 +++++++ classes/KodiCMS/Model/Widget/Part.php | 26 ++ classes/KodiCMS/Part.php | 127 ++++++++ classes/Model/API/Page/Part.php | 3 + classes/Model/Page/Part.php | 3 + classes/Model/Widget/Part.php | 3 + classes/Part.php | 3 + composer.json | 25 ++ config/cache.php | 5 + config/permissions.php | 10 + i18n/ru.php | 18 ++ init.php | 106 +++++++ install/schema.sql | 16 + media/css/parts.css | 14 + media/js/controller/parts.js | 300 ++++++++++++++++++ views/part/item.php | 84 +++++ views/part/items.php | 14 + 24 files changed, 1011 insertions(+) create mode 100644 classes/Controller/API/Page/Parts.php create mode 100644 classes/Filter/Decorator.php create mode 100644 classes/Filter/Default.php create mode 100644 classes/KodiCMS/Controller/API/Page/Parts.php create mode 100644 classes/KodiCMS/Filter/Decorator.php create mode 100644 classes/KodiCMS/Filter/Default.php create mode 100644 classes/KodiCMS/Model/API/Page/Part.php create mode 100644 classes/KodiCMS/Model/Page/Part.php create mode 100644 classes/KodiCMS/Model/Widget/Part.php create mode 100644 classes/KodiCMS/Part.php create mode 100644 classes/Model/API/Page/Part.php create mode 100644 classes/Model/Page/Part.php create mode 100644 classes/Model/Widget/Part.php create mode 100644 classes/Part.php create mode 100644 composer.json create mode 100644 config/cache.php create mode 100644 config/permissions.php create mode 100644 i18n/ru.php create mode 100644 init.php create mode 100644 install/schema.sql create mode 100644 media/css/parts.css create mode 100644 media/js/controller/parts.js create mode 100644 views/part/item.php create mode 100644 views/part/items.php diff --git a/classes/Controller/API/Page/Parts.php b/classes/Controller/API/Page/Parts.php new file mode 100644 index 0000000..5d1fe62 --- /dev/null +++ b/classes/Controller/API/Page/Parts.php @@ -0,0 +1,3 @@ + + * @link http://kodicms.ru + * @copyright (c) 2012-2014 butschster + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +class KodiCMS_Controller_API_Page_Parts extends Controller_System_Api { + + public function get_get() + { + $page_id = $this->param('pid', NULL, TRUE); + + $parts = Model_API::factory('api_page_part') + ->get_all($page_id, $this->fields); + + $this->response($parts); + } + + public function rest_get() + { + return $this->get_get(); + } + + public function rest_put() + { + $id = $this->param('id', NULL, TRUE); + $part = ORM::factory('page_part', (int) $id); + + $response = $part + ->values($this->params()) + ->save() + ->object(); + + $response['is_developer'] = (int) Auth::has_permissions('administrator, developer'); + $this->response($response); + } + + public function rest_post() + { + $part = ORM::factory('page_part'); + + $response = $part + ->values($this->params()) + ->save() + ->as_array(); + + $response['is_developer'] = (int) Auth::has_permissions('administrator, developer'); + $this->response($response); + } + + public function rest_delete() + { + $id = $this->param('id', NULL, TRUE); + + $part = ORM::factory('page_part', (int) $id); + $part->delete(); + } + + public function post_reorder() + { + if (!ACL::check('parts.reorder')) + { + return; + } + + $ids = $this->param('ids', array()); + ORM::factory('page_part')->sort($ids); + } +} \ No newline at end of file diff --git a/classes/KodiCMS/Filter/Decorator.php b/classes/KodiCMS/Filter/Decorator.php new file mode 100644 index 0000000..5f5917a --- /dev/null +++ b/classes/KodiCMS/Filter/Decorator.php @@ -0,0 +1,10 @@ + + * @link http://kodicms.ru + * @copyright (c) 2012-2014 butschster + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +class KodiCMS_Model_API_Page_Part extends Model_API { + + protected $_table_name = 'page_parts'; + protected $_secured_columns = array( + 'content', 'content_html' + ); + + public function get_all($page_id, $fields = array()) + { + $fields = $this->prepare_param($fields); + + $parts = DB::select('id', 'name') + ->select_array($this->filtered_fields($fields)) + ->from($this->table_name()) + ->where('page_id', '=', (int) $page_id) + ->order_by('position') + ->cache_tags(array('page_parts')) + ->cached((int) Config::get('cache', 'page_parts')) + ->execute() + ->as_array(); + + $is_developer = (int) Auth::has_permissions('administrator, developer'); + + foreach ($parts as & $part) + { + $part['is_developer'] = $is_developer; + } + + return $parts; + } + +} \ No newline at end of file diff --git a/classes/KodiCMS/Model/Page/Part.php b/classes/KodiCMS/Model/Page/Part.php new file mode 100644 index 0000000..4d80473 --- /dev/null +++ b/classes/KodiCMS/Model/Page/Part.php @@ -0,0 +1,107 @@ + + * @link http://kodicms.ru + * @copyright (c) 2012-2014 butschster + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +class KodiCMS_Model_Page_Part extends ORM +{ + const PART_NOT_PROTECTED = 0; + const PART_PROTECTED = 1; + + protected $_reload_on_wakeup = FALSE; + + protected $_belongs_to = array( + 'page' => array( + 'model' => 'page', + 'foreign_key' => 'page_id' + ) + ); + + public function labels() + { + return array( + 'name' => __('Parn name'), + 'filter_id' => __('Filter'), + 'content' => __('Content'), + 'content_html' => __('HTML content'), + 'is_protected' => __('Is protected'), + 'is_expanded' => __('Is expanded'), + 'is_indexable' => __('Is indexable') + ); + } + + public function rules() + { + return array( + 'name' => array( + array('not_empty'), + array('max_length', array(':value', 50)) + ) + ); + } + + public function before_save() + { + if ($this->filter_id === NULL) + { + $this->filter_id = Config::get('site', 'default_html_editor'); + } + + if ($this->is_protected === NULL) + { + $this->is_protected = self::PART_NOT_PROTECTED; + } + + if ($this->name === NULL) + { + $this->name = 'part'; + } + + if ($this->filter_id !== NULL) + { + $filter = WYSIWYG::get_filter($this->filter_id); + $this->content_html = $filter->apply($this->content); + } + + Observer::notify('part_before_save', $this); + + return TRUE; + } + + public function after_save() + { + Observer::notify('part_after_save', $this); + + Cache::instance()->delete_tag('page_parts'); + return parent::after_save(); + } + + public function after_delete($id) + { + Cache::instance()->delete_tag('page_parts'); + return parent::after_save(); + } + + public function sort(array $positions) + { + foreach ($positions as $pos => $id) + { + DB::update($this->table_name()) + ->set(array( + 'position' => $pos + )) + ->where('id', '=', $id) + ->execute($this->_db); + } + + Cache::instance()->delete_tag('page_parts'); + + return $this; + } + +} \ No newline at end of file diff --git a/classes/KodiCMS/Model/Widget/Part.php b/classes/KodiCMS/Model/Widget/Part.php new file mode 100644 index 0000000..40fcea2 --- /dev/null +++ b/classes/KodiCMS/Model/Widget/Part.php @@ -0,0 +1,26 @@ + + * @link http://kodicms.ru + * @copyright (c) 2012-2014 butschster + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +class KodiCMS_Model_Widget_Part { + + public $block = NULL; + protected $_html = NULL; + + public function __construct($block, $html) + { + $this->block = $block; + $this->_html = $html; + } + + public function __toString() + { + return (string) $this->_html; + } +} \ No newline at end of file diff --git a/classes/KodiCMS/Part.php b/classes/KodiCMS/Part.php new file mode 100644 index 0000000..7682085 --- /dev/null +++ b/classes/KodiCMS/Part.php @@ -0,0 +1,127 @@ + + * @link http://kodicms.ru + * @copyright (c) 2012-2014 butschster + * @license http://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt + */ +class KodiCMS_Part +{ + /** + * + * @var array + */ + protected static $_cache = array(); + + /** + * + * @param Model_Page_Front $page + * @param string $part + * @param boolean $inherit + * @return boolean + */ + public static function exists(Model_Page_Front $page, $part, $inherit = FALSE) + { + self::_load_parts($page->id()); + + if (isset(self::$_cache[$page->id()][$part])) + { + return TRUE; + } + else if ($inherit !== FALSE AND $page->parent() instanceof Model_Page_Front) + { + return self::exists($this->parent(), $part, TRUE); + } + + return FALSE; + } + + /** + * + * @param Model_Page_Front $page + * @param string $part + * @param boolean $inherit + * @param integer $cache_lifetime + * @return void + */ + public static function content(Model_Page_Front $page, $part = 'body', $inherit = FALSE, $cache_lifetime = NULL, array $tags = array()) + { + if (self::exists($page, $part)) + { + $view = self::get($page->id(), $part); + + if ($cache_lifetime !== NULL AND ! Fragment::load($page->id() . $part . Request::current()->uri(), (int) $cache_lifetime)) + { + echo $view; + + Fragment::save_with_tags((int) $cache_lifetime, array('page_parts')); + } + else if ($cache_lifetime === NULL) + { + echo $view; + } + } + else if ($inherit !== FALSE AND $page->parent() instanceof Model_Page_Front) + { + self::content($page->parent(), $part, TRUE, $cache_lifetime); + } + } + + /** + * + * @param integer $page_id + * @param string $part + * @return View + */ + public static function get( $page, $part ) + { + $html = NULL; + + $page_id = ($page instanceof Model_Page_Front) ? $page->id() : (int) $page; + + self::_load_parts($page_id); + + if (empty(self::$_cache[$page_id][$part])) + { + return NULL; + } + + if (self::$_cache[$page_id][$part] instanceof Model_Page_Part) + { + $html = View_Front::factory() + ->render_html(self::$_cache[$page_id][$part]->content_html); + } + else if (self::$_cache[$page_id][$part] instanceof Kohana_View) + { + $html = self::$_cache[$page_id][$part]->render(); + } + + return $html; + } + + /** + * + * @param integer $page_id + * @return array|NULL + */ + final private static function _load_parts($page_id) + { + $page_id = (int) $page_id; + + if (!array_key_exists($page_id, self::$_cache)) + { + self::$_cache[$page_id] = DB::select('name', 'content', 'content_html') + ->from('page_parts') + ->where('page_id', '=', $page_id) + ->cache_tags(array('page_parts')) + ->as_object('Model_Page_Part') + ->cached((int) Config::get('cache', 'page_parts')) + ->execute() + ->as_array('name'); + } + + return self::$_cache[$page_id]; + } +} \ No newline at end of file diff --git a/classes/Model/API/Page/Part.php b/classes/Model/API/Page/Part.php new file mode 100644 index 0000000..878af71 --- /dev/null +++ b/classes/Model/API/Page/Part.php @@ -0,0 +1,3 @@ + Date::DAY +); diff --git a/config/permissions.php b/config/permissions.php new file mode 100644 index 0000000..aed6f81 --- /dev/null +++ b/config/permissions.php @@ -0,0 +1,10 @@ + array( + array( + 'action' => 'parts', + 'description' => 'Manage parts' + ) + ) +); \ No newline at end of file diff --git a/i18n/ru.php b/i18n/ru.php new file mode 100644 index 0000000..8f66348 --- /dev/null +++ b/i18n/ru.php @@ -0,0 +1,18 @@ + 'Добавить часть страницы', + 'Parn name' => 'Название', + 'HTML content' => 'HTML контент', + 'Is expanded' => 'Развернут', + 'Content of page part :part_name is protected from changes.' + => 'Контент части страницы :part_name защищен от изменений.', + 'Is protected' => 'Защищена', + 'Is indexable' => 'Индексировать', + 'Filter' => 'Фильтр', + 'Page part options' => 'Опции части страницы', + 'Remove part :part_name' => 'Удалить часть :part_name', + 'Remove part :part_name?' => 'Удалить часть :part_name?', + 'Double click to edit part name.' + => 'Для редактирования заголовка кликнете по нему два раза', +); \ No newline at end of file diff --git a/init.php b/init.php new file mode 100644 index 0000000..6f757dd --- /dev/null +++ b/init.php @@ -0,0 +1,106 @@ +css(NULL, ADMIN_RESOURCES . 'css/parts.css'); + +Observer::observe('view_page_edit_plugins_top', function($page) { + if ($page->loaded()) + { + echo View::factory('part/items'); + } +}); + +Observer::observe('controller_before_page_edit', function() { + Assets::package(array('jquery-ui', 'parts')); +}); + +// Если страницы загружена, загружаем части страниц в качестве виджетов и помещаем +// в блоки с названием частей страниц +Observer::observe('frontpage_found', function($page) { + $layout = $page->get_layout_object(); + + $widgets = array(); + + foreach ($layout->blocks() as $block) + { + if (!Part::exists($page, $block)) + { + continue; + } + + $widgets['part_' . $block] = new Model_Widget_Part($block, Part::get($page, $block)); + } + + Context::instance()->register_widgets($widgets); +}); + +// Загрузка JS кода на страницы редактирования +Observer::observe(array('controller_before_page_edit', 'controller_before_page_add'), function() { + Assets::js('controller.parts', ADMIN_RESOURCES . 'js/controller/parts.js', 'global'); +}); + +// Сохранение контента частей страниц +Observer::observe('page_edit_after_save', function($page) { + $parts = Arr::get(Request::initial()->post(), 'part_content', array()); + + $indexable_content = ''; + + foreach ($parts as $id => $content) + { + $part = ORM::factory('page_part', (int) $id); + + if ((bool) $part->is_indexable) + { + $indexable_content .= ' ' . $part->content; + } + + if ($content == $part->content) + { + continue; + } + + $part + ->values(array('content' => $content)) + ->save(); + } + + if (in_array($page->status_id, Model_Page_Front::get_statuses())) + { + Search::instance()->add_to_index('pages', $page->id, $page->title, $indexable_content, '', array( + 'uri' => $page->get_uri() + )); + } + else + { + Search::instance()->remove_from_index('pages', $page->id); + } +}); + +Observer::observe('update_search_index', function() { + + $pages = ORM::factory('page')->find_all(); + + foreach ($pages as $page) + { + $indexable_content = ''; + + $parts = ORM::factory('page_part') + ->where('page_id', '=', $page->id) + ->where('is_indexable', '=', 1) + ->find_all(); + + foreach ($parts as $part) + { + $indexable_content .= ' ' . $part->content; + } + + Search::instance()->add_to_index('pages', $page->id, $page->title, $indexable_content, '', array( + 'uri' => $page->get_uri() + )); + } +}); + +// Чистка кеша частей страниц при редактирвании или удалении страницы +Observer::observe(array('page_add_after_save', 'page_edit_after_save', 'page_delete'), function($page) { + Cache::instance()->delete_tag('page_parts'); +}); diff --git a/install/schema.sql b/install/schema.sql new file mode 100644 index 0000000..c543732 --- /dev/null +++ b/install/schema.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS `__TABLE_PREFIX__page_parts` ( + `id` int(11) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(100) DEFAULT NULL, + `filter_id` varchar(25) DEFAULT NULL, + `content` longtext, + `content_html` longtext, + `page_id` int(11) unsigned DEFAULT NULL, + `is_protected` tinyint(4) DEFAULT '0', + `is_expanded` tinyint(1) DEFAULT '1', + `is_indexable` tinyint(1) NOT NULL DEFAULT '0', + `position` int(11) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `page_id` (`page_id`), + KEY `name` (`name`), + CONSTRAINT `__TABLE_PREFIX__page_parts_ibfk_1` FOREIGN KEY (`page_id`) REFERENCES `__TABLE_PREFIX__pages` (`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8 ; \ No newline at end of file diff --git a/media/css/parts.css b/media/css/parts.css new file mode 100644 index 0000000..f34a35b --- /dev/null +++ b/media/css/parts.css @@ -0,0 +1,14 @@ +.part .panel-heading { + padding-left: 0; +} + +.part .panel-heading-sortable-handler { + float: left; + width: 30px; + text-align: center; + background: #333; + margin: -5px 10px 0 0; + padding-top: 6px; + height: 32px; + cursor: move; +} \ No newline at end of file diff --git a/media/js/controller/parts.js b/media/js/controller/parts.js new file mode 100644 index 0000000..c263503 --- /dev/null +++ b/media/js/controller/parts.js @@ -0,0 +1,300 @@ +$(function() { + cms.models.part = Backbone.Model.extend({ + urlRoot: Api.build_url('page-parts'), + defaults: { + name: 'part', + filter_id: DEFAULT_HTML_EDITOR, + page_id: PAGE_ID, + content: '', + is_protected: 0, + is_expanded: 1, + is_indexable: 0, + position: 0 + }, + + parse: function(response, xhr) { + if(response.type == 'POST') { + return response.response; + } + + return response; + }, + + validate: function(attrs) { + if(!$.trim(attrs.name)) + return 'Name must be set'; + }, + + switchProtected: function() { + this.save({is_protected: this.get('is_protected') == 1 ? 0 : 1}); + }, + + toggleMinimize: function() { + this.save({is_expanded: this.get('is_expanded') == 1 ? 0 : 1}); + }, + + switchIndexable: function() { + this.save({is_indexable: this.get('is_indexable') == 1 ? 0 : 1}); + }, + + changeFilter: function(filter_id) { + if(this.get('filter_id') != filter_id) + this.save({filter_id: filter_id}); + }, + + destroyFilter: function() { + cms.filters.switchOff( 'pageEditPartContent-' + this.get('name') ); + }, + + clear: function() { + this.destroy(); + } + }); + + cms.collections.parts = Backbone.Collection.extend({ + url: Api.build_url('page-parts'), + model: cms.models.part, + parse: function(response, xhr) { + return response.response; + }, + comparator: function(a) { + return a.get('position'); + }, + setOrder: function (data) { + Api.post('page-parts.reorder', {ids: data}, function (response) { + + }); + } + }); + + cms.views.part = Backbone.View.extend({ + tagName: 'div', + + template: _.template($('#part-body').html()), + attributes: function () { + return { + 'data-id': this.model.id + }; + }, + events: { + 'click .part-options-button': 'toggleOptions', + 'click .part-minimize-button': 'toggleMinimize', + 'dblclick .panel-heading ': 'toggleMinimize', + 'change .item-filter': 'changeFilter', + 'change .is_protected': 'switchProtected', + 'change .is_indexable': 'switchIndexable', + 'click .item-remove': 'clear', + 'click .part-rename': 'editName', + 'keypress .edit-name': 'updateOnEnter' + }, + + updateOnEnter: function(e) { + if (e.keyCode == 13) this.closeEditName(); + this.input.val(this.input.val().replace(/[^a-z0-9\-\_]/, '')); + }, + + editName: function(e) { + if(this.model.get('is_protected') == 1 && this.model.get('is_developer') == 0) return; + + if(this.$el.hasClass("editing")) { + this.closeEditName(); + } else { + this.$el.addClass("editing"); + this.input.show().focus(); + this.$el.find('.part-name').hide(); + } + return false; + }, + + closeEditName: function() { + if(this.model.get('is_protected') == 1 && this.model.get('is_developer') == 0) return; + + this.$el.removeClass("editing"); + var value = $.trim(this.input.val()); + this.model.save({name: value}); + this.render(); + + return false; + }, + + toggleMinimize: function(e) { + e.preventDefault(); + + if(this.model.get('is_expanded') == 1) { + this.$el + .find('.part-minimize-button i') + .addClass('fa-chevron-down') + .removeClass('fa-chevron-up') + .end() + .find('.item-filter-cont').hide() + .end() + .find('.part-textarea').slideUp(); + } else { + this.$el.find('.part-minimize-button i') + .addClass('fa-chevron-up') + .removeClass('fa-chevron-down') + .end() + .find('.item-filter-cont').show() + .end() + .find('.part-textarea').slideDown(); + } + + this.model.toggleMinimize(); + }, + + changeFilter: function() { + var filter_id = this.$el.find('.item-filter').val(); + this.model.changeFilter(filter_id); + cms.filters.switchOn( 'pageEditPartContent-' + this.model.get('name'), filter_id ); + }, + + toggleOptions: function(e) { + e.preventDefault(); + + this.$el.find('.part-options').toggle(); + }, + + switchProtected: function() { + this.model.switchProtected(); + }, + + switchIndexable: function() { + this.model.switchIndexable(); + }, + + initialize: function() { + this.model.on('add', this.render, this); + this.model.on('destroy', this.remove, this); + }, + + // Re-render the titles of the todo item. + render: function() { + this.$el.html(this.template(this.model.toJSON())); + this.$el.data('id', this.model.id); + + this.input = this.$el.find('.edit-name').hide(); + + if(this.model.get('is_protected') == 1) { + this.$el.find('.is_protected').check(); + } + + if(this.model.get('is_indexable') == 1) { + this.$el.find('.is_indexable').check(); + } + + return this; + }, + + // Remove the item, destroy the model. + clear: function(e) { + e.preventDefault(); + + if (confirm(__('Remove part :part_name?', {":part_name": this.model.get('name')}))) this.model.clear(); + } + }); + + cms.views.parts = Backbone.View.extend({ + el: $("#pageEditParts"), + initialize: function() { + var $self = this; + this.collection.fetch({ + data: { + pid: PAGE_ID, + fields: ['filter_id','content','content_html','page_id','is_protected','is_expanded', 'is_indexable'] + }, + success: function () { + $self.render(); + } + }); + + this.$el.sortable({ + axis: "y", + handle: ".panel-heading-sortable-handler", + receive: _.bind(function(event, ui) { + // do something here? + }, this), + remove: _.bind(function(event, ui) { + // do something here? + }, this), + update: _.bind(function(event, ui) { + var list = ui.item.context.parentNode; + this.collection.setOrder($(list).sortable('toArray', {attribute: 'data-id'})); + }, this) + }); + }, + + render: function() { + this.clear(); + this.collection.each(function(part) { + this.addPart(part); + }, this); + + this.collection.on('add', this.render, this); + }, + + clear: function() { + this.$el.empty(); + }, + + addPart: function(part) { + var view = new cms.views.part({model: part}); + + this.$el.append(view.render().el); + + view.changeFilter(); + } + }); + + cms.views.PartPanel = Backbone.View.extend({ + el: $("#pageEditPartsPanel"), + + initialize: function() { + if(PAGE_ID == 0) + this.$el.remove(); + }, + + events: { + 'click #pageEditPartAddButton': 'createPart' + }, + + createPart: function(e) { + e.preventDefault(); + + this.model = new cms.models.part(); + + if(this.collection.length == 0) + this.model.set('name', 'body'); + + var i = 0; + this.collection.each(function(part) { + if(part.get('name') == this.model.get('name')) { + do { + i++; + this.model.set('name', 'part' + i); + } while (this.model.get('name') == part.get('name')); + } + + }, this); + + + this.model.save(); + + this.model.on("sync", function() { + this.collection.each(function(part) { + part.destroyFilter(); + }, this); + this.collection.add(this.model); + this.model.off('sync'); + }, this); + + } + }); + + var PartCollection = new cms.collections.parts(); + var AppParts = new cms.views.parts({ + collection: PartCollection + }); + var AppEdit = new cms.views.PartPanel({ + collection: PartCollection + }); +}) \ No newline at end of file diff --git a/views/part/item.php b/views/part/item.php new file mode 100644 index 0000000..7248f75 --- /dev/null +++ b/views/part/item.php @@ -0,0 +1,84 @@ + \ No newline at end of file diff --git a/views/part/items.php b/views/part/items.php new file mode 100644 index 0000000..4ba9472 --- /dev/null +++ b/views/part/items.php @@ -0,0 +1,14 @@ + + +
+ + +
+ 'pageEditPartAddButton', + 'icon' => UI::icon( 'plus' ), + 'data-hotkeys' => 'ctrl+a', + 'class' => 'btn-default' + ) ); ?> +
+ \ No newline at end of file