partkeepr

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

commit 64b5591f270d60cf38eab484d7776c9a52706181
parent 477cdcdec1e9b54c75818739b15a9db36c722991
Author: Felicia Hummel <felicitus@felicitus.org>
Date:   Tue, 16 Aug 2016 19:10:18 +0200

Merge remote-tracking branch 'origin/master'

Diffstat:
Msrc/PartKeepr/DoctrineReflectionBundle/Filter/AdvancedSearchFilter.php | 112+++++++++++++++++++++++++++++++++----------------------------------------------
Msrc/PartKeepr/DoctrineReflectionBundle/Filter/Filter.php | 11+++++++++--
Msrc/PartKeepr/DoctrineReflectionBundle/Tests/AdvancedSearchFilterTest.php | 130++++++++++++++++++++++++++++++++++++++++----------------------------------------
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/BarcodeScanner/Action.js | 44++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/BarcodeScanner/Actions/AddPart.js | 176+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/BarcodeScanner/Actions/AddRemoveStock.js | 239+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/BarcodeScanner/Actions/SearchPart.js | 149+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/BarcodeScanner/ActionsComboBox.js | 16++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/BarcodeScanner/Manager.js | 178+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Editor/Editor.js | 15++++++++++-----
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Editor/EditorGrid.js | 23++++++++++++++++++++++-
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Footprint/FootprintNavigation.js | 50++++++++++++++++++++++++++------------------------
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Grid/AppliedFiltersToolbar.js | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/AddRemoveStockWindow.js | 174+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartDistributorGrid.js | 22++++++++++++++++------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartEditor.js | 45+++++++++++++++++++++++++++++++++++++++++++--
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartEditorWindow.js | 299++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartManufacturerGrid.js | 12+++++++++++-
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartDisplay.js | 9+++++----
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartFilterPanel.js | 829+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartStockHistory.js | 2+-
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartsManager.js | 8+++++---
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Project/ProjectReport.js | 41+++++++++++++++++++++--------------------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationEditor.js | 2+-
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationNavigation.js | 6+++---
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Panel.js | 33+++++++++++++++++++++++----------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/PreferenceEditor.js | 13+++++++++----
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/BarcodeScannerConfiguration.js | 264+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/FulltextSearch.js | 29+++++++++++++++--------------
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/RequiredPartDistributorFields.js | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/RequiredPartFields.js | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/RequiredPartManufacturerFields.js | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/User/UserGrid.js | 2+-
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FieldSelector.js | 55++++++++++++++++++++++++++++++++++++++-----------------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/PagingToolbar.js | 19++++++++++++++++++-
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/ReloadableComboBox.js | 47+++++++++++++++++++++++++++++++----------------
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Data/store/BarcodeScannerActionsStore.js | 20++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/PartKeepr.js | 206++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Util/Filter.js | 27+++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Util/FilterPlugin.js | 51+++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/form/field/SearchField.js | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Msrc/PartKeepr/FrontendBundle/Resources/views/index.html.twig | 23+++++++++++++++++------
42 files changed, 3037 insertions(+), 688 deletions(-)

diff --git a/src/PartKeepr/DoctrineReflectionBundle/Filter/AdvancedSearchFilter.php b/src/PartKeepr/DoctrineReflectionBundle/Filter/AdvancedSearchFilter.php @@ -77,7 +77,6 @@ class AdvancedSearchFilter extends AbstractFilter public function apply(ResourceInterface $resource, QueryBuilder $queryBuilder) { $metadata = $this->getClassMetadata($resource); - $fieldNames = array_flip($metadata->getFieldNames()); $request = $this->requestStack->getCurrentRequest(); if (null === $request) { @@ -93,60 +92,10 @@ class AdvancedSearchFilter extends AbstractFilter /** * @var Filter $filter */ - if (isset($fieldNames[$filter->getProperty()]) && $filter->getAssociation() === null) { - if ($filter->hasSubFilters()) { - $subFilterExpressions = []; - - foreach ($filter->getSubFilters() as $subFilter) { - /** - * @var Filter $subFilter - */ - if ($subFilter->getAssociation() !== null) { - $this->addJoins($queryBuilder, $subFilter); - } - - $subFilterExpressions[] = $this->getFilterExpression($queryBuilder, $subFilter); - } - - $expressions = call_user_func_array([$queryBuilder->expr(), "orX"], $subFilterExpressions); - $queryBuilder->andWhere($expressions); - } else { - $queryBuilder->andWhere( - $this->getFilterExpression($queryBuilder, $filter) - ); - } - - } else { - if ($filter->getAssociation() !== null) { - // Pull in associations - $this->addJoins($queryBuilder, $filter); - } - - $filter->setValue($this->getFilterValueFromUrl($filter->getValue())); - - if ($filter->hasSubFilters()) { - $subFilterExpressions = []; + $queryBuilder->andWhere( + $this->getFilterExpression($queryBuilder, $filter) + ); - foreach ($filter->getSubFilters() as $subFilter) { - /** - * @var Filter $subFilter - */ - if ($subFilter->getAssociation() !== null) { - $this->addJoins($queryBuilder, $subFilter); - } - - $subFilterExpressions[] = $this->getFilterExpression($queryBuilder, $subFilter); - } - - $expressions = call_user_func_array([$queryBuilder->expr(), "orX"], $subFilterExpressions); - $queryBuilder->andWhere($expressions); - } else { - $queryBuilder->andWhere( - $this->getFilterExpression($queryBuilder, $filter) - ); - } - - } } foreach ($sorters as $sorter) { @@ -161,6 +110,8 @@ class AdvancedSearchFilter extends AbstractFilter $this->applyOrderByExpression($queryBuilder, $sorter); } + //echo $queryBuilder->getDQL(); + } /** @@ -225,11 +176,11 @@ class AdvancedSearchFilter extends AbstractFilter $parent = 'o'; } - $fullAssociation .= '.' . $association; + $fullAssociation .= '.'.$association; $alias = $this->getAlias($fullAssociation); - $queryBuilder->join($parent . '.' . $association, $alias); + $queryBuilder->join($parent.'.'.$association, $alias); } $this->joins[] = $filter->getAssociation(); @@ -247,12 +198,37 @@ class AdvancedSearchFilter extends AbstractFilter */ private function getFilterExpression(QueryBuilder $queryBuilder, Filter $filter) { + if ($filter->hasSubFilters()) { + $subFilterExpressions = []; + + foreach ($filter->getSubFilters() as $subFilter) { + + /** + * @var $subFilter Filter + */ + if ($subFilter->getAssociation() !== null) { + $this->addJoins($queryBuilder, $subFilter); + } + + $subFilterExpressions[] = $this->getFilterExpression($queryBuilder, $subFilter); + } + + + if ($filter->getType() == Filter::TYPE_AND) { + return call_user_func_array([$queryBuilder->expr(), "andX"], $subFilterExpressions); + } else { + return call_user_func_array([$queryBuilder->expr(), "orX"], $subFilterExpressions); + } + } + if ($filter->getAssociation() !== null) { - $alias = $this->getAlias('o.' . $filter->getAssociation()) . '.' . $filter->getProperty(); + $this->addJoins($queryBuilder, $filter); + $alias = $this->getAlias('o.'.$filter->getAssociation()).'.'.$filter->getProperty(); } else { - $alias = 'o.' . $filter->getProperty(); + $alias = 'o.'.$filter->getProperty(); } + if (strtolower($filter->getOperator()) == Filter::OPERATOR_IN) { if (!is_array($filter->getValue())) { throw new \Exception('Value needs to be an array for the IN operator'); @@ -260,7 +236,7 @@ class AdvancedSearchFilter extends AbstractFilter return $queryBuilder->expr()->in($alias, $filter->getValue()); } else { - $paramName = ':param' . $this->parameterCount; + $paramName = ':param'.$this->parameterCount; $this->parameterCount++; $queryBuilder->setParameter($paramName, $filter->getValue()); @@ -287,7 +263,7 @@ class AdvancedSearchFilter extends AbstractFilter return $queryBuilder->expr()->like($alias, $paramName); break; default: - throw new \Exception('Unknown operator ' . $filter->getOperator()); + throw new \Exception('Unknown operator '.$filter->getOperator()); } } } @@ -305,9 +281,9 @@ class AdvancedSearchFilter extends AbstractFilter private function applyOrderByExpression(QueryBuilder $queryBuilder, Sorter $sorter) { if ($sorter->getAssociation() !== null) { - $alias = $this->getAlias('o.' . $sorter->getAssociation()) . '.' . $sorter->getProperty(); + $alias = $this->getAlias('o.'.$sorter->getAssociation()).'.'.$sorter->getProperty(); } else { - $alias = 'o.' . $sorter->getProperty(); + $alias = 'o.'.$sorter->getProperty(); } return $queryBuilder->addOrderBy($alias, $sorter->getDirection()); @@ -359,7 +335,7 @@ class AdvancedSearchFilter extends AbstractFilter private function getAlias($property) { if (!array_key_exists($property, $this->aliases)) { - $this->aliases[$property] = 't' . count($this->aliases); + $this->aliases[$property] = 't'.count($this->aliases); } return $this->aliases[$property]; @@ -372,7 +348,7 @@ class AdvancedSearchFilter extends AbstractFilter * * @throws \Exception * - * @return array An array containing the property, operator and value keys + * @return Filter */ private function extractJSONFilters($data) { @@ -384,7 +360,6 @@ class AdvancedSearchFilter extends AbstractFilter $property = array_pop($associations); - $filter->setAssociation(implode('.', $associations)); $filter->setProperty($property); } else { @@ -392,12 +367,17 @@ class AdvancedSearchFilter extends AbstractFilter $filter->setProperty($data->property); } } elseif (property_exists($data, "subfilters")) { + if (property_exists($data, 'type')) { + $filter->setType(strtolower($data->type)); + } + if (is_array($data->subfilters)) { $subfilters = []; foreach ($data->subfilters as $subfilter) { $subfilters[] = $this->extractJSONFilters($subfilter); } $filter->setSubFilters($subfilters); + return $filter; } else { throw new \Exception("The subfilters must be an array of objects"); @@ -413,7 +393,7 @@ class AdvancedSearchFilter extends AbstractFilter } if (property_exists($data, 'value')) { - $filter->setValue($data->value); + $filter->setValue($this->getFilterValueFromUrl($data->value)); } else { throw new \Exception('No value specified'); } diff --git a/src/PartKeepr/DoctrineReflectionBundle/Filter/Filter.php b/src/PartKeepr/DoctrineReflectionBundle/Filter/Filter.php @@ -36,19 +36,24 @@ class Filter implements AssociationPropertyInterface /** * The type + * * @var string */ private $type; + /** * @var string */ private $operator; + /** * @var string */ private $value; + /** * SubFilters. + * * @var array */ private $subFilters; @@ -57,6 +62,7 @@ class Filter implements AssociationPropertyInterface public function __construct($type = self::TYPE_AND) { $this->setType($type); + $this->setSubFilters([]); } /** @@ -90,6 +96,8 @@ class Filter implements AssociationPropertyInterface /** * @param string $operator + * + * @throws \Exception Thrown if an invalid operator was passed */ public function setOperator($operator) { @@ -136,4 +144,4 @@ class Filter implements AssociationPropertyInterface return count($this->subFilters) > 0; } -}- \ No newline at end of file +} diff --git a/src/PartKeepr/DoctrineReflectionBundle/Tests/AdvancedSearchFilterTest.php b/src/PartKeepr/DoctrineReflectionBundle/Tests/AdvancedSearchFilterTest.php @@ -32,18 +32,18 @@ class AdvancedSearchFilterTest extends WebTestCase { $client = static::makeClient(true); - $filter = array( - array( + $filter = [ + [ "property" => "storageLocation.name", "operator" => "=", - "value" => "test" - ) - ); + "value" => "test", + ], + ]; $client->request( 'GET', - "/api/parts?filter=" . json_encode($filter), + "/api/parts?filter=".json_encode($filter), [], [], ['CONTENT_TYPE' => 'application/json'] @@ -69,18 +69,18 @@ class AdvancedSearchFilterTest extends WebTestCase { $client = static::makeClient(true); - $filter = array( - array( + $filter = [ + [ "property" => "name", "operator" => "=", - "value" => "FOOBAR" - ) - ); + "value" => "FOOBAR", + ], + ]; $client->request( 'GET', - "/api/parts?filter=" . json_encode($filter), + "/api/parts?filter=".json_encode($filter), [], [], ['CONTENT_TYPE' => 'application/json'] @@ -111,17 +111,17 @@ class AdvancedSearchFilterTest extends WebTestCase */ $iriConverter = $this->getContainer()->get('api.iri_converter'); - $filter = array( - array( + $filter = [ + [ "property" => "storageLocation", "operator" => "=", - "value" => $iriConverter->getIriFromItem($this->fixtures->getReference("storagelocation.first")) - ) - ); + "value" => $iriConverter->getIriFromItem($this->fixtures->getReference("storagelocation.first")), + ], + ]; $client->request( 'GET', - "/api/parts?filter=" . json_encode($filter), + "/api/parts?filter=".json_encode($filter), [], [], ['CONTENT_TYPE' => 'application/json'] @@ -143,20 +143,20 @@ class AdvancedSearchFilterTest extends WebTestCase */ $iriConverter = $this->getContainer()->get('api.iri_converter'); - $filter = array( - array( + $filter = [ + [ "property" => "storageLocation", "operator" => "IN", "value" => [ $iriConverter->getIriFromItem($this->fixtures->getReference("storagelocation.first")), - $iriConverter->getIriFromItem($this->fixtures->getReference("storagelocation.second")) - ] - ) - ); + $iriConverter->getIriFromItem($this->fixtures->getReference("storagelocation.second")), + ], + ], + ]; $client->request( 'GET', - "/api/parts?filter=" . json_encode($filter), + "/api/parts?filter=".json_encode($filter), [], [], ['CONTENT_TYPE' => 'application/json'] @@ -173,18 +173,18 @@ class AdvancedSearchFilterTest extends WebTestCase { $client = static::makeClient(true); - $filter = array( - array( + $filter = [ + [ "property" => "storageLocation.name", "operator" => "LIKE", - "value" => "%test%" - ) - ); + "value" => "%test%", + ], + ]; $client->request( 'GET', - "/api/parts?filter=" . json_encode($filter), + "/api/parts?filter=".json_encode($filter), [], [], ['CONTENT_TYPE' => 'application/json'] @@ -201,17 +201,17 @@ class AdvancedSearchFilterTest extends WebTestCase { $client = static::makeClient(true); - $order = array( - array( + $order = [ + [ "property" => "storageLocation.name", - "direction" => "ASC" - ) - ); + "direction" => "ASC", + ], + ]; $client->request( 'GET', - "/api/parts?order=" . json_encode($order), + "/api/parts?order=".json_encode($order), [], [], ['CONTENT_TYPE' => 'application/json'] @@ -227,28 +227,28 @@ class AdvancedSearchFilterTest extends WebTestCase { $client = static::makeClient(true); - $filter = array( - array( - "mode" => "OR", - "subfilters" => array( - array( + $filter = [ + [ + "type" => "OR", + "subfilters" => [ + [ "property" => "storageLocation.name", "operator" => "=", - "value" => "test" - ), - array( + "value" => "test", + ], + [ "property" => "storageLocation.name", "operator" => "=", - "value" => "test2" - ) - ) - ) - ); + "value" => "test2", + ], + ], + ], + ]; $client->request( 'GET', - "/api/parts?filter=" . json_encode($filter), + "/api/parts?filter=".json_encode($filter), [], [], ['CONTENT_TYPE' => 'application/json'] @@ -264,28 +264,28 @@ class AdvancedSearchFilterTest extends WebTestCase { $client = static::makeClient(true); - $filter = array( - array( - "mode" => "OR", - "subfilters" => array( - array( + $filter = [ + [ + "type" => "OR", + "subfilters" => [ + [ "property" => "name", "operator" => "=", - "value" => "FOOBAR" - ), - array( + "value" => "FOOBAR", + ], + [ "property" => "name", "operator" => "=", - "value" => "FOOBAR2" - ) - ) - ) - ); + "value" => "FOOBAR2", + ], + ], + ], + ]; $client->request( 'GET', - "/api/parts?filter=" . json_encode($filter), + "/api/parts?filter=".json_encode($filter), [], [], ['CONTENT_TYPE' => 'application/json'] diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BarcodeScanner/Action.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BarcodeScanner/Action.js @@ -0,0 +1,44 @@ +/** + * A barcode scanner action is an action which is executed when a barcode was scanned. + * + * The barcode manager decides which action(s) will be called based upon the configuration done in the system + * preferences. + */ +Ext.define("PartKeepr.BarcodeScanner.Action", { + statics: { + /** + * @var {String} The name of the action + */ + actionName: "", + + /** + * @var {String} The description of the action + */ + actionDescription: "" + }, + + /** + * @var {String} Contains the data returned by the barcode manager + */ + data: "", + + /** + * @var {String} Contains the configuration + */ + config: {}, + + + constructor: function (config, data) + { + this.config = config; + this.data = data; + }, + + /** + * Executes an action + */ + execute: function () + { + console.error("The execute function of any PartKeepr.BarcodeScanner.Action subclass must be overridden!"); + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BarcodeScanner/Actions/AddPart.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BarcodeScanner/Actions/AddPart.js @@ -0,0 +1,176 @@ +/** + * Searches the configured part field(s). If no record was found, the create part window is being opened. + */ +Ext.define("PartKeepr.BarcodeScanner.Actions.AddPart", { + extend: "PartKeepr.BarcodeScanner.Action", + + statics: { + actionName: i18n("Add Part"), + actionDescription: i18n("Searches for a part. If the part is not found, a create part window is opened"), + + /** + * Configures the action + */ + configure: function (configuration) + { + configuration = Ext.applyIf(configuration, { + searchFields: [], + searchMode: 'fixed' + }); + + var modelFieldSelector = Ext.create({ + xtype: 'modelFieldSelector', + id: 'searchPartFieldSelector', + border: false, + sourceModel: PartKeepr.PartBundle.Entity.Part, + initiallyChecked: configuration.searchFields, + flex: 1 + }); + + var saveButton = Ext.create("Ext.button.Button", { + text: i18n("OK"), + iconCls: 'fugue-icon disk', + }); + + var cancelButton = Ext.create("Ext.button.Button", { + text: i18n("Cancel"), + iconCls: 'web-icon cancel' + }); + + var bottomToolbar = Ext.create("Ext.toolbar.Toolbar", { + enableOverflow: true, + margin: '10px', + defaults: {minWidth: 100}, + dock: 'bottom', + ui: 'footer', + items: [saveButton, cancelButton] + }); + + var window = Ext.create('Ext.window.Window', { + title: i18n("Add/Remove Stock Configuration"), + height: 400, + modal: true, + width: 600, + layout: { + type: 'vbox', + pack: 'start', + align: 'stretch' + }, + items: [ + { + html: i18n("Select the field(s) to be searched"), + border: false, + bodyStyle: 'padding: 5px; background:transparent;', + }, + modelFieldSelector, + { + xtype: 'radiogroup', + layout: 'vbox', + itemId: 'searchMode', + items: [ + { + boxLabel: i18n("Search string as-is"), + name: 'searchMode', + inputValue: "fixed", + checked: configuration.searchMode == "fixed" ? true : false + }, + { + boxLabel: i18n("Search beginning of string (string*)"), + name: 'searchMode', + inputValue: "beginning", + checked: configuration.searchMode == "beginning" ? true : false + }, { + boxLabel: i18n("Search middle of string (*string*)"), + name: 'searchMode', + inputValue: "any", + checked: configuration.searchMode == "any" ? true : false + + } + ] + } + ], + dockedItems: bottomToolbar + } + ).show(); + + saveButton.setHandler(function () + { + var selection = modelFieldSelector.getChecked(); + var fields = []; + + for (var i = 0; i < selection.length; i++) { + fields.push(selection[i].data.data); + } + configuration.searchFields = fields; + + configuration.searchMode = this.down("#searchMode").getValue().searchMode; + this.close(); + }, window); + + cancelButton.setHandler(function () + { + this.close(); + + }, window); + + } + }, + + execute: function () + { + this.searchStore = Ext.create("Ext.data.Store", { + model: 'PartKeepr.PartBundle.Entity.Part', + autoLoad: false, + autoSync: false, + remoteFilter: true, + remoteSort: true + }); + + var subFilters = []; + var searchValue; + + switch (this.config.searchMode) { + case "beginning": + searchValue = this.data + "%"; + break; + case "any": + searchValue = "%" + this.data + "%"; + break; + default: + searchValue = this.data; + break; + } + + for (var i = 0; i < this.config.searchFields.length; i++) { + subFilters.push(Ext.create("PartKeepr.util.Filter", { + property: this.config.searchFields[i], + operator: "LIKE", + value: searchValue + })); + } + + this.filter = Ext.create("PartKeepr.util.Filter", { + type: "OR", + subfilters: subFilters + }); + + this.searchStore.on("load", this.onDataLoaded, this); + this.searchStore.addFilter(this.filter, true); + this.searchStore.load({start: 0}); + }, + onDataLoaded: function () + { + if (this.searchStore.getCount() === 0) { + var defaults = {}, i; + + for (i = 0; i < this.config.searchFields.length; i++) { + defaults[this.config.searchFields[i]] = this.data; + + } + + PartKeepr.getApplication().getPartManager().onItemAdd(defaults); + + + } + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BarcodeScanner/Actions/AddRemoveStock.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BarcodeScanner/Actions/AddRemoveStock.js @@ -0,0 +1,239 @@ +Ext.define("PartKeepr.BarcodeScanner.Actions.AddRemoveStock", { + extend: "PartKeepr.BarcodeScanner.Action", + + statics: { + actionName: i18n("Add/Remove Stock"), + actionDescription: i18n("Searches for a part and then allows the user to increase/decrease the stock level"), + configure: function (configuration) + { + configuration = Ext.applyIf(configuration, { + searchFields: [], + searchMode: 'fixed' + }); + + var modelFieldSelector = Ext.create({ + xtype: 'modelFieldSelector', + id: 'searchPartFieldSelector', + border: false, + sourceModel: PartKeepr.PartBundle.Entity.Part, + initiallyChecked: configuration.searchFields, + flex: 1 + }); + + var saveButton = Ext.create("Ext.button.Button", { + text: i18n("OK"), + iconCls: 'fugue-icon disk', + }); + + var cancelButton = Ext.create("Ext.button.Button", { + text: i18n("Cancel"), + iconCls: 'web-icon cancel' + }); + + var bottomToolbar = Ext.create("Ext.toolbar.Toolbar", { + enableOverflow: true, + margin: '10px', + defaults: {minWidth: 100}, + dock: 'bottom', + ui: 'footer', + items: [saveButton, cancelButton] + }); + + var window = Ext.create('Ext.window.Window', { + title: i18n("Add/Remove Stock Configuration"), + height: 400, + modal: true, + width: 600, + layout: { + type: 'vbox', + pack: 'start', + align: 'stretch' + }, + items: [ + { + html: i18n("Select the field(s) to be searched"), + border: false, + bodyStyle: 'padding: 5px; background:transparent;', + }, + modelFieldSelector, + { + xtype: 'radiogroup', + layout: 'vbox', + itemId: 'searchMode', + items: [ + { + boxLabel: i18n("Search string as-is"), + name: 'searchMode', + inputValue: "fixed", + checked: configuration.searchMode == "fixed" ? true : false + }, + { + boxLabel: i18n("Search beginning of string (string*)"), + name: 'searchMode', + inputValue: "beginning", + checked: configuration.searchMode == "beginning" ? true : false + }, { + boxLabel: i18n("Search middle of string (*string*)"), + name: 'searchMode', + inputValue: "any", + checked: configuration.searchMode == "any" ? true : false + + } + ] + } + ], + dockedItems: bottomToolbar + } + ).show(); + + saveButton.setHandler(function () + { + var selection = modelFieldSelector.getChecked(); + var fields = []; + + for (var i = 0; i < selection.length; i++) { + fields.push(selection[i].data.data); + } + configuration.searchFields = fields; + configuration.searchMode = this.down("#searchMode").getValue().searchMode; + this.close(); + }, window); + + cancelButton.setHandler(function () + { + this.close(); + + }, window); + + } + }, + + execute: function () + { + this.searchStore = Ext.create("Ext.data.Store", { + model: 'PartKeepr.PartBundle.Entity.Part', + autoLoad: false, + autoSync: false, + remoteFilter: true, + remoteSort: true + }); + + var subFilters = []; + var searchValue; + + switch (this.config.searchMode) { + case "beginning": + searchValue = this.data + "%"; + break; + case "any": + searchValue = "%" + this.data + "%"; + break; + default: + searchValue = this.data; + break; + } + + for (var i = 0; i < this.config.searchFields.length; i++) { + subFilters.push(Ext.create("PartKeepr.util.Filter", { + property: this.config.searchFields[i], + operator: "LIKE", + value: searchValue + })); + } + + this.filter = Ext.create("PartKeepr.util.Filter", { + type: "OR", + subfilters: subFilters + }); + + this.searchStore.on("load", this.onDataLoaded, this); + this.searchStore.addFilter(this.filter, true); + this.searchStore.load({start: 0}); + }, + onDataLoaded: function () + { + if (this.searchStore.getCount() === 0) { + return; + } + + if (this.searchStore.getCount() > 1) { + var columns = [{header: 'Name', dataIndex: 'name', flex: 1}]; + + for (var i = 0; i < this.config.searchFields.length; i++) { + columns.push({header: this.config.searchFields[i], dataIndex: this.config.searchFields[i]}); + } + + + this.window = Ext.create("Ext.window.Window", { + itemId: "window", + listeners: { + show: function () + { + this.down("#grid").focus(); + this.down("#grid").getSelectionModel().selectRange(0, 0); + + } + }, + title: i18n("Multiple Parts found"), + width: 800, + height: 400, + layout: 'fit', + buttons: [ + { + text: i18n("OK"), + handler: function (btn) + { + var sel = btn.up("#window").down("#grid").getSelection(); + + if (sel.length === 1) { + btn.up("#window").fireEvent("recordSelected", sel[0]); + btn.up("#window").close(); + } + } + }, { + text: i18n("Cancel"), + handler: function (btn) + { + btn.up("#window").close(); + } + } + ], + items: { + xtype: 'grid', + store: this.searchStore, + columns: columns, + itemId: 'grid', + listeners: { + rowdblclick: function (grid, record) + { + grid.up("#window").fireEvent("recordSelected", record); + grid.up("#window").close(); + }, + rowkeydown: function (grid, record, tr, rowIndex, e) + { + if (e.event.code === "Escape") { + grid.up("#window").close(); + } + + if (e.event.code === "Enter") { + grid.up("#window").fireEvent("recordSelected", record); + grid.up("#window").close(); + } + } + } + } + }); + + this.window.on("recordSelected", this.combinedAddRemoveStockWindow, this); + this.window.show(); + } else { + this.combinedAddRemoveStockWindow(this.searchStore.getAt(0)); + } + }, + combinedAddRemoveStockWindow: function (record) + { + var j = Ext.create("PartKeepr.Components.Part.AddRemoveStockWindow", {record: record}); + + j.show(); + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BarcodeScanner/Actions/SearchPart.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BarcodeScanner/Actions/SearchPart.js @@ -0,0 +1,149 @@ +Ext.define("PartKeepr.BarcodeScanner.Actions.SearchPart", { + extend: "PartKeepr.BarcodeScanner.Action", + + statics: { + actionName: i18n("Search Part"), + actionDescription: i18n("Searches for a part in the parts list"), + configure: function (configuration) + { + configuration = Ext.applyIf(configuration, { + searchFields: [], + searchMode: 'fixed' + }); + + var modelFieldSelector = Ext.create({ + xtype: 'modelFieldSelector', + id: 'searchPartFieldSelector', + border: false, + sourceModel: PartKeepr.PartBundle.Entity.Part, + initiallyChecked: configuration.searchFields, + flex: 1 + }); + + var saveButton = Ext.create("Ext.button.Button", { + text: i18n("OK"), + iconCls: 'fugue-icon disk', + }); + + var cancelButton = Ext.create("Ext.button.Button", { + text: i18n("Cancel"), + iconCls: 'web-icon cancel' + }); + + var bottomToolbar = Ext.create("Ext.toolbar.Toolbar", { + enableOverflow: true, + margin: '10px', + defaults: {minWidth: 100}, + dock: 'bottom', + ui: 'footer', + items: [saveButton, cancelButton] + }); + + var window = Ext.create('Ext.window.Window', { + title: i18n("Search Part Configuration"), + height: 400, + modal: true, + width: 600, + layout: { + type: 'vbox', + pack: 'start', + align: 'stretch' + }, + items: [ + { + html: i18n("Select all fields to be searched"), + border: false, + bodyStyle: 'padding: 5px; background:transparent;', + }, + modelFieldSelector, + { + xtype: 'radiogroup', + layout: 'vbox', + itemId: 'searchMode', + items: [ + { + boxLabel: i18n("Search string as-is"), + name: 'searchMode', + inputValue: "fixed", + checked: configuration.searchMode == "fixed" ? true : false + }, + { + boxLabel: i18n("Search beginning of string (string*)"), + name: 'searchMode', + inputValue: "beginning", + checked: configuration.searchMode == "beginning" ? true : false + }, { + boxLabel: i18n("Search middle of string (*string*)"), + name: 'searchMode', + inputValue: "any", + checked: configuration.searchMode == "any" ? true : false + + } + ] + } + ], + dockedItems: bottomToolbar + } + ).show(); + + saveButton.setHandler(function () + { + var selection = modelFieldSelector.getChecked(); + var fields = []; + + for (var i = 0; i < selection.length; i++) { + fields.push(selection[i].data.data); + } + configuration.searchFields = fields; + configuration.searchMode = this.down("#searchMode").getValue().searchMode; + this.close(); + }, window); + + cancelButton.setHandler(function () + { + this.close(); + + }, window); + + } + }, + + execute: function () + { + var subFilters = []; + var searchValue; + + switch (this.config.searchMode) { + case "beginning": + searchValue = this.data + "%"; + break; + case "any": + searchValue = "%" + this.data + "%"; + break; + default: + searchValue = this.data; + break; + } + + for (var i = 0; i < this.config.searchFields.length; i++) { + subFilters.push(Ext.create("PartKeepr.util.Filter", { + property: this.config.searchFields[i], + operator: "LIKE", + value: searchValue + })); + } + + this.filter = Ext.create("PartKeepr.util.Filter", { + type: "OR", + subfilters: subFilters + }); + + var store = PartKeepr.getApplication().getPartManager().getStore(); + + store.getFilters().clear(); + store.addFilter(this.filter, true); + store.currentPage = 1; + store.load({start: 0}); + + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BarcodeScanner/ActionsComboBox.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BarcodeScanner/ActionsComboBox.js @@ -0,0 +1,16 @@ +Ext.define("PartKeepr.BarcodeScanner.ActionsComboBox", { + extend: "Ext.form.field.ComboBox", + displayField: "name", + valueField: "action", + queryMode: "local", + editable: false, + emptyText: i18n("Select an action"), + forceSelection: true, + xtype: 'barcodescannerActions', + returnObject: true, + initComponent: function () + { + this.store = Ext.create("PartKeepr.Data.store.BarcodeScannerActionsStore"); + this.callParent(arguments); + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BarcodeScanner/Manager.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BarcodeScanner/Manager.js @@ -0,0 +1,178 @@ +Ext.define("PartKeepr.BarcodeScanner.Manager", { + monitor: false, + monitoredKeys: "", + + barcodeInputField: null, + + registerBarcodeScannerHotkey: function () + { + this.monitor = false; + this.runnerTask = new Ext.util.DelayedTask(function () + { + this.stopKeyMonitoring(); + }, this); + + Ext.get(document).on("keydown", this.onKeyPress, this, { + priority: 10000 + }); + }, + /** + * Stops monitoring and executes the action found in the intercepted keys. + */ + stopKeyMonitoring: function () + { + this.monitor = false; + this.runnerTask.cancel(); + + this.executeAction(this.monitoredKeys); + this.monitoredKeys = ""; + }, + /** + * Starts monitoring for input events, up to a configured timeout. + */ + startKeyMonitoring: function () + { + this.monitoredKeys = ""; + this.monitor = true; + this.runnerTask.delay(PartKeepr.getApplication().getSystemPreference("partkeepr.barcodeScanner.timeout", 500)); + }, + /** + * Intercepts keypresses when a barcode scanner hotkey was detected up to the configured timeout. + */ + onKeyPress: function (e) + { + var hotKeyPressed = false; + + var hotKey = PartKeepr.getApplication().getSystemPreference("partkeepr.barcodeScanner.key", ""); + + if (hotKey === "") { + return; + } + + if (e.event.key === hotKey) { + hotKeyPressed = true; + } + + if (PartKeepr.getApplication().getSystemPreference("partkeepr.barcodeScanner.modifierCtrl", false)) { + if (!e.ctrlKey) { + hotKeyPressed = false; + } + } + + if (PartKeepr.getApplication().getSystemPreference("partkeepr.barcodeScanner.modifierShift", false)) { + if (!e.shiftKey) { + hotKeyPressed = false; + } + } + + if (PartKeepr.getApplication().getSystemPreference("partkeepr.barcodeScanner.modifierAlt", false)) { + if (!e.altKey) { + hotKeyPressed = false; + } + } + + if (hotKeyPressed) { + this.startKeyMonitoring(); + return; + } + + + if (this.monitor) { + if (PartKeepr.getApplication().getSystemPreference("partkeepr.barcodeScanner.enter", true)) { + if (e.event.code == "Enter") { + this.stopKeyMonitoring(); + return; + } + } + + if (!e.isSpecialKey()) { + this.monitoredKeys += e.event.key; + } + this.runnerTask.delay( + PartKeepr.getApplication().getSystemPreference("partkeepr.barcodeScanner.timeout", 500)); + e.stopEvent(); + } + }, + /** + * Returns a list of all class names which provide actions. + * + * @return {Array} An array of action class names + */ + getActions: function () + { + var actions = [ + "PartKeepr.BarcodeScanner.Actions.SearchPart", + "PartKeepr.BarcodeScanner.Actions.AddRemoveStock", + "PartKeepr.BarcodeScanner.Actions.AddPart" + ]; + + return actions; + }, + /** + * Executes an action by parsing the input and deciding which action to execute. + * + * @param {String} input The intercepted keys + */ + executeAction: function (input) + { + var actions = this.getActionsByInput(input); + + for (var i = 0; i < actions.length; i++) { + if (actions[i] !== null) { + actions[i].execute(); + } + } + }, + getActionsByInput: function (input) + { + var i, actions = PartKeepr.getApplication().getSystemPreference("partkeepr.barcodeScanner.actions", []), + foundActions = []; + + var barcodeScannerActionsStore = Ext.create("Ext.data.Store", { + fields: ["code", "action", "configuration"], + data: [] + }); + + var actionStore = Ext.create("PartKeepr.Data.store.BarcodeScannerActionsStore"); + + for (i = 0; i < actions.length; i++) { + var item = actions[i]; + + barcodeScannerActionsStore.add({ + code: item.code, + action: actionStore.findRecord("action", item.action), + configuration: item.config + }); + } + + barcodeScannerActionsStore.sort( + function (data1, data2) + { + if (data1.get("code").length == data2.get("code").length) { + return 0; + } + if (data1.get("code").length > data2.get("code").length) { + return -1; + } else { + return 1; + } + } + ); + + var barcodeScannerActions = barcodeScannerActionsStore.getData(); + var code, className, config; + + for (i = 0; i < barcodeScannerActions.getCount(); i++) { + code = barcodeScannerActions.getAt(i).get("code"); + + if (input.substr(0, code.length) === code) { + className = barcodeScannerActions.getAt(i).get("action").get("action"); + config = barcodeScannerActions.getAt(i).get("configuration"); + + foundActions.push(Ext.create(className, config, input.substr(code.length))); + } + } + + return foundActions; + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Editor/Editor.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Editor/Editor.js @@ -55,6 +55,7 @@ Ext.define('PartKeepr.Editor', { }, onCancelEdit: function () { + this.record.reject(); this.fireEvent("editorClose", this); }, newItem: function (defaults) @@ -101,12 +102,16 @@ Ext.define('PartKeepr.Editor', { this.getForm().updateRecord(this.record); - this.fireEvent("itemSave", this.record); + if (this.fireEvent("itemSave", this.record)) { + this.record.save({ + callback: this._onSave, + scope: this + }); + return true; + } else { + return false; + } - this.record.save({ - callback: this._onSave, - scope: this - }); }, _onSave: function (record, response) { diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Editor/EditorGrid.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Editor/EditorGrid.js @@ -190,9 +190,15 @@ Ext.define('PartKeepr.EditorGrid', { grid: this }); + this.appliedFiltersToolbar = Ext.create("PartKeepr.Grid.AppliedFiltersToolbar", { + dock: 'bottom', + targetStore: this.store + }); + this.dockedItems = new Array(); this.dockedItems.push(this.bottomToolbar); + this.dockedItems.push(this.appliedFiltersToolbar); if (this.enableTopToolbar) { this.dockedItems.push(this.topToolbar); @@ -209,11 +215,25 @@ Ext.define('PartKeepr.EditorGrid', { this.getSelectionModel().on("select", this._onItemSelect, this); this.getSelectionModel().on("deselect", this._onItemDeselect, this); this.getView().on("itemkeydown", this._onItemKeyPress, this); + this.getStore().on("filterchange", this._onFilterChange, this); if (this.automaticPageSize) { this.on("resize", this.reassignPageSize, this); } }, + _onFilterChange: function () + { + var filters = this.getStore().getFilters(); + + if (filters.length > 0) { + this.bottomToolbar.down("#filter").show(); + } else { + this.bottomToolbar.down("#filter").hide(); + } + + this.appliedFiltersToolbar.updateFilters(filters); + + }, /** * Re-calculates and re-assigns the page size for the assigned store. * @@ -269,7 +289,8 @@ Ext.define('PartKeepr.EditorGrid', { this.deleteButton.disable(); } }, - _onItemKeyPress: function (view, record, item, index, e) { + _onItemKeyPress: function (view, record, item, index, e) + { if (e.getKey() == e.ENTER || e.getKey() == e.TAB) { this._onItemEdit(view, record); } diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Footprint/FootprintNavigation.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Footprint/FootprintNavigation.js @@ -7,35 +7,37 @@ Ext.define("PartKeepr.FootprintNavigation", { * @var {Ext.data.Store} */ store: null, - items: [ - { - xtype: 'partkeepr.FootprintTree', - region: 'center', - rootVisible: false - }, { - xtype: 'partkeepr.FootprintGrid', - resizable: true, - split: true, - region: 'south', - height: "50%", - titleProperty: "name", - viewConfig: { - plugins: { - ddGroup: 'FootprintCategoryTree', - ptype: 'gridviewdragdrop', - enableDrop: false - } - }, - enableDragDrop: true - } - ], initComponent: function () { + this.items = [ + { + xtype: 'partkeepr.FootprintTree', + region: 'center', + rootVisible: false + }, { + xtype: 'partkeepr.FootprintGrid', + resizable: true, + split: true, + store: this.store, + region: 'south', + height: "50%", + titleProperty: "name", + viewConfig: { + plugins: { + ddGroup: 'FootprintCategoryTree', + ptype: 'gridviewdragdrop', + enableDrop: false + } + }, + enableDragDrop: true, + + } + ]; + this.callParent(arguments); this.down("partkeepr\\.FootprintTree").on("itemclick", this.onCategoryClick, this); - this.down("partkeepr\\.FootprintGrid").setStore(this.store); this.down("partkeepr\\.FootprintGrid").on("itemAdd", this.onAddFootprint, this); this.down("partkeepr\\.FootprintGrid").on("itemDelete", function (id) { @@ -57,7 +59,7 @@ Ext.define("PartKeepr.FootprintNavigation", { */ onCategoryClick: function (tree, record) { - var filter = Ext.create("Ext.util.Filter", { + var filter = Ext.create("PartKeepr.util.Filter", { property: 'category', operator: 'IN', value: this.getChildrenIds(record) diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Grid/AppliedFiltersToolbar.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Grid/AppliedFiltersToolbar.js @@ -0,0 +1,51 @@ +Ext.define("PartKeepr.Grid.AppliedFiltersToolbar", { + extend: "Ext.grid.Panel", + store: { + fields: ['name', 'filter'] + }, + hideHeaders: true, + targetStore: null, + + initComponent: function () + { + this.columns = [ + { + dataIndex: "name", + flex: 1 + }, { + xtype: 'actioncolumn', + width: 25, + items: [ + { + iconCls: 'fugue-icon funnel--minus', + tooltip: i18n("Remove filter"), + handler: function (grid, rowIndex, colIndex, item, e, record) + { + this.targetStore.getFilters().remove(record.get("filter")); + this.updateFilters(this.targetStore.getFilters()); + + }, + scope: this + } + ] + } + ]; + + this.callParent(arguments); + + }, + updateFilters: function (filters) + { + var i; + + this.store.removeAll(); + + for (i = 0; i < filters.getCount(); i++) { + this.store.add({ + name: filters.getAt(i).getFilterDescription(), + filter: filters.getAt(i) + }); + } + + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/AddRemoveStockWindow.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/AddRemoveStockWindow.js @@ -0,0 +1,174 @@ +/** + * This class defines a window which is used to in- or decrease the stock level for a specific part. Logic and service + * calls are not contained in this window, and need to be implemented from the caller. + */ +Ext.define('PartKeepr.Components.Part.AddRemoveStockWindow', { + extend: 'Ext.window.Window', + + // Configurations + constrainHeader: true, + width: 305, + height: 180, + + resizable: false, + + // We set the title later + title: i18n("Add/Remove Stock"), + + layout: 'anchor', + bodyStyle: { + padding: "5px" + }, + + record: null, + + /* + * Initializes the window with the quantity and price fields. The price field is hidden when a stock decrease + * happens. + */ + initComponent: function () + { + + this.quantityField = Ext.create("Ext.form.field.Number", { + value: 0, // The initial value is 0, to indicate that this is a number field + //minValue: 1, // The minimum value is 1. That way we force the user to enter a value + width: 100, + listeners: { + specialkey: { + fn: function (field, e) + { + if (e.getKey() == e.ENTER) { + this.onOKClick(); + } + }, + scope: this + } + } + }); + + this.commentField = Ext.create("Ext.form.field.Text", { + anchor: '100%', + fieldLabel: i18n("Comment"), + maxLength: 255, + enforceMaxLength: true, + listeners: { + specialkey: { + fn: function (field, e) + { + if (e.getKey() == e.ENTER) { + this.onOKClick(); + } + }, + scope: this + } + } + }); + + this.form = Ext.create("Ext.form.Panel", { + border: false, + bodyStyle: 'background-color: transparent', + items: [ + { + xtype: 'fieldcontainer', + fieldLabel: i18n("Quantity"), + layout: 'hbox', + items: [ + this.quantityField, { + width: 75, + xtype: 'displayfield', + margin: "0 0 0 5", + value: this.partUnitName + } + ] + }, this.commentField + ] + }); + + this.items = this.form; + + this.addButton = Ext.create("Ext.button.Button", { + text: i18n("Add"), + iconCls: 'web-icon brick_add', + handler: this.onAddClick, + scope: this + }); + + this.removeButton = Ext.create("Ext.button.Button", { + text: i18n("Remove"), + iconCls: 'web-icon brick_delete', + handler: this.onRemoveClick, + scope: this + }); + + this.buttons = [ + { + text: i18n("Close"), + handler: this.onCloseClick, + iconCls: "web-icon cancel", + scope: this + }, this.addButton, this.removeButton + ]; + this.on("show", function () + { + this.quantityField.focus(); + this.quantityField.selectText(0); + }, this, { + delay: 100 + }); + this.callParent(); + }, + /** + * Closes the window + */ + onCloseClick: function () + { + this.close(); + }, + onAddClick: function () + { + if (this.form.getForm().isValid()) { + var qty = Math.abs(this.quantityField.getValue()); + + this.record.callPutAction("addStock", { + quantity: qty, + comment: this.commentField.getValue() + }, null, true); + this.close(); + } + + + }, + onRemoveClick: function () + { + if (this.form.getForm().isValid()) { + var qty = Math.abs(this.quantityField.getValue()); + + this.record.callPutAction("removeStock", { + quantity: qty, + comment: this.commentField.getValue() + }, null, true); + this.close(); + } + + + }, + /** + * Checks if the form is valid. If yes, execute the callback. + */ + onOKClick: function () + { + if (this.form.getForm().isValid()) { + var qty = this.quantityField.getValue(); + + if (qty < 0) { + this.onRemoveClick(); + } else { + if (qty > 0) { + this.onAddClick(); + } else { + Ext.Msg.alert(i18n("Invalid quantity"), i18n("The quantity must be not equal to 0")); + } + } + } + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartDistributorGrid.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartDistributorGrid.js @@ -15,7 +15,7 @@ Ext.define('PartKeepr.PartDistributorGrid', { }); - this.editing = Ext.create('Ext.grid.plugin.CellEditing', { + this.editing = Ext.create('Ext.grid.plugin.RowEditing', { clicksToEdit: 1 }); @@ -60,7 +60,7 @@ Ext.define('PartKeepr.PartDistributorGrid', { editor: { xtype: 'DistributorComboBox', returnObject: true, - allowBlank: true + allowBlank: false } }, { header: i18n("Order Number"), @@ -68,7 +68,7 @@ Ext.define('PartKeepr.PartDistributorGrid', { flex: 1, editor: { xtype: 'textfield', - allowBlank: true + allowBlank: this.isOptional("orderNumber") } }, { header: i18n("Packaging Unit"), @@ -84,7 +84,7 @@ Ext.define('PartKeepr.PartDistributorGrid', { header: i18n("Price per Item"), dataIndex: 'price', flex: 1, - renderer: function (val, p, rec) + renderer: function (val) { return PartKeepr.getApplication().formatCurrency(val); }, @@ -106,7 +106,7 @@ Ext.define('PartKeepr.PartDistributorGrid', { flex: 1, editor: { xtype: 'urltextfield', - allowBlank: true, + allowBlank: this.isOptional("sku"), triggerCls: 'x-form-trigger-link', getUrl: function () @@ -114,7 +114,7 @@ Ext.define('PartKeepr.PartDistributorGrid', { var distributor = this.ownerCt.context.record.getDistributor(); if (distributor !== null) { - skuurl = distributor.get("skuurl"); + var skuurl = distributor.get("skuurl"); if (skuurl) { skuurl = skuurl.replace("%s", this.value); @@ -156,5 +156,15 @@ Ext.define('PartKeepr.PartDistributorGrid', { onSelectChange: function (selModel, selections) { this.deleteButton.setDisabled(selections.length === 0); + }, + isOptional: function (field) + { + var fields = PartKeepr.getApplication().getSystemPreference("partkeepr.partDistributor.requiredFields", []); + + if (Ext.Array.contains(fields, field)) { + return false; + } else { + return true; + } } }); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartEditor.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartEditor.js @@ -86,6 +86,7 @@ Ext.define('PartKeepr.PartEditor', { { xtype: 'textfield', fieldLabel: i18n("Description"), + allowBlank: this.isOptional("description"), name: 'description' }, { layout: 'column', @@ -138,6 +139,7 @@ Ext.define('PartKeepr.PartEditor', { xtype: 'textarea', fieldLabel: i18n("Comment"), name: 'comment', + allowBlank: this.isOptional("comment"), anchor: '100% ' + overallHeight }, { @@ -152,6 +154,7 @@ Ext.define('PartKeepr.PartEditor', { xtype: 'textfield', fieldLabel: i18n("Status"), flex: 1, + allowBlank: this.isOptional("status"), name: 'status' }, { xtype: 'checkbox', @@ -164,7 +167,8 @@ Ext.define('PartKeepr.PartEditor', { }, { xtype: 'textfield', fieldLabel: i18n("Condition"), - name: 'partCondition' + name: 'partCondition', + allowBlank: this.isOptional("partCondition"), }, { xtype: 'fieldcontainer', layout: 'hbox', @@ -174,6 +178,7 @@ Ext.define('PartKeepr.PartEditor', { labelWidth: 150, fieldLabel: i18n("Internal Part Number"), name: 'internalPartNumber', + allowBlank: this.isOptional("internalPartNumber"), flex: 1 }, { xtype: 'displayfield', @@ -297,7 +302,13 @@ Ext.define('PartKeepr.PartEditor', { */ onItemSave: function () { - var removeRecords = [], j; + var removeRecords = [], j, errors = [], + minDistributorCount = PartKeepr.getApplication().getSystemPreference( + "partkeepr.part.constraints.distributorCount", 0), + minManufacturerCount = PartKeepr.getApplication().getSystemPreference( + "partkeepr.part.constraints.manufacturerCount", 0), + minAttachmentCount = PartKeepr.getApplication().getSystemPreference( + "partkeepr.part.constraints.attachmentCount", 0); /** * Iterate through all records and check if a valid distributor @@ -314,6 +325,11 @@ Ext.define('PartKeepr.PartEditor', { this.record.distributors().remove(removeRecords); } + if (this.record.distributors().getCount() < minDistributorCount) { + errors.push( + Ext.String.format(i18n("The number of distributors must be greater than {0}"), minDistributorCount)); + } + removeRecords = []; /** @@ -350,6 +366,16 @@ Ext.define('PartKeepr.PartEditor', { this.record.manufacturers().remove(removeRecords); } + if (this.record.manufacturers().getCount() < minManufacturerCount) { + errors.push( + Ext.String.format(i18n("The number of manufacturers must be greater than {0}"), minManufacturerCount)); + } + + if (this.record.attachments().getCount() < minAttachmentCount) { + errors.push( + Ext.String.format(i18n("The number of attachments must be greater than {0}"), minAttachmentCount)); + } + // Force footprint to be "null" when the checkbox is checked. if (this.footprintNone.getValue() === true) { this.record.setFootprint(null); @@ -372,6 +398,11 @@ Ext.define('PartKeepr.PartEditor', { this.record.stockLevels().add(stockLevel); } } + + if (errors.length > 0) { + Ext.Msg.alert(i18n("Error"), errors.join("<br/>")); + return false; + } }, onEditStart: function () { @@ -453,5 +484,15 @@ Ext.define('PartKeepr.PartEditor', { } this.fireEvent("_titleChange", tmpTitle); + }, + isOptional: function (field) + { + var fields = PartKeepr.getApplication().getSystemPreference("partkeepr.part.requiredFields", []); + + if (Ext.Array.contains(fields, field)) { + return false; + } else { + return true; + } } }); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartEditorWindow.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartEditorWindow.js @@ -4,143 +4,164 @@ * <p>The PartEditorWindow encapsulates the PartKeepr.PartEditor within a window.</p> */ Ext.define('PartKeepr.PartEditorWindow', { - extend: 'Ext.window.Window', - - /* Constrain the window to fit the viewport */ - constrainHeader: true, - - /* Fit the editor within the window */ - layout: 'fit', - - /* Width and height settings */ - width: 600, - minWidth: 600, - minHeight: 415, - height: 415, - - saveText: i18n("Save"), - cancelText: i18n("Cancel"), - - /* Default edit mode. If mode = "create", we show additional fields */ - partMode: 'edit', - title: i18n("Add Part"), - - saveButtonReenableTask: null, - - /** - * Creates the part editor and put it into the window. - */ - initComponent: function () { - this.editor = Ext.create("PartKeepr.PartEditor", { - border: false, - partMode: this.partMode, - enableButtons: false - }); - - /* If the edit mode is "create", we need to enlarge the window a bit to fit the fields without scrolling */ - if (this.partMode && this.partMode == "create") { - this.height = 500; - this.minHeight = 500; - } - - this.items = [ this.editor ]; - - /** - * We need a delay, since if others are listening for "editorClose", the dialog plus the record could be destroyed - * before any following listeners have a chance to receive the record, resulting in strange problems. - */ - this.editor.on("editorClose", function (context) { this.close();}, this, { delay: 200 }); - - this.editor.on("_titleChange", function (val) { this.setTitle(val); }, this); - this.editor.on("itemSaved", this.onItemSaved, this); - - this.saveButton = Ext.create("Ext.button.Button", { - text: this.saveText, - iconCls: 'fugue-icon disk', - handler: Ext.bind(this.onItemSave, this) - }); - - this.cancelButton = Ext.create("Ext.button.Button", { - text: this.cancelText, - iconCls: 'web-icon cancel', - handler: Ext.bind(this.onCancelEdit, this) - }); - - this.bottomToolbar = Ext.create("Ext.toolbar.Toolbar", { - enableOverflow: true, - defaults: {minWidth: 100}, - dock: 'bottom', - ui: 'footer', - pack: 'start', - items: [ this.saveButton, this.cancelButton ] - }); - - this.dockedItems = [ this.bottomToolbar ]; - - this.keepOpenCheckbox = Ext.create("Ext.form.field.Checkbox", { - boxLabel: i18n("Create blank item after save") - }); - - this.createCopyCheckbox = Ext.create("Ext.form.field.Checkbox", { - boxLabel: i18n("Create Copy after save") - }); - - this.copyPartDataCheckbox = Ext.create("Ext.form.field.Checkbox", { - boxLabel: i18n("Takeover all data"), - disabled: true - }); - - if (this.partMode == "create") { - this.bottomToolbar.add(this.keepOpenCheckbox); - this.bottomToolbar.add(this.copyPartDataCheckbox); - } else { - this.bottomToolbar.add(this.createCopyCheckbox); - } - - this.keepOpenCheckbox.on("change", this.onKeepOpenCheckboxClick, this); - - this.editor.keepOpenCheckbox = this.keepOpenCheckbox; - this.editor.copyPartDataCheckbox = this.copyPartDataCheckbox; - this.editor.createCopyCheckbox = this.createCopyCheckbox; - - this.callParent(); - }, - onCancelEdit: function () { - this.editor.onCancelEdit(); - }, - /** - * Listens to the keepOpenCheckbox clicks and enables/disables the copyPartDataCheckbox - * @param value - */ - onKeepOpenCheckboxClick: function (field, value) { - if (value) { - this.copyPartDataCheckbox.enable(); - } else { - this.copyPartDataCheckbox.disable(); - } - }, - /** - * Called when the save button was clicked - */ - onItemSave: function () { - if (!this.editor.getForm().isValid()) { return; } - - // Disable the save button to indicate progress - this.saveButton.disable(); - - // Sanity: If the save process fails, re-enable the button after 30 seconds - if (this.saveButtonReenableTask === null){ - this.saveButtonReenableTask = new Ext.util.DelayedTask(function(){ this.saveButton.enable(); }, this); - this.on( 'destroy', function(){ this.saveButtonReenableTask.cancel(); }, this ); - } - this.saveButtonReenableTask.delay(30000); - - this.editor._onItemSave(); - }, - /** - * Called when the item was saved - */ - onItemSaved: function () { - this.saveButton.enable(); - } + extend: 'Ext.window.Window', + + /* Constrain the window to fit the viewport */ + constrainHeader: true, + + /* Fit the editor within the window */ + layout: 'fit', + + /* Width and height settings */ + width: 600, + minWidth: 600, + minHeight: 415, + height: 415, + + saveText: i18n("Save"), + cancelText: i18n("Cancel"), + + /* Default edit mode. If mode = "create", we show additional fields */ + partMode: 'edit', + title: i18n("Add Part"), + + saveButtonReenableTask: null, + + /** + * Creates the part editor and put it into the window. + */ + initComponent: function () + { + this.editor = Ext.create("PartKeepr.PartEditor", { + border: false, + partMode: this.partMode, + enableButtons: false + }); + + /* If the edit mode is "create", we need to enlarge the window a bit to fit the fields without scrolling */ + if (this.partMode && this.partMode == "create") { + this.height = 500; + this.minHeight = 500; + } + + this.items = [this.editor]; + + /** + * We need a delay, since if others are listening for "editorClose", the dialog plus the record could be destroyed + * before any following listeners have a chance to receive the record, resulting in strange problems. + */ + this.editor.on("editorClose", function (context) + { + this.close(); + }, this, {delay: 200}); + + this.editor.on("_titleChange", function (val) + { + this.setTitle(val); + }, this); + this.editor.on("itemSaved", this.onItemSaved, this); + + this.saveButton = Ext.create("Ext.button.Button", { + text: this.saveText, + iconCls: 'fugue-icon disk', + handler: Ext.bind(this.onItemSave, this) + }); + + this.cancelButton = Ext.create("Ext.button.Button", { + text: this.cancelText, + iconCls: 'web-icon cancel', + handler: Ext.bind(this.onCancelEdit, this) + }); + + this.bottomToolbar = Ext.create("Ext.toolbar.Toolbar", { + enableOverflow: true, + defaults: {minWidth: 100}, + dock: 'bottom', + ui: 'footer', + pack: 'start', + items: [this.saveButton, this.cancelButton] + }); + + this.dockedItems = [this.bottomToolbar]; + + this.keepOpenCheckbox = Ext.create("Ext.form.field.Checkbox", { + boxLabel: i18n("Create blank item after save") + }); + + this.createCopyCheckbox = Ext.create("Ext.form.field.Checkbox", { + boxLabel: i18n("Create Copy after save") + }); + + this.copyPartDataCheckbox = Ext.create("Ext.form.field.Checkbox", { + boxLabel: i18n("Takeover all data"), + disabled: true + }); + + if (this.partMode == "create") { + this.bottomToolbar.add(this.keepOpenCheckbox); + this.bottomToolbar.add(this.copyPartDataCheckbox); + } else { + this.bottomToolbar.add(this.createCopyCheckbox); + } + + this.keepOpenCheckbox.on("change", this.onKeepOpenCheckboxClick, this); + + this.editor.keepOpenCheckbox = this.keepOpenCheckbox; + this.editor.copyPartDataCheckbox = this.copyPartDataCheckbox; + this.editor.createCopyCheckbox = this.createCopyCheckbox; + + this.callParent(); + }, + onCancelEdit: function () + { + this.editor.onCancelEdit(); + }, + /** + * Listens to the keepOpenCheckbox clicks and enables/disables the copyPartDataCheckbox + * @param value + */ + onKeepOpenCheckboxClick: function (field, value) + { + if (value) { + this.copyPartDataCheckbox.enable(); + } else { + this.copyPartDataCheckbox.disable(); + } + }, + /** + * Called when the save button was clicked + */ + onItemSave: function () + { + if (!this.editor.getForm().isValid()) { + return; + } + + // Disable the save button to indicate progress + this.saveButton.disable(); + + // Sanity: If the save process fails, re-enable the button after 30 seconds + if (this.saveButtonReenableTask === null) { + this.saveButtonReenableTask = new Ext.util.DelayedTask(function () + { + this.saveButton.enable(); + }, this); + this.on('destroy', function () + { + this.saveButtonReenableTask.cancel(); + }, this); + } + this.saveButtonReenableTask.delay(30000); + + if (!this.editor._onItemSave()) { + this.saveButton.enable(); + } + }, + /** + * Called when the item was saved + */ + onItemSaved: function () + { + this.saveButton.enable(); + } }); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartManufacturerGrid.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/Editor/PartManufacturerGrid.js @@ -69,7 +69,7 @@ Ext.define('PartKeepr.PartManufacturerGrid', { flex: 0.4, editor: { xtype: 'textfield', - allowBlank: true + allowBlank: this.isOptional("partNumber") } } ]; @@ -109,5 +109,15 @@ Ext.define('PartKeepr.PartManufacturerGrid', { onSelectChange: function (selModel, selections) { this.deleteButton.setDisabled(selections.length === 0); + }, + isOptional: function (field) + { + var fields = PartKeepr.getApplication().getSystemPreference("partkeepr.partManufacturer.requiredFields", []); + + if (Ext.Array.contains(fields, field)) { + return false; + } else { + return true; + } } }); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartDisplay.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartDisplay.js @@ -41,6 +41,9 @@ Ext.define('PartKeepr.PartDisplay', { displayName: i18n("Needs Review"), type: 'boolean' }, + internalPartNumber: { + displayName: i18n("Internal Part Number") + }, projectNames: { displayName: i18n("Used in Projects") }, @@ -128,7 +131,7 @@ Ext.define('PartKeepr.PartDisplay', { this.infoGrid = Ext.create("Ext.grid.property.Grid", { listeners: { - 'beforeedit': function (e) + 'beforeedit': function () { return false; } @@ -170,8 +173,6 @@ Ext.define('PartKeepr.PartDisplay', { var values = {}, value; - var recordData = this.record.getData(); - for (var i in this.fieldConfigs) { value = this.record.get(i); if (value !== undefined) { @@ -241,7 +242,7 @@ Ext.define('PartKeepr.PartDisplay', { /** * Callback after the part is loaded */ - onPartLoaded: function (record) + onPartLoaded: function () { this.setValues(this.record); } diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartFilterPanel.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartFilterPanel.js @@ -23,6 +23,11 @@ Ext.define('PartKeepr.PartFilterPanel', { autoScroll: true, /** + * The applied filters + */ + appliedFilters: [], + + /** * Fixed body background color style */ bodyStyle: 'background:#DBDBDB;', @@ -54,6 +59,8 @@ Ext.define('PartKeepr.PartFilterPanel', { internalPartNumberFilter: null, commentFilter: null, + filterControls: [], + /** * Initializes the component */ @@ -132,8 +139,27 @@ Ext.define('PartKeepr.PartFilterPanel', { } ]; + this.store.getFilters().on("endupdate", this._onFilterRemove, this); this.callParent(); }, + _onFilterRemove: function () + { + var filterPlugin; + if (this.suspendRemovals) { + return; + } + + for (var i = 0; i < this.filterControls.length; i++) { + filterPlugin = this.filterControls[i].findPlugin("filter"); + + if (filterPlugin instanceof PartKeepr.Util.FilterPlugin) { + if (!this.store.getFilters().contains(filterPlugin.getFilter())) { + this.filterControls[i].disableFilter(); + } + } + } + + }, /** * Applies the parameters from the filter panel to the proxy, then * reload the store to refresh the grid. @@ -143,14 +169,26 @@ Ext.define('PartKeepr.PartFilterPanel', { */ onApply: function () { - var filters = this.getFilters(); + var i; - this.store.clearFilter(true); + this.appliedFilters = this.getFilters(); - if (filters.length !== 0) { - this.store.addFilter(this.getFilters(), true); + this.suspendRemovals = true; + if (this.appliedFilters.disableFilters.length !== 0) { + for (i = 0; i < this.appliedFilters.disableFilters.length; i++) { + this.store.removeFilter(this.appliedFilters.disableFilters[i], true); + } } + if (this.appliedFilters.enableFilters.length !== 0) { + for (i = 0; i < this.appliedFilters.enableFilters.length; i++) { + this.store.addFilter(this.appliedFilters.enableFilters[i], true); + } + } + + this.suspendRemovals = false; + + this.store.load(); }, /** @@ -200,23 +238,47 @@ Ext.define('PartKeepr.PartFilterPanel', { this.storageLocationFilter = Ext.create("PartKeepr.StorageLocationComboBox", { flex: 1, forceSelection: true, + plugins: [ + Ext.create("PartKeepr.Util.FilterPlugin", { + getFilterFn: function () + { + return { + property: 'storageLocation', + operator: "=", + value: this.storageLocationFilter.getValue() + }; + }, + listeners: { + scope: this, + disable: function () + { + this.storageLocationFilter.setValue(""); + this.storageLocationFilterCheckbox.setValue(false); + + } + }, + scope: this + }) + ], listeners: { - select: function () + select: function (cmp) { + cmp.enableFilter(); this.storageLocationFilterCheckbox.setValue(true); }, scope: this } }); + this.filterControls.push(this.storageLocationFilter); + this.storageLocationFilterCheckbox = Ext.create("Ext.form.field.Checkbox", { width: "20px", listeners: { change: function (obj, value) { - if (!value) { - this.storageLocationFilter.setValue(""); + this.storageLocationFilter.disableFilter(); } }, scope: this @@ -235,6 +297,59 @@ Ext.define('PartKeepr.PartFilterPanel', { // Create the category scope field this.categoryFilter = Ext.create("Ext.form.RadioGroup", { fieldLabel: i18n("Category Scope"), + plugins: [ + Ext.create("PartKeepr.Util.FilterPlugin", { + getFilterFn: function () + { + if (this.partManager !== null) { + if (this.categoryFilter.getValue().category === "all") { + if (this.partManager.getSelectedCategory() !== null) { + if (!this.partManager.getSelectedCategory().isRoot()) { + return { + id: 'categoryFilter', + property: 'category', + operator: 'IN', + value: this.partManager.getChildrenIds( + this.partManager.getSelectedCategory()) + }; + } + } + } else { + var selectedCategory = this.partManager.getSelectedCategory(); + + if (selectedCategory === null) { + selectedCategory = this.partManager.tree.getRootNode().firstChild; + } + + return { + id: 'categoryFilter', + property: 'category', + operator: '=', + value: selectedCategory.getId() + }; + } + } + + return {}; + }, + listeners: { + scope: this, + disable: function () + { + this.categoryFilter.setValue({category: "all"}); + + } + }, + scope: this + }) + ], + listeners: { + change: function (cmp) + { + cmp.enableFilter(); + }, + scope: this + }, columns: 1, items: [ { @@ -250,12 +365,66 @@ Ext.define('PartKeepr.PartFilterPanel', { } ] }); + + this.filterControls.push(this.categoryFilter); } + // Create the stock level filter field this.stockFilter = Ext.create("Ext.form.RadioGroup", { fieldLabel: i18n("Stock Mode"), columns: 1, + plugins: [ + Ext.create("PartKeepr.Util.FilterPlugin", { + getFilterFn: function () + { + if (this.stockFilter.getValue().stock !== "any") { + switch (this.stockFilter.getValue().stock) { + case "zero": + return { + property: 'stockLevel', + operator: "=", + value: 0 + }; + case "nonzero": + return { + property: 'stockLevel', + operator: ">", + value: 0 + }; + case "below": + return { + property: 'lowStock', + operator: "=", + value: true + }; + } + + return {}; + } + }, + listeners: { + scope: this, + disable: function () + { + this.categoryFilter.setValue({stock: "any"}); + + } + }, + scope: this + }) + ], + listeners: { + change: function (cmp) + { + if (cmp.getValue().stock === "any") { + cmp.disableFilter(); + } else { + cmp.enableFilter(); + } + }, + scope: this + }, items: [ { boxLabel: i18n("Any Stock Level"), @@ -278,23 +447,141 @@ Ext.define('PartKeepr.PartFilterPanel', { ] }); + this.filterControls.push(this.stockFilter); + this.partsWithoutPrice = Ext.create("Ext.form.field.Checkbox", { fieldLabel: i18n("Item Price"), - boxLabel: i18n("Show Parts without Price only") + boxLabel: i18n("Show Parts without Price only"), + plugins: [ + Ext.create("PartKeepr.Util.FilterPlugin", { + getFilterFn: function () + { + if (this.partsWithoutPrice.getValue() === true) { + return { + property: 'averagePrice', + operator: '=', + value: 0 + }; + } + + }, + listeners: { + scope: this, + disable: function () + { + this.partsWithoutPrice.setValue(false); + + } + }, + scope: this + }) + ], + listeners: { + change: function (cmp) + { + if (cmp.getValue()) { + cmp.enableFilter(); + + } else { + cmp.disableFilter(); + } + }, + scope: this + }, }); + this.filterControls.push(this.partsWithoutPrice); + this.distributorOrderNumberFilter = Ext.create("Ext.form.field.Text", { fieldLabel: i18n("Order Number"), - anchor: '100%' + anchor: '100%', + plugins: [ + Ext.create("PartKeepr.Util.FilterPlugin", { + getFilterFn: function () + { + return { + property: 'distributors.orderNumber', + operator: "LIKE", + value: "%" + this.distributorOrderNumberFilter.getValue() + "%" + }; + }, + listeners: { + scope: this, + disable: function () + { + this.distributorOrderNumberFilter.setValue(""); + } + }, + scope: this + }) + ], + listeners: { + change: function (cmp) + { + if (cmp.getValue() !== "") { + cmp.enableFilter(); + } else { + cmp.disableFilter(); + } + }, + scope: this + } }); + this.filterControls.push(this.distributorOrderNumberFilter); + this.manufacturerPartNumberFilter = Ext.create("Ext.form.field.Text", { fieldLabel: i18n("Manufacturer Part Number"), - anchor: '100%' + anchor: '100%', + plugins: [ + Ext.create("PartKeepr.Util.FilterPlugin", { + getFilterFn: function () + { + return { + property: 'manufacturers.partNumber', + operator: "LIKE", + value: "%" + this.manufacturerPartNumberFilter.getValue() + "%" + }; + }, + listeners: { + scope: this, + disable: function () + { + this.manufacturerPartNumberFilter.setValue(""); + } + }, + scope: this + }) + ], + listeners: { + change: function (cmp) + { + if (cmp.getValue() !== "") { + cmp.enableFilter(); + } else { + cmp.disableFilter(); + } + }, + scope: this + } }); + this.filterControls.push(this.manufacturerPartNumberFilter); + this.createDateField = Ext.create("Ext.form.field.Date", { - flex: 1 + flex: 1, + listeners: { + change: function () + { + if (this.createDateFilterSelect.getValue() !== "" && this.createDateField.getValue() !== "") { + this.createDateFilter.enableFilter(); + } else { + this.createDateFilter.disableFilter(); + } + + }, + scope: this + } }); var filter = Ext.create('Ext.data.Store', { @@ -315,28 +602,134 @@ Ext.define('PartKeepr.PartFilterPanel', { value: '', triggerAction: 'all', displayField: 'name', - valueField: 'type' + valueField: 'type', + listeners: { + select: function () + { + if (this.createDateFilterSelect.getValue() !== "" && this.createDateField.getValue() !== "") { + this.createDateFilter.enableFilter(); + } else { + this.createDateFilter.disableFilter(); + } + }, + scope: this + } }); - this.createDateFilter = { + this.createDateFilter = Ext.create({ xtype: 'fieldcontainer', anchor: '100%', fieldLabel: i18n("Create date"), layout: 'hbox', border: false, - items: [this.createDateFilterSelect, this.createDateField] - }; + items: [this.createDateFilterSelect, this.createDateField], + plugins: [ + Ext.create("PartKeepr.Util.FilterPlugin", { + getFilterFn: function () + { + return { + property: 'createDate', + operator: this.createDateFilterSelect.getValue(), + value: this.createDateField.getValue() + }; + }, + listeners: { + scope: this, + disable: function () + { + this.createDateFilterSelect.setValue(""); + this.createDateField.setValue(""); + } + }, + scope: this + }) + ] + }); + + this.filterControls.push(this.createDateFilter); this.partsWithoutStockRemovals = Ext.create("Ext.form.field.Checkbox", { fieldLabel: i18n("Stock Settings"), - boxLabel: i18n("Show Parts without stock removals only") + boxLabel: i18n("Show Parts without stock removals only"), + plugins: [ + Ext.create("PartKeepr.Util.FilterPlugin", { + getFilterFn: function () + { + return { + property: 'removals', + operator: '=', + value: false + }; + + }, + listeners: { + scope: this, + disable: function () + { + this.partsWithoutStockRemovals.setValue(false); + + } + }, + scope: this + }) + ], + listeners: { + change: function (cmp) + { + if (cmp.getValue()) { + cmp.enableFilter(); + + } else { + cmp.disableFilter(); + } + }, + scope: this + }, }); + this.filterControls.push(this.partsWithoutStockRemovals); + this.needsReview = Ext.create("Ext.form.field.Checkbox", { fieldLabel: i18n("Needs Review"), - boxLabel: i18n("Show Parts that need to reviewed only") + boxLabel: i18n("Show Parts that need to reviewed only"), + plugins: [ + Ext.create("PartKeepr.Util.FilterPlugin", { + getFilterFn: function () + { + return { + property: 'needsReview', + operator: '=', + value: true + }; + + }, + listeners: { + scope: this, + disable: function () + { + this.needsReview.setValue(false); + + } + }, + scope: this + }) + ], + listeners: { + change: function (cmp) + { + if (cmp.getValue()) { + cmp.enableFilter(); + + } else { + cmp.disableFilter(); + } + }, + scope: this + }, }); + this.filterControls.push(this.needsReview); + this.manufacturerFilterCheckbox = Ext.create("Ext.form.field.Checkbox", { width: "20px", listeners: { @@ -344,7 +737,7 @@ Ext.define('PartKeepr.PartFilterPanel', { { if (!value) { - this.manufacturerFilterCombo.setValue(""); + this.manufacturerFilterCombo.disableFilter(); } }, scope: this @@ -353,15 +746,40 @@ Ext.define('PartKeepr.PartFilterPanel', { this.manufacturerFilterCombo = Ext.create("PartKeepr.ManufacturerComboBox", { flex: 1, + plugins: [ + Ext.create("PartKeepr.Util.FilterPlugin", { + getFilterFn: function () + { + return { + property: 'manufacturers.manufacturer', + operator: "=", + value: this.manufacturerFilterCombo.getValue() + }; + }, + listeners: { + scope: this, + disable: function () + { + this.manufacturerFilterCombo.setValue(""); + this.manufacturerFilterCheckbox.setValue(false); + + } + }, + scope: this + }) + ], listeners: { - select: function () + select: function (cmp) { + cmp.enableFilter(); this.manufacturerFilterCheckbox.setValue(true); }, scope: this } }); + this.filterControls.push(this.manufacturerFilterCombo); + this.manufacturerFilter = Ext.create("Ext.form.FieldContainer", { layout: 'hbox', items: [this.manufacturerFilterCheckbox, this.manufacturerFilterCombo], @@ -374,7 +792,7 @@ Ext.define('PartKeepr.PartFilterPanel', { change: function (obj, value) { if (!value) { - this.distributorFilterCombo.setValue(""); + this.distributorFilterCombo.disableFilter(); } }, scope: this @@ -383,9 +801,32 @@ Ext.define('PartKeepr.PartFilterPanel', { this.distributorFilterCombo = Ext.create("PartKeepr.DistributorComboBox", { flex: 1, + plugins: [ + Ext.create("PartKeepr.Util.FilterPlugin", { + getFilterFn: function () + { + return { + property: 'distributors.distributor', + operator: "=", + value: this.distributorFilterCombo.getValue() + }; + }, + listeners: { + scope: this, + disable: function () + { + this.distributorFilterCombo.setValue(""); + this.distributorFilterCheckbox.setValue(false); + + } + }, + scope: this + }) + ], listeners: { - select: function () + select: function (cmp) { + cmp.enableFilter(); this.distributorFilterCheckbox.setValue(true); }, scope: this @@ -395,16 +836,19 @@ Ext.define('PartKeepr.PartFilterPanel', { this.distributorFilter = Ext.create("Ext.form.FieldContainer", { layout: 'hbox', items: [this.distributorFilterCheckbox, this.distributorFilterCombo], - fieldLabel: i18n("Distributor") + fieldLabel: i18n("Distributor"), + }); + this.filterControls.push(this.distributorFilterCombo); + this.footprintFilterCheckbox = Ext.create("Ext.form.field.Checkbox", { width: "20px", listeners: { change: function (obj, value) { if (!value) { - this.footprintFilterCombo.setValue(""); + this.footprintFilterCombo.disableFilter(); } }, scope: this @@ -413,15 +857,40 @@ Ext.define('PartKeepr.PartFilterPanel', { this.footprintFilterCombo = Ext.create("PartKeepr.FootprintComboBox", { flex: 1, + plugins: [ + Ext.create("PartKeepr.Util.FilterPlugin", { + getFilterFn: function () + { + return { + property: 'footprint', + operator: "=", + value: this.footprintFilterCombo.getValue() + }; + }, + listeners: { + scope: this, + disable: function () + { + this.footprintFilterCombo.setValue(""); + this.footprintFilterCheckbox.setValue(false); + + } + }, + scope: this + }) + ], listeners: { - select: function () + select: function (cmp) { + cmp.enableFilter(); this.footprintFilterCheckbox.setValue(true); }, scope: this } }); + this.filterControls.push(this.footprintFilterCombo); + this.footprintFilter = Ext.create("Ext.form.FieldContainer", { layout: 'hbox', items: [this.footprintFilterCheckbox, this.footprintFilterCombo], @@ -432,24 +901,155 @@ Ext.define('PartKeepr.PartFilterPanel', { this.statusFilter = Ext.create("Ext.form.field.Text", { fieldLabel: i18n("Status"), - anchor: '100%' + anchor: '100%', + plugins: [ + Ext.create("PartKeepr.Util.FilterPlugin", { + getFilterFn: function () + { + return { + property: 'status', + operator: "LIKE", + value: "%" + this.statusFilter.getValue() + "%" + }; + }, + listeners: { + scope: this, + disable: function () + { + this.statusFilter.setValue(""); + } + }, + scope: this + }) + ], + listeners: { + change: function (cmp) + { + if (cmp.getValue() !== "") { + cmp.enableFilter(); + } else { + cmp.disableFilter(); + } + }, + scope: this + } }); + this.filterControls.push(this.statusFilter); + this.conditionFilter = Ext.create("Ext.form.field.Text", { fieldLabel: i18n("Condition"), - anchor: '100%' + anchor: '100%', + plugins: [ + Ext.create("PartKeepr.Util.FilterPlugin", { + getFilterFn: function () + { + return { + property: 'partCondition', + operator: "LIKE", + value: "%" + this.conditionFilter.getValue() + "%" + }; + }, + listeners: { + scope: this, + disable: function () + { + this.conditionFilter.setValue(""); + } + }, + scope: this + }) + ], + listeners: { + change: function (cmp) + { + if (cmp.getValue() !== "") { + cmp.enableFilter(); + } else { + cmp.disableFilter(); + } + }, + scope: this + } }); + this.filterControls.push(this.conditionFilter); + this.internalPartNumberFilter = Ext.create("Ext.form.field.Text", { fieldLabel: i18n("Internal Part Number"), - anchor: '100%' + anchor: '100%', + plugins: [ + Ext.create("PartKeepr.Util.FilterPlugin", { + getFilterFn: function () + { + return { + property: 'internalPartNumber', + operator: "LIKE", + value: "%" + this.internalPartNumberFilter.getValue() + "%" + }; + }, + listeners: { + scope: this, + disable: function () + { + this.internalPartNumberFilter.setValue(""); + } + }, + scope: this + }) + ], + listeners: { + change: function (cmp) + { + if (cmp.getValue() !== "") { + cmp.enableFilter(); + } else { + cmp.disableFilter(); + } + }, + scope: this + } }); + this.filterControls.push(this.internalPartNumberFilter); + this.commentFilter = Ext.create("Ext.form.field.Text", { fieldLabel: i18n("Comment"), - anchor: '100%' + anchor: '100%', + plugins: [ + Ext.create("PartKeepr.Util.FilterPlugin", { + getFilterFn: function () + { + return { + property: 'comment', + operator: "LIKE", + value: "%" + this.commentFilter.getValue() + "%" + }; + }, + listeners: { + scope: this, + disable: function () + { + this.commentFilter.setValue(""); + } + }, + scope: this + }) + ], + listeners: { + change: function (cmp) + { + if (cmp.getValue() !== "") { + cmp.enableFilter(); + } else { + cmp.disableFilter(); + } + }, + scope: this + } }); + this.filterControls.push(this.commentFilter); }, /** * Applies the filter parameters to the passed extraParams object. @@ -457,166 +1057,25 @@ Ext.define('PartKeepr.PartFilterPanel', { */ getFilters: function () { - var filters = []; - - if (this.storageLocationFilterCheckbox.getValue() === true) { - filters.push(Ext.create("Ext.util.Filter", { - property: 'storageLocation', - operator: "=", - value: this.storageLocationFilter.getValue() - })); - } - - if (this.partManager !== null) { - if (this.categoryFilter.getValue().category === "all") { - if (this.partManager.getSelectedCategory() !== null) { - filters.push(Ext.create("Ext.util.Filter", { - id: 'categoryFilter', - property: 'category', - operator: 'IN', - value: this.partManager.getChildrenIds(this.partManager.getSelectedCategory()) - })); + var enableFilters = [], + disableFilters = [], + filterPlugin; + + for (var i = 0; i < this.filterControls.length; i++) { + filterPlugin = this.filterControls[i].findPlugin("filter"); + + if (filterPlugin instanceof PartKeepr.Util.FilterPlugin) { + if (filterPlugin.isEnabled()) { + enableFilters.push(this.filterControls[i].findPlugin("filter").getFilter()); + } else { + disableFilters.push(this.filterControls[i].findPlugin("filter").getFilter()); } - } else { - filters.push(Ext.create("Ext.util.Filter", { - id: 'categoryFilter', - property: 'category', - operator: '=', - value: this.partManager.getSelectedCategory() - })); } } - if (this.partsWithoutPrice.getValue() === true) { - filters.push(Ext.create("Ext.util.Filter", { - property: 'averagePrice', - operator: '=', - value: 0 - })); - } - - if (this.createDateFilterSelect.getValue() !== "") { - filters.push(Ext.create("Ext.util.Filter", { - property: 'createDate', - operator: this.createDateFilterSelect.getValue(), - value: this.createDateField.getValue() - })); - } - - if (this.partsWithoutStockRemovals.getValue() === true) { - filters.push(Ext.create("Ext.util.Filter", { - property: 'removals', - operator: "=", - value: false - })); - } - - if (this.needsReview.getValue() === true) { - filters.push(Ext.create("Ext.util.Filter", { - property: 'needsReview', - operator: "=", - value: true - })); - } - - - if (this.stockFilter.getValue().stock !== "any") { - switch (this.stockFilter.getValue().stock) { - case "zero": - filters.push(Ext.create("Ext.util.Filter", { - property: 'stockLevel', - operator: "=", - value: 0 - })); - break; - case "nonzero": - filters.push(Ext.create("Ext.util.Filter", { - property: 'stockLevel', - operator: ">", - value: 0 - })); - break; - case "below": - filters.push(Ext.create("Ext.util.Filter", { - property: 'lowStock', - operator: "=", - value: true - })); - break; - } - } - - if (this.distributorOrderNumberFilter.getValue() !== "") { - filters.push(Ext.create("Ext.util.Filter", { - property: 'distributors.orderNumber', - operator: "LIKE", - value: "%" + this.distributorOrderNumberFilter.getValue() + "%" - })); - } - - if (this.manufacturerPartNumberFilter.getValue() !== "") { - filters.push(Ext.create("Ext.util.Filter", { - property: 'manufacturers.partNumber', - operator: "LIKE", - value: "%" + this.manufacturerPartNumberFilter.getValue() + "%" - })); - } - - if (this.distributorFilterCheckbox.getValue() === true) { - filters.push(Ext.create("Ext.util.Filter", { - property: 'distributors.distributor', - operator: "=", - value: this.distributorFilterCombo.getValue() - })); - } - - if (this.manufacturerFilterCheckbox.getValue() === true) { - filters.push(Ext.create("Ext.util.Filter", { - property: 'manufacturers.manufacturer', - operator: "=", - value: this.manufacturerFilterCombo.getValue() - })); - } - - if (this.footprintFilterCheckbox.getValue() === true) { - filters.push(Ext.create("Ext.util.Filter", { - property: 'footprint', - operator: "=", - value: this.footprintFilterCombo.getValue() - })); - } - - if (this.statusFilter.getValue() !== "") { - filters.push(Ext.create("Ext.util.Filter", { - property: 'status', - operator: "LIKE", - value: "%" + this.statusFilter.getValue() + "%" - })); - } - - if (this.conditionFilter.getValue() !== "") { - filters.push(Ext.create("Ext.util.Filter", { - property: 'condition', - operator: "LIKE", - value: "%" + this.conditionFilter.getValue() + "%" - })); - } - - if (this.internalPartNumberFilter.getValue() !== "") { - filters.push(Ext.create("Ext.util.Filter", { - property: 'internalPartNumber', - operator: "LIKE", - value: "%" + this.internalPartNumberFilter.getValue() + "%" - })); - } - - if (this.commentFilter.getValue() !== "") { - filters.push(Ext.create("Ext.util.Filter", { - property: 'comment', - operator: "LIKE", - value: "%" + this.commentFilter.getValue() + "%" - })); - } - return filters; + return { + disableFilters: disableFilters, + enableFilters: enableFilters + }; } }); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartStockHistory.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartStockHistory.js @@ -15,7 +15,7 @@ Ext.define('PartKeepr.PartStockHistory', { */ onActivate: function () { - var filter = Ext.create("Ext.util.Filter", { + var filter = Ext.create("PartKeepr.util.Filter", { property: 'part', operator: '=', value: this.part diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartsManager.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartsManager.js @@ -267,7 +267,7 @@ Ext.define('PartKeepr.PartManager', { onCategoryClick: function (tree, record) { this.selectedCategory = record; - var filter = Ext.create("Ext.util.Filter", { + var filter = Ext.create("PartKeepr.util.Filter", { id: 'categoryFilter', property: 'category', operator: 'IN', @@ -417,14 +417,16 @@ Ext.define('PartKeepr.PartManager', { /** * Creates a new, empty part editor window */ - onItemAdd: function () { + onItemAdd: function (defaults) { var j = Ext.create("PartKeepr.PartEditorWindow", { partMode: 'create' }); var defaultPartUnit = PartKeepr.getApplication().getPartUnitStore().findRecord("default", true); - var record = Ext.create("PartKeepr.PartBundle.Entity.Part"); + Ext.apply(defaults, {}); + + var record = Ext.create("PartKeepr.PartBundle.Entity.Part", defaults); if (this.getSelectedCategory() !== null) { record.setCategory(this.getSelectedCategory()); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Project/ProjectReport.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Project/ProjectReport.js @@ -254,7 +254,7 @@ Ext.define('PartKeepr.ProjectReportView', { filterIds.push(distributors.getAt(i).getDistributor().getId()); } - var filter = Ext.create("Ext.util.Filter", { + var filter = Ext.create("PartKeepr.util.Filter", { property: "@id", operator: 'in', value: filterIds @@ -316,32 +316,33 @@ Ext.define('PartKeepr.ProjectReportView', { var partCount = this.reportResult.store.count(); var cheapestDistributor, activeDistributor; var lowestPrice; - var firstPositive; + var firstPositive; var activeRecord; + var currentPrice; for (var i = 0; i < partCount; i++) { activeRecord = this.reportResult.store.getAt(i); - firstPositive = true; - lowestPrice = 0; - cheapestDistributor = null; + firstPositive = true; + lowestPrice = 0; + cheapestDistributor = null; for (var j = 0; j < activeRecord.getPart().distributors().count(); j++) { activeDistributor = activeRecord.getPart().distributors().getAt(j); - currentPrice = parseFloat(activeDistributor.get("price")); - - if (currentPrice != 0) { - if (firstPositive) { - lowestPrice = currentPrice; - cheapestDistributor = activeDistributor; - firstPositive = false; - } - else { - if (currentPrice < lowestPrice) { - lowestPrice = currentPrice; - cheapestDistributor = activeDistributor; - } - } - } + currentPrice = parseFloat(activeDistributor.get("price")); + + if (currentPrice != 0) { + if (firstPositive) { + lowestPrice = currentPrice; + cheapestDistributor = activeDistributor; + firstPositive = false; + } + else { + if (currentPrice < lowestPrice) { + lowestPrice = currentPrice; + cheapestDistributor = activeDistributor; + } + } + } } if (cheapestDistributor !== null) { diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationEditor.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationEditor.js @@ -115,7 +115,7 @@ Ext.define('PartKeepr.StorageLocationEditor', { { if (!this.record.phantom) { this.down('#containedParts').setVisible(true); - var filter = Ext.create("Ext.util.Filter", { + var filter = Ext.create("PartKeepr.util.Filter", { property: "storageLocation", operator: "=", value: this.record.getId() diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationNavigation.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationNavigation.js @@ -19,7 +19,8 @@ Ext.define("PartKeepr.StorageLocationNavigation", { xtype: 'partkeepr.StorageLocationGrid', resizable: true, split: true, - titleProperty: "name" + titleProperty: "name", + store: this.store }; if (this.verticalLayout) { @@ -56,7 +57,6 @@ Ext.define("PartKeepr.StorageLocationNavigation", { this.callParent(arguments); this.getTree().on("itemclick", this.onCategoryClick, this); - this.getGrid().setStore(this.store); this.getGrid().on("storageLocationMultiAdd", this.onMultiAddStorageLocation, this); @@ -94,7 +94,7 @@ Ext.define("PartKeepr.StorageLocationNavigation", { this.setCategoryFilter(record); }, setCategoryFilter: function (record) { - var filter = Ext.create("Ext.util.Filter", { + var filter = Ext.create("PartKeepr.util.Filter", { property: 'category', operator: 'IN', value: this.getChildrenIds(record) diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Panel.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Panel.js @@ -3,9 +3,14 @@ Ext.define('PartKeepr.Components.SystemPreferences.Panel', { title: i18n("System Preferences"), layout: 'border', - initComponent: function () { + initComponent: function () + { var settings = [ - "PartKeepr.Components.SystemPreferences.Preferences.FulltextSearch" + "PartKeepr.Components.SystemPreferences.Preferences.FulltextSearch", + "PartKeepr.Components.SystemPreferences.Preferences.RequiredPartFields", + "PartKeepr.Components.SystemPreferences.Preferences.RequiredPartManufacturerFields", + "PartKeepr.Components.SystemPreferences.Preferences.RequiredPartDistributorFields", + "PartKeepr.Components.SystemPreferences.Preferences.BarcodeScannerConfiguration" ]; var settingItems = [], item; @@ -22,7 +27,7 @@ Ext.define('PartKeepr.Components.SystemPreferences.Panel', { for (var i = 0; i < settings; i++) { item = Ext.create(settings[i]); - settingItems.push(item) + settingItems.push(item); } this.navigation = Ext.create("PartKeepr.Components.SystemPreferences.Tree", @@ -32,9 +37,10 @@ Ext.define('PartKeepr.Components.SystemPreferences.Panel', { width: 200 }); - this.navigation.on("itemclick", function (record, item) { + this.navigation.on("itemclick", function (record, item) + { if (typeof item.data.target === "function") { - this.openSettingsItem(item.data.target["$className"]); + this.openSettingsItem(item.data.target.$className); } }, this); @@ -51,8 +57,9 @@ Ext.define('PartKeepr.Components.SystemPreferences.Panel', { this.callParent(); }, - openSettingsItem: function (target) { - targetClass = Ext.ClassManager.get(target); + openSettingsItem: function (target) + { + var targetClass = Ext.ClassManager.get(target); var config = { title: targetClass.title, @@ -60,10 +67,17 @@ Ext.define('PartKeepr.Components.SystemPreferences.Panel', { iconCls: targetClass.iconCls }; - var j = Ext.create(target, config); + for (var i = 0; i < this.cards.items.length; i++) { + if (this.cards.items.getAt(i).$className === targetClass.$className) { + this.cards.setActiveItem(this.cards.items.getAt(i)); + return; + } + } + var j = Ext.create(target, config); this.cards.items.add(j); this.cards.setActiveItem(j); + }, statics: { iconCls: 'fugue-icon gear', @@ -72,4 +86,4 @@ Ext.define('PartKeepr.Components.SystemPreferences.Panel', { menuPath: [{text: i18n("System")}] } -});- \ No newline at end of file +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/PreferenceEditor.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/PreferenceEditor.js @@ -6,6 +6,8 @@ Ext.define('PartKeepr.Components.SystemPreferences.PreferenceEditor', { cancelText: i18n("Cancel"), layout: 'anchor', change: false, + border: false, + scrollable: true, autoScroll: true, defaults: { anchor: '100%', @@ -14,7 +16,8 @@ Ext.define('PartKeepr.Components.SystemPreferences.PreferenceEditor', { enableButtons: true, titleProperty: 'name', - initComponent: function () { + initComponent: function () + { if (this.enableButtons) { this.saveButton = Ext.create("Ext.button.Button", { text: this.saveText, @@ -44,10 +47,12 @@ Ext.define('PartKeepr.Components.SystemPreferences.PreferenceEditor', { this.callParent(); }, - onCancelEdit: function () { + onCancelEdit: function () + { this.fireEvent("editorClose", this); }, - onSave: function () { - console.log("You need to override PreferenceEditor::onSave") + onSave: function () + { + console.log("You need to override PreferenceEditor::onSave"); } }); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/BarcodeScannerConfiguration.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/BarcodeScannerConfiguration.js @@ -0,0 +1,264 @@ +Ext.define('PartKeepr.Components.SystemPreferences.Preferences.BarcodeScannerConfiguration', { + extend: 'PartKeepr.Components.SystemPreferences.PreferenceEditor', + + initComponent: function () + { + var modifierItems = [ + { + xtype: 'checkbox', + boxLabel: "Ctrl", + itemId: 'barcodeScannerModifierCtrl', + }, + { + xtype: 'checkbox', + boxLabel: "Shift", + itemId: 'barcodeScannerModifierShift' + }, + { + xtype: 'checkbox', + boxLabel: "Alt", + itemId: 'barcodeScannerModifierAlt' + } + ]; + + this.barcodeScannerActionsStore = Ext.create("Ext.data.Store", { + fields: ["code", "action", "configuration"], + data: [] + }); + + this.deleteButton = Ext.create("Ext.button.Button", { + text: i18n("Delete"), + tooltip: i18n("Delete"), + iconCls: "web-icon delete", + handler: Ext.bind(function () + { + this.barcodeScannerActionsStore.remove( + this.barcodeScannerActionsGrid.getSelectionModel().getSelection()); + }, this), + disabled: true + }); + + this.addButton = Ext.create("Ext.button.Button", { + text: i18n("Add"), + tooltip: i18n("Add"), + iconCls: "web-icon add", + handler: Ext.bind(function () + { + var rec = this.barcodeScannerActionsStore.add({}); + + this.editing.startEdit(rec[0], 0); + }, this) + }); + + this.topToolbar = Ext.create("Ext.toolbar.Toolbar", { + dock: 'top', + enableOverflow: true, + items: [this.addButton, this.deleteButton] + }); + + this.editing = Ext.create('Ext.grid.plugin.RowEditing', { + clicksToEdit: 1, + + }); + + this.barcodeScannerActionsGrid = Ext.create({ + height: 200, + plugins: [this.editing], + xtype: 'grid', + dockedItems: [this.topToolbar], + itemId: 'barcodeScannerActionsGrid', + store: this.barcodeScannerActionsStore, + columns: [ + { + text: i18n("Code"), dataIndex: 'code', flex: 1, + editor: { + xtype: 'textfield' + } + }, + { + text: i18n("Action"), dataIndex: 'action', flex: 1, + renderer: function (v) + { + if (v instanceof Ext.data.Model) { + return v.get("name"); + } else { + return i18n("No action selected"); + } + }, + editor: { + xtype: 'barcodescannerActions', + } + }, { + text: i18n("Description"), + flex: 3, + renderer: function (v, m, record) + { + if (record.get("action") instanceof Ext.data.Model) { + return record.get("action").get("description"); + } else { + return ""; + } + + } + }, + { + xtype: 'actioncolumn', + items: [ + { + iconCls: 'fugue-icon pencil', + tooltip: i18n("Configure"), + handler: function (view, rowIndex, colIndex, item, e, record) + { + var config = record.get("configuration"); + + if (typeof config === "undefined") { + config = {}; + record.set("configuration", config); + } + + if (record.get("action") instanceof Ext.data.Model) { + + + Ext.ClassManager.get(record.get("action").get("action")).configure(config); + } + } + } + ] + } + ], + }); + + this.barcodeScannerActionsGrid.getSelectionModel().on("select", this._onItemSelect, this); + this.barcodeScannerActionsGrid.getSelectionModel().on("deselect", this._onItemDeselect, this); + + this.items = [ + { + fieldLabel: i18n("Modifier"), + xtype: 'checkboxgroup', + layout: { + type: 'vbox', + align: 'left' + }, + items: modifierItems + }, + { + fieldLabel: i18n("Key"), + xtype: 'textfield', + itemId: 'barcodeScannerKey', + minLength: 1, + maxLength: 1, + enforceMaxLength: true + }, { + fieldLabel: i18n("Timeout (ms)"), + xtype: 'numberfield', + minValue: 100, + maxValue: 3000, + itemId: 'barcodeScannerTimeout' + + }, { + boxLabel: i18n("Use enter/return to indicate scan input end"), + xtype: 'checkbox', + hideEmptyLabel: false, + itemId: 'barcodeScannerEnter' + }, { + xtype: 'fieldcontainer', + fieldLabel: i18n("Actions"), + items: [this.barcodeScannerActionsGrid] + } + ]; + + this.callParent(arguments); + + this.down("#barcodeScannerKey").setValue( + PartKeepr.getApplication().getSystemPreference("partkeepr.barcodeScanner.key", "")); + this.down("#barcodeScannerModifierAlt").setValue( + PartKeepr.getApplication().getSystemPreference("partkeepr.barcodeScanner.modifierAlt", false)); + this.down("#barcodeScannerModifierShift").setValue( + PartKeepr.getApplication().getSystemPreference("partkeepr.barcodeScanner.modifierShift", false)); + this.down("#barcodeScannerModifierCtrl").setValue( + PartKeepr.getApplication().getSystemPreference("partkeepr.barcodeScanner.modifierCtrl", false)); + this.down("#barcodeScannerEnter").setValue( + PartKeepr.getApplication().getSystemPreference("partkeepr.barcodeScanner.enter", true)); + this.down("#barcodeScannerTimeout").setValue( + PartKeepr.getApplication().getSystemPreference("partkeepr.barcodeScanner.timeout", 500)); + + var actions = PartKeepr.getApplication().getSystemPreference("partkeepr.barcodeScanner.actions", []); + var actionStore = Ext.create("PartKeepr.Data.store.BarcodeScannerActionsStore"); + + for (var i = 0; i < actions.length; i++) { + var item = actions[i]; + + this.barcodeScannerActionsStore.add({ + code: item.code, + action: actionStore.findRecord("action", item.action), + configuration: item.config + }); + } + }, + onSave: function () + { + PartKeepr.getApplication().setSystemPreference("partkeepr.barcodeScanner.key", + this.down("#barcodeScannerKey").getValue()); + PartKeepr.getApplication().setSystemPreference("partkeepr.barcodeScanner.modifierAlt", + this.down("#barcodeScannerModifierAlt").getValue()); + PartKeepr.getApplication().setSystemPreference("partkeepr.barcodeScanner.modifierShift", + this.down("#barcodeScannerModifierShift").getValue()); + PartKeepr.getApplication().setSystemPreference("partkeepr.barcodeScanner.modifierCtrl", + this.down("#barcodeScannerModifierCtrl").getValue()); + PartKeepr.getApplication().setSystemPreference("partkeepr.barcodeScanner.enter", + this.down("#barcodeScannerEnter").getValue()); + PartKeepr.getApplication().setSystemPreference("partkeepr.barcodeScanner.timeout", + this.down("#barcodeScannerTimeout").getValue()); + + var data = this.down("#barcodeScannerActionsGrid").getStore().getData(); + var actions = []; + + for (var i = 0; i < data.length; i++) { + var item = data.getAt(i); + + actions.push({ + code: item.get("code"), + action: item.get("action").get("action"), + config: item.get("configuration") + }); + } + + PartKeepr.getApplication().setSystemPreference("partkeepr.barcodeScanner.actions", actions); + + PartKeepr.getApplication().getBarcodeScannerManager().registerBarcodeScannerHotkey(); + + }, + /** + * Called when an item was selected + */ + _onItemSelect: function (selectionModel, record) + { + this._updateDeleteButton(selectionModel, record); + this.fireEvent("itemSelect", record); + }, + /** + * Called when an item was deselected + */ + _onItemDeselect: function (selectionModel, record) + { + this._updateDeleteButton(selectionModel, record); + this.fireEvent("itemDeselect", record); + }, + /** + * Called when an item was selected. Enables/disables the delete button. + */ + _updateDeleteButton: function () + { + /* Right now, we support delete on a single record only */ + if (this.barcodeScannerActionsGrid.getSelectionModel().getCount() == 1) { + this.deleteButton.enable(); + } else { + this.deleteButton.disable(); + } + }, + statics: { + iconCls: 'fugue-icon barcode', + title: i18n('Barcode Scanner Configuration'), + menuPath: [] + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/FulltextSearch.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/FulltextSearch.js @@ -1,17 +1,14 @@ Ext.define('PartKeepr.Components.SystemPreferences.Preferences.FulltextSearch', { extend: 'PartKeepr.Components.SystemPreferences.PreferenceEditor', - scrollable: true, - layout: 'anchor', - border: false, - defaults: { - anchor: '100%' - }, - initComponent: function () { + + initComponent: function () + { this.fieldSelector = Ext.create("PartKeepr.Components.Widgets.FieldSelector", { height: 300, sourceModel: PartKeepr.PartBundle.Entity.Part, - initiallyChecked: PartKeepr.getApplication().getSystemPreference("partkeepr.part.search.fields", ["name", "description", "comment", "internalPartNumber"]) + initiallyChecked: PartKeepr.getApplication().getSystemPreference("partkeepr.part.search.fields", + ["name", "description", "comment", "internalPartNumber"]) }); this.items = [ @@ -40,11 +37,14 @@ Ext.define('PartKeepr.Components.SystemPreferences.Preferences.FulltextSearch', { xtype: "fieldcontainer", fieldLabel: i18n("Search Fields"), - items: [{ - style: "padding-top: 4px; padding-bottom: 5px;", - html: i18n("Select all fields which are searched when entering a search term in the upper-right search field within the part manager"), - border: false - }, this.fieldSelector] + items: [ + { + style: "padding-top: 4px; padding-bottom: 5px;", + html: i18n( + "Select all fields which are searched when entering a search term in the upper-right search field within the part manager"), + border: false + }, this.fieldSelector + ] } ]; @@ -58,7 +58,8 @@ Ext.define('PartKeepr.Components.SystemPreferences.Preferences.FulltextSearch', this.down("#searchModeSplit").setValue(false); } }, - onSave: function () { + onSave: function () + { var selection = this.fieldSelector.getChecked(); var fields = []; diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/RequiredPartDistributorFields.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/RequiredPartDistributorFields.js @@ -0,0 +1,51 @@ +Ext.define('PartKeepr.Components.SystemPreferences.Preferences.RequiredPartDistributorFields', { + extend: 'PartKeepr.Components.SystemPreferences.PreferenceEditor', + + initComponent: function () + { + + this.fieldSelector = Ext.create("PartKeepr.Components.Widgets.FieldSelector", { + height: 300, + sourceModel: PartKeepr.PartBundle.Entity.PartDistributor, + recurseSubModels: false, + excludeFields: [ + "@id" + ], + initiallyChecked: PartKeepr.getApplication().getSystemPreference("partkeepr.partDistributor.requiredFields", + []) + }); + + this.items = [ + { + xtype: "fieldcontainer", + fieldLabel: i18n("Required Fields"), + items: [ + { + border: false, + html: "The field <strong>Distributor</strong> is always required.", + style: "padding-top: 4px; padding-bottom: 5px;", + }, + this.fieldSelector + ] + } + ]; + + this.callParent(arguments); + }, + onSave: function () + { + var selection = this.fieldSelector.getChecked(); + var fields = []; + + for (var i = 0; i < selection.length; i++) { + fields.push(selection[i].data.data); + } + + PartKeepr.getApplication().setSystemPreference("partkeepr.partDistributor.requiredFields", fields); + }, + statics: { + iconCls: 'fugue-icon block--plus', + title: i18n('Part Distributor'), + menuPath: [{text: i18n("Required Fields")}] + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/RequiredPartFields.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/RequiredPartFields.js @@ -0,0 +1,97 @@ +Ext.define('PartKeepr.Components.SystemPreferences.Preferences.RequiredPartFields', { + extend: 'PartKeepr.Components.SystemPreferences.PreferenceEditor', + + initComponent: function () + { + + this.fieldSelector = Ext.create("PartKeepr.Components.Widgets.FieldSelector", { + height: 150, + sourceModel: PartKeepr.PartBundle.Entity.Part, + recurseSubModels: false, + excludeFields: [ + "@id", + "name", + "needsReview", + "stockLevel", + "averagePrice", + "createDate", + "removals", + "lowStock", + "minStockLevel" + ], + initiallyChecked: PartKeepr.getApplication().getSystemPreference("partkeepr.part.requiredFields", []) + }); + + this.items = [ + { + xtype: "fieldcontainer", + fieldLabel: i18n("Required Fields"), + items: [ + { + border: false, + html: "The fields <strong>Name</strong>, <strong>Category</strong> and <strong>Storage Location</strong> are always required.", + style: "padding-top: 4px; padding-bottom: 5px;", + }, + this.fieldSelector + ] + }, + { + xtype: 'fieldcontainer', + fieldLabel: i18n("Minimum Numbers"), + items: [ + { + fieldLabel: i18n("Distributors"), + xtype: 'numberfield', + minValue: 0, + id: 'requirePartDistributorsAmount' + }, + { + fieldLabel: i18n("Manufacturers"), + xtype: 'numberfield', + minValue: 0, + id: 'requirePartManufacturersAmount' + }, + { + fieldLabel: i18n("Attachments"), + xtype: 'numberfield', + minValue: 0, + id: 'requirePartAttachmentsAmount' + } + ] + } + + ]; + + this.callParent(arguments); + + this.down("#requirePartDistributorsAmount").setValue( + PartKeepr.getApplication().getSystemPreference("partkeepr.part.constraints.distributorCount", 0)); + this.down("#requirePartManufacturersAmount").setValue( + PartKeepr.getApplication().getSystemPreference("partkeepr.part.constraints.manufacturerCount", 0)); + this.down("#requirePartAttachmentsAmount").setValue( + PartKeepr.getApplication().getSystemPreference("partkeepr.part.constraints.attachmentCount", 0)); + + }, + onSave: function () + { + var selection = this.fieldSelector.getChecked(); + var fields = []; + + for (var i = 0; i < selection.length; i++) { + fields.push(selection[i].data.data); + } + + PartKeepr.getApplication().setSystemPreference("partkeepr.part.requiredFields", fields); + PartKeepr.getApplication().setSystemPreference("partkeepr.part.constraints.distributorCount", + this.down("#requirePartDistributorsAmount").getValue()); + PartKeepr.getApplication().setSystemPreference("partkeepr.part.constraints.manufacturerCount", + this.down("#requirePartManufacturersAmount").getValue()); + PartKeepr.getApplication().setSystemPreference("partkeepr.part.constraints.attachmentCount", + this.down("#requirePartAttachmentsAmount").getValue()); + }, + statics: { + iconCls: 'fugue-icon block--plus', + title: i18n('Part'), + menuPath: [{text: i18n("Required Fields")}] + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/RequiredPartManufacturerFields.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/RequiredPartManufacturerFields.js @@ -0,0 +1,51 @@ +Ext.define('PartKeepr.Components.SystemPreferences.Preferences.RequiredPartManufacturerFields', { + extend: 'PartKeepr.Components.SystemPreferences.PreferenceEditor', + + initComponent: function () + { + + this.fieldSelector = Ext.create("PartKeepr.Components.Widgets.FieldSelector", { + height: 300, + sourceModel: PartKeepr.PartBundle.Entity.PartManufacturer, + recurseSubModels: false, + excludeFields: [ + "@id" + ], + initiallyChecked: PartKeepr.getApplication().getSystemPreference( + "partkeepr.partManufacturer.requiredFields", []) + }); + + this.items = [ + { + xtype: "fieldcontainer", + fieldLabel: i18n("Required Fields"), + items: [ + { + border: false, + html: "The field <strong>Manufacturer</strong> is always required.", + style: "padding-top: 4px; padding-bottom: 5px;", + }, + this.fieldSelector + ] + } + ]; + + this.callParent(arguments); + }, + onSave: function () + { + var selection = this.fieldSelector.getChecked(); + var fields = []; + + for (var i = 0; i < selection.length; i++) { + fields.push(selection[i].data.data); + } + + PartKeepr.getApplication().setSystemPreference("partkeepr.partManufacturer.requiredFields", fields); + }, + statics: { + iconCls: 'fugue-icon block--plus', + title: i18n('Part Manufacturer'), + menuPath: [{text: i18n("Required Fields")}] + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/User/UserGrid.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/User/UserGrid.js @@ -60,7 +60,7 @@ Ext.define('PartKeepr.UserGrid', { items: this.providerCombo }); - this.filter = Ext.create("Ext.util.Filter", { + this.filter = Ext.create("PartKeepr.util.Filter", { property: "provider", operator: "=", value: "" diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FieldSelector.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FieldSelector.js @@ -1,5 +1,6 @@ Ext.define('PartKeepr.Components.Widgets.FieldSelector', { extend: 'Ext.tree.Panel', + xtype: 'modelFieldSelector', store: { folderSort: true, sorters: [ @@ -14,6 +15,10 @@ Ext.define('PartKeepr.Components.Widgets.FieldSelector', { selModel: { mode: 'MULTI' }, + + /** + * @var {Array} Contains the nodes which are checked by default + */ initiallyChecked: [], /** @@ -21,8 +26,20 @@ Ext.define('PartKeepr.Components.Widgets.FieldSelector', { */ visitedModels: [], - initComponent: function () { + /** + * @var {Boolean} True to recurse into associations, false otherwise. + */ + recurseSubModels: true, + + /** + * @var {Array} An array which excludes the fields listed + */ + excludeFields: [], + + initComponent: function () + { this.callParent(arguments); + this.visitedModels = []; var rootNode = this.getRootNode(); rootNode.set("text", this.sourceModel.getName()); @@ -51,26 +68,30 @@ Ext.define('PartKeepr.Components.Widgets.FieldSelector', { checked = true; } - node.appendChild({ - text: fields[i].name, - leaf: true, - checked: checked, - data: prefix + fields[i].name - }); + if (!Ext.Array.contains(this.excludeFields, prefix + fields[i].name)) { + node.appendChild({ + text: fields[i].name, + leaf: true, + checked: checked, + data: prefix + fields[i].name + }); + } } else { - for (var j = 0; j < this.visitedModels.length; j++) { - if (this.visitedModels[j] === fields[i].reference.cls.getName()) { - return; + if (this.recurseSubModels) { + for (var j = 0; j < this.visitedModels.length; j++) { + if (this.visitedModels[j] === fields[i].reference.cls.getName()) { + return; + } } - } - var childNode = node.appendChild({ - text: fields[i].name, - expanded: true, - leaf: false - }); + var childNode = node.appendChild({ + text: fields[i].name, + expanded: true, + leaf: false + }); - this.treeMaker(childNode, fields[i].reference.cls, prefix + fields[i].name + "."); + this.treeMaker(childNode, fields[i].reference.cls, prefix + fields[i].name + "."); + } } } diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/PagingToolbar.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/PagingToolbar.js @@ -3,7 +3,8 @@ Ext.define("PartKeepr.PagingToolbar", { grid: null, - getPagingItems: function () { + getPagingItems: function () + { var items = this.callParent(arguments); items.push(Ext.create("PartKeepr.Exporter.GridExporterButton", { @@ -12,6 +13,22 @@ Ext.define("PartKeepr.PagingToolbar", { iconCls: "fugue-icon application-export", disabled: this.store.isLoading() })); + + items.push(Ext.create({ + itemId: 'filter', + xtype: 'button', + iconCls: 'fugue-icon funnel', + tooltip: i18n("Reset Filter"), + hidden: true, handler: function () + { + this.store.getFilters().removeAll(); + this.store.currentPage = 1; + this.store.load({start: 0}); + + }, + scope: this + })); + return items; } }); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/ReloadableComboBox.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/ReloadableComboBox.js @@ -1,5 +1,5 @@ -Ext.define("PartKeepr.ReloadableComboBox",{ - extend:"Ext.form.field.ComboBox", +Ext.define("PartKeepr.ReloadableComboBox", { + extend: "Ext.form.field.ComboBox", alias: 'widget.ReloadableComboBox', displayField: 'name', valueField: '@id', @@ -19,19 +19,34 @@ Ext.define("PartKeepr.ReloadableComboBox",{ scope: 'this' } }, - initComponent: function () { - this.listenersStore = this.store.on({ - scope: this, - // Workaround to remember the value when loading - beforeload: function () { - this._oldValue = this.getSelection(); - }, - // Set the old value when load is complete - load: function () { - this.setSelection(this._oldValue); - } - }); - - this.callParent(); + initComponent: function () + { + this.listenersStore = this.store.on({ + scope: this, + // Workaround to remember the value when loading + beforeload: function () + { + this._oldValue = this.getSelection(); + }, + // Set the old value when load is complete + load: function () + { + this.setSelection(this._oldValue); + } + }); + + this.callParent(); + }, + getErrors: function (value) + { + var errors = this.callParent([value]); + + if (this.allowBlank !== true) { + if (this.getValue() === null) { + errors.push(i18n("This field is required")); + } + } + + return errors; } }); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Data/store/BarcodeScannerActionsStore.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Data/store/BarcodeScannerActionsStore.js @@ -0,0 +1,20 @@ +Ext.define("PartKeepr.Data.store.BarcodeScannerActionsStore", { + extend: "Ext.data.Store", + fields: ["action", "name", "description"], + constructor: function () + { + this.callParent(arguments); + + var actions = PartKeepr.getApplication().getBarcodeScannerManager().getActions(); + + for (var i = 0; i < actions.length; i++) { + this.add({ + "action": actions[i], + "name": Ext.ClassManager.get(actions[i]).actionName, + "description": Ext.ClassManager.get(actions[i]).actionDescription + }); + } + + } + +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/PartKeepr.js b/src/PartKeepr/FrontendBundle/Resources/public/js/PartKeepr.js @@ -6,11 +6,13 @@ Ext.application({ name: 'PartKeepr', loginManager: null, - init: function () { + init: function () + { }, - launch: function () { + launch: function () + { Ext.setGlyphFontFamily('FontAwesome'); Ext.get("loading").hide(); Ext.setLocale('en_US'); @@ -46,12 +48,14 @@ Ext.application({ this.loginManager.on("logout", this.onLogout, this); this.loginManager.login(); }, - onAppMenuClick: function (item) { + onAppMenuClick: function (item) + { if (typeof item.target === "function") { this.openAppItem(item.target["$className"]); } }, - openAppItem: function (target) { + openAppItem: function (target) + { targetClass = Ext.ClassManager.get(target); var config = { @@ -69,22 +73,26 @@ Ext.application({ j.show(); } }, - getParameter: function (parameter) { + getParameter: function (parameter) + { if (window.parameters[parameter] !== undefined) { return window.parameters[parameter]; } }, - getLoginManager: function () { + getLoginManager: function () + { return this.loginManager; }, - getPartManager: function () { + getPartManager: function () + { return this.partManager; }, /** * Handles the login function. Initializes the part manager window, * enables the menu bar and creates the stores+loads them. */ - onLogin: function () { + onLogin: function () + { this.createGlobalStores(); var initialUserPreferences = Ext.decode(this.getLoginManager().getUser().get("initialUserPreferences")); @@ -115,7 +123,8 @@ Ext.application({ this.getStatusbar().setConnected(); }, - onLogout: function () { + onLogout: function () + { this.menuBar.disable(); this.centerPanel.removeAll(true); this.getStatusbar().setDisconnected(); @@ -129,7 +138,8 @@ Ext.application({ * @param none * @return nothing */ - recreatePartManager: function () { + recreatePartManager: function () + { this.centerPanel.remove(this.partManager); this.getPartManager().destroy(); @@ -139,7 +149,8 @@ Ext.application({ * Creates the part manager. While this is usually only done after login, it can also happen when the user changes * the "compact" preference. */ - createPartManager: function () { + createPartManager: function () + { this.partManager = Ext.create("PartKeepr.PartManager", { title: i18n("Part Manager"), compactLayout: PartKeepr.getApplication().getUserPreference("partkeepr.partmanager.compactlayout", false), @@ -152,7 +163,8 @@ Ext.application({ /** * Sets the initial user preferences, which are applied into the userPreferenceStore after login. */ - setInitialUserPreferences: function (obj) { + setInitialUserPreferences: function (obj) + { PartKeepr.initialUserPreferences = obj; }, /** @@ -161,10 +173,12 @@ Ext.application({ * This method checks if the user has disabled tips, and if so, this method * avoids showing the window. */ - displayTipOfTheDayWindow: function () { + displayTipOfTheDayWindow: function () + { if (!Ext.data.StoreManager.lookup('TipOfTheDayStore') || !Ext.data.StoreManager.lookup( 'TipOfTheDayStore').isLoaded() || !Ext.data.StoreManager.lookup( - 'TipOfTheDayHistoryStore') || !Ext.data.StoreManager.lookup('TipOfTheDayHistoryStore').isLoaded() || !this.getUserPreferenceStore().isLoaded() + 'TipOfTheDayHistoryStore') || !Ext.data.StoreManager.lookup( + 'TipOfTheDayHistoryStore').isLoaded() || !this.getUserPreferenceStore().isLoaded() ) { this.displayTipWindowTask.delay(100); return; @@ -181,7 +195,8 @@ Ext.application({ /** * Displays a message-of-the-day */ - displayMOTD: function () { + displayMOTD: function () + { Ext.MessageBox.alert(i18n("Message of the day"), window.parameters.motd); }, /** @@ -190,7 +205,8 @@ Ext.application({ * @param none * @return nothing */ - doSystemStatusCheck: function () { + doSystemStatusCheck: function () + { var call = new PartKeepr.ServiceCall("api", "system_status"); call.setHandler(Ext.bind(this.onSystemStatusCheck, this)); call.doCall(); @@ -199,7 +215,8 @@ Ext.application({ * Handler for the schema check * @param data The data returned from the server */ - onSystemStatusCheck: function (data) { + onSystemStatusCheck: function (data) + { if (data.schemaStatus !== "complete") { alert(i18n("Your database schema is not up-to-date! Please re-run setup immediately!")); } @@ -216,7 +233,8 @@ Ext.application({ * @param none * @return nothing */ - doUnacknowledgedNoticesCheck: function () { + doUnacknowledgedNoticesCheck: function () + { this.systemNoticeStore.load({ scope: this, callback: this.onUnacknowledgedNoticesCheck @@ -226,14 +244,16 @@ Ext.application({ * Handler for the unacknowledged system notices check * @param data The data returned from the server */ - onUnacknowledgedNoticesCheck: function () { + onUnacknowledgedNoticesCheck: function () + { if (this.systemNoticeStore.count() > 0) { this.statusBar.systemNoticeButton.show(); } else { this.statusBar.systemNoticeButton.hide(); } }, - createGlobalStores: function () { + createGlobalStores: function () + { this.footprintStore = Ext.create("Ext.data.Store", { model: 'PartKeepr.FootprintBundle.Entity.Footprint', @@ -289,10 +309,19 @@ Ext.application({ autoLoad: false }); + this.barcodeScannerManager = Ext.create("PartKeepr.BarcodeScanner.Manager"); + this.systemPreferenceStore = Ext.create("PartKeepr.data.store.SystemPreferenceStore", { model: 'PartKeepr.SystemPreferenceBundle.Entity.SystemPreference', - autoLoad: true + autoLoad: true, + listeners: { + scope: this, + "load": function () + { + this.barcodeScannerManager.registerBarcodeScannerHotkey(); + } + } }); this.tipOfTheDayStore = Ext.create("PartKeepr.data.store.TipOfTheDayStore"); @@ -300,16 +329,25 @@ Ext.application({ this.systemNoticeStore = Ext.create("PartKeepr.data.store.SystemNoticeStore"); }, - storeLoaded: function (store) { + getBarcodeScannerManager: function () + { + return this.barcodeScannerManager; + + }, + storeLoaded: function (store) + { store._loaded = true; }, - setAdmin: function (admin) { + setAdmin: function (admin) + { this.admin = admin; }, - isAdmin: function () { + isAdmin: function () + { return this.admin; }, - getSystemPreferenceStore: function () { + getSystemPreferenceStore: function () + { return this.systemPreferenceStore; }, /** @@ -319,7 +357,8 @@ Ext.application({ * @param defaultValue A default value to return (optional) * @returns the key value, or defaultValue if preference key was not found */ - getSystemPreference: function (key, defaultValue) { + getSystemPreference: function (key, defaultValue) + { var record = this.systemPreferenceStore.findRecord("preferenceKey", key); if (record) { @@ -341,7 +380,8 @@ Ext.application({ * @param key The key to set * @param value The value to set */ - setSystemPreference: function (key, value) { + setSystemPreference: function (key, value) + { var record = this.systemPreferenceStore.findRecord("preferenceKey", key); value = Ext.encode(value); @@ -365,7 +405,8 @@ Ext.application({ * @param defaultValue A default value to return (optional) * @returns the key value, or defaultValue if preference key was not found */ - getUserPreference: function (key, defaultValue) { + getUserPreference: function (key, defaultValue) + { var record = this.userPreferenceStore.findRecord("preferenceKey", key); if (record) { @@ -387,7 +428,8 @@ Ext.application({ * @param key The key to set * @param value The value to set */ - setUserPreference: function (key, value) { + setUserPreference: function (key, value) + { var record = this.userPreferenceStore.findRecord("preferenceKey", key); value = Ext.encode(value); @@ -404,31 +446,40 @@ Ext.application({ this.userPreferenceStore.add(j); } }, - getUserPreferenceStore: function () { + getUserPreferenceStore: function () + { return this.userPreferenceStore; }, - getUnitStore: function () { + getUnitStore: function () + { return this.unitStore; }, - getPartUnitStore: function () { + getPartUnitStore: function () + { return this.partUnitStore; }, - getFootprintStore: function () { + getFootprintStore: function () + { return this.footprintStore; }, - getManufacturerStore: function () { + getManufacturerStore: function () + { return this.manufacturerStore; }, - getDistributorStore: function () { + getDistributorStore: function () + { return this.distributorStore; }, - getDefaultPartUnit: function () { + getDefaultPartUnit: function () + { return this.partUnitStore.findRecord("default", true); }, - getUserStore: function () { + getUserStore: function () + { return this.userStore; }, - getSiPrefixStore: function () { + getSiPrefixStore: function () + { return this.siPrefixStore; }, /** @@ -439,7 +490,8 @@ Ext.application({ * Wikipedia Entry for the "Micro" Si Prefix: http://en.wikipedia.org/wiki/Micro- * */ - convertMicroToMu: function (value) { + convertMicroToMu: function (value) + { /** * Since the Si-Prefix for "micro" is μ, but keyboard have "µ" on it * (note: both chars might look identical, depending on your font), we need @@ -451,8 +503,8 @@ Ext.application({ /** * Creates the main view of PartKeepr. */ - createLayout: function () { - + createLayout: function () + { this.statusBar = Ext.create("PartKeepr.Statusbar"); this.messageLog = this.createMessageLog(); @@ -466,6 +518,7 @@ Ext.application({ }); + this.menuBar = Ext.create("PartKeepr.MenuBar"); this.menuBar.disable(); @@ -487,10 +540,12 @@ Ext.application({ }); }, - addItem: function (item) { + addItem: function (item) + { this.centerPanel.add(item); }, - createMessageLog: function () { + createMessageLog: function () + { return Ext.create("PartKeepr.MessageLog", { height: 200, hidden: true, @@ -501,7 +556,8 @@ Ext.application({ region: 'south', listeners: { beforecollapse: Ext.bind( - function (obj) { + function (obj) + { this.hideMessageLog(); return false; }, @@ -509,10 +565,12 @@ Ext.application({ } }); }, - log: function (message) { + log: function (message) + { this.logMessage(message, "none"); }, - logMessage: function (message, severity) { + logMessage: function (message, severity) + { if (message != i18n("Ready.")) { var r = Ext.ModelManager.create({ message: message, @@ -523,13 +581,16 @@ Ext.application({ this.messageLog.getStore().add(r); } }, - hideMessageLog: function () { + hideMessageLog: function () + { this.messageLog.hide(); }, - showMessageLog: function () { + showMessageLog: function () + { this.messageLog.show(); }, - toggleMessageLog: function () { + toggleMessageLog: function () + { if (this.messageLog.isHidden()) { this.showMessageLog(); } else { @@ -537,7 +598,8 @@ Ext.application({ } }, - getStatusbar: function () { + getStatusbar: function () + { return this.statusBar; }, /** @@ -547,7 +609,8 @@ Ext.application({ * * @param {string} username The username to set */ - setUsername: function (username) { + setUsername: function (username) + { this.username = username; this.getStatusbar().setCurrentUser(username); }, @@ -555,10 +618,12 @@ Ext.application({ * Returns the current username * @returns {string} */ - getUsername: function () { + getUsername: function () + { return this.username; }, - formatCurrency: function (value) { + formatCurrency: function (value) + { var format = Ext.util.Format; format.currencyPrecision = PartKeepr.getApplication().getUserPreference( "partkeepr.formatting.currency.numdecimals", 2); @@ -578,24 +643,28 @@ Ext.application({ } }); -PartKeepr.getSession = function () { +PartKeepr.getSession = function () +{ alert("This should not be called."); return "hli2ong0ktnise68p9f5nu6nk1"; }; -PartKeepr.log = function (message) { +PartKeepr.log = function (message) +{ PartKeepr.getApplication().log(message); }; /** * <p>This static method returns the instance of the application.</p> - * @return {Object} The application + * @return {PartKeepr} The application */ -PartKeepr.getApplication = function () { +PartKeepr.getApplication = function () +{ return PartKeepr.application; }; -PartKeepr.getBasePath = function () { +PartKeepr.getBasePath = function () +{ var href = document.getElementsByTagName('base')[0].href; @@ -610,19 +679,23 @@ PartKeepr.getBasePath = function () { return href; }; -PartKeepr.getImagePath = function () { +PartKeepr.getImagePath = function () +{ return "image.php"; }; -PartKeepr.setMaxUploadSize = function (size) { +PartKeepr.setMaxUploadSize = function (size) +{ PartKeepr.maxUploadSize = size; }; -PartKeepr.getMaxUploadSize = function () { +PartKeepr.getMaxUploadSize = function () +{ return PartKeepr.maxUploadSize; }; -PartKeepr.bytesToSize = function (bytes) { +PartKeepr.bytesToSize = function (bytes) +{ var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; if (bytes === 0) { return '0 Bytes'; @@ -631,15 +704,18 @@ PartKeepr.bytesToSize = function (bytes) { return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i]; }; -PartKeepr.setAvailableImageFormats = function (formats) { +PartKeepr.setAvailableImageFormats = function (formats) +{ PartKeepr.imageFormats = formats; }; -PartKeepr.getAvailableImageFormats = function () { +PartKeepr.getAvailableImageFormats = function () +{ return PartKeepr.imageFormats; }; -PartKeepr.serializeRecords = function (records) { +PartKeepr.serializeRecords = function (records) +{ var finalData = []; for (var i = 0; i < records.length; i++) { diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Util/Filter.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Util/Filter.js @@ -1,6 +1,13 @@ Ext.define('PartKeepr.util.Filter', { extend: 'Ext.util.Filter', + config: { + /** + * @cfg {String} [property=null] + * The property to filter on. Required unless a {@link #filterFn} is passed. + */ + subfilters: [], + }, /** * Creates new Filter. * @param {Object} config Config object @@ -23,6 +30,26 @@ Ext.define('PartKeepr.util.Filter', { //</debug> this.initConfig(config); }, + getFilterDescription: function () { + var config = this.getInitialConfig(), + i, subfilterData = []; + + if (config.property !== null && config.value !== null && config.operator !== null) { + subfilterData.push(config.property + " " + config.operator + " " + config.value); + } + + if (config.subfilters instanceof Array && config.subfilters.length > 0) { + for (i=0;i<config.subfilters.length;i++) { + subfilterData.push("(" + config.subfilters[i].getFilterDescription()+ ")"); + } + } + + if (config.type && config.type.toLowerCase() == "or") { + return subfilterData.join(" OR "); + } else { + return subfilterData.join(" AND "); + } + }, preventConvert: { 'in': 1, diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Util/FilterPlugin.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Util/FilterPlugin.js @@ -0,0 +1,51 @@ +Ext.define("PartKeepr.Util.FilterPlugin", { + extend: "Ext.AbstractPlugin", + + ptype: 'filter', + + mixins: [ + 'Ext.mixin.Observable' + ], + + filter: null, + enabled: false, + + constructor: function (config) + { + this.mixins.observable.constructor.call(this, config); + this.callParent([config]); + }, + init: function (ownerComponent) + { + this.ownerComponent = ownerComponent; + + ownerComponent.disableFilter = Ext.bind(this.disable, this); + ownerComponent.enableFilter = Ext.bind(this.enable, this); + }, + getFilter: function () + { + var filter = this.getFilterFn.call(this.scope); + + if (this.filter === null) { + this.filter = Ext.create("PartKeepr.util.Filter", filter); + } else { + this.filter.setConfig(filter); + } + + return this.filter; + }, + isEnabled: function () + { + return this.enabled; + }, + enable: function () + { + this.fireEvent("enable"); + this.enabled = true; + }, + disable: function () + { + this.fireEvent("disable"); + this.enabled = false; + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/form/field/SearchField.js b/src/PartKeepr/FrontendBundle/Resources/public/js/form/field/SearchField.js @@ -64,16 +64,30 @@ Ext.define('PartKeepr.form.field.SearchField', { } }, - initComponent: function () { + initComponent: function () + { + this.store.on("filterchange", this.onFilterChange, this); + this.filter = Ext.create("PartKeepr.util.Filter"); this.callParent(arguments); }, + onFilterChange: function () + { + if (!this.store.getFilters().contains(this.filter)) { + this.setValue(""); + + this.getTrigger("clear").hide(); + this.hasSearch = false; + } + + }, /** * Handles special keys used in this field. * * Enter: Starts the search * Escape: Removes the search and clears the field contents */ - keyHandler: function (field, e) { + keyHandler: function (field, e) + { switch (e.getKey()) { case e.ENTER: this.startSearch(); @@ -86,7 +100,8 @@ Ext.define('PartKeepr.form.field.SearchField', { /** * Resets the search field to empty and re-triggers the store to load the matching records. */ - resetSearch: function () { + resetSearch: function () + { var me = this, store = me.store; @@ -100,7 +115,9 @@ Ext.define('PartKeepr.form.field.SearchField', { if (me.hasSearch) { - store.getFilters().clear(); + if (store.getFilters().contains(this.filter)) { + store.getFilters().remove(this.filter); + } store.currentPage = 1; store.load({start: 0}); @@ -112,52 +129,73 @@ Ext.define('PartKeepr.form.field.SearchField', { /** * Starts the search with the entered value. */ - startSearch: function () { + startSearch: function () + { var me = this, store = me.store, searchValue = me.getValue(), searchTerms = searchValue.split(" "), splitTerms = true, + orSubFilters = [], i, j, subFilters = []; if (this.splitSearchTermSystemPreference !== null) { - splitTerms = Boolean(PartKeepr.getApplication().getSystemPreference(this.splitSearchTermSystemPreference, this.splitSearchTermSystemPreferenceDefaults)); + splitTerms = Boolean(PartKeepr.getApplication().getSystemPreference(this.splitSearchTermSystemPreference, + this.splitSearchTermSystemPreferenceDefaults)); } if (this.searchFieldSystemPreference !== null) { - var fields = PartKeepr.getApplication().getSystemPreference(this.searchFieldSystemPreference, this.searchFieldSystemPreferenceDefaults) + var fields = PartKeepr.getApplication().getSystemPreference(this.searchFieldSystemPreference, + this.searchFieldSystemPreferenceDefaults); - for (i = 0; i < fields.length; i++) { - if (splitTerms === true) { - for (j = 0; j < searchTerms.length; j++) { - subFilters.push(this.createFilter(fields[i], searchTerms[j])); + if (splitTerms === true) { + for (j = 0; j < searchTerms.length; j++) { + orSubFilters = []; + for (i = 0; i < fields.length; i++) { + orSubFilters.push(this.createFilter(fields[i], searchTerms[j])); } - } else { - subFilters.push(this.createFilter(fields[i], searchValue)); + + subFilters.push(Ext.create("PartKeepr.util.Filter", { + type: "OR", + subfilters: orSubFilters + })); } - } + this.filter.setConfig({ + type: "AND", + subfilters: subFilters + }); - this.filter = Ext.create("PartKeepr.util.Filter", { + } else { + for (i = 0; i < fields.length; i++) { + subFilters.push(this.createFilter(fields[i], searchValue)); + } + + this.filter.setConfig({ type: "OR", subfilters: subFilters }); + } + + } else { if (splitTerms === true) { for (j = 0; j < searchTerms.length; j++) { - for (j = 0; j < searchTerms.length; j++) { - subFilters.push(this.createFilter(this.targetField, searchTerms[j])); - } + subFilters.push(this.createFilter(this.targetField, searchTerms[j])); } - this.filter = Ext.create("PartKeepr.util.Filter", { + this.filter.setConfig({ type: "OR", subfilters: subFilters }); } else { - this.filter = this.createFilter(this.targetField, searchValue); + this.filter.setConfig({ + property: this.targetField, + value: "%" + searchValue+ "%", + operator: 'like' + }); } } @@ -172,15 +210,18 @@ Ext.define('PartKeepr.form.field.SearchField', { } this.filter.setValue(searchValue); - store.getFilters().clear(); - store.setFilters(this.filter); - store.currentPage = 1; - store.load({start: 0}); + + if (!store.getFilters().contains(this.filter)) { + store.getFilters().add(this.filter); + } + + store.getFilters().itemChanged(this.filter); me.hasSearch = true; this.getTrigger("clear").show(); }, - createFilter: function (property, term) { + createFilter: function (property, term) + { return Ext.create("PartKeepr.util.Filter", { property: property, value: "%" + term + "%", @@ -192,7 +233,8 @@ Ext.define('PartKeepr.form.field.SearchField', { * * @param {Ext.data.Store} store The store to set */ - setStore: function (store) { + setStore: function (store) + { this.store = store; } }); diff --git a/src/PartKeepr/FrontendBundle/Resources/views/index.html.twig b/src/PartKeepr/FrontendBundle/Resources/views/index.html.twig @@ -51,8 +51,7 @@ 'js/packages/extjs6/packages/ux/classic/src/TreePicker.js' 'js/packages/extjs6/packages/ux/classic/src/TabCloseMenu.js' 'js/packages/extjs6/packages/ux/classic/src/statusbar/StatusBar.js' - 'js/packages/extjs6/packages/ux/classic/src/IFrame.js' - %} + 'js/packages/extjs6/packages/ux/classic/src/IFrame.js' %} <script type="text/javascript" src="{{ asset_url }}"></script> {% endjavascripts %} @@ -73,8 +72,7 @@ {% javascripts output='js/compiled/models.js' - 'bundles/doctrinereflection/*' - %} + 'bundles/doctrinereflection/*' %} <script type="text/javascript" src="{{ asset_url }}"></script> {% endjavascripts %} @@ -110,6 +108,8 @@ '@PartKeeprFrontendBundle/Resources/public/js/ExtJS/Bugfixes/Ext.data.Model-EXTJS-15037.js' '@PartKeeprFrontendBundle/Resources/public/js/Util/JsonWithAssociationsWriter.js' '@PartKeeprFrontendBundle/Resources/public/js/PartKeepr.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/Grid/AppliedFiltersToolbar.js' + '@PartKeeprFrontendBundle/Resources/public/js/Util/FilterPlugin.js' '@PartKeeprFrontendBundle/Resources/public/js/compat.js' '@PartKeeprFrontendBundle/Resources/public/js/Ext.ux/NumericField.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/Widgets/TreePicker.js' @@ -131,6 +131,7 @@ '@PartKeeprFrontendBundle/Resources/public/js/Data/store/PartCategoryStore.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' '@PartKeeprFrontendBundle/Resources/public/js/Data/store/UserPreferenceStore.js' '@PartKeeprFrontendBundle/Resources/public/js/ExtJS/Enhancements/Ext.tree.View-missingMethods.js' '@PartKeeprFrontendBundle/Resources/public/js/ExtJS/Enhancements/Ext.form.Basic-AssociationSupport.js' @@ -246,14 +247,24 @@ '@PartKeeprFrontendBundle/Resources/public/js/Components/SystemPreferences/Tree.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/SystemPreferences/PreferenceEditor.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/FulltextSearch.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/RequiredPartFields.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/RequiredPartManufacturerFields.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/RequiredPartDistributorFields.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/BarcodeScannerConfiguration.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/BarcodeScanner/Manager.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/BarcodeScanner/Action.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/BarcodeScanner/ActionsComboBox.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/BarcodeScanner/Actions/AddRemoveStock.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/Part/AddRemoveStockWindow.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/BarcodeScanner/Actions/AddPart.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/BarcodeScanner/Actions/SearchPart.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/Widgets/FieldSelector.js' '@PartKeeprFrontendBundle/Resources/public/js/Models/Message.js' '@PartKeeprFrontendBundle/Resources/public/js/Ext.ux.Wizard.Card.js' '@PartKeeprFrontendBundle/Resources/public/js/Ext.ux.Wizard.Header.js' '@PartKeeprFrontendBundle/Resources/public/js/Ext.ux.Wizard.js' '@PartKeeprFrontendBundle/Resources/public/js/Ext.ux.Wizard.CardLayout.js' - '@PartKeeprFrontendBundle/Resources/public/js/php.default.min.js' - %} + '@PartKeeprFrontendBundle/Resources/public/js/php.default.min.js' %} <script type="text/javascript" src="{{ asset_url }}"></script> {% endjavascripts %} </head>