partkeepr

fork of partkeepr
git clone https://git.e1e0.net/partkeepr.git
Log | Files | Refs | Submodules | README | LICENSE

commit 5f55567ca0cf4a294d8afbf210d1cd43dfc2df9c
parent c47773cb27461b663bdad27d65725eb2bf43d079
Author: Felicia Hummel <felicia@drachenkatze.org>
Date:   Sun,  8 Jul 2018 01:40:49 +0200

Added initial part details view with add/remove functionality for the mobile frontend

Diffstat:
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartsManager.js | 24+-----------------------
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Data/store/BaseStore.js | 190+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Data/store/PartStore.js | 28++++++++++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/views/index.html.twig | 2++
Asrc/PartKeepr/MobileFrontendBundle/Resources/public/css/PartKeepr-mobile.css | 15+++++++++++++++
Asrc/PartKeepr/MobileFrontendBundle/Resources/public/images/partkeepr-header.png | 0
Asrc/PartKeepr/MobileFrontendBundle/Resources/public/js/Components/Part/Details/Controller.js | 35+++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/MobileFrontendBundle/Resources/public/js/Components/Part/Details/Overview.js | 19+++++++++++++++++++
Asrc/PartKeepr/MobileFrontendBundle/Resources/public/js/Components/Part/Details/Panel.js | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/MobileFrontendBundle/Resources/public/js/Components/Part/PartController.js | 38++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/MobileFrontendBundle/Resources/public/js/Components/Part/PartGrid.js | 26++++++++++++++++++++++++++
Asrc/PartKeepr/MobileFrontendBundle/Resources/public/js/Components/Part/PartPanel.js | 35+++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/MobileFrontendBundle/Resources/public/js/PartKeeprMobile.js | 63+++++----------------------------------------------------------
Msrc/PartKeepr/MobileFrontendBundle/Resources/views/index.html.twig | 15+++++++++++----
14 files changed, 507 insertions(+), 85 deletions(-)

diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartsManager.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartsManager.js @@ -579,29 +579,7 @@ Ext.define('PartKeepr.PartManager', { * Creates the store */ createStore: function (config) { - Ext.Object.merge(config, { - autoLoad: true, - autoSync: false, // Do not change. If true, new (empty) records would be immediately commited to the database. - remoteFilter: true, - remoteSort: true, - pageSize: 50 - }); - - this.store = Ext.create('Ext.data.Store', config); - - // Workaround for bug http://www.sencha.com/forum/showthread.php?133767-Store.sync()-does-not-update-dirty-flag&p=607093#post607093 - this.store.on('write', function (store, operation) { - var success = operation.wasSuccessful(); - if (success) - { - Ext.each(operation.records, function (record) { - if (record.dirty) - { - record.commit(); - } - }); - } - }); + this.store = Ext.create('PartKeepr.Data.Store.PartStore'); }, /** * Returns the store diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Data/store/BaseStore.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Data/store/BaseStore.js @@ -0,0 +1,189 @@ +Ext.define("PartKeepr.Data.Store.BaseStore", { + extend: "Ext.data.Store", + + autoSync: false, // Do not change. If true, new (empty) records would be immediately committed to the database. + remoteFilter: true, + remoteSort: true, + pageSize: 15, + + /** + * @var {Boolean} Specifies if the search field has an active search + */ + hasSearch: false, + + /** + * @cfg {String} Specifies the target property to search + */ + targetField: 'query', + + /** + * @cfg {String} Specifies the system property which defines all fields to be searched + */ + searchFieldSystemPreference: null, + + /** + * @cfg {Array} Specifies the default fields to be searched + */ + searchFieldSystemPreferenceDefaults: [], + + /** + * @cfg {String} Specifies the system property which defines if the search terms should be splitted + */ + splitSearchTermSystemPreference: null, + + /** + * @cfg {String} Specifies the default for search term splitting + */ + splitSearchTermSystemPreferenceDefaults: true, + + /** + * @var {Ext.util.Filter} The filter set by the search field + */ + searchFilter: null, + + listeners: { + // Workaround for bug http://www.sencha.com/forum/showthread.php?133767-Store.sync()-does-not-update-dirty-flag&p=607093#post607093 + // TODO: Check if this is still present in ExtJS 6.x + write: function (store, operation) { + var success = operation.wasSuccessful(); + if (success) { + Ext.each(operation.records, function (record) + { + if (record.dirty) { + record.commit(); + } + }); + } + } + }, + + constructor: function () { + this.searchFilter = Ext.create("PartKeepr.util.Filter"); + this.callParent(arguments); + }, + + doSearch: function (searchValue) { + if (typeof searchValue !== "string") { + return; + } + + var searchTerms = searchValue.split(" "), + splitTerms = true, + orSubFilters = [], + i, + j, + subFilters = []; + + if (this.splitSearchTermSystemPreference !== null) { + splitTerms = Boolean(PartKeepr.getApplication().getSystemPreference(this.splitSearchTermSystemPreference, + this.splitSearchTermSystemPreferenceDefaults)); + } + + if (this.searchFieldSystemPreference !== null) { + var fields = PartKeepr.getApplication().getSystemPreference(this.searchFieldSystemPreference, + this.searchFieldSystemPreferenceDefaults); + + if (splitTerms === true) { + for (j = 0; j < searchTerms.length; j++) { + orSubFilters = []; + for (i = 0; i < fields.length; i++) { + orSubFilters.push(this.createSearchFilter(fields[i], searchTerms[j])); + } + + subFilters.push(Ext.create("PartKeepr.util.Filter", { + type: "OR", + subfilters: orSubFilters + })); + } + + this.searchFilter.setConfig({ + type: "AND", + subfilters: subFilters + }); + + } else { + for (i = 0; i < fields.length; i++) { + subFilters.push(this.createSearchFilter(fields[i], searchValue)); + } + + this.searchFilter.setConfig({ + type: "OR", + subfilters: subFilters + }); + } + + + } else { + if (splitTerms === true) { + for (j = 0; j < searchTerms.length; j++) { + subFilters.push(this.createSearchFilter(this.targetField, searchTerms[j])); + } + + this.searchFilter.setConfig({ + type: "OR", + subfilters: subFilters + }); + } else { + this.searchFilter.setConfig({ + property: this.targetField, + value: "%" + searchValue+ "%", + operator: 'like' + }); + } + } + + if (searchValue.length < 1) { + this.resetSearch(); + return; + } + + if (this.isLoading()) { + Ext.defer(this.startSearch, 200, this); + return; + } + + this.searchFilter.setValue(searchValue); + + if (!this.getFilters().contains(this.searchFilter)) { + this.getFilters().add(this.searchFilter); + } + + this.getFilters().itemChanged(this.searchFilter); + + this.hasSearch = true; + }, + + createSearchFilter: function (property, term) { + return Ext.create("PartKeepr.util.Filter", { + property: property, + value: "%" + term + "%", + operator: 'like' + }); + }, + /** + * Resets the search field to empty and re-triggers the store to load the matching records. + */ + resetSearch: function () + { + if (this.isLoading()) { + Ext.defer(this.resetSearch, 200, this); + return; + } + + this.searchFilter.setValue(''); + + if (this.hasSearch) { + + if (this.getFilters().contains(this.searchFilter)) { + this.getFilters().remove(this.searchFilter); + } + + this.currentPage = 1; + this.load({start: 0}); + this.hasSearch = false; + + } + } + + +});+ \ No newline at end of file diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Data/store/PartStore.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Data/store/PartStore.js @@ -0,0 +1,27 @@ +Ext.define("PartKeepr.Data.Store.PartStore", { + extend: "PartKeepr.Data.Store.BaseStore", + + alias: 'store.PartStore', + model: "PartKeepr.PartBundle.Entity.Part", + + autoLoad: true, + + pageSize: 50, + groupField: 'categoryPath', + + searchFieldSystemPreference: "partkeepr.part.search.field", + searchFieldSystemPreferenceDefaults: ["name", "description", "comment", "internalPartNumber"], + splitSearchTermSystemPreference: "partkeepr.part.search.split", + splitSearchTermSystemPreferenceDefaults: true, + + sorters: [ + { + property: 'category.categoryPath', + direction: 'ASC' + }, + { + property: 'name', + direction: 'ASC' + } + ] +});+ \ No newline at end of file diff --git a/src/PartKeepr/FrontendBundle/Resources/views/index.html.twig b/src/PartKeepr/FrontendBundle/Resources/views/index.html.twig @@ -69,6 +69,7 @@ '@PartKeeprFrontendBundle/Resources/public/js/Data/HydraField.js' '@PartKeeprFrontendBundle/Resources/public/js/Data/HydraTreeModel.js' '@PartKeeprFrontendBundle/Resources/public/js/Data/store/ModelStore.js' + '@PartKeeprFrontendBundle/Resources/public/js/Data/store/BaseStore.js' '@PartKeeprFrontendBundle/Resources/public/js/ExtJS/Bugfixes/Ext.form.field.Checkbox.EXTJS-21886.js' '@PartKeeprFrontendBundle/Resources/public/js/ExtJS/Enhancements/Ext.view.Table-renderCell.js' '@PartKeeprFrontendBundle/Resources/public/js/ExtJS/Enhancements/Ext.data.field.Date-ISO8601.js' %} @@ -182,6 +183,7 @@ '@PartKeeprFrontendBundle/Resources/public/js/Data/HydraReader.js' '@PartKeeprFrontendBundle/Resources/public/js/Data/HydraTreeReader.js' '@PartKeeprFrontendBundle/Resources/public/js/Data/store/PartCategoryStore.js' + '@PartKeeprFrontendBundle/Resources/public/js/Data/store/PartStore.js' '@PartKeeprFrontendBundle/Resources/public/js/Data/store/FootprintCategoryStore.js' '@PartKeeprFrontendBundle/Resources/public/js/Data/store/StorageLocationCategoryStore.js' '@PartKeeprFrontendBundle/Resources/public/js/Data/store/BarcodeScannerActionsStore.js' diff --git a/src/PartKeepr/MobileFrontendBundle/Resources/public/css/PartKeepr-mobile.css b/src/PartKeepr/MobileFrontendBundle/Resources/public/css/PartKeepr-mobile.css @@ -0,0 +1,14 @@ +.partkeeprLogo { + height: 22px !important; + width: 112px !important; + background-image: url(../images/partkeepr-header.png); + background-repeat: no-repeat; +} + +.partDetails h1 { + font-size: 2em; +} + +.partDetails h2 { + font-size: 1.4em; +}+ \ No newline at end of file diff --git a/src/PartKeepr/MobileFrontendBundle/Resources/public/images/partkeepr-header.png b/src/PartKeepr/MobileFrontendBundle/Resources/public/images/partkeepr-header.png Binary files differ. diff --git a/src/PartKeepr/MobileFrontendBundle/Resources/public/js/Components/Part/Details/Controller.js b/src/PartKeepr/MobileFrontendBundle/Resources/public/js/Components/Part/Details/Controller.js @@ -0,0 +1,34 @@ +Ext.define("PartKeepr.Components.Part.Details.Controller", { + extend: 'Ext.app.ViewController', + + alias: 'controller.PartDetailsController', + + closePanel: function () { + this.getView().hide(); + }, + reloadPart: function () { + this.getView().getPart().load({ + success: Ext.bind(this.getView().updatePart, this.getView()) + }); + }, + promptAddStock: function () { + Ext.Msg.prompt(i18n("Add Stock"), i18n("Amount"), this.addStock, this); + }, + addStock: function (button, value) { + if (button === "ok") { + this.getView().getPart().callPutAction("addStock",{ + quantity: value + },Ext.bind(this.reloadPart, this),false); + } + }, + promptRemoveStock: function () { + Ext.Msg.prompt(i18n("Remove Stock"), i18n("Amount"), this.removeStock, this); + }, + removeStock: function (button, value) { + if (button === "ok") { + this.getView().getPart().callPutAction("removeStock",{ + quantity: value + },Ext.bind(this.reloadPart, this),false); + } + } +});+ \ No newline at end of file diff --git a/src/PartKeepr/MobileFrontendBundle/Resources/public/js/Components/Part/Details/Overview.js b/src/PartKeepr/MobileFrontendBundle/Resources/public/js/Components/Part/Details/Overview.js @@ -0,0 +1,18 @@ +Ext.define("PartKeepr.Components.Part.Details.Overview", { + extend: "Ext.panel.Panel", + + xtype: "PartDetailsOverview", + + tpl: [ + '<h1>{name}</h1>', + '<h2>{description}</h2>', + '<hr/>', + '<p><span class="x-fa fa-folder"/> {categoryPath}</p>', + '<hr/>', + '<p><span class="x-fa fa-database"/> Stock Level: {stockLevel} <span style="margin-left: 16px;" class="x-fa fa-arrow-circle-down"/> Minimum Stock Level {minStockLevel}</p>', + ], + + setPart: function (part) { + this.setData(part.getData()); + } +});+ \ No newline at end of file diff --git a/src/PartKeepr/MobileFrontendBundle/Resources/public/js/Components/Part/Details/Panel.js b/src/PartKeepr/MobileFrontendBundle/Resources/public/js/Components/Part/Details/Panel.js @@ -0,0 +1,101 @@ +Ext.define("PartKeepr.Components.Part.Details.Panel", { + extend: 'Ext.tab.Panel', + + xtype: 'PartDetails', + + controller: 'PartDetailsController', + + shadow: true, + width: "90%", + height: "90%", + centered: true, + modal: true, + closable: true, + + cls: 'partDetails', + + part: null, + + activeTab: 0, + tabBar: { + layout: { + pack: 'center', + align: 'center' + }, + docked: 'bottom', + defaults: { + iconAlign: 'top' + } + }, + defaults: { + scrollable: true + }, + items: [{ + xtype: 'titlebar', + docked: 'top', + title: '', + items: [ + { + align: 'right', + text: i18n("Add Stock"), + ui: 'action', + handler: 'promptAddStock' + }, + { + align: 'right', + text: i18n("Remove Stock"), + ui: 'action', + handler: 'promptRemoveStock' + }, + { + iconCls: 'x-fa fa-close', + align: 'right', + handler: 'closePanel' + }] + },{ + title: i18n('General Info'), + padding: '24px', + items: {xtype: "PartDetailsOverview"}, + cls: 'card', + iconCls: 'x-fa fa-info-circle' + }, { + title: 'Favorites', + html: 'Badges <em>(like the 4, below)</em> can be added by setting <code>badgeText</code> when creating a tab or by using <code>setBadgeText()</code> on the tab later.', + cls: 'card', + iconCls: 'x-fa fa-star', + badgeText: '4' + }, { + title: 'Downloads', + id: 'tab3', + html: 'Badge labels will truncate if the text is wider than the tab.', + badgeText: 'Overflow test', + cls: 'card', + iconCls: 'x-fa fa-download', + hidden: (Ext.filterPlatform('ie10') && Ext.os.is.Phone) ? true : false + }, { + title: 'Settings', + html: 'Tabbars are <code>ui:"dark"</code> by default, but also have light variants.', + cls: 'card', + iconCls: 'x-fa fa-gear', + hidden: (Ext.filterPlatform('ie10') && Ext.os.is.Phone) ? true : false + }, { + title: 'User', + html: '<span class="action">User tapped User</span>', + cls: 'card', + iconCls: 'x-fa fa-user' + }], + + setPart: function (part) { + this.part = part; + + this.down("titlebar").setTitle(part.get("name")); + this.down("PartDetailsOverview").setPart(part); + }, + updatePart: function () { + console.log(this.part); + this.setPart(this.part); + }, + getPart: function () { + return this.part; + } +});+ \ No newline at end of file diff --git a/src/PartKeepr/MobileFrontendBundle/Resources/public/js/Components/Part/PartController.js b/src/PartKeepr/MobileFrontendBundle/Resources/public/js/Components/Part/PartController.js @@ -0,0 +1,37 @@ +Ext.define("PartKeepr.Components.Part.PartController", { + extend: 'Ext.app.ViewController', + + alias: 'controller.PartController', + + reload: function () { + var view = this.getView(); + + view.down("PartGrid").getStore().reload(); + }, + search: function (field) { + var value = field.getValue(); + this.getView().down("#searchField").hide(); + this.getView().down("PartGrid").getStore().doSearch(value); + }, + resetSearch: function () { + this.getView().down("PartGrid").getStore().resetSearch(); + this.getView().down("#searchField").hide(); + }, + revealSearchField: function () { + if (this.getView().down("#searchField").isHidden()) + { + this.getView().down("#searchField").show(); + this.getView().down("#searchField").focus(); + } else { + this.getView().down("#searchField").hide(); + this.getView().down("PartGrid").getStore().doSearch( + this.getView().down("#searchField").getValue() + ) + } + }, + showPartDetails: function (part) { + Ext.Viewport.down("PartDetails").setPart(part); + Ext.Viewport.down("PartDetails").show(); + } + + });+ \ No newline at end of file diff --git a/src/PartKeepr/MobileFrontendBundle/Resources/public/js/Components/Part/PartGrid.js b/src/PartKeepr/MobileFrontendBundle/Resources/public/js/Components/Part/PartGrid.js @@ -0,0 +1,25 @@ +Ext.define("PartKeepr.Components.Part.PartGrid", { + extend: "Ext.grid.Grid", + + xtype: 'PartGrid', + + store: { + type: 'PartStore' + }, + + plugins: { + type: 'pagingtoolbar' + }, + + columns: [{ + text: i18n("Name"), + dataIndex: 'name', + flex: 1, + minWidth: 150 + }], + listeners: { + itemtap: function (view, index, target, part) { + this.up("PartPanel").getController().showPartDetails(part); + } + } +});+ \ No newline at end of file diff --git a/src/PartKeepr/MobileFrontendBundle/Resources/public/js/Components/Part/PartPanel.js b/src/PartKeepr/MobileFrontendBundle/Resources/public/js/Components/Part/PartPanel.js @@ -0,0 +1,34 @@ +Ext.define("PartKeepr.Components.Part.PartPanel", { + extend: "Ext.panel.Panel", + + xtype: 'PartPanel', + + controller: 'PartController', + + title: 'Parts List', + iconCls: 'partkeeprLogo', + tools: [{ + showAnimation: { + type: 'slide', + direction: 'down' + + }, + xtype: 'textfield', + itemId: 'searchField', + hidden: true, + placeHolder: 'Search', + listeners: { + action: 'search', + clearicontap: 'resetSearch' + } + }, { + type: 'search', + handler: 'revealSearchField' + }, { + type: 'refresh', + handler: 'reload' + }, {type: 'menu'}], + items: [{xtype: 'PartGrid'}], + layout: 'fit', + fullscreen: true +});+ \ No newline at end of file diff --git a/src/PartKeepr/MobileFrontendBundle/Resources/public/js/PartKeeprMobile.js b/src/PartKeepr/MobileFrontendBundle/Resources/public/js/PartKeeprMobile.js @@ -21,71 +21,18 @@ Ext.application({ this.loginManager.on("login", this.onLogin, this); this.loginManager.on("logout", this.onLogout, this); - Ext.Viewport.add(this.loginManager.loginDialog); this.loginManager.login(); }, test: function () { - var config = { - autoLoad: true, - model: "PartKeepr.PartBundle.Entity.Part", - autoSync: false, // Do not change. If true, new (empty) records would be immediately committed to the database. - remoteFilter: true, - remoteSort: true, - pageSize: 50, - groupField: 'categoryPath', - sorters: [ - { - property: 'category.categoryPath', - direction: 'ASC' - }, - { - property: 'name', - direction: 'ASC' - } - ] - }; - - var store = Ext.create("Ext.data.Store", config); - - var grid = Ext.create('Ext.grid.Grid', { - - store: store, - - grouped: true, - features: [{ftype: 'grouping', groupHeaderTpl: 'Subject: {name}'}], - - columns: [ - { - text: 'Name', - hidden: true, - dataIndex: "categoryPath", - flex: 1, - // Adjust the header text when grouped by this column: - }, - { - text: i18n("Name"), - dataIndex: 'name', - flex: 1, - minWidth: 150 - } - ] + var panel = Ext.create({xtype: "PartPanel"}); + var partDetails = Ext.create({xtype: "PartDetails", hidden: true}); + + Ext.Viewport.add(panel); + Ext.Viewport.add(partDetails); - }); - var panel = Ext.create("Ext.panel.Panel", { - title: 'Parts List', - iconCls: 'partkeeprLogo', - tools: [ - {type: 'refresh'}, - {type: 'search'}, - {type: 'menu'} - ], - items: [grid], - layout: 'fit', - fullscreen: true - }); }, getLoginManager: function () { diff --git a/src/PartKeepr/MobileFrontendBundle/Resources/views/index.html.twig b/src/PartKeepr/MobileFrontendBundle/Resources/views/index.html.twig @@ -12,10 +12,8 @@ {% stylesheets filter='cssrewrite' 'js/packages/extjs6/build/modern/theme-material/resources/theme-material-all.css' - 'atelierspierrot/famfamfam-silk-sprite/silk-icons-sprite.css' - 'spritesheets/fugue-16.css' - 'spritesheets/partkeepr.css' - 'bundles/partkeeprfrontend/css/PartKeepr.css' %} + 'bundles/partkeeprmobilefrontend/css/PartKeepr-mobile.css' + %} <link rel="stylesheet" href="{{ asset_url }}"/> {% endstylesheets %} @@ -26,6 +24,7 @@ {% if debug %} {% javascripts output='js/compiled/mobile_extjs.js' 'js/packages/extjs6/build/ext-modern-all-debug.js' + 'js/packages/extjs6/build/modern/theme-material/theme-material.js' %} <script type="text/javascript" src="{{ asset_url }}"></script> {% endjavascripts %} @@ -44,6 +43,7 @@ '@PartKeeprFrontendBundle/Resources/public/js/Data/HydraField.js' '@PartKeeprFrontendBundle/Resources/public/js/Data/HydraTreeModel.js' '@PartKeeprFrontendBundle/Resources/public/js/Data/store/ModelStore.js' + '@PartKeeprFrontendBundle/Resources/public/js/Data/store/BaseStore.js' '@PartKeeprFrontendBundle/Resources/public/js/ExtJS/Bugfixes/Ext.form.field.Checkbox.EXTJS-21886.js' '@PartKeeprFrontendBundle/Resources/public/js/ExtJS/Enhancements/Ext.view.Table-renderCell.js' '@PartKeeprFrontendBundle/Resources/public/js/ExtJS/Enhancements/Ext.data.field.Date-ISO8601.js' %} @@ -61,6 +61,7 @@ {% endjavascripts %} {% javascripts output='js/compiled/mobile_app.js' + '@PartKeeprFrontendBundle/Resources/public/js/Data/store/PartStore.js' '@PartKeeprFrontendBundle/Resources/public/js/ExtJS/Enhancements/Ext.data.Store.getFieldValue.js' '@PartKeeprFrontendBundle/Resources/public/js/Util/i18n.js' '@PartKeeprFrontendBundle/Resources/public/js/Data/store/CurrencyStore.js' @@ -98,6 +99,12 @@ '@PartKeeprFrontendBundle/Resources/public/js/Models/Message.js' '@PartKeeprMobileFrontendBundle/Resources/public/js/Components/Auth/LoginDialog.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/Auth/LoginController.js' + '@PartKeeprMobileFrontendBundle/Resources/public/js/Components/Part/PartPanel.js' + '@PartKeeprMobileFrontendBundle/Resources/public/js/Components/Part/PartGrid.js' + '@PartKeeprMobileFrontendBundle/Resources/public/js/Components/Part/PartController.js' + '@PartKeeprMobileFrontendBundle/Resources/public/js/Components/Part/Details/Panel.js' + '@PartKeeprMobileFrontendBundle/Resources/public/js/Components/Part/Details/Controller.js' + '@PartKeeprMobileFrontendBundle/Resources/public/js/Components/Part/Details/Overview.js' '@PartKeeprFrontendBundle/Resources/public/js/php.default.min.js' %} <script type="text/javascript" src="{{ asset_url }}"></script> {% endjavascripts %}