partkeepr

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

commit 3b5ff037b5ffe1510b6e28b5f27bb12c676020d7
parent 142387dd0574202ebf61a131207de215cce892ac
Author: Felicia Hummel <felicitus@felicitus.org>
Date:   Mon, 26 Dec 2016 20:09:31 +0100

Merge pull request #759 from partkeepr/PartKeepr-753

Part keepr 753
Diffstat:
Mapp/AppKernel.php | 1+
Mapp/config/config_partkeepr.yml | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/BatchJobBundle/Action/ExecuteBatchJobAction.php | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/BatchJobBundle/DependencyInjection/PartKeeprBatchJobExtension.php | 22++++++++++++++++++++++
Asrc/PartKeepr/BatchJobBundle/Entity/BatchJob.php | 146+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/BatchJobBundle/Entity/BatchJobQueryField.php | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/BatchJobBundle/Entity/BatchJobUpdateField.php | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/BatchJobBundle/PartKeeprBatchJobBundle.php | 9+++++++++
Asrc/PartKeepr/BatchJobBundle/Resources/config/actions.xml | 16++++++++++++++++
Msrc/PartKeepr/DoctrineReflectionBundle/Resources/views/model.js.twig | 2++
Msrc/PartKeepr/FrontendBundle/Resources/public/css/PartKeepr.css | 4++++
Asrc/PartKeepr/FrontendBundle/Resources/public/images/form/trigger-ellipsis.png | 0
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/BatchJob/BatchJobEditor.js | 281+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/BatchJob/BatchJobEditorComponent.js | 24++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/BatchJob/BatchJobExecutionWindow.js | 253+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/BatchJob/BatchJobGrid.js | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/BatchJob/BatchJobUpdateExpression.js | 161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/BatchJob/BatchJobUpdateExpressionWindow.js | 26++++++++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/MenuBar.js | 15++++++++-------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Panel.js | 3++-
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/ActionsConfiguration.js | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FieldSelectTrigger.js | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FieldSelectorWindow.js | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FilterExpression.js | 68++++++--------------------------------------------------------------
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FilterExpressionWindow.js | 26++++++++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/PagingToolbar.js | 21++++++++++-----------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraModel.js | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraProxy.js | 22++++++++++++++++++++--
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Data/store/ModelStore.js | 25+++++++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/PartKeepr.js | 3+++
Msrc/PartKeepr/FrontendBundle/Resources/views/index.html.twig | 11+++++++++++
Msrc/PartKeepr/ImageBundle/Tests/ImageControllerTest.php | 2+-
32 files changed, 1972 insertions(+), 84 deletions(-)

diff --git a/app/AppKernel.php b/app/AppKernel.php @@ -65,6 +65,7 @@ class AppKernel extends Kernel $bundles[] = new PartKeepr\StatisticBundle\PartKeeprStatisticBundle(); $bundles[] = new PartKeepr\SystemPreferenceBundle\PartKeeprSystemPreferenceBundle(); $bundles[] = new PartKeepr\ImportBundle\PartKeeprImportBundle(); + $bundles[] = new PartKeepr\BatchJobBundle\PartKeeprBatchJobBundle(); return array_merge($bundles, $this->getCustomBundles()); } diff --git a/app/config/config_partkeepr.yml b/app/config/config_partkeepr.yml @@ -118,6 +118,63 @@ services: arguments: - { groups: [ "default" ] } + resource.batchjob.item_operation.execute: + class: "Dunglas\ApiBundle\Api\Operation\Operation" + public: false + factory: [ "@api.operation_factory", "createItemOperation" ] + arguments: + - "@resource.batchjob" # Resource + - [ "PUT" ] # Methods + - "/batch_jobs/{id}/execute" # Path + - "partkeepr.batchjob.execute" # Controller + - "BatchJobExecute" # Route name + + resource.batchjob: + parent: "api.resource" + arguments: [ 'PartKeepr\BatchJobBundle\Entity\BatchJob' ] + tags: [ { name: "api.resource" } ] + calls: + - method: "initFilters" + arguments: [ [ "@doctrine_reflection_service.search_filter" ] ] + - method: "initNormalizationContext" + arguments: [ { groups: [ "default" ] } ] + - method: "initDenormalizationContext" + arguments: + - { groups: [ "default" ] } + - method: "initItemOperations" + arguments: [ [ "@resource.batchjob.item_operation.get", "@resource.batchjob.item_operation.put", "@resource.batchjob.item_operation.delete", "@resource.batchjob.item_operation.execute" ] ] + + + + + + resource.batchjob_queryfield: + parent: "api.resource" + arguments: [ 'PartKeepr\BatchJobBundle\Entity\BatchJobQueryField' ] + tags: [ { name: "api.resource" } ] + calls: + - method: "initFilters" + arguments: [ [ "@doctrine_reflection_service.search_filter" ] ] + - method: "initNormalizationContext" + arguments: [ { groups: [ "default" ] } ] + - method: "initDenormalizationContext" + arguments: + - { groups: [ "default" ] } + + resource.batchjob_updatefield: + parent: "api.resource" + arguments: [ 'PartKeepr\BatchJobBundle\Entity\BatchJobUpdateField' ] + tags: [ { name: "api.resource" } ] + calls: + - method: "initFilters" + arguments: [ [ "@doctrine_reflection_service.search_filter" ] ] + - method: "initNormalizationContext" + arguments: [ { groups: [ "default" ] } ] + - method: "initDenormalizationContext" + arguments: + - { groups: [ "default" ] } + + resource.footprint_image.item_operation.get: class: "Dunglas\ApiBundle\Api\Operation\Operation" public: false diff --git a/src/PartKeepr/BatchJobBundle/Action/ExecuteBatchJobAction.php b/src/PartKeepr/BatchJobBundle/Action/ExecuteBatchJobAction.php @@ -0,0 +1,157 @@ +<?php +namespace PartKeepr\BatchJobBundle\Action; + +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\QueryBuilder; +use Dunglas\ApiBundle\Action\ActionUtilTrait; +use Dunglas\ApiBundle\Api\IriConverter; +use Dunglas\ApiBundle\Exception\RuntimeException; +use Dunglas\ApiBundle\Model\DataProviderInterface; +use PartKeepr\BatchJobBundle\Entity\BatchJob; +use PartKeepr\BatchJobBundle\Entity\BatchJobQueryField; +use PartKeepr\CategoryBundle\Exception\RootNodeNotFoundException; +use PartKeepr\DoctrineReflectionBundle\Filter\AdvancedSearchFilter; +use PartKeepr\DoctrineReflectionBundle\Services\ReflectionService; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\PropertyAccess\PropertyAccess; + +class ExecuteBatchJobAction +{ + use ActionUtilTrait; + + /** + * @var DataProviderInterface + */ + private $dataProvider; + + /** + * @var EntityManager + */ + private $em; + + /** + * @var IriConverter + */ + private $iriConverter; + + /** + * @var AdvancedSearchFilter + */ + private $advancedSearchFilter; + + /** + * @var ReflectionService + */ + private $reflectionService; + + public function __construct( + DataProviderInterface $dataProvider, + IriConverter $iriConverter, + EntityManager $em, + AdvancedSearchFilter $advancedSearchFilter, + ReflectionService $reflectionService + ) { + $this->dataProvider = $dataProvider; + $this->iriConverter = $iriConverter; + $this->em = $em; + $this->advancedSearchFilter = $advancedSearchFilter; + $this->reflectionService = $reflectionService; + } + + /** + * Executes a batch action + * + * @param Request $request + * @param string $id + * + * @throws RuntimeException|RootNodeNotFoundException + * + * @return array|\Dunglas\ApiBundle\Model\PaginatorInterface|\Traversable + */ + public function __invoke(Request $request, $id) + { + list($resourceType) = $this->extractAttributes($request); + + /** + * @var $batchJob BatchJob + */ + $batchJob = $this->getItem($this->dataProvider, $resourceType, $id); + + $queryFields = []; + $updateFields = []; + + if ($request->request->has("queryFields")) { + $queryFields = json_decode($request->request->get("queryFields"), true); + } + + if ($request->request->has("updateFields")) { + $updateFields = json_decode($request->request->get("updateFields"), true); + } + + $queryFilters = []; + + foreach ($batchJob->getBatchJobQueryFields() as $batchJobQueryField) { + $queryFilter = new \stdClass(); + $queryFilter->property = $batchJobQueryField->getProperty(); + $queryFilter->operator = $batchJobQueryField->getOperator(); + $queryFilter->value = $batchJobQueryField->getValue(); + + if ($batchJobQueryField->getDynamic()) { + foreach ($queryFields as $queryField) { + if ($queryField["property"] == $batchJobQueryField->getProperty()) { + $queryFilter->value = $queryField["value"]; + } + } + } + + $queryFilters[] = $queryFilter; + } + + $updateFieldConfigs = []; + + foreach ($batchJob->getBatchJobUpdateFields() as $batchJobUpdateField) { + $updateFieldConfig = new \stdClass(); + $updateFieldConfig->property = $batchJobUpdateField->getProperty(); + $updateFieldConfig->value = $batchJobUpdateField->getValue(); + + if ($batchJobUpdateField->getDynamic()) { + foreach ($updateFields as $updateField) { + if ($updateField["property"] == $batchJobUpdateField->getProperty()) { + $updateFieldConfig->value = $updateField["value"]; + } + } + } + + + $updateFieldConfigs[] = $updateFieldConfig; + } + + $configuration = $this->advancedSearchFilter->extractConfiguration($queryFilters, []); + + $qb = new QueryBuilder($this->em); + $qb->select("o")->from($this->reflectionService->convertExtJSToPHPClassName($batchJob->getBaseEntity()), "o"); + + $this->advancedSearchFilter->filter($qb, $configuration['filters'], $configuration['sorters']); + + $data = $qb->getQuery()->getResult(); + + $accessor = PropertyAccess::createPropertyAccessor(); + + foreach ($data as $item) { + foreach ($updateFieldConfigs as $updateField) { + + try { + $value = $this->iriConverter->getItemFromIri($updateField->value); + } catch (\Exception $e) { + $value = $updateField->value; + } + + $accessor->setValue($item, $updateField->property, $value); + } + } + + $this->em->flush(); + + return []; + } +} diff --git a/src/PartKeepr/BatchJobBundle/DependencyInjection/PartKeeprBatchJobExtension.php b/src/PartKeepr/BatchJobBundle/DependencyInjection/PartKeeprBatchJobExtension.php @@ -0,0 +1,22 @@ +<?php + + +namespace PartKeepr\BatchJobBundle\DependencyInjection; + +use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader; +use Symfony\Component\HttpKernel\DependencyInjection\Extension; + +class PartKeeprBatchJobExtension extends Extension +{ + + /** + * {@inheritdoc} + */ + public function load(array $configs, ContainerBuilder $container) + { + $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('actions.xml'); + } +} diff --git a/src/PartKeepr/BatchJobBundle/Entity/BatchJob.php b/src/PartKeepr/BatchJobBundle/Entity/BatchJob.php @@ -0,0 +1,146 @@ +<?php +namespace PartKeepr\BatchJobBundle\Entity; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\ORM\Mapping as ORM; +use PartKeepr\CoreBundle\Entity\BaseEntity; +use PartKeepr\DoctrineReflectionBundle\Annotation\TargetService; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * @ORM\Entity + * @TargetService(uri="/api/batch_jobs") + */ +class BatchJob extends BaseEntity +{ + /** + * Holds the batch job name. + * + * @ORM\Column(length=64,unique=true) + * @Groups({"default"}) + * + * @var string + */ + private $name; + + /** + * Holds the batch job query fields + * + * @ORM\OneToMany(targetEntity="PartKeepr\BatchJobBundle\Entity\BatchJobQueryField",mappedBy="batchJob",cascade={"persist", "remove"}, orphanRemoval=true) + * @Groups({"default"}) + * + * @var ArrayCollection + */ + private $batchJobQueryFields; + + /** + * Batch Job Update Fields + * + * @ORM\OneToMany(targetEntity="PartKeepr\BatchJobBundle\Entity\BatchJobUpdateField",mappedBy="batchJob",cascade={"persist", "remove"}, orphanRemoval=true) + * @Groups({"default"}) + * + * @var ArrayCollection + */ + private $batchJobUpdateFields; + + /** + * Holds the base entity to query against + * @ORM\Column() + * + * @Groups({"default"}) + * @var string + */ + private $baseEntity; + + public function __construct() + { + $this->batchJobQueryFields = new ArrayCollection(); + $this->batchJobUpdateFields = new ArrayCollection(); + } + + /** + * @return ArrayCollection + */ + public function getBatchJobUpdateFields() + { + return $this->batchJobUpdateFields; + } + + /** + * @return string + */ + public function getBaseEntity() + { + return $this->baseEntity; + } + + /** + * @param string $baseEntity + */ + public function setBaseEntity($baseEntity) + { + $this->baseEntity = $baseEntity; + } + + /** + * @return ArrayCollection + */ + public function getBatchJobQueryFields() + { + return $this->batchJobQueryFields; + } + + /** + * @param $batchJobQueryField BatchJobQueryField + */ + public function addBatchJobQueryField($batchJobQueryField) + { + if ($batchJobQueryField instanceof BatchJobQueryField) { + $batchJobQueryField->setBatchJob($this); + } + $this->batchJobQueryFields->add($batchJobQueryField); + } + + public function removeBatchJobQueryField($batchJobQueryField) + { + if ($batchJobQueryField instanceof BatchJobQueryField) { + $batchJobQueryField->setBatchJob(null); + } + $this->batchJobQueryFields->removeElement($batchJobQueryField); + } + + /** + * @param $batchJobUpdateField BatchJobUpdateField + */ + public function addBatchJobUpdateField($batchJobUpdateField) + { + if ($batchJobUpdateField instanceof BatchJobUpdateField) { + $batchJobUpdateField->setBatchJob($this); + } + $this->batchJobUpdateFields->add($batchJobUpdateField); + } + + public function removeBatchJobUpdateField($batchJobUpdateField) + { + if ($batchJobUpdateField instanceof BatchJobUpdateField) { + $batchJobUpdateField->setBatchJob(null); + } + $this->batchJobUpdateFields->removeElement($batchJobUpdateField); + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } +} diff --git a/src/PartKeepr/BatchJobBundle/Entity/BatchJobQueryField.php b/src/PartKeepr/BatchJobBundle/Entity/BatchJobQueryField.php @@ -0,0 +1,164 @@ +<?php +namespace PartKeepr\BatchJobBundle\Entity; + +use Doctrine\ORM\Mapping as ORM; +use PartKeepr\CoreBundle\Entity\BaseEntity; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * Represents a batch job query field. + * + * @ORM\Entity + */ +class BatchJobQueryField extends BaseEntity +{ + /** + * The part this batch job query field refers to. + * + * @ORM\ManyToOne(targetEntity="PartKeepr\BatchJobBundle\Entity\BatchJob", inversedBy="batchJobQueryFields") + * + * @var BatchJob + */ + private $batchJob = null; + + /** + * The field name to query + * + * @ORM\Column(length=255) + * @Groups({"default"}) + * @var string + */ + private $property; + + /** + * The operator to use + * + * @ORM\Column(length=64) + * @Groups({"default"}) + * @var string + */ + private $operator; + + /** + * The value. May be an array if the operator is IN + * + * @ORM\Column(type="text") + * @Groups({"default"}) + * @var string + */ + private $value; + + /** + * The description + * + * @ORM\Column(type="text") + * @Groups({"default"}) + * @var string + */ + private $description; + + /** + * Defines if the value is dynamic (=the user gets prompted upon running the batch job which value to use) + * + * @Groups({"default"}) + * @ORM\Column(type="boolean") + * @var bool + */ + private $dynamic; + + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * @param string $description + */ + public function setDescription($description) + { + $this->description = $description; + } + + /** + * @return BatchJob + */ + public function getBatchJob() + { + return $this->batchJob; + } + + /** + * @param BatchJob $batchJob + */ + public function setBatchJob($batchJob = null) + { + $this->batchJob = $batchJob; + } + + /** + * @return string + */ + public function getProperty() + { + return $this->property; + } + + /** + * @param string $property + */ + public function setProperty($property) + { + $this->property = $property; + } + + /** + * @return string + */ + public function getOperator() + { + return $this->operator; + } + + /** + * @param string $operator + */ + public function setOperator($operator) + { + $this->operator = $operator; + } + + /** + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * @param string $value + */ + public function setValue($value) + { + $this->value = $value; + } + + /** + * @return mixed + */ + public function getDynamic() + { + return $this->dynamic; + } + + /** + * @param mixed $dynamic + */ + public function setDynamic($dynamic) + { + $this->dynamic = $dynamic; + } +} diff --git a/src/PartKeepr/BatchJobBundle/Entity/BatchJobUpdateField.php b/src/PartKeepr/BatchJobBundle/Entity/BatchJobUpdateField.php @@ -0,0 +1,139 @@ +<?php +namespace PartKeepr\BatchJobBundle\Entity; + +use Doctrine\ORM\Mapping as ORM; +use PartKeepr\CoreBundle\Entity\BaseEntity; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * Represents a batch job update field + * + * @ORM\Entity + */ +class BatchJobUpdateField extends BaseEntity +{ + /** + * The part this batch job update field refers to. + * + * @ORM\ManyToOne(targetEntity="PartKeepr\BatchJobBundle\Entity\BatchJob", inversedBy="batchJobQueryFields") + * + * @var BatchJob + */ + private $batchJob = null; + + /** + * The field name to update + * + * @ORM\Column(length=255) + * @Groups({"default"}) + * @var string + */ + private $property; + + /** + * The value to set + * + * @ORM\Column(type="text") + * @Groups({"default"}) + * @var string + */ + private $value; + + /** + * The description + * + * @ORM\Column(type="text") + * @Groups({"default"}) + * @var string + */ + private $description; + + /** + * Defines if the value is dynamic (=the user gets prompted upon running the batch job which value to use) + * + * @Groups({"default"}) + * @ORM\Column(type="boolean") + * @var bool + */ + private $dynamic; + + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * @param string $description + */ + public function setDescription($description) + { + $this->description = $description; + } + + /** + * @return BatchJob + */ + public function getBatchJob() + { + return $this->batchJob; + } + + /** + * @param BatchJob $batchJob + */ + public function setBatchJob($batchJob = null) + { + $this->batchJob = $batchJob; + } + + /** + * @return string + */ + public function getProperty() + { + return $this->property; + } + + /** + * @param string $property + */ + public function setProperty($property) + { + $this->property = $property; + } + + /** + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * @param string $value + */ + public function setValue($value) + { + $this->value = $value; + } + + /** + * @return mixed + */ + public function getDynamic() + { + return $this->dynamic; + } + + /** + * @param mixed $dynamic + */ + public function setDynamic($dynamic) + { + $this->dynamic = $dynamic; + } +} diff --git a/src/PartKeepr/BatchJobBundle/PartKeeprBatchJobBundle.php b/src/PartKeepr/BatchJobBundle/PartKeeprBatchJobBundle.php @@ -0,0 +1,9 @@ +<?php +namespace PartKeepr\BatchJobBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +class PartKeeprBatchJobBundle extends Bundle +{ + +} diff --git a/src/PartKeepr/BatchJobBundle/Resources/config/actions.xml b/src/PartKeepr/BatchJobBundle/Resources/config/actions.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" ?> + +<container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + + <services> + <service id="partkeepr.batchjob.execute" class="PartKeepr\BatchJobBundle\Action\ExecuteBatchJobAction"> + <argument type="service" id="api.data_provider"/> + <argument type="service" id="api.iri_converter"/> + <argument type="service" id="doctrine.orm.default_entity_manager"/> + <argument type="service" id="doctrine_reflection_service.search_filter"/> + <argument type="service" id="doctrine_reflection_service"/> + </service> + </services> +</container> diff --git a/src/PartKeepr/DoctrineReflectionBundle/Resources/views/model.js.twig b/src/PartKeepr/DoctrineReflectionBundle/Resources/views/model.js.twig @@ -65,3 +65,5 @@ Ext.define('{{ className }}', { {% endif %} } }); + +PartKeepr.Data.Store.ModelStore.addModel('{{ className }}', ''); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/css/PartKeepr.css b/src/PartKeepr/FrontendBundle/Resources/public/css/PartKeepr.css @@ -67,6 +67,10 @@ background-image: url(../images/form/trigger-help.gif); } +.x-form-trigger-ellipsis { + background-image: url(../images/form/trigger-ellipsis.png); +} + .partkeepr-logo { height: 26px; padding-right: 30px; diff --git a/src/PartKeepr/FrontendBundle/Resources/public/images/form/trigger-ellipsis.png b/src/PartKeepr/FrontendBundle/Resources/public/images/form/trigger-ellipsis.png Binary files differ. diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BatchJob/BatchJobEditor.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BatchJob/BatchJobEditor.js @@ -0,0 +1,281 @@ +Ext.define('PartKeepr.BatchJobEditor', { + extend: 'PartKeepr.Editor', + alias: 'widget.BatchJobEditor', + + defaults: { + anchor: '100%', + labelWidth: 110 + }, + layout: { + type: 'vbox', + align: 'stretch', + pack: 'start' + }, + + items: [ + { + xtype: 'textfield', + name: 'name', + fieldLabel: i18n("Batch Job Name") + }, + { + xtype: 'combo', + store: Ext.StoreManager.lookup("ModelStore"), + displayField: 'model', + queryMode: 'local', + itemId: 'baseEntity', + editable: false, + forceSelection: true, + valueField: 'model', + name: 'baseEntity', + fieldLabel: i18n("Base Entity") + }, + { + xtype: 'fieldcontainer', + fieldLabel: i18n("Query Fields"), + layout: 'fit', + flex: 1, + items: [ + { + xtype: 'grid', + itemId: 'batchJobQueryFields', + plugins: { + ptype: 'cellediting', + clicksToEdit: 1 + }, + bbar: [ + { + xtype: 'button', + iconCls: 'fugue-icon database--plus', + itemId: 'addQueryFieldButton', + text: i18n("Add Query Field…") + }, { + xtype: 'button', + iconCls: 'fugue-icon database--minus', + text: i18n("Delete Query Field"), + itemId: 'deleteQueryFieldButton', + disabled: true + } + ], + columns: [ + { + header: i18n("Property"), + dataIndex: "property", + flex: 1 + }, { + header: i18n("Operator"), + dataIndex: "operator", + flex: 0.5 + }, { + header: i18n("Value"), + dataIndex: "value", + flex: 1 + }, { + xtype: 'checkcolumn', + header: i18n("Dynamic"), + editable: true, + dataIndex: "dynamic", + flex: 0.5 + }, { + header: i18n("Description"), + editable: true, + dataIndex: "description", + editor: { + xtype: 'textfield', + allowBlank: true + }, + flex: 2 + } + ] + } + ] + }, + { + xtype: 'fieldcontainer', + fieldLabel: i18n("Update Fields"), + layout: 'fit', + flex: 1, + items: [ + { + xtype: 'grid', + itemId: 'batchJobUpdateFields', + plugins: { + ptype: 'cellediting', + pluginId: 'cellediting', + clicksToEdit: 1 + }, + bbar: [ + { + xtype: 'button', + iconCls: 'fugue-icon database--plus', + itemId: 'addUpdateFieldButton', + text: i18n("Add Update Field…") + }, { + xtype: 'button', + iconCls: 'fugue-icon database--minus', + text: i18n("Delete Update Field"), + itemId: 'deleteUpdateFieldButton', + disabled: true + } + ], + columns: [ + { + header: i18n("Property"), + dataIndex: "property", + flex: 1 + }, { + header: i18n("Value"), + dataIndex: "value", + flex: 1 + }, { + xtype: 'checkcolumn', + header: i18n("Dynamic"), + editable: true, + dataIndex: "dynamic", + flex: 0.5 + }, { + header: i18n("Description"), + editable: true, + dataIndex: "description", + editor: { + xtype: 'textfield', + allowBlank: true + }, + flex: 2 + } + ] + } + ] + } + ], + saveText: i18n("Save Batch Job"), + + initComponent: function () + { + this.on("startEdit", this.onEditStart, this, { delay: 50 }); + + this.callParent(arguments); + + this.down("#addQueryFieldButton").on("click", this.onAddQueryFieldButtonClick, this); + this.down("#deleteQueryFieldButton").on("click", this.onDeleteQueryFieldButtonClick, this); + this.down("#batchJobQueryFields").getSelectionModel().on("selectionchange", this.onSelectionChange, this); + this.down("#addUpdateFieldButton").on("click", this.onAddUpdateFieldButtonClick, this); + this.down("#deleteUpdateFieldButton").on("click", this.onDeleteUpdateFieldButtonClick, this); + this.down("#batchJobUpdateFields").getSelectionModel().on("selectionchange", this.onUpdateSelectionChange, + this); + + + }, + beforeFieldSelection: function (selectorField) + { + var sourceModel = Ext.ClassManager.get(this.down("#baseEntity").getValue()); + + if (sourceModel === null) { + Ext.Msg.alert(i18n("Base Entity not selected"), + i18n("You need to select a base entity to perform the query against")); + return; + } + + selectorField.setBaseEntity(sourceModel); + + return true; + }, + /** + * Enables or disables the delete button, depending on the row selection + */ + onSelectionChange: function (selModel, selections) + { + this.down("#deleteQueryFieldButton").setDisabled(selections.length === 0); + }, + /** + * Enables or disables the delete button, depending on the row selection + */ + onUpdateSelectionChange: function (selModel, selections) + { + this.down("#deleteUpdateFieldButton").setDisabled(selections.length === 0); + }, + onAddQueryFieldButtonClick: function () + { + var sourceModel = Ext.ClassManager.get(this.down("#baseEntity").getValue()); + + if (sourceModel === null) { + Ext.Msg.alert(i18n("Base Entity not selected"), + i18n("You need to select a base entity to perform the query against")); + return; + } + this.addFilterWindow = Ext.create("PartKeepr.Widgets.FilterExpressionWindow", { + sourceModel: sourceModel, + listeners: { + "applyfilter": this.onAddFilter, + scope: this + } + }); + this.addFilterWindow.show(); + }, + onAddUpdateFieldButtonClick: function () + { + var sourceModel = Ext.ClassManager.get(this.down("#baseEntity").getValue()); + + if (sourceModel === null) { + Ext.Msg.alert(i18n("Base Entity not selected"), + i18n("You need to select a base entity to perform the query against")); + return; + } + + var j = Ext.create("PartKeepr.Components.BatchJob.BatchJobUpdateExpressionWindow", { + sourceModel: sourceModel, + listeners: { + applyexpression: this.onApplyUpdateExpression, + scope: this + } + }); + j.show(); + }, + onApplyUpdateExpression: function (field, value) + { + this.down("#batchJobUpdateFields").getStore().add({ + property: field, + value: value + }); + }, + onAddFilter: function (filter) + { + this.down("#batchJobQueryFields").getStore().add({ + property: filter.getProperty(), + operator: filter.getOperator(), + value: filter.getValue() + }); + }, + onDeleteQueryFieldButtonClick: function () + { + var selection = this.down("#batchJobQueryFields").getSelectionModel().getSelection()[0]; + if (selection) { + this.down("#batchJobQueryFields").getStore().remove(selection); + } + }, + onDeleteUpdateFieldButtonClick: function () + { + var selection = this.down("#batchJobUpdateFields").getSelectionModel().getSelection()[0]; + if (selection) { + this.down("#batchJobUpdateFields").getStore().remove(selection); + } + }, + /** + * Re-bind the store after an item was saved + */ + _onItemSaved: function (record) + { + this.down("#batchJobQueryFields").bindStore(record.batchJobQueryFields()); + }, + /** + * Bind the store as soon as the view was rendered. + */ + onEditStart: function () + { + var store = this.record.batchJobQueryFields(); + this.down("#batchJobQueryFields").bindStore(store); + + var store2 = this.record.batchJobUpdateFields(); + this.down("#batchJobUpdateFields").bindStore(store2); + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BatchJob/BatchJobEditorComponent.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BatchJob/BatchJobEditorComponent.js @@ -0,0 +1,24 @@ +Ext.define('PartKeepr.BatchJobEditorComponent', { + extend: 'PartKeepr.EditorComponent', + alias: 'widget.BatchJobEditorComponent', + navigationClass: 'PartKeepr.BatchJobGrid', + editorClass: 'PartKeepr.BatchJobEditor', + newItemText: i18n("New Batch Job"), + model: 'PartKeepr.BatchJobBundle.Entity.BatchJob', + initComponent: function () { + this.createStore({ + sorters: [{ + property: 'name', + direction:'ASC' + }] + }); + + this.callParent(); + }, + statics: { + iconCls: 'fugue-icon task', + title: i18n('Batch Jobs'), + closable: true, + menuPath: [{text: i18n("Edit")}] + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BatchJob/BatchJobExecutionWindow.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BatchJob/BatchJobExecutionWindow.js @@ -0,0 +1,253 @@ +Ext.define("PartKeepr.Components.BatchJob.BatchJobExecutionWindow", { + extend: "Ext.window.Window", + + width: 800, + height: 400, + layout: 'fit', + title: i18n("Execute Batch Job"), + items: [ + { + xtype: 'form', + items: [ + { + xtype: 'fieldcontainer', + fieldLabel: i18n("Query Fields"), + items: [ + { + xtype: 'grid', + height: 100, + itemId: 'queryFields', + viewConfig: { + markDirty: false + }, + columns: [] + } + ] + }, + { + xtype: 'fieldcontainer', + fieldLabel: i18n("Update Fields"), + items: [ + { + xtype: 'grid', + height: 100, + itemId: 'updateFields', + viewConfig: { + markDirty: false + }, + columns: [ + { + header: '', + width: 32, + renderer: function (val, metaData, record) + { + if (record.get("value") == "") { + return '<span title="OK" style="vertical-align: top;" class="web-icon error"></span>'; + } else { + return '<span title="OK" style="vertical-align: top;" class="web-icon accept"></span>'; + } + + } + }, + { + header: i18n("Property"), + flex: 1, + dataIndex: "property" + }, { + header: i18n("Description"), + flex: 2, + dataIndex: "description" + }, + { + header: i18n("Value"), + flex: 1, + dataIndex: "value" + } + ] + } + ] + } + ] + } + ], + + bbar: [ + { + xtype: 'button', + text: i18n("Execute Batch Job"), + disabled: true, + itemId: 'executeBatchJob' + } + ], + + batchJob: null, + + initComponent: function () + { + var i; + + this.callParent(arguments); + + var batchJobQueryFields = this.batchJob.batchJobQueryFields(); + batchJobQueryFields.setRemoteFilter(false); + batchJobQueryFields.filter({property: 'dynamic', operator: '=', value: true}); + + for (i = 0; i < batchJobQueryFields.getCount(); i++) { + batchJobQueryFields.getAt(i).set("value", ""); + } + + this.down("#queryFields").setStore(batchJobQueryFields); + + var batchJobUpdateFields = this.batchJob.batchJobUpdateFields(); + batchJobUpdateFields.setRemoteFilter(false); + batchJobUpdateFields.filter({property: 'dynamic', operator: '=', value: true}); + + for (i = 0; i < batchJobUpdateFields.getCount(); i++) { + batchJobUpdateFields.getAt(i).set("value", ""); + } + + this.down("#updateFields").setStore(batchJobUpdateFields); + + var columns = [ + { + header: '', + width: 32, + renderer: function (val, metaData, record) + { + if (record.get("value") == "") { + return '<span title="OK" style="vertical-align: top;" class="web-icon error"></span>'; + } else { + return '<span title="OK" style="vertical-align: top;" class="web-icon accept"></span>'; + } + + } + }, + { + header: i18n("Property"), + flex: 1, + dataIndex: "property" + }, { + header: i18n("Description"), + flex: 2, + dataIndex: "description" + }, { + header: i18n("Value"), + flex: 1, + dataIndex: "value" + }, { + width: 100, + xtype: 'widgetcolumn', + itemId: 'foobar', + widget: { + + xtype: 'button', + text: i18n("Set Value…"), + handler: this.onSetValueClick, + scope: this + } + } + ]; + + this.down("#updateFields").reconfigure(null, columns); + this.down("#queryFields").reconfigure(null, columns); + + this.validateExecuteBatchJobButton(); + this.down("#executeBatchJob").on("click", this.onExecuteBatchJob, this); + }, + onExecuteBatchJob: function () + { + var i, queryFieldConfig = [], updateFieldConfig = []; + + for (i = 0; i < this.batchJob.batchJobQueryFields().getCount(); i++) { + queryFieldConfig.push({ + property: this.batchJob.batchJobQueryFields().getAt(i).get("property"), + value: this.batchJob.batchJobQueryFields().getAt(i).get("value") + }); + } + + for (i = 0; i < this.batchJob.batchJobUpdateFields().getCount(); i++) { + updateFieldConfig.push({ + property: this.batchJob.batchJobUpdateFields().getAt(i).get("property"), + value: this.batchJob.batchJobUpdateFields().getAt(i).get("value") + }); + } + + this.batchJob.callPutAction("execute", { + queryFields: Ext.encode(queryFieldConfig), + updateFields: Ext.encode(updateFieldConfig) + }); + }, + validateExecuteBatchJobButton: function () + { + var valid = true, i; + + var batchJobQueryFields = this.batchJob.batchJobQueryFields(); + + for (i = 0; i < batchJobQueryFields.getCount(); i++) { + if (batchJobQueryFields.getAt(i).get("value") == "") { + valid = false; + } + } + + var batchJobUpdateFields = this.batchJob.batchJobUpdateFields(); + + for (i = 0; i < batchJobUpdateFields.getCount(); i++) { + if (batchJobUpdateFields.getAt(i).get("value") == "") { + valid = false; + } + } + + this.down("#executeBatchJob").setDisabled(!valid); + }, + onSetValueClick: function (widgetColumn) + { + this.editingRecord = widgetColumn.getWidgetRecord(); + + + var baseEntity = this.batchJob.get("baseEntity"); + + var baseModel = Ext.create(baseEntity); + + var type = baseModel.getFieldType(this.editingRecord.get("property")); + + if (type.type === "field") { + Ext.Msg.prompt(i18n("Enter the value"), this.editingRecord.get("description"), this.onValueEntered, this); + } else { + this.entitySelector = Ext.create("Ext.window.Window", { + items: Ext.create("PartKeepr.Widgets.EntityPicker", { + model: Ext.ClassManager.get(type.reference), + listeners: { + entityselect: this.onEntitySelect, + scope: this + }, + ittemId: "entitySelectorPanel" + }), + title: i18n("Select entity"), + width: "80%", + height: "80%", + modal: true, + layout: 'fit', + maximizable: true, + closeAction: 'destroy' + }); + + this.entitySelector.show(); + } + + + }, + onValueEntered: function (btn, value) + { + if (btn === "ok") { + this.editingRecord.set("value", value); + } + + this.validateExecuteBatchJobButton(); + }, + onEntitySelect: function (entity) + { + this.editingRecord.set("value", entity.getId()); + this.entitySelector.close(); + this.validateExecuteBatchJobButton(); + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BatchJob/BatchJobGrid.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BatchJob/BatchJobGrid.js @@ -0,0 +1,49 @@ +Ext.define('PartKeepr.BatchJobGrid', { + extend: 'PartKeepr.EditorGrid', + alias: 'widget.BatchJobGrid', + columns: [ + {header: i18n("Batch Job"), dataIndex: 'name', flex: 1} + ], + addButtonText: i18n("Add Batch Job"), + addButtonIconCls: 'fugue-icon task--plus', + deleteButtonText: i18n("Delete Batch Job"), + deleteButtonIconCls: 'fugue-icon task--minus', + automaticPageSize: true, + initComponent: function () + { + this.callParent(); + + this.executeBatchJob = Ext.create("Ext.button.Button", { + iconCls: 'fugue-icon task--arrow', + tooltip: i18n("Execute Batch Job"), + handler: this.onExecuteBatchJob, + disabled: true, + scope: this + }); + this.topToolbar.insert(2, {xtype: 'tbseparator'}); + this.topToolbar.insert(3, this.executeBatchJob); + }, + /** + * Called when an item was selected. Enables/disables the delete button. + */ + _updateDeleteButton: function () + { + this.callParent(arguments); + + /* Right now, we support delete on a single record only */ + if (this.getSelectionModel().getCount() == 1) { + this.executeBatchJob.enable(); + } else { + this.executeBatchJob.disable(); + } + }, + onExecuteBatchJob: function () + { + var selection = this.getSelectionModel().getSelection(); + + var j = Ext.create("PartKeepr.Components.BatchJob.BatchJobExecutionWindow", { + batchJob: selection[0] + }); + j.show(); + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BatchJob/BatchJobUpdateExpression.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BatchJob/BatchJobUpdateExpression.js @@ -0,0 +1,161 @@ +Ext.define("PartKeepr.Components.BatchJob.BatchJobUpdateExpression", { + extend: "Ext.form.Panel", + + xtype: 'partkeepr.batchjobupdateexpression', + + layout: { + type: 'vbox', + align: 'stretch', + pack: 'start' + }, + + minHeight: 150, + minWidth: 400, + width: 400, + height: 150, + requires: [], + + bbar: [ + { + xtype: 'button', + itemId: 'apply', + disabled: true, + text: i18n("Apply") + } + ], + items: [ + { + xtype: 'fieldcontainer', + fieldLabel: i18n("Field"), + layout: 'hbox', + items: [ + { + flex: 1, + xtype: 'textfield', + itemId: "field", + emptyText: i18n("Select a field"), + readOnly: true + }, + { + width: 100, + xtype: 'button', + itemId: "selectField", + text: i18n("Select field") + } + ] + }, + { + xtype: 'fieldcontainer', + fieldLabel: i18n("Value"), + flex: 1, + itemId: 'value', + layout: 'hbox', + border: false, + items: [ + { + disabled: true, + itemId: "valueField", + xtype: 'textfield', + flex: 1 + }, + { + width: 100, + xtype: 'button', + hidden: true, + itemId: "selectEntity", + text: i18n("Select Entity") + } + ] + }, + ], + + sourceModel: null, + objectFilter: null, + + initComponent: function () + { + this.callParent(arguments); + + this.down("#selectField").on("click", this.onFieldSelectClick, this); + this.down("#selectEntity").on("click", this.onEntitySelectClick, this); + this.down("#apply").on("click", this.onApplyClick, this); + }, + onApplyClick: function () + { + this.fireEvent("applyexpression", this.selectedField.data.data.name, this.down("#valueField").getValue()); + }, + validateApplyButton: function () + { + var applyButton = this.down("#apply"); + + if (this.selectedField.data.data.type == "manytoone") { + if (this.down("#valueField").getValue() === "") { + applyButton.setDisabled(true); + return; + } + } + + applyButton.setDisabled(false); + }, + onFieldSelectClick: function () + { + this.modelFieldSelectorWindow = Ext.create("PartKeepr.Components.Widgets.FieldSelectorWindow", { + sourceModel: this.sourceModel + }); + this.modelFieldSelectorWindow.on("fieldSelect", function (field) + { + this.updateValueFieldState(field); + this.down("#field").setValue(field.data.data.name); + }, this); + this.modelFieldSelectorWindow.show(); + }, + updateValueFieldState: function (record) + { + this.selectedField = record; + + + if (record.data.data.type == "manytoone") { + this.down("#selectEntity").show(); + this.down("#valueField").setReadOnly(true); + } else { + this.down("#selectEntity").hide(); + this.down("#valueField").setReadOnly(false); + } + + this.down("#valueField").setDisabled(false); + + this.validateApplyButton(); + }, + onEntitySelectClick: function () + { + this.entitySelector = Ext.create("Ext.window.Window", { + items: Ext.create("PartKeepr.Widgets.EntityPicker", { + model: this.selectedField.data.data.reference, + listeners: { + entityselect: this.onEntitySelect, + scope: this + }, + ittemId: "entitySelectorPanel" + }), + title: i18n("Select entity"), + width: "80%", + height: "80%", + modal: true, + layout: 'fit', + maximizable: true, + closeAction: 'destroy' + }); + + this.entitySelector.show(); + }, + /** + * @param entity {Ext.data.Model} The entity + */ + onEntitySelect: function (entity) + { + this.down("#valueField").setValue(entity.getId()); + + this.entitySelector.close(); + this.validateApplyButton(); + }, +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BatchJob/BatchJobUpdateExpressionWindow.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/BatchJob/BatchJobUpdateExpressionWindow.js @@ -0,0 +1,26 @@ +Ext.define("PartKeepr.Components.BatchJob.BatchJobUpdateExpressionWindow", { + extend: "Ext.window.Window", + modal: true, + layout: 'fit', + title: i18n("Add Filter Expression"), + sourceModel: null, + + initComponent: function () + { + this.items = { + xtype: "partkeepr.batchjobupdateexpression", + sourceModel: this.sourceModel, + listeners: { + "applyexpression": this.onApplyExpression, + scope: this + } + }; + this.callParent(arguments); + }, + + onApplyExpression: function (field, value) + { + this.fireEvent("applyexpression", field, value); + this.close(); + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/MenuBar.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/MenuBar.js @@ -7,10 +7,10 @@ Ext.define('PartKeepr.MenuBar', { }, createMenu: function (target, menuPath, root) { - var item = menuPath.shift(); + var item = menuPath.shift(), newItem; if (item === undefined) { - var newItem = {text: target.title, iconCls: target.iconCls, target: target}; + newItem = {text: target.title, iconCls: target.iconCls, target: target}; root.menu.push(newItem); return root; @@ -26,7 +26,7 @@ Ext.define('PartKeepr.MenuBar', { } if (foundItem === false) { - var newItem = {menu: []}; + newItem = {menu: []}; Ext.applyIf(newItem, item); @@ -40,7 +40,7 @@ Ext.define('PartKeepr.MenuBar', { return root; }, initComponent: function () { - var target, menus, menuItemIterator, menuPathIterator; + var target, menuItemIterator; this.ui = "mainmenu"; @@ -59,6 +59,7 @@ Ext.define('PartKeepr.MenuBar', { "PartKeepr.UserEditorComponent", "PartKeepr.PartMeasurementUnitEditorComponent", "PartKeepr.UnitEditorComponent", + "PartKeepr.BatchJobEditorComponent", // View Menu "PartKeepr.SummaryStatisticsPanel", @@ -66,7 +67,7 @@ Ext.define('PartKeepr.MenuBar', { "PartKeepr.SystemInformationGrid", "PartKeepr.ProjectReportView", "PartKeepr.SystemNoticeEditorComponent", - "PartKeepr.StockHistoryGrid" + "PartKeepr.StockHistoryGrid", ]; @@ -74,11 +75,11 @@ Ext.define('PartKeepr.MenuBar', { target = Ext.ClassManager.get(menuItems[menuItemIterator]); if (!target) { - console.log("Error: " + menuItems[menuItemIterator] + " not found!"); + Ext.raise("Error: " + menuItems[menuItemIterator] + " not found!"); } if (!target.menuPath) { - console.log("Error: " + menuItems[menuItemIterator] + " has no menuPath defined!"); + Ext.raise("Error: " + menuItems[menuItemIterator] + " has no menuPath defined!"); } this.createMenu(target, target.menuPath, this.menu); } diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Panel.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Panel.js @@ -10,7 +10,8 @@ Ext.define('PartKeepr.Components.SystemPreferences.Panel', { "PartKeepr.Components.SystemPreferences.Preferences.RequiredPartFields", "PartKeepr.Components.SystemPreferences.Preferences.RequiredPartManufacturerFields", "PartKeepr.Components.SystemPreferences.Preferences.RequiredPartDistributorFields", - "PartKeepr.Components.SystemPreferences.Preferences.BarcodeScannerConfiguration" + "PartKeepr.Components.SystemPreferences.Preferences.BarcodeScannerConfiguration", + "PartKeepr.Components.SystemPreferences.Preferences.ActionsConfiguration" ]; var settingItems = [], item; diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/ActionsConfiguration.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/SystemPreferences/Preferences/ActionsConfiguration.js @@ -0,0 +1,159 @@ +Ext.define('PartKeepr.Components.SystemPreferences.Preferences.ActionsConfiguration', { + extend: 'PartKeepr.Components.SystemPreferences.PreferenceEditor', + + initComponent: function () + { + + this.batchJobStore = Ext.create("Ext.data.Store", { + model: 'PartKeepr.BatchJobBundle.Entity.BatchJob', + autoLoad: true + }); + + this.actionsStore = Ext.create("Ext.data.Store", { + fields: [ + {name: 'baseEntity'}, + {name: 'action'}, + {name: 'batchJob'}, + {name: 'batchJobName'} + ] + }); + + this.items = [ + { + xtype: 'fieldcontainer', + fieldLabel: i18n("Actions"), + items: [ + { + xtype: 'grid', + height: 200, + itemId: 'actionGrid', + plugins: { + ptype: 'cellediting', + clicksToEdit: 1, + pluginId: 'editing' + }, + listeners: { + selectionchange: this.onSelectionChange, + edit: this.onGridEdit, + scope: this + }, + store: this.actionsStore, + columns: [ + { + header: i18n("Entity"), + flex: 1, + dataIndex: "baseEntity", + editor: { + xtype: 'combo', + store: Ext.StoreManager.lookup("ModelStore"), + displayField: 'model', + queryMode: 'local', + editable: false, + forceSelection: true, + valueField: 'model', + } + }, { + header: i18n("Action"), + dataIndex: "action", + flex: 1, + editor: { + xtype: 'textfield' + } + }, { + header: i18n("Batch Job"), + dataIndex: "batchJob", + flex: 1, + editor: { + xtype: 'combo', + store: this.batchJobStore, + displayField: 'name', + editable: false, + forceSelection: true, + valueField: '@id', + }, + renderer: this.renderBatchJob, + scope: this + } + ], + bbar: [ + { + xtype: 'button', + text: i18n("Add Action"), + itemId: 'actionAdd', + handler: this.onAddAction, + scope: this + }, { + xtype: 'button', + text: i18n("Delete Action"), + disabled: true, + itemId: 'actionDelete' + } + ] + } + ] + }, + ]; + + this.callParent(arguments); + + var actions = PartKeepr.getApplication().getSystemPreference("partkeepr.actions", []); + + for (var i = 0; i < actions.length; i++) { + this.actionsStore.add(actions[i]); + } + + }, + onGridEdit: function (editor, e) { + if (e.field === "batchJob") { + var batchJob = this.batchJobStore.getById(e.value); + + if (batchJob !== null) { + e.record.set("batchJobName", batchJob.get("name")); + } + } + }, + renderBatchJob: function (value, metaData, record) + { + return record.get("batchJobName"); + }, + onAddAction: function () + { + var grid = this.down("#actionGrid"); + grid.getPlugin("editing").cancelEdit(); + + grid.getStore().insert(0, {}); + + grid.getPlugin("editing").startEdit(0, 0); + }, + onSelectionChange: function (grid, selection) + { + if (selection.length === 1) { + this.down("#actionDelete").setDisabled(false); + } else { + this.down("#actionDelete").setDisabled(true); + } + }, + onSave: function () + { + var data = this.down("#actionGrid").getStore().getData(); + var actions = []; + + for (var i = 0; i < data.length; i++) { + var item = data.getAt(i); + + actions.push({ + baseEntity: item.get("baseEntity"), + action: item.get("action"), + batchJob: item.get("batchJob"), + batchJobName: item.get("batchJobName") + }); + } + + PartKeepr.getApplication().setSystemPreference("partkeepr.actions", actions); + }, + statics: { + iconCls: 'fugue-icon task--arrow', + title: i18n('Actions'), + menuPath: [] + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FieldSelectTrigger.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FieldSelectTrigger.js @@ -0,0 +1,62 @@ +/** + * A part picker with an attached grid. + */ +Ext.define("PartKeepr.Components.Widgets.FieldSelectTrigger", { + extend: "Ext.form.field.Picker", + xtype: 'fieldSelectTrigger', + selectedValue: null, + editable: false, + + config: { + baseEntity: null + }, + + /** + * Initializes the component. + */ + initComponent: function () + { + this.callParent(); + this.createPicker(); + + // Automatically expand field when focused + this.on("focus", function () + { + this.onTriggerClick(); + }, this); + }, + createPicker: function () + { + this.fireEvent("beforeSelect", this); + + this.modelFieldSelector = Ext.create({ + xtype: 'modelFieldSelector', + border: false, + sourceModel: this.baseEntity, + useCheckBoxes: false, + flex: 1 + }); + + this.picker = Ext.create("Ext.panel.Panel", { + shrinkWrapDock: 2, + layout: 'fit', + floating: true, + focusOnToFront: false, + manageHeight: false, + height: 300, + minWidth: 350, + shadow: false, + ownerCmp: this, + items: [this.modelFieldSelector] + }); + + this.modelFieldSelector.on("select", + function (selModel, record) + { + this.setValue(record.data.data.name); + this.collapse(); + }, this); + + return this.picker; + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FieldSelectorWindow.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FieldSelectorWindow.js @@ -0,0 +1,69 @@ +Ext.define("PartKeepr.Components.Widgets.FieldSelectorWindow", { + extend: 'Ext.window.Window', + + layout: 'fit', + width: 600, + height: 600, + title: i18n("Select Field"), + + config: { + sourceModel: null + }, + + initComponent: function () + { + var modelFieldSelector = Ext.create({ + xtype: 'modelFieldSelector', + border: false, + sourceModel: this.sourceModel, + useCheckBoxes: false, + flex: 1, + listeners: { + selectionchange: function (selectionModel, selected) + { + var addFieldButton = this.down("#addSelectedField"); + + if (selected.length == 1 && selected[0].data.data.type !== "onetomany") { + addFieldButton.enable(); + } else { + addFieldButton.disable(); + } + }, + scope: this + } + }); + + modelFieldSelector.on("itemdblclick", function (view, record) + { + if (record.data.data && record.data.data.type !== "onetomany") { + + this.fireEvent("fieldSelect", record); + this.close(); + } + }, this); + + this.items = modelFieldSelector; + + this.bbar = [ + { + xtype: 'button', + itemId: 'addSelectedField', + disabled: true, + text: i18n("Add selected Field"), + iconCls: 'fugue-icon flask--plus', + handler: function () + { + var selection = modelFieldSelector.getSelection(); + + if (selection.length == 1 && selection[0].data.data.type !== "onetomany") { + this.fireEvent("fieldSelect", selection[0]); + this.close(); + } + }, + scope: this + } + ]; + + this.callParent(arguments); + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FilterExpression.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FilterExpression.js @@ -241,69 +241,13 @@ Ext.define("PartKeepr.Widgets.FilterExpression", { }, onFieldSelectClick: function () { - var modelFieldSelector = Ext.create({ - xtype: 'modelFieldSelector', - id: 'searchPartFieldSelector', - border: false, - sourceModel: this.sourceModel, - useCheckBoxes: false, - flex: 1, - listeners: { - selectionchange: function (selectionModel, selected) - { - var addFieldButton = this.up("#filterSelectWindow").down("#addSelectedField"); - - if (selected.length == 1 && selected[0].data.data.type !== "onetomany") { - addFieldButton.enable(); - } else { - addFieldButton.disable(); - } - } - } - }); - - modelFieldSelector.on("itemdblclick", function (view, record) - { - if (record.data.data && record.data.data.type !== "onetomany") { - - this.down("#field").setValue(record.data.data.name); - - this.updateValueFieldState(record); - - this.modelFieldSelectorWindow.close(); - } - }, this); - - this.modelFieldSelectorWindow = Ext.create("Ext.window.Window", { - layout: 'fit', - width: 600, - height: 600, - title: i18n("Select Field"), - itemId: 'filterSelectWindow', - items: modelFieldSelector, - bbar: [ - { - xtype: 'button', - itemId: 'addSelectedField', - disabled: true, - text: i18n("Add selected Field"), - iconCls: 'fugue-icon flask--plus', - handler: function () - { - var selection = modelFieldSelector.getSelection(); - - if (selection.length == 1 && selection[0].data.data.type !== "onetomany") { - this.down("#field").setValue(selection[0].data.data.name); - - this.updateValueFieldState(selection[0]); - this.modelFieldSelectorWindow.close(); - } - }, - scope: this - } - ] + this.modelFieldSelectorWindow = Ext.create("PartKeepr.Components.Widgets.FieldSelectorWindow", { + sourceModel: this.sourceModel }); - + this.modelFieldSelectorWindow.on("fieldSelect", function (field) { + this.updateValueFieldState(field); + this.down("#field").setValue(field.data.data.name); + }, this); this.modelFieldSelectorWindow.show(); }, updateValueFieldState: function (record) diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FilterExpressionWindow.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/FilterExpressionWindow.js @@ -0,0 +1,26 @@ +Ext.define("PartKeepr.Widgets.FilterExpressionWindow", { + extend: "Ext.window.Window", + modal: true, + layout: 'fit', + title: i18n("Add Filter Expression"), + sourceModel: null, + + initComponent: function () + { + this.items = { + xtype: "partkeepr.filterexpression", + sourceModel: this.sourceModel, + listeners: { + "applyfilter": this.onAddFilter, + scope: this + } + }; + this.callParent(arguments); + }, + + onAddFilter: function (filter) + { + this.fireEvent("applyfilter", filter); + this.close(); + } +}); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/PagingToolbar.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/PagingToolbar.js @@ -48,21 +48,20 @@ Ext.define("PartKeepr.PagingToolbar", { return items; }, - onAddFilterClick: function () { - this.addFilterWindow = Ext.create("Ext.window.Window", { - layout: 'fit', - items: { - xtype: "partkeepr.filterexpression", - sourceModel: this.getStore().getModel(), - listeners: { - "applyfilter": this.onAddFilter, - scope: this - } + onAddFilterClick: function () + { + this.addFilterWindow = Ext.create("PartKeepr.Widgets.FilterExpressionWindow", { + + sourceModel: this.getStore().getModel(), + listeners: { + "applyfilter": this.onAddFilter, + scope: this } }); this.addFilterWindow.show(); }, - onAddFilter: function (filter) { + onAddFilter: function (filter) + { this.getStore().addFilter(filter); this.addFilterWindow.close(); } diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraModel.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraModel.js @@ -40,6 +40,65 @@ Ext.define("PartKeepr.data.HydraModel", { return ret; }, /** + * Returns the field type for a given field path as an object with the following properties: + * + * { + * type: "field", "onetomany" or "manytoone" + * reference: Only set if the type is "onetomany" or "manytoone" - holds the class name for the relation + * } + */ + getFieldType: function (fieldName) + { + var ret = null, role, tmp, i; + + for (i=0;i<this.fields.length;i++) { + if (this.fields[i].getName() === fieldName) { + if (this.fields[i].reference !== null) { + ret = { + type: "onetomany", + reference: this.fields[i].reference + }; + } else { + ret = { + type: "field" + }; + } + } + } + + if (this.associations[fieldName]) { + return { + type: "manytoone", + reference: this.associations[fieldName].type + }; + } + + if (ret === null) { + // The field is undefined, attempt to retrieve data via associations + var parts = fieldName.split("."); + + if (parts.length < 2) { + return null; + } + + for (i=0;i<this.fields.length;i++) { + if (this.fields[i].getName() === parts[0]) { + parts.shift(); + tmp = Ext.create(this.fields[i].reference.type); + return tmp.getFieldType(parts.join(".")); + } + } + + if (this.associations[parts[0]]) { + role = this.associations[parts[0]]; + tmp = Ext.create(role.type); + parts.shift(); + return tmp.getFieldType(parts.join(".")); + } + } + return ret; + }, + /** * Gets all of the data from this Models *loaded* associations. It does this * recursively. For example if we have a User which hasMany Orders, and each Order * hasMany OrderItems, it will return an object like this: diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraProxy.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraProxy.js @@ -189,7 +189,7 @@ Ext.define("PartKeepr.data.HydraProxy", { request.setCallback(function (options, success, response) { - this.processCallActionResponse(options, success, response, ignoreException); + this.processCallActionResponse(options, success, response, ignoreException, action); if (Ext.isFunction(callback)) { callback(options, success, response); @@ -198,9 +198,21 @@ Ext.define("PartKeepr.data.HydraProxy", { this.sendRequest(request); }, - processCallActionResponse: function (options, success, response, ignoreException) + processCallActionResponse: function (options, success, response, ignoreException, action) { + var i; + if (success === true) { + var actions = PartKeepr.getApplication().getSystemPreference("partkeepr.actions", []); + + for (i = 0; i < actions.length; i++) { + if (this.getModel().$className === actions[i].baseEntity && action == actions[i].action) { + PartKeepr.BatchJobBundle.Entity.BatchJob.load(actions[i].batchJob, { + scope: this, + success: this.onBatchJobLoaded + }); + } + } return; } @@ -208,6 +220,12 @@ Ext.define("PartKeepr.data.HydraProxy", { this.showException(response); } }, + onBatchJobLoaded: function (record) { + var j = Ext.create("PartKeepr.Components.BatchJob.BatchJobExecutionWindow", { + batchJob: record + }); + j.show(); + }, showException: function (response) { PartKeepr.ExceptionWindow.showException(response); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Data/store/ModelStore.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Data/store/ModelStore.js @@ -0,0 +1,25 @@ +Ext.define("PartKeepr.Data.Store.ModelStore", { + extend: "Ext.data.Store", + + storeId: "ModelStore", + + fields: [ + { + name: "model", type: "string" + }, { + name: "description", type: "string" + } + ], + + statics: { + addModel: function (model, description) + { + Ext.StoreManager.lookup("ModelStore").add({ + model: model, + description: description + }); + } + } +}); + +Ext.create("PartKeepr.Data.Store.ModelStore"); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/PartKeepr.js b/src/PartKeepr/FrontendBundle/Resources/public/js/PartKeepr.js @@ -359,6 +359,9 @@ Ext.application({ */ getSystemPreference: function (key, defaultValue) { + if (this.systemPreferenceStore === undefined) { + return defaultValue; + } var record = this.systemPreferenceStore.findRecord("preferenceKey", key); if (record) { diff --git a/src/PartKeepr/FrontendBundle/Resources/views/index.html.twig b/src/PartKeepr/FrontendBundle/Resources/views/index.html.twig @@ -62,6 +62,7 @@ '@PartKeeprFrontendBundle/Resources/public/js/Data/field/Array.js' '@PartKeeprFrontendBundle/Resources/public/js/Data/HydraModel.js' '@PartKeeprFrontendBundle/Resources/public/js/Data/HydraTreeModel.js' + '@PartKeeprFrontendBundle/Resources/public/js/Data/store/ModelStore.js' '@PartKeeprFrontendBundle/Resources/public/js/ExtJS/Enhancements/Ext.data.field.Date-ISO8601.js' %} <script type="text/javascript" src="{{ asset_url }}"></script> @@ -92,6 +93,7 @@ '@PartKeeprFrontendBundle/Resources/public/js/Components/Importer/ImportFieldMatcherGrid.js' '@PartKeeprFrontendBundle/Resources/public/js/Data/store/OperatorStore.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/Widgets/FilterExpression.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/Widgets/FilterExpressionWindow.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/ModelTreeMaker/ModelTreeMaker.js' '@PartKeeprFrontendBundle/Resources/public/js/Util/Blob.js' '@PartKeeprFrontendBundle/Resources/public/js/Util/FileSaver.js' @@ -131,6 +133,8 @@ '@PartKeeprFrontendBundle/Resources/public/js/Ext.ux/ClearableComboBox.js' '@PartKeeprFrontendBundle/Resources/public/js/Util/ServiceCall.js' '@PartKeeprFrontendBundle/Resources/public/js/org.jerrymouse.util.locale/locale.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/Widgets/FieldSelectorWindow.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/Widgets/FieldSelectTrigger.js' '@PartKeeprFrontendBundle/Resources/public/js/ExtJS/Enhancements/Ext.grid.plugin.CellEditing-associationSupport.js' '@PartKeeprFrontendBundle/Resources/public/js/ExtJS/Enhancements/Ext.grid.plugin.Editing-associationSupport.js' '@PartKeeprFrontendBundle/Resources/public/js/ExtJS/Enhancements/Ext.form.field.ComboBox-associationSupport.js' @@ -234,6 +238,12 @@ '@PartKeeprFrontendBundle/Resources/public/js/Components/Footprint/FootprintEditorComponent.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/Footprint/FootprintNavigation.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/Footprint/FootprintGrid.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/BatchJob/BatchJobEditor.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/BatchJob/BatchJobEditorComponent.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/BatchJob/BatchJobGrid.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/BatchJob/BatchJobUpdateExpression.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/BatchJob/BatchJobUpdateExpressionWindow.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/BatchJob/BatchJobExecutionWindow.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/User/UserEditorComponent.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/SystemNotice/SystemNoticeEditorComponent.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationEditorComponent.js' @@ -264,6 +274,7 @@ '@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/SystemPreferences/Preferences/ActionsConfiguration.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' diff --git a/src/PartKeepr/ImageBundle/Tests/ImageControllerTest.php b/src/PartKeepr/ImageBundle/Tests/ImageControllerTest.php @@ -3,7 +3,6 @@ namespace PartKeepr\ImageBundle\Tests; use PartKeepr\CoreBundle\Tests\WebTestCase; -use PartKeepr\ImageBundle\Entity\TempImage; use Symfony\Component\HttpFoundation\File\UploadedFile; class ImageControllerTest extends WebTestCase @@ -63,6 +62,7 @@ class ImageControllerTest extends WebTestCase $uri ); + $this->assertEquals(404, $client->getResponse()->getStatusCode()); } }