diff --git a/docs/manual.md b/docs/manual.md new file mode 100644 index 0000000..e05bc78 --- /dev/null +++ b/docs/manual.md @@ -0,0 +1,38 @@ +# Semi-Structured - manage partly-defined objects + +## Types and Instances + +TODO: add more instructions. + + +## Planned Features + +These are some things I plan to add to the application: +- bulk extract from json to custom field: a way to define a Custom Field, and + then systematically populate its value from the json blob. +- GUI/Wizard for adding custom field - this should be easier. +- Have Status field for instances, that allows customized values (per class), + and also allows to mark some of the values as Archived. +- A Custom Field that will allow referring to instances of a different Type. + For instance, have a Type for "OS" and a Type for "Software", and allow the + Software type to have a field of "Supported OS" with a full selector. +- Customize Icon for type and/or instance. +- Allow instances to have their own view/edit Policy. + +## Export and Import + +There's an Export feature from the Instance Search which will produce full data, +and there are Conduit methods that allow mass export as well. + +For import, currently only available method is to use Conduit and make 2 calls +per each instance - see `examples/load-xkcd.py` for an example. + + +## Dashboards + +There's some strange interaction between Dashboards and Instance queries: +Instance queries can be added to dashboards, but they can't be //updated// +directly from dashboards. +Adding a query from the dashboard's Create Panel button will list all saved +queries for all instance types; It's easier to find the query in the Item List +page, and use "Add to Dashboard" from the Use Results drop-down. diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9e4c604..ef30821 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1,168 +1,170 @@ 2, 'class' => array( 'SemiStructuredBaseController' => 'controller/SemiStructuredBaseController.php', 'SemiStructuredDAO' => 'storage/SemiStructuredDAO.php', 'SemiStructuredDataApplication' => 'application/SemiStructuredDataApplication.php', 'SemiStructuredInstanceConfiguredCustomField' => 'customfield/SemiStructuredInstanceConfiguredCustomField.php', 'SemiStructuredInstanceConfiguredCustomFieldStorage' => 'storage/SemiStructuredInstanceConfiguredCustomFieldStorage.php', 'SemiStructuredInstanceCustomField' => 'customfield/SemiStructuredInstanceCustomField.php', 'SemiStructuredInstanceCustomFieldNumericIndex' => 'storage/SemiStructuredInstanceCustomFieldNumericIndex.php', 'SemiStructuredInstanceCustomFieldStringIndex' => 'storage/SemiStructuredInstanceCustomFieldStringIndex.php', 'SemiStructuredInstanceEditConduitAPIMethod' => 'conduit/SemiStructuredInstanceEditConduitAPIMethod.php', 'SemiStructuredInstanceSearchConduitAPIMethod' => 'conduit/SemiStructuredInstanceSearchConduitAPIMethod.php', 'SemiStructuredObjectInstance' => 'storage/SemiStructuredObjectInstance.php', 'SemiStructuredObjectInstanceClassTransaction' => 'xaction/instance/SemiStructuredObjectInstanceClassTransaction.php', 'SemiStructuredObjectInstanceController' => 'controller/instance/SemiStructuredObjectInstanceController.php', 'SemiStructuredObjectInstanceDescriptionTransaction' => 'xaction/instance/SemiStructuredObjectInstanceDescriptionTransaction.php', 'SemiStructuredObjectInstanceEditController' => 'controller/instance/SemiStructuredObjectInstanceEditController.php', 'SemiStructuredObjectInstanceEditEngine' => 'editor/SemiStructuredObjectInstanceEditEngine.php', 'SemiStructuredObjectInstanceFerretEngine' => 'search/SemiStructuredObjectInstanceFerretEngine.php', 'SemiStructuredObjectInstanceFulltextEngine' => 'search/SemiStructuredObjectInstanceFulltextEngine.php', 'SemiStructuredObjectInstanceListController' => 'controller/instance/SemiStructuredObjectInstanceListController.php', 'SemiStructuredObjectInstanceMailReceiver' => 'mail/SemiStructuredObjectInstanceMailReceiver.php', 'SemiStructuredObjectInstanceNameTransaction' => 'xaction/instance/SemiStructuredObjectInstanceNameTransaction.php', 'SemiStructuredObjectInstancePHIDType' => 'phid/SemiStructuredObjectInstancePHIDType.php', 'SemiStructuredObjectInstanceQuery' => 'query/SemiStructuredObjectInstanceQuery.php', 'SemiStructuredObjectInstanceRawDataTransaction' => 'xaction/instance/SemiStructuredObjectInstanceRawDataTransaction.php', 'SemiStructuredObjectInstanceReplyHandler' => 'mail/SemiStructuredObjectInstanceReplyHandler.php', 'SemiStructuredObjectInstanceSearchEngine' => 'query/SemiStructuredObjectInstanceSearchEngine.php', 'SemiStructuredObjectInstanceTransaction' => 'storage/SemiStructuredObjectInstanceTransaction.php', 'SemiStructuredObjectInstanceTransactionComment' => 'storage/SemiStructuredObjectInstanceTransactionComment.php', 'SemiStructuredObjectInstanceTransactionEditor' => 'editor/SemiStructuredObjectInstanceTransactionEditor.php', 'SemiStructuredObjectInstanceTransactionQuery' => 'query/SemiStructuredObjectInstanceTransactionQuery.php', 'SemiStructuredObjectInstanceTransactionType' => 'xaction/instance/SemiStructuredObjectInstanceTransactionType.php', 'SemiStructuredObjectInstanceViewController' => 'controller/instance/SemiStructuredObjectInstanceViewController.php', 'SemiStructuredObjectNewInstanceController' => 'controller/class/SemiStructuredObjectNewInstanceController.php', 'SemiStructuredObjectType' => 'storage/SemiStructuredObjectType.php', 'SemiStructuredObjectTypeArchiveController' => 'controller/class/SemiStructuredObjectTypeArchiveController.php', 'SemiStructuredObjectTypeController' => 'controller/class/SemiStructuredObjectTypeController.php', 'SemiStructuredObjectTypeCustomFieldsTransaction' => 'xaction/class/SemiStructuredObjectTypeCustomFieldsTransaction.php', 'SemiStructuredObjectTypeDescriptionTransaction' => 'xaction/class/SemiStructuredObjectTypeDescriptionTransaction.php', 'SemiStructuredObjectTypeEditConduitAPIMethod' => 'conduit/SemiStructuredObjectTypeEditConduitAPIMethod.php', 'SemiStructuredObjectTypeEditController' => 'controller/class/SemiStructuredObjectTypeEditController.php', 'SemiStructuredObjectTypeEditEngine' => 'editor/SemiStructuredObjectTypeEditEngine.php', 'SemiStructuredObjectTypeFerretEngine' => 'search/SemiStructuredObjectTypeFerretEngine.php', 'SemiStructuredObjectTypeFulltextEngine' => 'search/SemiStructuredObjectTypeFulltextEngine.php', 'SemiStructuredObjectTypeListController' => 'controller/class/SemiStructuredObjectTypeListController.php', 'SemiStructuredObjectTypeMailReceiver' => 'mail/SemiStructuredObjectTypeMailReceiver.php', 'SemiStructuredObjectTypeNameTransaction' => 'xaction/class/SemiStructuredObjectTypeNameTransaction.php', + 'SemiStructuredObjectTypePHIDSearchField' => 'search/field/SemiStructuredObjectTypePHIDSearchField.php', 'SemiStructuredObjectTypePHIDType' => 'phid/SemiStructuredObjectTypePHIDType.php', 'SemiStructuredObjectTypeQuery' => 'query/SemiStructuredObjectTypeQuery.php', 'SemiStructuredObjectTypeRemarkupRule' => 'remarkup/SemiStructuredObjectTypeRemarkupRule.php', 'SemiStructuredObjectTypeReplyHandler' => 'mail/SemiStructuredObjectTypeReplyHandler.php', 'SemiStructuredObjectTypeSearchConduitAPIMethod' => 'conduit/SemiStructuredObjectTypeSearchConduitAPIMethod.php', 'SemiStructuredObjectTypeSearchEngine' => 'query/SemiStructuredObjectTypeSearchEngine.php', 'SemiStructuredObjectTypeStatusTransaction' => 'xaction/class/SemiStructuredObjectTypeStatusTransaction.php', 'SemiStructuredObjectTypeTransaction' => 'storage/SemiStructuredObjectTypeTransaction.php', 'SemiStructuredObjectTypeTransactionComment' => 'storage/SemiStructuredObjectTypeTransactionComment.php', 'SemiStructuredObjectTypeTransactionEditor' => 'editor/SemiStructuredObjectTypeTransactionEditor.php', 'SemiStructuredObjectTypeTransactionQuery' => 'query/SemiStructuredObjectTypeTransactionQuery.php', 'SemiStructuredObjectTypeTransactionType' => 'xaction/class/SemiStructuredObjectTypeTransactionType.php', 'SemiStructuredObjectTypeViewController' => 'controller/class/SemiStructuredObjectTypeViewController.php', 'SemiStructuredPatchList' => 'storage/SemiStructuredPatchList.php', 'SemiStructuredSchemaSpec' => 'storage/SemiStructuredSchemaSpec.php', 'SemiStructuredStaticSearchField' => '_to_upstream/SemiStructuredStaticSearchField.php', ), 'function' => array(), 'xmap' => array( 'SemiStructuredBaseController' => 'PhabricatorController', 'SemiStructuredDAO' => 'PhabricatorLiskDAO', 'SemiStructuredDataApplication' => 'PhabricatorApplication', 'SemiStructuredInstanceConfiguredCustomField' => array( 'SemiStructuredInstanceCustomField', 'PhabricatorStandardCustomFieldInterface', ), 'SemiStructuredInstanceConfiguredCustomFieldStorage' => 'PhabricatorCustomFieldStorage', 'SemiStructuredInstanceCustomField' => 'PhabricatorCustomField', 'SemiStructuredInstanceCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage', 'SemiStructuredInstanceCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', 'SemiStructuredInstanceEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'SemiStructuredInstanceSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'SemiStructuredObjectInstance' => array( 'SemiStructuredDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorSubscribableInterface', 'PhabricatorFlaggableInterface', 'PhabricatorProjectInterface', 'PhabricatorFerretInterface', 'PhabricatorFulltextInterface', 'PhabricatorMentionableInterface', 'PhabricatorConduitResultInterface', 'PhabricatorCustomFieldInterface', 'PhabricatorDestructibleInterface', ), 'SemiStructuredObjectInstanceClassTransaction' => 'SemiStructuredObjectInstanceTransactionType', 'SemiStructuredObjectInstanceController' => 'SemiStructuredBaseController', 'SemiStructuredObjectInstanceDescriptionTransaction' => 'SemiStructuredObjectInstanceTransactionType', 'SemiStructuredObjectInstanceEditController' => 'SemiStructuredBaseController', 'SemiStructuredObjectInstanceEditEngine' => 'PhabricatorEditEngine', 'SemiStructuredObjectInstanceFerretEngine' => 'PhabricatorFerretEngine', 'SemiStructuredObjectInstanceFulltextEngine' => 'PhabricatorFulltextEngine', - 'SemiStructuredObjectInstanceListController' => 'SemiStructuredObjectTypeController', + 'SemiStructuredObjectInstanceListController' => 'SemiStructuredObjectInstanceController', 'SemiStructuredObjectInstanceMailReceiver' => 'PhabricatorObjectMailReceiver', 'SemiStructuredObjectInstanceNameTransaction' => 'SemiStructuredObjectInstanceTransactionType', 'SemiStructuredObjectInstancePHIDType' => 'PhabricatorPHIDType', 'SemiStructuredObjectInstanceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'SemiStructuredObjectInstanceRawDataTransaction' => 'SemiStructuredObjectInstanceTransactionType', 'SemiStructuredObjectInstanceReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'SemiStructuredObjectInstanceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'SemiStructuredObjectInstanceTransaction' => 'PhabricatorModularTransaction', 'SemiStructuredObjectInstanceTransactionComment' => 'PhabricatorApplicationTransactionComment', 'SemiStructuredObjectInstanceTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'SemiStructuredObjectInstanceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'SemiStructuredObjectInstanceTransactionType' => 'PhabricatorModularTransactionType', 'SemiStructuredObjectInstanceViewController' => 'SemiStructuredObjectInstanceController', 'SemiStructuredObjectNewInstanceController' => 'SemiStructuredObjectTypeController', 'SemiStructuredObjectType' => array( 'SemiStructuredDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', 'PhabricatorMentionableInterface', 'PhabricatorSubscribableInterface', 'PhabricatorProjectInterface', 'PhabricatorConduitResultInterface', 'PhabricatorFlaggableInterface', 'PhabricatorDestructibleInterface', 'PhabricatorFerretInterface', 'PhabricatorFulltextInterface', ), 'SemiStructuredObjectTypeArchiveController' => 'SemiStructuredBaseController', 'SemiStructuredObjectTypeController' => 'SemiStructuredBaseController', 'SemiStructuredObjectTypeCustomFieldsTransaction' => 'SemiStructuredObjectTypeTransactionType', 'SemiStructuredObjectTypeDescriptionTransaction' => 'SemiStructuredObjectTypeTransactionType', 'SemiStructuredObjectTypeEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'SemiStructuredObjectTypeEditController' => 'SemiStructuredBaseController', 'SemiStructuredObjectTypeEditEngine' => 'PhabricatorEditEngine', 'SemiStructuredObjectTypeFerretEngine' => 'PhabricatorFerretEngine', 'SemiStructuredObjectTypeFulltextEngine' => 'PhabricatorFulltextEngine', 'SemiStructuredObjectTypeListController' => 'SemiStructuredBaseController', 'SemiStructuredObjectTypeMailReceiver' => 'PhabricatorObjectMailReceiver', 'SemiStructuredObjectTypeNameTransaction' => 'SemiStructuredObjectTypeTransactionType', + 'SemiStructuredObjectTypePHIDSearchField' => 'PhabricatorSearchField', 'SemiStructuredObjectTypePHIDType' => 'PhabricatorPHIDType', 'SemiStructuredObjectTypeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'SemiStructuredObjectTypeRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'SemiStructuredObjectTypeReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'SemiStructuredObjectTypeSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'SemiStructuredObjectTypeSearchEngine' => 'PhabricatorApplicationSearchEngine', 'SemiStructuredObjectTypeStatusTransaction' => 'SemiStructuredObjectTypeTransactionType', 'SemiStructuredObjectTypeTransaction' => 'PhabricatorModularTransaction', 'SemiStructuredObjectTypeTransactionComment' => 'PhabricatorApplicationTransactionComment', 'SemiStructuredObjectTypeTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'SemiStructuredObjectTypeTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'SemiStructuredObjectTypeTransactionType' => 'PhabricatorModularTransactionType', 'SemiStructuredObjectTypeViewController' => 'SemiStructuredObjectTypeController', 'SemiStructuredPatchList' => 'PhabricatorSQLPatchList', 'SemiStructuredSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'SemiStructuredStaticSearchField' => 'PhabricatorSearchField', ), )); diff --git a/src/application/SemiStructuredDataApplication.php b/src/application/SemiStructuredDataApplication.php index 20f0932..13a3f95 100644 --- a/src/application/SemiStructuredDataApplication.php +++ b/src/application/SemiStructuredDataApplication.php @@ -1,67 +1,72 @@ array( $this->getQueryRoutePattern() => 'SemiStructuredObjectTypeListController', $this->getEditRoutePattern('editclass/') => 'SemiStructuredObjectTypeEditController', 'type/(?:(?P\d+)/)?' => array( '' => 'SemiStructuredObjectTypeViewController', - 'items/' => 'SemiStructuredObjectInstanceListController', - $this->getQueryRoutePattern() => - 'SemiStructuredObjectInstanceListController', 'archive/' => 'SemiStructuredObjectTypeArchiveController', ), // If the ID field is called `id`, the EditEngine thinks this is // editing an existing object. 'type/(?:(?P\d+)/)?' => array( 'new/' => 'SemiStructuredObjectNewInstanceController', + 'items/' => 'SemiStructuredObjectInstanceListController', + $this->getQueryRoutePattern() => + 'SemiStructuredObjectInstanceListController', + ), + + 'instance/' => array( + $this->getQueryRoutePattern() => + 'SemiStructuredObjectInstanceListController', ), $this->getEditRoutePattern('editinstance/') => 'SemiStructuredObjectInstanceEditController', 'instance/(?:(?P\d+)/)?' => 'SemiStructuredObjectInstanceViewController', ), ); } } diff --git a/src/controller/SemiStructuredBaseController.php b/src/controller/SemiStructuredBaseController.php index d9fd017..c2871e7 100644 --- a/src/controller/SemiStructuredBaseController.php +++ b/src/controller/SemiStructuredBaseController.php @@ -1,15 +1,10 @@ newApplicationMenu(); - // ->setSearchEngine(new PhabricatorBadgesSearchEngine()); - } - public function shouldAllowPublic() { return true; } } diff --git a/src/controller/class/SemiStructuredObjectTypeController.php b/src/controller/class/SemiStructuredObjectTypeController.php index 8b206cf..eba01bf 100644 --- a/src/controller/class/SemiStructuredObjectTypeController.php +++ b/src/controller/class/SemiStructuredObjectTypeController.php @@ -1,89 +1,87 @@ objectType = $object_type; return $this; } public function getObjectType() { return $this->objectType; } - // TODO ??? public function buildApplicationMenu() { return $this->buildSideNavView()->getMenu(); } - protected function buildHeaderView() { $viewer = $this->getViewer(); $object_type = $this->getObjectType(); if ($object_type->isArchived()) { $status_icon = 'fa-ban'; $status_color = 'dark'; } else { $status_icon = 'fa-check'; $status_color = 'bluegrey'; } $status_name = idx( SemiStructuredObjectType::getStatusNameMap(), $object_type->getStatus()); return id(new PHUIHeaderView()) ->setHeader($object_type->getName()) ->setUser($viewer) ->setPolicyObject($object_type) ->setStatus($status_icon, $status_color, $status_name) ->setHeaderIcon(SemiStructuredDataApplication::ICON_OBJECT_TYPE); } protected function buildApplicationCrumbs() { $object_type = $this->getObjectType(); $uri = $object_type->getURI(); $crumbs = parent::buildApplicationCrumbs(); $crumbs->addTextCrumb($object_type->getName(), $uri); $crumbs->setBorder(true); return $crumbs; } protected function buildSideNavView($filter = null) { $viewer = $this->getViewer(); $object_type = $this->getObjectType(); $id = $object_type->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $object_type, PhabricatorPolicyCapability::CAN_EDIT); $nav = id(new AphrontSideNavFilterView()) ->setBaseURI(new PhutilURI($this->getApplicationURI())); $nav->addLabel(pht('Object Type')); $nav->addFilter( 'view', pht('View Object Type'), $object_type->getURI(), SemiStructuredDataApplication::ICON_OBJECT_TYPE); $nav->addFilter( 'instances', pht('View Instances'), $this->getApplicationURI("/type/{$id}/items/"), 'fa-group'); $nav->selectFilter($filter); return $nav; } } diff --git a/src/controller/instance/SemiStructuredObjectInstanceController.php b/src/controller/instance/SemiStructuredObjectInstanceController.php index 9f45643..eb0eeea 100644 --- a/src/controller/instance/SemiStructuredObjectInstanceController.php +++ b/src/controller/instance/SemiStructuredObjectInstanceController.php @@ -1,74 +1,86 @@ object = $object; + $this->setObjectType($object->getClass()); return $this; } public function getObject() { return $this->object; } + public function setObjectType(SemiStructuredObjectType $object_type) { + $this->objectType = $object_type; + return $this; + } + + public function getObjectType() { + return $this->objectType; + } + protected function buildHeaderView() { $viewer = $this->getViewer(); $object = $this->getObject(); $object_type = $object->getClass(); if ($object_type->isArchived()) { $status_icon = 'fa-ban'; $status_color = 'dark'; } else { $status_icon = 'fa-check'; $status_color = 'bluegrey'; } $status_name = idx( SemiStructuredObjectType::getStatusNameMap(), $object_type->getStatus()); return id(new PHUIHeaderView()) ->setHeader( pht( '%s: %d %s', $object_type->getName(), $object->getID(), $object->getName())) ->setUser($viewer) ->setPolicyObject($object_type) ->setStatus($status_icon, $status_color, $status_name) ->setHeaderIcon(SemiStructuredDataApplication::ICON_OBJECT_TYPE); } protected function buildApplicationCrumbs() { $object = $this->getObject(); - $object_type = $object->getClass(); + $object_type = $this->getObjectType(); $can_create = true; $crumbs = parent::buildApplicationCrumbs(); $crumbs->addTextCrumb( $object_type->getName(), $object_type->getURI('items/')); - $crumbs->addTextCrumb( - pht('%d %s', $object->getID(), $object->getName()), $object->getURI()); + if ($object) { + $crumbs->addTextCrumb( + pht('%d %s', $object->getID(), $object->getName()), $object->getURI()); + } $crumbs->setBorder(true); $crumbs->addAction( id(new PHUIListItemView()) ->setName(pht('Create %s', $object_type->getName())) ->setHref($object_type->getURI('new/')) ->setIcon('fa-plus-square') ->setWorkflow(!$can_create) ->setDisabled(!$can_create)); return $crumbs; } } diff --git a/src/controller/instance/SemiStructuredObjectInstanceListController.php b/src/controller/instance/SemiStructuredObjectInstanceListController.php index 0f5a8a2..51d71af 100644 --- a/src/controller/instance/SemiStructuredObjectInstanceListController.php +++ b/src/controller/instance/SemiStructuredObjectInstanceListController.php @@ -1,60 +1,57 @@ getViewer(); - $id = $request->getURIData('id'); - - $object_type = id(new SemiStructuredObjectTypeQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->executeOne(); + $object_type = SemiStructuredObjectTypeQuery::loadOneById( + $viewer, + $request->getURIData('typeid')); if (!$object_type) { return new Aphront404Response(); } $this->setObjectType($object_type); $query_key = $request->getURIData('queryKey'); $controller = id(new PhabricatorApplicationSearchController()) ->setQueryKey($query_key) ->setSearchEngine( id(new SemiStructuredObjectInstanceSearchEngine()) ->setObjectType($object_type)) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); } public function buildSideNavView($filter = null) { $user = $this->getRequest()->getUser(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); id(new SemiStructuredObjectInstanceSearchEngine()) ->setObjectType($this->getObjectType()) ->setViewer($user) ->addNavigationItems($nav->getMenu()); $nav->selectFilter($filter); return $nav; } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); $object_type = $this->getObjectType(); id(new SemiStructuredObjectInstanceEditEngine()) ->setObjectType($object_type) ->setViewer($this->getViewer()) ->addActionToCrumbs($crumbs); return $crumbs; } } diff --git a/src/query/SemiStructuredObjectInstanceSearchEngine.php b/src/query/SemiStructuredObjectInstanceSearchEngine.php index 38d6371..53e5c29 100644 --- a/src/query/SemiStructuredObjectInstanceSearchEngine.php +++ b/src/query/SemiStructuredObjectInstanceSearchEngine.php @@ -1,207 +1,311 @@ objectType = $object_type; return $this; } public function getObjectType() { return $this->objectType; } public function getResultTypeDescription() { return pht('Object Instances'); } public function getApplicationClassName() { return 'SemiStructuredDataApplication'; } public function newQuery() { $query = id(new SemiStructuredObjectInstanceQuery()); if ($this->getObjectType()) { $query->withObjectType($this->getObjectType()); } return $query; } protected function buildCustomSearchFields() { $fields = array( ); if ($this->getObjectType()) { $fields[] = id(new SemiStructuredStaticSearchField()) ->setKey('objecttype') ->setLabel(pht('Object Type')) ->setValue($this->getObjectType()->getName()); + $fields[] = id(new SemiStructuredObjectTypePHIDSearchField()) + ->setKey('classPHIDs') + ->setLabel(pht('Object Type PHID')) + ->setValue($this->getObjectType()->getPHID()); } return $fields; } protected function getURI($path) { - return "/semistruct/type/{$this->getObjectType()->getID()}/{$path}"; + if ($this->getObjectType()) { + return "/semistruct/type/{$this->getObjectType()->getID()}/{$path}"; + } + return "/semistruct/instance/{$path}"; + } + + public function getQueryResultsPageURI($query_key) { + $this->loadObjectTypeFromQuery($query_key); + return parent::getQueryResultsPageURI($query_key); + } + + public function getCustomizeURI($query_key, $object_phid, $context_phid) { + $this->loadObjectTypeFromQuery($query_key); + return parent::getCustomizeURI($query_key, $object_phid, $context_phid); + } + + public function isBuiltinQuery($query_key) { + $this->loadObjectTypeFromQuery($query_key); + + $builtins = $this->getBuiltinQueries(); + $raw = $this->getRawQueryNames(); + return isset($builtins[$query_key]) || isset($raw[$query_key]); + + } + + public function getQueryManagementURI() { + if ($this->getObjectType()) { + return parent::getQueryManagementURI(); + } + return '/semistruct/'; } protected function getBuiltinQueryNames() { + $raw_names = $this->getRawQueryNames(); + + if (!$this->getObjectType()) { + return $raw_names; + } + + $names = array(); + $suffix = '-'.$this->getObjectType()->getId(); + + foreach ($raw_names as $key => $value) { + $names[$key.$suffix] = $value; + } + + return $names; + } + + private function getRawQueryNames() { $names = array(); $names['all'] = pht('All Instances'); return $names; } + private function loadObjectTypeFromQuery($query_key) { + $matches = null; + if (preg_match('/^(\w+)-(\d+)$/', $query_key, $matches)) { + $type_id = $matches[2]; + $query_key = $matches[1]; + + if (!$this->getObjectType()) { + $this->setObjectType( + SemiStructuredObjectTypeQuery::loadOneById( + $this->requireViewer(), + $type_id)); + } + return $matches[1]; + } + + return $query_key; + } + public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); - $viewer = $this->requireViewer(); + + $query_key = $this->loadObjectTypeFromQuery($query_key); switch ($query_key) { case 'all': return $query; case 'open': return $query->setParameter( 'statuses', array( SemiStructuredObjectType::STATUS_ACTIVE, )); } return parent::buildSavedQueryFromBuiltin($query_key); } + public function loadAllNamedQueries() { + $named_queries = parent::loadAllNamedQueries(); + + if (!$this->getObjectType()) { + return $named_queries; + } + + $target_class_phid = $this->getObjectType()->getPHID(); + + $saved_queries = id(new PhabricatorSavedQueryQuery()) + ->setViewer($this->requireViewer()) + ->withQueryKeys(mpull($named_queries, 'getQueryKey')) + ->execute(); + $saved_queries = mpull($saved_queries, 'getParameters', 'getQueryKey'); + + foreach ($named_queries as $key => $named_query) { + if ($named_query->getIsBuiltin()) { + continue; + } + + $class_phids = idxv($saved_queries, array($key, 'classPHIDs')); + + if (!$class_phids) { + continue; + } + + if (array_search($target_class_phid, $class_phids) === false) { + unset($named_queries[$key]); + } + + } + + return $named_queries; + } + protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); // TODO type id/phid // if ($map['statuses']) { // $query->withStatuses($map['statuses']); // } // if ($map['editable'] !== null) { // $query->withCanEdit($map['editable']); // } return $query; } protected function getRequiredHandlePHIDsForResultList( array $objects, PhabricatorSavedQuery $query) { return array(); } public function renderResultsDirectly(array $items) { return $this->renderResultList( $items, new PhabricatorSavedQuery(), array()); } protected function renderResultList( array $items, PhabricatorSavedQuery $query , array $handles) { $viewer = $this->requireViewer(); if ($items) { $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(mpull($items, 'getPHID')) ->withEdgeTypes( array( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, )); $edge_query->execute(); } $list = id(new PHUIObjectItemListView()) ->setViewer($viewer); foreach ($items as $instance) { $item = id(new PHUIObjectItemView()) ->setViewer($viewer) ->setHeader($instance->getName()) ->setHref($instance->getURI()) ->setObject($instance); $icon = id(new PHUIIconView()) ->setIcon($instance->getIcon()); $item->setImageIcon($icon); $item->setEpoch($instance->getDateModified()); $phid = $instance->getPHID(); $project_phids = $edge_query->getDestinationPHIDs(array($phid)); $project_handles = $viewer->loadHandles($project_phids); $item->addAttribute( id(new PHUIHandleTagListView()) ->setLimit(4) ->setNoDataString(pht('No Tags')) ->setSlim(true) ->setHandles($project_handles)); $list->addItem($item); } $result = new PhabricatorApplicationSearchResultView(); $result->setObjectList($list); $result->setNoDataString(pht('No objects found.')); return $result; } /* -( Export )------------------------------------------------------------ */ protected function newExportFields() { return array( id(new PhabricatorStringExportField()) ->setKey('name') ->setLabel(pht('Name')), id(new PhabricatorStringExportField()) ->setKey('description') ->setLabel(pht('Description')), id(new PhabricatorStringExportField()) ->setKey('rawData') ->setLabel(pht('Raw Data')), id(new PhabricatorPHIDExportField()) ->setKey('classPHID') ->setLabel(pht('Class PHID')), id(new PhabricatorURIExportField()) ->setKey('uri') ->setLabel(pht('URI')), ); } protected function newExportData(array $instances) { // $viewer = $this->requireViewer(); $export = array(); foreach ($instances as $instance) { $export[] = array( 'name' => $instance->getName(), 'uri' => PhabricatorEnv::getProductionURI($instance->getURI()), 'description' => $instance->getDescription(), 'classPHID' => $instance->getClassPHID(), 'rawData' => $instance->getRawData(), ); } return $export; } } diff --git a/src/query/SemiStructuredObjectTypeQuery.php b/src/query/SemiStructuredObjectTypeQuery.php index 9103282..48c1e55 100644 --- a/src/query/SemiStructuredObjectTypeQuery.php +++ b/src/query/SemiStructuredObjectTypeQuery.php @@ -1,84 +1,107 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withStatuses(array $statuses) { $this->statuses = $statuses; return $this; } public function withCanEdit($can_edit) { $this->canEdit = $can_edit; return $this; } public function newResultObject() { return new SemiStructuredObjectType(); } + // There might already be a similar solution in the Phorge code base. + public static function loadOneById(PhabricatorUser $viewer, $id) { + $viewer_key = $viewer->getCacheFragment(); + $cache = PhabricatorCaches::getRequestCache(); + $cache_key = self::class.'ById'; + + $collection = $cache->getKey($cache_key, array()); + $value = idxv($collection, array($viewer_key, $id)); + + if ($value) { + return $value; + } + + $value = (new self()) + ->setViewer($viewer) + ->withIds(array($id)) + ->executeOne(); + + $collection[$viewer_key][$id] = $value; + $cache->setKey($cache_key, $collection); + return $value; + } + protected function didFilterPage(array $items) { if ($this->canEdit) { $items = id(new PhabricatorPolicyFilter()) ->setViewer($this->getViewer()) ->requireCapabilities(array( PhabricatorPolicyCapability::CAN_EDIT, )) ->apply($items); } return $items; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'classes.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'classes.phid IN (%Ls)', $this->phids); } if ($this->statuses !== null) { $where[] = qsprintf( $conn, 'classes.status IN (%Ls)', $this->statuses); } return $where; } public function getQueryApplicationClass() { return 'SemiStructuredDataApplication'; } protected function getPrimaryTableAlias() { return 'classes'; } } diff --git a/src/search/field/SemiStructuredObjectTypePHIDSearchField.php b/src/search/field/SemiStructuredObjectTypePHIDSearchField.php new file mode 100644 index 0000000..89fda6e --- /dev/null +++ b/src/search/field/SemiStructuredObjectTypePHIDSearchField.php @@ -0,0 +1,41 @@ +value = $value; + return $this; + } + + public function readValueFromRequest(AphrontRequest $request) { + $id = $request->getURIData('typeid'); + if ($id === null) { + return null; + } + + $object_type = SemiStructuredObjectTypeQuery::loadOneById( + $this->getViewer(), + $id); + return array($object_type->getPHID()); + } + + + protected function getValueFromRequest(AphrontRequest $request, $key) { + throw new Exception('Did not expect call'); + } + + + protected function getValueForControl() { + return $this->value; + } + + protected function newControl() { + return id(new AphrontFormMarkupControl()) + ->setValue($this->value); + } + + +} diff --git a/src/storage/SemiStructuredObjectType.php b/src/storage/SemiStructuredObjectType.php index 296f30c..f060e03 100644 --- a/src/storage/SemiStructuredObjectType.php +++ b/src/storage/SemiStructuredObjectType.php @@ -1,204 +1,202 @@ setName('') ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) ->setEditPolicy($actor->getPHID()) ->setStatus(self::STATUS_ACTIVE); } public function getCustomFieldsConfig() { if (!$this->customFieldsConfig) { return array(); } return $this->customFieldsConfig; } public static function getStatusNameMap() { return array( self::STATUS_ACTIVE => pht('Active'), self::STATUS_ARCHIVED => pht('Archived'), ); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'customFieldsConfig' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort255', 'status' => 'text32', 'description' => 'text', ), ) + parent::getConfiguration(); } public function getPHIDType() { return SemiStructuredObjectTypePHIDType::TYPECONST; } public function isArchived() { return ($this->getStatus() == self::STATUS_ARCHIVED); } public function getURI($path = '') { return urisprintf('/semistruct/type/%d/%s', $this->getID(), $path); } public function getObjectName() { return pht('Object Type %d', $this->getID()); } public function getIcon() { // TODO copy the feature from Projects, where you can put a custom image // or select from a list. return SemiStructuredDataApplication::ICON_OBJECT_TYPE; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new SemiStructuredObjectTypeTransactionEditor(); } public function getApplicationTransactionTemplate() { return new SemiStructuredObjectTypeTransaction(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, // TODO can-create-instances ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( (new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of the object type.')), (new PhabricatorConduitSearchFieldSpecification()) ->setKey('description') ->setType('remarkup') ->setDescription(pht('The description.')), (new PhabricatorConduitSearchFieldSpecification()) ->setKey('status') ->setType('string') ->setDescription(pht('Status of this object data.')), (new PhabricatorConduitSearchFieldSpecification()) ->setKey('customFieldsConfig') ->setType('map') ->setDescription(pht('Custom Fields definition')), ); } public function getFieldValuesForConduit() { return array( 'name' => $this->getName(), 'description' => array( 'raw' => $this->getDescription(), ), 'status' => $this->getStatus(), 'customFieldsConfig' => $this->getCustomFieldsConfig(), ); } public function getConduitSearchAttachments() { return array(); } /* -( PhabricatorFerretInterface )----------------------------------------- */ public function newFerretEngine() { return new SemiStructuredObjectTypeFerretEngine(); } /* -( PhabricatorFulltextInterface )--------------------------------------- */ public function newFulltextEngine() { return new SemiStructuredObjectTypeFulltextEngine(); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); + // TODO destroy all instances.... $this->delete(); $this->saveTransaction(); } /* -( PhabricatorSubscribableInterface )----------------------------------- */ public function isAutomaticallySubscribed($phid) { return false; } }