partkeepr

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

commit 26136607dc0041ae815dcdbca235abae1b01efa3
parent 85bfb7877604abf613f4880325ed4656b12dd6bc
Author: Felicitus <felicitus@felicitus.org>
Date:   Sat, 25 Jul 2015 00:55:49 +0200

Initial implementation of hierarchical storage locations, re-added category POST operations

Diffstat:
Mapp/config/config.yml | 46++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/CategoryBundle/Controller/CategoryController.php | 37+++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Footprint/FootprintNavigation.js | 7+++++--
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationEditorComponent.js | 18++++++++++++------
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationGrid.js | 2+-
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationNavigation.js | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/FrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationTree.js | 39+++++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraProxy.js | 15+++++++++++++--
Msrc/PartKeepr/FrontendBundle/Resources/views/index.html.twig | 2++
Msrc/PartKeepr/StorageLocationBundle/Entity/StorageLocation.php | 32+++++++++++++++++++++++---------
10 files changed, 330 insertions(+), 20 deletions(-)

diff --git a/app/config/config.yml b/app/config/config.yml @@ -264,6 +264,27 @@ services: "hydra:title": "A custom operation" "returns": "xmls:string" + resource.footprint_category.collection_operation.post: + class: "Dunglas\ApiBundle\Api\Operation\Operation" + public: false + factory: [ "@api.operation_factory", "createCollectionOperation" ] + arguments: [ "@resource.footprint_category", "POST" ] + + resource.footprint_category.collection_operation.get_root: + class: "Dunglas\ApiBundle\Api\Operation\Operation" + public: false + factory: [ "@api.operation_factory", "createCollectionOperation" ] + arguments: + - "@resource.footprint_category" # Resource + - [ "GET" ] # Methods + - "/footprint_categories/getExtJSRootNode" # Path + - "PartKeeprCategoryBundle:Category:getExtJSRootNode" # Controller + - "FootprintCategoryGetRoot" # Route name + - # Context (will be present in Hydra documentation) + "@type": "hydra:Operation" + "hydra:title": "A custom operation" + "returns": "xmls:string" + resource.footprint_category.item_operation.get: class: "Dunglas\ApiBundle\Api\Operation\Operation" public: false @@ -287,6 +308,8 @@ services: arguments: [ "PartKeepr\FootprintBundle\Entity\FootprintCategory" ] tags: [ { name: "api.resource" } ] calls: + - method: "initCollectionOperations" + arguments: [ [ "@resource.footprint_category.collection_operation.get_root", "@resource.footprint_category.collection_operation.post" ] ] - method: "initItemOperations" arguments: [ [ "@resource.footprint_category.item_operation.get", "@resource.footprint_category.item_operation.put", "@resource.footprint_category.item_operation.delete", "@resource.footprint_category.item_operation.move" ] ] - method: "initNormalizationContext" @@ -449,6 +472,21 @@ services: "hydra:title": "A custom operation" "returns": "xmls:string" + resource.storage_location_category.collection_operation.get_root: + class: "Dunglas\ApiBundle\Api\Operation\Operation" + public: false + factory: [ "@api.operation_factory", "createCollectionOperation" ] + arguments: + - "@resource.storage_location_category" # Resource + - [ "GET" ] # Methods + - "/storage_location_categories/getExtJSRootNode" # Path + - "PartKeeprCategoryBundle:Category:getExtJSRootNode" # Controller + - "StorageLocationCategoryGetRoot" # Route name + - # Context (will be present in Hydra documentation) + "@type": "hydra:Operation" + "hydra:title": "A custom operation" + "returns": "xmls:string" + resource.storage_location_category.item_operation.get: class: "Dunglas\ApiBundle\Api\Operation\Operation" public: false @@ -467,11 +505,19 @@ services: factory: [ "@api.operation_factory", "createItemOperation" ] arguments: [ "@resource.storage_location_category", "DELETE" ] + resource.storage_location_category.collection_operation.post: + class: "Dunglas\ApiBundle\Api\Operation\Operation" + public: false + factory: [ "@api.operation_factory", "createCollectionOperation" ] + arguments: [ "@resource.storage_location_category", "POST" ] + resource.storage_location_category: parent: "api.resource" arguments: [ "PartKeepr\StorageLocationBundle\Entity\StorageLocationCategory" ] tags: [ { name: "api.resource" } ] calls: + - method: "initCollectionOperations" + arguments: [ [ "@resource.storage_location_category.collection_operation.get_root", "@resource.storage_location_category.collection_operation.post" ] ] - method: "initItemOperations" arguments: [ [ "@resource.storage_location_category.item_operation.get", "@resource.storage_location_category.item_operation.put", "@resource.storage_location_category.item_operation.delete", "@resource.storage_location_category.item_operation.move" ] ] - method: "initNormalizationContext" diff --git a/src/PartKeepr/CategoryBundle/Controller/CategoryController.php b/src/PartKeepr/CategoryBundle/Controller/CategoryController.php @@ -3,8 +3,10 @@ namespace PartKeepr\CategoryBundle\Controller; use Dunglas\ApiBundle\Controller\ResourceController; use FOS\RestBundle\Controller\Annotations\RequestParam; +use Gedmo\Tree\Entity\Repository\AbstractTreeRepository; use PartKeepr\CategoryBundle\Exception\MissingParentCategoryException; use PartKeepr\CategoryBundle\Exception\RootMayNotBeMovedException; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -45,4 +47,39 @@ class CategoryController extends ResourceController return new Response($request->request->get("parent")); } + + /** + * Returns the tree's root node wrapped in a virtual root node. + * + * This is required as ExtJS cannot replace the ID of their root node and cannot read in data one level below + * root. + * + * @param Request $request + * + * @return JsonResponse + */ + public function getExtJSRootNodeAction(Request $request) + { + $resource = $this->getResource($request); + + $repository = $this->getDoctrine()->getManager()->getRepository($resource->getEntityClass()); + + /** + * @var $repository AbstractTreeRepository + */ + $rootNode = $repository->getRootNodes()[0]; + + $data = $this->get('serializer')->normalize( + $rootNode, + 'json-ld', + $resource->getNormalizationContext() + ); + + $responseData = array("children" => $data); + + return new JsonResponse( + $responseData + ); + + } } diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Footprint/FootprintNavigation.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Footprint/FootprintNavigation.js @@ -10,7 +10,8 @@ Ext.define("PartKeepr.FootprintNavigation", { items: [ { xtype: 'partkeepr.FootprintTree', - region: 'center' + region: 'center', + rootVisible: false }, { xtype: 'partkeepr.FootprintGrid', resizable: true, @@ -45,10 +46,12 @@ Ext.define("PartKeepr.FootprintNavigation", { } ], root: { - "@id": PartKeepr.FootprintBundle.Entity.FootprintCategory.getProxy().getConfig("url") + "/1" + "@id": "@local-tree-root" }, model: "PartKeepr.FootprintBundle.Entity.FootprintCategory", proxy: { + ignoreLoadId: '@local-tree-root', + url: "/api/footprint_categories/getExtJSRootNode", type: "Hydra", appendId: false, reader: { diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationEditorComponent.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationEditorComponent.js @@ -1,16 +1,22 @@ Ext.define('PartKeepr.StorageLocationEditorComponent', { extend: 'PartKeepr.EditorComponent', alias: 'widget.StorageLocationEditorComponent', - navigationClass: 'PartKeepr.StorageLocationGrid', + navigationClass: 'PartKeepr.StorageLocationNavigation', editorClass: 'PartKeepr.StorageLocationEditor', newItemText: i18n("New Storage Location"), - model: 'PartKeepr.StorageLocation', + model: 'PartKeepr.StorageLocationBundle.Entity.StorageLocation', initComponent: function () { this.createStore({ - sorters: [{ - property: 'name', - direction:'ASC' - }] + sorters: [ + { + property: 'category.categoryPath', + direction: 'ASC' + },{ + property: 'name', + direction:'ASC' + } + ], + groupField: 'categoryPath' }); this.callParent(); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationGrid.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationGrid.js @@ -1,6 +1,6 @@ Ext.define('PartKeepr.StorageLocationGrid', { extend: 'PartKeepr.EditorGrid', - alias: 'widget.StorageLocationGrid', + xtype: 'partkeepr.StorageLocationGrid', automaticPageSize: true, diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationNavigation.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationNavigation.js @@ -0,0 +1,151 @@ +Ext.define("PartKeepr.StorageLocationNavigation", { + extend: 'Ext.panel.Panel', + + layout: 'border', + + /** + * @var {Ext.data.Store} + */ + store: null, + items: [ + { + xtype: 'partkeepr.StorageLocationTree', + region: 'center', + rootVisible: false + }, { + xtype: 'partkeepr.StorageLocationGrid', + resizable: true, + split: true, + region: 'south', + height: "50%", + viewConfig: { + plugins: { + ddGroup: 'StorageLocationTree', + ptype: 'gridviewdragdrop', + enableDrop: false + } + }, + enableDragDrop: true + } + ], + + initComponent: function () + { + this.callParent(arguments); + + this.treeStore = Ext.create("Ext.data.TreeStore", + { + remoteSort: false, + folderSort: true, + rootVisible: true, + autoLoad: true, + sorters: [ + { + property: 'name', + direction: 'ASC' + } + ], + root: { + "@id": "@local-tree-root" + }, + model: "PartKeepr.StorageLocationBundle.Entity.StorageLocationCategory", + proxy: { + ignoreLoadId: '@local-tree-root', + url: "/api/storage_location_categories/getExtJSRootNode", + type: "Hydra", + appendId: false, + reader: { + type: 'json' + } + + } + }); + + this.down("partkeepr\\.StorageLocationTree").setStore(this.treeStore); + this.down("partkeepr\\.StorageLocationTree").on("itemclick", this.onCategoryClick, this); + this.down("partkeepr\\.StorageLocationGrid").setStore(this.store); + this.down("partkeepr\\.StorageLocationGrid").on("itemAdd", this.onAddStorageLocation, this); + this.down("partkeepr\\.StorageLocationGrid").on("itemDelete", function (id) + { + this.fireEvent("itemDelete", id); + }, this + ); + this.down("partkeepr\\.StorageLocationGrid").on("itemEdit", function (id) + { + this.fireEvent("itemEdit", id); + }, this + ); + + }, + /** + * Applies the category filter to the store when a category is selected + * + * @param {Ext.tree.View} tree The tree view + * @param {Ext.data.Model} record the selected record + */ + onCategoryClick: function (tree, record) + { + var filter = Ext.create("Ext.util.Filter", { + property: 'category', + operator: 'IN', + value: this.getChildrenIds(record) + }); + + this.store.addFilter(filter); + }, + /** + * Returns the ID for this node and all child nodes + * + * @param {Ext.data.Model} The node + * @return Array + */ + getChildrenIds: function (node) + { + var childNodes = [node.getId()]; + + if (node.hasChildNodes()) { + for (var i = 0; i < node.childNodes.length; i++) { + childNodes = childNodes.concat(this.getChildrenIds(node.childNodes[i])); + } + } + + return childNodes; + }, + /** + * Called when a storage location is about to be added. This prepares the to-be-edited record with the proper category id. + */ + onAddStorageLocation: function () + { + var selection = this.down("partkeepr\\.StorageLocationTree").getSelection(); + + var category; + if (selection.length === 0) { + category = this.down("partkeepr\\.StorageLocationTree").getRootNode().getId(); + } else { + var item = selection.shift(); + category = item.getId(); + } + + this.fireEvent("itemAdd", { + category: category + }); + }, + /** + * Triggers a reload of the store when an edited record affects the store + */ + syncChanges: function () + { + this.down("partkeepr\\.StorageLocationGrid").getStore().load(); + }, + /** + * Returns the selection model of the storage location grid + * @return {Ext.selection.Model} The selection model + */ + getSelectionModel: function () + { + "use strict"; + return this.down("partkeepr\\.StorageLocationGrid").getSelectionModel(); + } + + +});+ \ No newline at end of file diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationTree.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationTree.js @@ -0,0 +1,38 @@ +Ext.define("PartKeepr.StorageLocationTree", { + extend: 'PartKeepr.CategoryEditorTree', + alias: 'widget.StorageLocationTree', + xtype: 'partkeepr.StorageLocationTree', + viewConfig: { + plugins: { + ptype: 'treeviewdragdrop', + sortOnDrop: true, + ddGroup: 'StorageLocationTree' + } + }, + folderSort: true, + + categoryModel: "PartKeepr.StorageLocationBundle.Entity.StorageLocationCategory", + + /** + * @cfg {String} text The path to the 'add' icon + */ + addButtonIcon: 'bundles/partkeeprfrontend/images/icons/footprint_add.png', + + /** + * @cfg {String} text The path to the 'delete' icon + */ + deleteButtonIcon: 'bundles/partkeeprfrontend/images/icons/footprint_delete.png', + + listeners: { + "foreignModelDrop": function (record, target) { + record.setCategory(target); + record.save({ + success: function() { + if (record.store && record.store.reload) { + record.store.reload(); + } + } + }); + } + } +});+ \ No newline at end of file diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraProxy.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Data/HydraProxy.js @@ -13,13 +13,22 @@ Ext.define("PartKeepr.data.HydraProxy", { defaultListenerScope: true, sortParam: "order", + /** + * An ID which should be ignored when loading items. Usually we use the item ID as URL as per JSON-LD spec, + * but sometimes you might require loading an item from the url parameter instead. + * + * This is mainly a workaround for ExtJS trees because we need a virtual root node for which the ID cannot be + * changed. + */ + ignoreLoadId: null, + constructor: function (config) { config.url = PartKeepr.getBasePath() + config.url; this.callParent(arguments); }, listeners: { - exception: function (reader, response, operation, eOpts) + exception: function (reader, response) { this.showException(response); } @@ -31,7 +40,9 @@ Ext.define("PartKeepr.data.HydraProxy", { // Set the URI to the ID, as JSON-LD operates on IRIs. if (request.getAction() == "read") { if (operation.getId()) { - request.setUrl(operation.getId()); + if (operation.getId() !== this.ignoreLoadId) { + request.setUrl(operation.getId()); + } } } diff --git a/src/PartKeepr/FrontendBundle/Resources/views/index.html.twig b/src/PartKeepr/FrontendBundle/Resources/views/index.html.twig @@ -148,6 +148,7 @@ '@PartKeeprFrontendBundle/Resources/public/js/Components/Project/ProjectEditorComponent.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationMultiAddWindow.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationMultiAddDialog.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationNavigation.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/Project/ProjectReport.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/Statistics/StatisticsChart.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/Statistics/StatisticsChartPanel.js' @@ -156,6 +157,7 @@ '@PartKeeprFrontendBundle/Resources/public/js/Components/TipOfTheDay/TipOfTheDayWindow.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/CategoryTree.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/CategoryEditor/CategoryEditorTree.js' + '@PartKeeprFrontendBundle/Resources/public/js/Components/StorageLocation/StorageLocationTree.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/Part/PartCategoryTree.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/Footprint/FootprintTree.js' '@PartKeeprFrontendBundle/Resources/public/js/Components/CategoryEditor/CategoryEditorWindow.js' diff --git a/src/PartKeepr/StorageLocationBundle/Entity/StorageLocation.php b/src/PartKeepr/StorageLocationBundle/Entity/StorageLocation.php @@ -2,10 +2,14 @@ namespace PartKeepr\StorageLocationBundle\Entity; use Doctrine\ORM\Mapping as ORM; +use PartKeepr\DoctrineReflectionBundle\Annotation\TargetService; use PartKeepr\Util\BaseEntity; +use PartKeepr\UploadedFileBundle\Annotation\UploadedFile; use Symfony\Component\Serializer\Annotation\Groups; -/** @ORM\Entity * */ +/** @ORM\Entity + * @TargetService(uri="/api/storage_locations") + */ class StorageLocation extends BaseEntity { /** @@ -22,7 +26,7 @@ class StorageLocation extends BaseEntity * @ORM\OneToOne(targetEntity="PartKeepr\StorageLocationBundle\Entity\StorageLocationImage", * mappedBy="storageLocation",cascade={"persist", "remove"}) * @Groups({"default"}) - * + * @UploadedFile() * @var StorageLocationImage */ private $image; @@ -96,20 +100,30 @@ class StorageLocation extends BaseEntity } /** - * Sets the storage location image + * Sets the footprint image + * + * @param StorageLocationImage $image The footprint image * - * @param StorageLocationImage $image The storage location image + * @return void */ - public function setImage(StorageLocationImage $image) + public function setImage($image) { - $this->image = $image; - $image->setStorageLocation($this); + if ($image instanceof StorageLocationImage) { + $image->setStorageLocation($this); + $this->image = $image; + } else { + // Because this is a 1:1 relationship. only allow the temporary image to be set when no image exists. + // If an image exists, the frontend needs to deliver the old file ID with the replacement property set. + if ($this->getImage() === null) { + $this->image = $image; + } + } } /** - * Returns the storage location image + * Returns the footprint image * - * @return StorageLocationImage The storage location image + * @return StorageLocationImage The footprint image */ public function getImage() {