diff --git a/composer.json b/composer.json index b5d56d4bd85..ab67ff278f9 100644 --- a/composer.json +++ b/composer.json @@ -74,7 +74,7 @@ "components/font-awesome": "~4.3.0", "piwik/device-detector": "~3.0", "oro/jsplumb": "~1.7", - "oro/moment-timezone": "0.3.*", + "oro/moment-timezone": "0.5.*", "vakata/jstree": "^3.2", "symfony/polyfill-php70":"1.*", "liuggio/excelbundle": "~2.1" diff --git a/src/Oro/Bundle/ActivityListBundle/Resources/public/js/app/components/activity-list-component.js b/src/Oro/Bundle/ActivityListBundle/Resources/public/js/app/components/activity-list-component.js index 5ab36fa6c17..cae77d95f85 100644 --- a/src/Oro/Bundle/ActivityListBundle/Resources/public/js/app/components/activity-list-component.js +++ b/src/Oro/Bundle/ActivityListBundle/Resources/public/js/app/components/activity-list-component.js @@ -157,7 +157,13 @@ define(function(require) { * @param {ActivityModel} model */ onViewActivity: function(model) { - this.initComments(model); + if (model.get('is_loaded') === true) { + this.initComments(model); + } else { + model.once('change:contentHTML', function(model) { + this.initComments(model); + }, this); + } }, /** diff --git a/src/Oro/Bundle/ActivityListBundle/Resources/public/js/app/views/activity-list-view.js b/src/Oro/Bundle/ActivityListBundle/Resources/public/js/app/views/activity-list-view.js index 97081722601..d165ebb80d5 100644 --- a/src/Oro/Bundle/ActivityListBundle/Resources/public/js/app/views/activity-list-view.js +++ b/src/Oro/Bundle/ActivityListBundle/Resources/public/js/app/views/activity-list-view.js @@ -326,8 +326,7 @@ define(function(require) { model = this.collection.findSameActivity(oldViewState.attrs); if (model) { view = this.getItemView(model); - model.set('is_loaded', false); - if (view && !oldViewState.collapsed) { + if (view && !oldViewState.collapsed && view.isCollapsed()) { view.toggle(); view.getAccorditionBody().addClass('in'); view.getAccorditionToggle().removeClass('collapsed'); diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractAddressTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractAddressTest.php index 91df5b4b3dc..7084d6e8cba 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractAddressTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractAddressTest.php @@ -68,7 +68,7 @@ public function testBeforeSave() $this->assertNotNull($address->getCreated()); $this->assertNotNull($address->getUpdated()); - $this->assertEquals($address->getCreated(), $address->getUpdated()); + $this->assertEquals($address->getCreated(), $address->getUpdated(), '', 1); } public function testGetRegionName() diff --git a/src/Oro/Bundle/CalendarBundle/Datagrid/MassAction/DeleteMassActionHandler.php b/src/Oro/Bundle/CalendarBundle/Datagrid/MassAction/DeleteMassActionHandler.php new file mode 100644 index 00000000000..dcbbb8abc9b --- /dev/null +++ b/src/Oro/Bundle/CalendarBundle/Datagrid/MassAction/DeleteMassActionHandler.php @@ -0,0 +1,46 @@ +getRecurringEvent()) { + $event = $entity->getRealCalendarEvent(); + $event->setCancelled(true); + + $childEvents = $event->getChildEvents(); + foreach ($childEvents as $childEvent) { + $childEvent->setCancelled(true); + } + } else { + if ($entity->getRecurrence() && $entity->getRecurrence()->getId()) { + $manager->remove($entity->getRecurrence()); + } + + if ($entity->getRecurringEvent()) { + $event = $entity->getRealCalendarEvent(); + $childEvents = $event->getChildEvents(); + foreach ($childEvents as $childEvent) { + $manager->remove($childEvent); + } + } + $manager->remove($entity); + } + + return $this; + } +} diff --git a/src/Oro/Bundle/CalendarBundle/Resources/config/datagrid.yml b/src/Oro/Bundle/CalendarBundle/Resources/config/datagrid.yml index f3c455f6907..a73616d1713 100644 --- a/src/Oro/Bundle/CalendarBundle/Resources/config/datagrid.yml +++ b/src/Oro/Bundle/CalendarBundle/Resources/config/datagrid.yml @@ -5,7 +5,7 @@ datagrid: type: orm query: select: - - partial event.{ id, start, recurrence } + - partial event.{ id, start, recurrence, cancelled } - event.id - CONCAT(CASE WHEN calendar.name IS NOT NULL THEN calendar.name ELSE CONCAT_WS(' ', owner.firstName, owner.lastName) END, '') AS name - event.title @@ -164,6 +164,14 @@ datagrid: icon: trash link: delete_link action_configuration: ['@oro_calendar.datagrid.action_permission_provider', "getInvitationPermissions"] + mass_actions: + delete: + type: delete + icon: trash + label: oro.grid.action.delete + entity_name: Oro\Bundle\CalendarBundle\Entity\CalendarEvent + data_identifier: event.id + handler: oro_calendar.datagrid.mass_action.handler.delete options: entityHint: calendar_events entity_pagination: true diff --git a/src/Oro/Bundle/CalendarBundle/Resources/config/services.yml b/src/Oro/Bundle/CalendarBundle/Resources/config/services.yml index 7b999409a31..388e8031208 100644 --- a/src/Oro/Bundle/CalendarBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/CalendarBundle/Resources/config/services.yml @@ -410,3 +410,7 @@ services: class: '%oro_calendar.validator.calendar_event.class%' tags: - { name: validator.constraint_validator, alias: oro_calendar.calendar_event_validator } + + oro_calendar.datagrid.mass_action.handler.delete: + class: Oro\Bundle\CalendarBundle\Datagrid\MassAction\DeleteMassActionHandler + parent: oro_datagrid.extension.mass_action.handler.delete diff --git a/src/Oro/Bundle/CalendarBundle/Tests/Functional/AbstractTestCase.php b/src/Oro/Bundle/CalendarBundle/Tests/Functional/AbstractTestCase.php new file mode 100644 index 00000000000..344820de195 --- /dev/null +++ b/src/Oro/Bundle/CalendarBundle/Tests/Functional/AbstractTestCase.php @@ -0,0 +1,353 @@ +initClient([]); + } + + /** + * Makes request to REST API resource and verifies the response is expected. + * + * Example: + * + * $this->sendRestApiRequest( + * [ + * 'method' => 'POST', // One of 'POST', 'GET', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD' + * 'url' => $this->getUrl('oro_api_post_foobar'), + * 'route' => 'oro_api_post_foobar', // name of route to generate the url, used if url is not passed + * 'routeParameters' => ['foo' => 'bar'], // parameters to generate the url, used if url is not passed + * 'parameters' => ['bar' => 'baz'], // extra parameters passed in URI of the request + * 'files' => [ // The files + * ... + * ], + * 'server' => [ // The server parameters (HTTP headers are referenced with a HTTP_ prefix as PHP does) + * ... + * ] + * ] + * ) + * + * + * @see \Oro\Bundle\TestFrameworkBundle\Test\Client::request + * + * @param array $parameters + */ + protected function restRequest(array $parameters) + { + // Assert parameters are expected + $this->assertArrayHasKey('method', $parameters, 'Failed asserting request method is specified.'); + $parameters['method'] = strtoupper($parameters['method']); + $this->assertContains( + $parameters['method'], + ['POST', 'GET', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'], + 'Failed asserting request method is expected.' + ); + + $defaultParameters = [ + 'routeParameters' => [], + 'parameters' => [], + 'files' => [], + 'server' => [], + 'content' => null, + ]; + + // Apply default parameters + $parameters = array_merge($defaultParameters, $parameters); + + if (!isset($parameters['url'])) { + $this->assertArrayHasKey('route', $parameters, 'Failed asserting request route is specified.'); + $parameters['url'] = $this->getUrl($parameters['route'], $parameters['routeParameters']); + } + + $this->client->request( + $parameters['method'], + $parameters['url'], + $parameters['parameters'], + $parameters['files'], + $parameters['server'], + $parameters['content'] + ); + } + + /** + * Makes request to REST API resource and verifies the response is expected. + * + * Example: + * + * $this->sendRestApiRequest( + * [ + * 'statusCode' => 200, // Expected status code of the response + * 'contentType' => 'application/json', // Expected content type of the response + * ] + * ) + * + * + * @param array $parameters + * @return array|string + */ + protected function getRestResponseContent(array $parameters) + { + // Assert parameters are expected + $this->assertArrayHasKey('statusCode', $parameters, 'Failed asserting response status code is specified.'); + + $defaultParameters = [ + 'contentType' => null, + ]; + + // Apply default parameters + $parameters = array_merge($defaultParameters, $parameters); + + $this->assertResponseStatusCodeEquals( + $this->client->getResponse(), + $parameters['statusCode'], + sprintf( + 'Failed asserting %s %s has expected status code in response.', + $this->client->getRequest()->getMethod(), + $this->client->getRequest()->getRequestUri() + ) + ); + + if (!empty($parameters['contentType'])) { + $this->assertResponseContentTypeEquals( + $this->client->getResponse(), + $parameters['contentType'], + sprintf( + 'Failed asserting %s %s has expected content type in response.', + $this->client->getRequest()->getMethod(), + $this->client->getRequest()->getRequestUri() + ) + ); + } + + $responseContent = $this->client->getResponse()->getContent(); + + if ($parameters['contentType'] == 'application/json') { + $responseContent = $this->jsonToArray($this->client->getResponse()->getContent()); + } + + return $responseContent; + } + + /** + * Asserts response is expected. Uses strict compare by default. Disabling strict compare will compare only + * intersection of expected response with actual response. + * + * @param array $expectedResponse + * @param array $actualResponse + * @param bool $strictCompare + */ + protected function assertResponseEquals(array $expectedResponse, array $actualResponse, $strictCompare = true) + { + $message = sprintf( + 'Failed asserting %s %s has expected content in response.', + $this->client->getRequest()->getMethod(), + $this->client->getRequest()->getRequestUri() + ); + + $this->sortArrayByKeyRecursively($expectedResponse); + $this->sortArrayByKeyRecursively($actualResponse); + + if ($strictCompare) { + $this->assertEquals( + $expectedResponse, + $actualResponse, + $message + ); + } else { + $this->assertArrayIntersectEquals( + $expectedResponse, + $actualResponse, + $message + ); + } + } + + /** + * Get instance of Doctrine's entity repository. + * + * @param string $entityName + * @return EntityRepository + */ + protected function getEntityRepository($entityName) + { + $result = $this->getDoctrine()->getRepository($entityName); + + $this->assertInstanceOf(EntityRepository::class, $result); + + return $result; + } + + /** + * Get instance of Doctrine's manager registry. + * + * @return ManagerRegistry + */ + protected function getDoctrine() + { + return $this->getContainer()->get('doctrine'); + } + + /** + * Get instance of Doctrine's entity manager. + * + * @param string $name + * @return EntityManager + */ + protected function getEntityManager($name = null) + { + $result = $this->getDoctrine()->getManager($name); + + $this->assertInstanceOf(EntityManager::class, $result); + + return $result; + } + + /** + * Reloads the same entity from the persistence + * + * @param mixed $entity + * @return mixed + */ + protected function reloadEntity($entity) + { + $id = $this->getIdentifierValues($entity); + return $this->getEntity(get_class($entity), $id); + } + + /** + * Get entity from the persistence + * + * @param string $className + * @param mixed $id + * @param boolean $optional + * @return mixed + */ + protected function getEntity($className, $id, $optional = false) + { + $className = ClassUtils::getRealClass($className); + + if (is_array($id) && count($id) == 1) { + $id = current($id); + } + + $result = $this->getEntityRepository($className)->find($id); + + if ($result && !$optional) { + $this->assertInstanceOf( + $className, + $result, + sprintf( + 'Failed asserting entity "%s" is existing in the persistence.', + $className + ) + ); + } + + return $result; + } + + /** + * Refresh all references in the context to pull updates from the persistence. + */ + public function refreshReferences() + { + $referenceRepository = $this->getReferenceRepository(); + + foreach ($referenceRepository->getReferences() as $name => $entity) { + /** @var EntityManager $entityManager */ + $entityManager = $this->getDoctrine()->getManager(); + $contains = $entityManager->contains($entity); + + if ($contains) { + $entityManager->refresh($entity); + } else { + $referenceRepository->setReference( + $name, + $this->reloadEntity($entity) + ); + } + } + } + + /** + * @param mixed $entity + * @return array + */ + protected function getIdentifierValues($entity) + { + $className = ClassUtils::getClass($entity); + $classMetadata = $this->getEntityManager()->getClassMetadata($className); + return $classMetadata->getIdentifierValues($entity); + } + + /** + * Sorts array by key recursively. This method is used to output failures of array response comparison in + * a more comprehensive way. + * + * @param array $array + * @return mixed + */ + protected static function sortArrayByKeyRecursively(array &$array) + { + ksort($array); + + foreach ($array as $key => &$value) { + if (is_array($value)) { + self::sortArrayByKeyRecursively($value); + } + } + } + + /** + * Assert response status code equals + * + * @param Response $response + * @param int $statusCode + * @param string|null $message + */ + public static function assertResponseStatusCodeEquals(Response $response, $statusCode, $message = null) + { + try { + \PHPUnit_Framework_TestCase::assertEquals($statusCode, $response->getStatusCode(), $message); + } catch (\PHPUnit_Framework_ExpectationFailedException $e) { + if ($statusCode < 400 + && $response->getStatusCode() >= 400 + && $response->headers->contains('Content-Type', 'application/json') + ) { + $content = self::jsonToArray($response->getContent()); + if (!empty($content['message'])) { + $errors = null; + if (!empty($content['errors'])) { + $errors = is_array($content['errors']) + ? json_encode($content['errors']) + : $content['errors']; + } + $e = new \PHPUnit_Framework_ExpectationFailedException( + $e->getMessage() + . ' Error message: ' . $content['message'] + . ($errors ? '. Errors: ' . $errors : ''), + $e->getComparisonFailure() + ); + } else { + $e = new \PHPUnit_Framework_ExpectationFailedException( + $e->getMessage() . ' Response content: ' . $response->getContent(), + $e->getComparisonFailure() + ); + } + } + throw $e; + } + } +} diff --git a/src/Oro/Bundle/CalendarBundle/Tests/Functional/Controller/RecurringCalendarEventMassDeleteTest.php b/src/Oro/Bundle/CalendarBundle/Tests/Functional/Controller/RecurringCalendarEventMassDeleteTest.php new file mode 100644 index 00000000000..684e2fd4d1b --- /dev/null +++ b/src/Oro/Bundle/CalendarBundle/Tests/Functional/Controller/RecurringCalendarEventMassDeleteTest.php @@ -0,0 +1,260 @@ +loadFixtures([LoadUserData::class]); // force load fixtures + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testUpdateRecurringEventRecurrenceClearsExceptions() + { + // Step 1. Create new recurring event + // Recurring event with occurrences: 2016-04-25, 2016-05-08, 2016-05-09, 2016-05-22 + $eventData = [ + 'title' => 'Test Recurring Event', + 'description' => 'Test Recurring Event Description', + 'allDay' => false, + 'calendar' => $this->getReference('oro_calendar:calendar:foo_user_1')->getId(), + 'start' => '2016-04-25T01:00:00+00:00', + 'end' => '2016-04-25T02:00:00+00:00', + 'recurrence' => [ + 'timeZone' => 'UTC', + 'recurrenceType' => Recurrence::TYPE_WEEKLY, + 'interval' => 2, + 'dayOfWeek' => [Recurrence::DAY_SUNDAY, Recurrence::DAY_MONDAY], + 'startTime' => '2016-04-25T01:00:00+00:00', + 'occurrences' => 4, + 'endTime' => '2016-06-10T01:00:00+00:00', + ] + ]; + + $this->restRequest( + [ + 'method' => 'POST', + 'url' => $this->getUrl('oro_api_post_calendarevent'), + 'server' => $this->generateWsseAuthHeader('foo_user_1', 'foo_user_1_api_key'), + 'content' => json_encode($eventData) + ] + ); + $response = $this->getRestResponseContent( + [ + 'statusCode' => 201, + 'contentType' => 'application/json' + ] + ); + /** @var CalendarEvent $recurringEvent */ + $recurringEvent = $this->getEntity(CalendarEvent::class, $response['id']); + + // Step 2. Create exception for the recurring event, exception represents changed event + $this->restRequest( + [ + 'method' => 'POST', + 'url' => $this->getUrl('oro_api_post_calendarevent'), + 'server' => $this->generateWsseAuthHeader('foo_user_1', 'foo_user_1_api_key'), + 'content' => json_encode( + [ + 'title' => 'Test Recurring Event Changed', + 'description' => 'Test Recurring Event Description', + 'allDay' => false, + 'calendar' => $this->getReference('oro_calendar:calendar:foo_user_1')->getId(), + 'start' => '2016-05-22T03:00:00+00:00', + 'end' => '2016-05-22T05:00:00+00:00', + 'recurringEventId' => $recurringEvent->getId(), + 'originalStart' => '2016-05-22T01:00:00+00:00', + ] + ) + ] + ); + $response = $this->getRestResponseContent( + [ + 'statusCode' => 201, + 'contentType' => 'application/json' + ] + ); + /** @var CalendarEvent $newEvent */ + $changedEventException = $this->getEntity(CalendarEvent::class, $response['id']); + + // Step 3. Create new simple calendar event + $eventData = [ + 'title' => 'Test Simple Event', + 'description' => 'Test Simple Event Description', + 'allDay' => false, + 'calendar' => $this->getReference('oro_calendar:calendar:foo_user_1')->getId(), + 'start' => '2016-04-27T01:00:00+00:00', + 'end' => '2016-04-27T02:00:00+00:00', + ]; + + $this->restRequest( + [ + 'method' => 'POST', + 'url' => $this->getUrl('oro_api_post_calendarevent'), + 'server' => $this->generateWsseAuthHeader('foo_user_1', 'foo_user_1_api_key'), + 'content' => json_encode($eventData) + ] + ); + $response = $this->getRestResponseContent( + [ + 'statusCode' => 201, + 'contentType' => 'application/json' + ] + ); + /** @var CalendarEvent $recurringEvent */ + $simpleEvent = $this->getEntity(CalendarEvent::class, $response['id']); + + // Step 4. Execute delete mass action + $url = $this->getUrl( + 'oro_datagrid_mass_action', + [ + 'gridName' => 'calendar-event-grid', + 'actionName' => 'delete', + 'inset' => 1, + 'values' => implode(',', [$simpleEvent->getId(), $changedEventException->getId()]), + ] + ); + $this->client->request('DELETE', $url, [], [], $this->generateBasicAuthHeader('foo_user_1', 'password')); + $result = $this->client->getResponse(); + $data = json_decode($result->getContent(), true); + $this->assertTrue($data['successful'] === true); + $this->assertTrue($data['count'] === 2); + + // Step 5. Get events via API and verify result is without removed items + $this->restRequest( + [ + 'method' => 'GET', + 'url' => $this->getUrl( + 'oro_api_get_calendarevents', + [ + 'calendar' => $this->getReference('oro_calendar:calendar:foo_user_1')->getId(), + 'start' => '2016-04-01T01:00:00+00:00', + 'end' => '2016-06-01T01:00:00+00:00', + 'subordinate' => true, + ] + ), + 'server' => $this->generateWsseAuthHeader('foo_user_1', 'foo_user_1_api_key') + ] + ); + + $response = $this->getRestResponseContent( + [ + 'statusCode' => 200, + 'contentType' => 'application/json' + ] + ); + + $expectedResponse = [ + [ + 'id' => $recurringEvent->getId(), + 'title' => "Test Recurring Event", + 'description' => "Test Recurring Event Description", + 'start' => "2016-04-25T01:00:00+00:00", + 'end' => "2016-04-25T02:00:00+00:00", + 'allDay' => false, + 'attendees' => [], + ], + [ + 'id' => $recurringEvent->getId(), + 'title' => "Test Recurring Event", + 'description' => "Test Recurring Event Description", + 'start' => "2016-05-08T01:00:00+00:00", + 'end' => "2016-05-08T02:00:00+00:00", + 'allDay' => false, + 'attendees' => [], + ], + [ + 'id' => $recurringEvent->getId(), + 'title' => "Test Recurring Event", + 'description' => "Test Recurring Event Description", + 'start' => "2016-05-09T01:00:00+00:00", + 'end' => "2016-05-09T02:00:00+00:00", + 'allDay' => false, + 'attendees' => [], + ] + ]; + + $actualIntersect = self::getRecursiveArrayIntersect($response, $expectedResponse); + \PHPUnit_Framework_TestCase::assertEquals( + $expectedResponse, + $actualIntersect, + null + ); + + // Step 6. Get exception event via API and verify it is cancelled + $this->restRequest( + [ + 'method' => 'GET', + 'url' => $this->getUrl('oro_api_get_calendarevent', ['id' => $changedEventException->getId()]), + 'server' => $this->generateWsseAuthHeader('foo_user_1', 'foo_user_1_api_key') + ] + ); + + $response = $this->getRestResponseContent( + [ + 'statusCode' => 200, + 'contentType' => 'application/json' + ] + ); + + $expectedResponse = [ + 'id' => $changedEventException->getId(), + 'title' => "Test Recurring Event Changed", + 'description' => "Test Recurring Event Description", + 'start' => "2016-05-22T03:00:00+00:00", + 'end' => "2016-05-22T05:00:00+00:00", + 'allDay' => false, + 'attendees' => [], + 'recurringEventId' => $recurringEvent->getId(), + 'isCancelled' => true, + ]; + + $this->assertResponseEquals($expectedResponse, $response, false); + } + + /** + * Get intersect of $target array with values of keys in $source array. If key is an array in both places then + * the value of this key will be returned as intersection as well. + * + * @param array $source + * @param array $target + * @return array + */ + public static function getRecursiveArrayIntersect(array $target, array $source) + { + $result = []; + foreach (array_keys($source) as $key) { + if (array_key_exists($key, $target)) { + if (is_array($target[$key]) && is_array($source[$key])) { + $result[$key] = self::getRecursiveArrayIntersect($target[$key], $source[$key]); + } else { + $result[$key] = $target[$key]; + } + } + } + + return $result; + } +} diff --git a/src/Oro/Bundle/CalendarBundle/Tests/Functional/DataFixtures/LoadUserData.php b/src/Oro/Bundle/CalendarBundle/Tests/Functional/DataFixtures/LoadUserData.php index 11d62893589..5289f17b1eb 100644 --- a/src/Oro/Bundle/CalendarBundle/Tests/Functional/DataFixtures/LoadUserData.php +++ b/src/Oro/Bundle/CalendarBundle/Tests/Functional/DataFixtures/LoadUserData.php @@ -4,12 +4,14 @@ use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\Yaml\Yaml; +use Doctrine\Common\Collections\Criteria; use Doctrine\Common\DataFixtures\AbstractFixture; use Doctrine\Common\DataFixtures\DependentFixtureInterface; use Doctrine\Common\Persistence\ObjectManager; +use Oro\Bundle\UserBundle\Entity\UserApi; + class LoadUserData extends AbstractFixture implements DependentFixtureInterface, ContainerAwareInterface { /** @@ -55,5 +57,33 @@ public function load(ObjectManager $manager) $userManager->updateUser($user); $this->setReference($user->getUsername(), $user); } + + $user = $userManager->createUser(); + $role = $manager->getRepository('OroUserBundle:Role')->findOneBy(['role' => 'ROLE_ADMINISTRATOR']); + $apiKey = new UserApi(); + $apiKey->setApiKey('foo_user_1_api_key'); + $apiKey->setOrganization($organization); + $user->setUsername('foo_user_1') + ->setPlainPassword('password') + ->setEmail('foo_user_1@example.com') + ->setFirstName('Billy') + ->setLastName('Wilf') + ->addApiKey($apiKey) + ->setOrganization($organization) + ->addOrganization($organization) + ->addRole($role) + ->setEnabled(true); + $userManager->updateUser($user); + $this->setReference('oro_calendar:user:foo_user_1', $user); + + $calendar = $manager->getRepository('OroCalendarBundle:Calendar') + ->findOneBy( + [ + 'owner' => $user, + 'organization' => $user->getOrganization(), + ] + ); + + $this->setReference('oro_calendar:calendar:foo_user_1', $calendar); } } diff --git a/src/Oro/Bundle/DataGridBundle/Datasource/Orm/OrmDatasource.php b/src/Oro/Bundle/DataGridBundle/Datasource/Orm/OrmDatasource.php index fcba92e9888..805e5d2f58b 100644 --- a/src/Oro/Bundle/DataGridBundle/Datasource/Orm/OrmDatasource.php +++ b/src/Oro/Bundle/DataGridBundle/Datasource/Orm/OrmDatasource.php @@ -36,6 +36,9 @@ class OrmDatasource implements DatasourceInterface, ParameterBinderAwareInterfac /** @var array|null */ protected $queryHints; + /** @var array */ + protected $countQueryHints = []; + /** @var ConfigProcessorInterface */ protected $configProcessor; @@ -120,6 +123,14 @@ public function getCountQb() return $this->countQb; } + /** + * @return array + */ + public function getCountQueryHints() + { + return $this->countQueryHints; + } + /** * Returns query builder * @@ -181,5 +192,8 @@ protected function processConfigs(array $config) if (isset($config['hints'])) { $this->queryHints = $config['hints']; } + if (isset($config['count_hints'])) { + $this->countQueryHints = $config['count_hints']; + } } } diff --git a/src/Oro/Bundle/DataGridBundle/Extension/MassAction/DeleteMassActionHandler.php b/src/Oro/Bundle/DataGridBundle/Extension/MassAction/DeleteMassActionHandler.php index 424ce73601a..d66478eb5d0 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/MassAction/DeleteMassActionHandler.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/MassAction/DeleteMassActionHandler.php @@ -235,7 +235,7 @@ protected function doDelete(MassActionHandlerArgs $args) if ($entity) { $deletedIds[] = $identifierValue; - $manager->remove($entity); + $this->processDelete($entity, $manager); $iteration++; if ($iteration % self::FLUSH_BATCH_SIZE == 0) { @@ -251,4 +251,17 @@ protected function doDelete(MassActionHandlerArgs $args) return $this->getDeleteResponse($args, $iteration); } + + /** + * @param object $entity + * @param EntityManager $manager + * + * @return DeleteMassActionHandler + */ + protected function processDelete($entity, EntityManager $manager) + { + $manager->remove($entity); + + return $this; + } } diff --git a/src/Oro/Bundle/DataGridBundle/Extension/Pager/Orm/Pager.php b/src/Oro/Bundle/DataGridBundle/Extension/Pager/Orm/Pager.php index 77bd59187ce..65fb9fc6f13 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/Pager/Orm/Pager.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/Pager/Orm/Pager.php @@ -4,12 +4,12 @@ use Doctrine\ORM\Query; use Doctrine\ORM\QueryBuilder; - use Oro\Bundle\BatchBundle\ORM\Query\QueryCountCalculator; use Oro\Bundle\BatchBundle\ORM\QueryBuilder\CountQueryBuilderOptimizer; -use Oro\Bundle\DataGridBundle\Extension\Pager\PagerInterface; use Oro\Bundle\DataGridBundle\Extension\Pager\AbstractPager; +use Oro\Bundle\DataGridBundle\Extension\Pager\PagerInterface; use Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper; +use Oro\Component\DoctrineUtils\ORM\QueryHintResolver; class Pager extends AbstractPager implements PagerInterface { @@ -19,6 +19,9 @@ class Pager extends AbstractPager implements PagerInterface /** @var QueryBuilder */ protected $countQb; + /** @var array */ + protected $countQueryHints = []; + /** @var boolean */ protected $isTotalCalculated = false; @@ -37,12 +40,16 @@ class Pager extends AbstractPager implements PagerInterface /** @var CountQueryBuilderOptimizer */ protected $countQueryBuilderOptimizer; + /** @var QueryHintResolver */ + protected $queryHintResolver; + /** @var string */ protected $aclPermission = 'VIEW'; public function __construct( AclHelper $aclHelper, CountQueryBuilderOptimizer $countQueryOptimizer, + QueryHintResolver $queryHintResolver, $maxPerPage = 10, QueryBuilder $qb = null ) { @@ -51,6 +58,7 @@ public function __construct( $this->aclHelper = $aclHelper; $this->countQueryBuilderOptimizer = $countQueryOptimizer; + $this->queryHintResolver = $queryHintResolver; } /** @@ -76,10 +84,12 @@ public function getQueryBuilder() /** * @param QueryBuilder $countQb + * @param array $queryHints */ - public function setCountQb(QueryBuilder $countQb) + public function setCountQb(QueryBuilder $countQb, $queryHints = []) { $this->countQb = $countQb; + $this->countQueryHints = $queryHints; $this->isTotalCalculated = false; } @@ -96,6 +106,7 @@ public function computeNbResult() if (!$this->skipAclCheck) { $query = $this->aclHelper->apply($query, $this->aclPermission); } + $this->queryHintResolver->resolveHints($query, $this->countQueryHints); $useWalker = null; if ($this->skipCountWalker !== null) { diff --git a/src/Oro/Bundle/DataGridBundle/Extension/Pager/OrmPagerExtension.php b/src/Oro/Bundle/DataGridBundle/Extension/Pager/OrmPagerExtension.php index bfe1c9e53e8..73f5ef824fb 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/Pager/OrmPagerExtension.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/Pager/OrmPagerExtension.php @@ -55,7 +55,7 @@ public function visitDatasource(DatagridConfiguration $config, DatasourceInterfa { if ($datasource instanceof OrmDatasource) { if ($datasource->getCountQb()) { - $this->pager->setCountQb($datasource->getCountQb()); + $this->pager->setCountQb($datasource->getCountQb(), $datasource->getCountQueryHints()); } $this->pager->setQueryBuilder($datasource->getQueryBuilder()); $this->pager->setSkipAclCheck($config->isDatasourceSkipAclApply()); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/config/extensions.yml b/src/Oro/Bundle/DataGridBundle/Resources/config/extensions.yml index 0b0ff807bbd..eee29da9066 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/config/extensions.yml +++ b/src/Oro/Bundle/DataGridBundle/Resources/config/extensions.yml @@ -36,6 +36,7 @@ services: arguments: - '@oro_security.acl_helper' - '@oro_batch.orm.query_builder.count_query_optimizer' + - '@oro_entity.query_hint_resolver' class: %oro_datagrid.extension.pager.orm.pager.class% oro_datagrid.extension.orm_sorter: diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/mobile/oro.grid.less b/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/mobile/oro.grid.less index 7a0c9e3907c..d2bc7add402 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/mobile/oro.grid.less +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/mobile/oro.grid.less @@ -2,7 +2,7 @@ margin: 0; } .grid-toolbar { - padding: @contentPadding 10px; + padding: 0 10px @contentPadding; margin-bottom: -@contentPadding; &:after { content: ''; @@ -70,6 +70,9 @@ .forScreen(430px, height, 32px); } } +.oro-datagrid:first-child > .toolbar:first-child > .grid-toolbar { + padding-top: @contentPadding; +} .other-scroll-container { margin: 10px 10px 10px 10px; diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/oro.grid.less b/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/oro.grid.less index d4605a3a9b7..fbfd0565fa3 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/oro.grid.less +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/oro.grid.less @@ -26,6 +26,10 @@ border-radius: 0; } +.grid-toolbar-tools { + margin-bottom: 7px; +} + .visible-items-counter { height: 32px; line-height: 27px; diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/toolbar.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/toolbar.js index 58e3a956244..b99ad3002a5 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/toolbar.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/toolbar.js @@ -72,11 +72,14 @@ define([ this.subviews = { pagination: new this.pagination(_.defaults({collection: this.collection}, options.pagination)), itemsCounter: new this.itemsCounter(_.defaults({collection: this.collection}, options.itemsCounter)), - pageSize: new this.pageSize(_.defaults({collection: this.collection}, options.pageSize)), actionsPanel: new this.actionsPanel(_.extend({className: ''}, options.actionsPanel)), extraActionsPanel: new this.extraActionsPanel() }; + if (_.result(options.pageSize, 'hide') !== true) { + this.subviews.pageSize = new this.pageSize(_.defaults({collection: this.collection}, options.pageSize)); + } + if (options.addSorting) { this.subviews.sortingDropdown = new this.sortingDropdown({ collection: this.collection, @@ -109,10 +112,7 @@ define([ * @return {*} */ enable: function() { - this.subviews.pagination.enable(); - this.subviews.pageSize.enable(); - this.subviews.actionsPanel.enable(); - this.subviews.extraActionsPanel.enable(); + _.invoke(this.subviews, 'enable'); return this; }, @@ -122,10 +122,7 @@ define([ * @return {*} */ disable: function() { - this.subviews.pagination.disable(); - this.subviews.pageSize.disable(); - this.subviews.actionsPanel.disable(); - this.subviews.extraActionsPanel.disable(); + _.invoke(this.subviews, 'disable'); return this; }, @@ -151,7 +148,9 @@ define([ $pagination.attr('class', this.$(this.selector.pagination).attr('class')); this.$(this.selector.pagination).replaceWith($pagination); - this.$(this.selector.pagesize).append(this.subviews.pageSize.render().$el); + if (this.subviews.pageSize) { + this.$(this.selector.pagesize).append(this.subviews.pageSize.render().$el); + } this.$(this.selector.actionsPanel).append(this.subviews.actionsPanel.render().$el); this.$(this.selector.itemsCounter).replaceWith(this.subviews.itemsCounter.render().$el); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/translations/messages.en.yml b/src/Oro/Bundle/DataGridBundle/Resources/translations/messages.en.yml index d40f6424d84..d9407d5b2e6 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/translations/messages.en.yml +++ b/src/Oro/Bundle/DataGridBundle/Resources/translations/messages.en.yml @@ -23,6 +23,7 @@ oro: columns_data.label: Columns data appearance_type.label: Appearance type appearance_data.label: Appearance data + duplicate.label: Duplicate %entity% appearance: grid: Grid board: Kanban Board diff --git a/src/Oro/Bundle/DataGridBundle/Resources/views/js/toolbar.html.twig b/src/Oro/Bundle/DataGridBundle/Resources/views/js/toolbar.html.twig index 0669082c192..ceedadba041 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/views/js/toolbar.html.twig +++ b/src/Oro/Bundle/DataGridBundle/Resources/views/js/toolbar.html.twig @@ -1,6 +1,6 @@ diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailUser.php b/src/Oro/Bundle/EmailBundle/Entity/EmailUser.php index 99e928c73ed..a09938bae6b 100644 --- a/src/Oro/Bundle/EmailBundle/Entity/EmailUser.php +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailUser.php @@ -21,7 +21,11 @@ * name="oro_email_user", * indexes={ * @ORM\Index(name="seen_idx", columns={"is_seen", "mailbox_owner_id"}), - * @ORM\Index(name="received_idx", columns={"received", "is_seen", "mailbox_owner_id"}) + * @ORM\Index(name="received_idx", columns={"received", "is_seen", "mailbox_owner_id"}), + * @ORM\Index( + * name="user_owner_id_mailbox_owner_id_organization_id", + * columns={"user_owner_id", "mailbox_owner_id", "organization_id"} + * ) * } * ) * @ORM\HasLifecycleCallbacks diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml b/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml index 574ffa4206b..854c99d5c28 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml @@ -144,6 +144,10 @@ datagrid: JOIN act_f.origin act_o WHERE act_o.isActive = true AND eu.id = act_eu.id ) + count_hints: + - { name: oro.use_index, value: user_owner_id_mailbox_owner_id_organization_id } + hints: + - { name: oro.use_index, value: user_owner_id_mailbox_owner_id_organization_id } columns: contacts: diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailBodyTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailBodyTest.php index 3f6a272ac79..e6fd39b6000 100644 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailBodyTest.php +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailBodyTest.php @@ -66,7 +66,7 @@ public function testBeforeSave() $this->assertEquals(false, $entity->getBodyIsText()); $this->assertEquals(false, $entity->getHasAttachments()); $this->assertEquals(false, $entity->getPersistent()); - $this->assertGreaterThanOrEqual($createdAt, $entity->getCreated()); + $this->assertGreaterThanOrEqual($entity->getCreated(), $createdAt); } public function testTextBodyGetterAndSetter() diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTest.php index 677ae55266d..2f583fee799 100644 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTest.php +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTest.php @@ -102,7 +102,7 @@ public function testBeforeSave() $createdAt = new \DateTime('now', new \DateTimeZone('UTC')); $this->assertEquals(Email::NORMAL_IMPORTANCE, $entity->getImportance()); - $this->assertGreaterThanOrEqual($createdAt, $entity->getCreated()); + $this->assertGreaterThanOrEqual($entity->getCreated(), $createdAt); } public function testIsHeadGetterAndSetter() diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailThreadTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailThreadTest.php index 89e60af52da..99bc57fc0bb 100644 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailThreadTest.php +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailThreadTest.php @@ -32,7 +32,7 @@ public function testBeforeSave() $createdAt = new \DateTime('now', new \DateTimeZone('UTC')); $this->assertEquals(Email::NORMAL_IMPORTANCE, $entity->getImportance()); - $this->assertGreaterThanOrEqual($createdAt, $entity->getCreated()); + $this->assertGreaterThanOrEqual($entity->getCreated(), $createdAt); } /** diff --git a/src/Oro/Bundle/EntityBundle/DependencyInjection/Compiler/SqlWalkerPass.php b/src/Oro/Bundle/EntityBundle/DependencyInjection/Compiler/SqlWalkerPass.php new file mode 100644 index 00000000000..8c158422c1d --- /dev/null +++ b/src/Oro/Bundle/EntityBundle/DependencyInjection/Compiler/SqlWalkerPass.php @@ -0,0 +1,26 @@ +getDefinition('doctrine.orm.configuration') + ->addMethodCall( + 'setDefaultQueryHint', + [ + Query::HINT_CUSTOM_OUTPUT_WALKER, + SqlWalker::class, + ] + ); + } +} diff --git a/src/Oro/Bundle/EntityBundle/OroEntityBundle.php b/src/Oro/Bundle/EntityBundle/OroEntityBundle.php index 2de9cdae4c1..021d70a5105 100644 --- a/src/Oro/Bundle/EntityBundle/OroEntityBundle.php +++ b/src/Oro/Bundle/EntityBundle/OroEntityBundle.php @@ -20,6 +20,7 @@ use Oro\Bundle\EntityBundle\DependencyInjection\Compiler\VirtualRelationProvidersCompilerPass; use Oro\Bundle\EntityBundle\DependencyInjection\Compiler\CustomGridFieldValidatorCompilerPass; use Oro\Bundle\EntityBundle\DependencyInjection\Compiler\DataCollectorCompilerPass; +use Oro\Bundle\EntityBundle\DependencyInjection\Compiler\SqlWalkerPass; class OroEntityBundle extends Bundle { @@ -56,6 +57,7 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new EntityFieldHandlerPass()); $container->addCompilerPass(new CustomGridFieldValidatorCompilerPass()); $container->addCompilerPass(new DataCollectorCompilerPass()); + $container->addCompilerPass(new SqlWalkerPass()); if ($container instanceof ExtendedContainerBuilder) { $container->addCompilerPass(new GeneratedValueStrategyListenerPass()); diff --git a/src/Oro/Bundle/EntityBundle/Resources/public/js/field-choice.js b/src/Oro/Bundle/EntityBundle/Resources/public/js/field-choice.js index ccffb2a2e96..1d5d9db5825 100644 --- a/src/Oro/Bundle/EntityBundle/Resources/public/js/field-choice.js +++ b/src/Oro/Bundle/EntityBundle/Resources/public/js/field-choice.js @@ -106,6 +106,10 @@ define(function(require) { instance = this.element.data('select2'); }, + _destroy: function() { + this.element.data('select2').destroy(); + }, + _setOption: function(key, value) { if ($.isPlainObject(value)) { $.extend(this.options[key], value); diff --git a/src/Oro/Bundle/FilterBundle/Filter/DuplicateFilter.php b/src/Oro/Bundle/FilterBundle/Filter/DuplicateFilter.php new file mode 100644 index 00000000000..9ac8b6b5f81 --- /dev/null +++ b/src/Oro/Bundle/FilterBundle/Filter/DuplicateFilter.php @@ -0,0 +1,44 @@ +' : '='; + + $qb = clone $ds->getQueryBuilder(); + $qb + ->resetDqlPart('orderBy') + ->resetDqlPart('where') + ->select($fieldName) + ->groupBy($fieldName) + ->having(sprintf('COUNT(%s) %s 1', $fieldName, $operator)); + list($dql) = $this->createDQLWithReplacedAliases($ds, $qb); + + return $ds->expr()->in($fieldName, $dql); + } +} diff --git a/src/Oro/Bundle/FilterBundle/Filter/FilterUtility.php b/src/Oro/Bundle/FilterBundle/Filter/FilterUtility.php index d32de1f43d5..c3e78e232dd 100644 --- a/src/Oro/Bundle/FilterBundle/Filter/FilterUtility.php +++ b/src/Oro/Bundle/FilterBundle/Filter/FilterUtility.php @@ -10,6 +10,7 @@ class FilterUtility const CONDITION_KEY = 'filter_condition'; const BY_HAVING_KEY = 'filter_by_having'; const ENABLED_KEY = 'enabled'; + const VISIBLE_KEY = 'visible'; const TYPE_KEY = 'type'; const FRONTEND_TYPE_KEY = 'ftype'; const DATA_NAME_KEY = 'data_name'; diff --git a/src/Oro/Bundle/FilterBundle/Grid/Extension/Configuration.php b/src/Oro/Bundle/FilterBundle/Grid/Extension/Configuration.php index eae7774d865..646b1856e2e 100644 --- a/src/Oro/Bundle/FilterBundle/Grid/Extension/Configuration.php +++ b/src/Oro/Bundle/FilterBundle/Grid/Extension/Configuration.php @@ -51,6 +51,7 @@ public function getConfigTreeBuilder() ->end() ->booleanNode(FilterUtility::BY_HAVING_KEY)->end() ->booleanNode(FilterUtility::ENABLED_KEY)->defaultTrue()->end() + ->booleanNode(FilterUtility::VISIBLE_KEY)->defaultTrue()->end() ->booleanNode(FilterUtility::TRANSLATABLE_KEY)->defaultTrue()->end() ->end() ->end() diff --git a/src/Oro/Bundle/FilterBundle/Resources/config/filters.yml b/src/Oro/Bundle/FilterBundle/Resources/config/filters.yml index 7a34f5691dc..5e488ade96d 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/config/filters.yml +++ b/src/Oro/Bundle/FilterBundle/Resources/config/filters.yml @@ -105,6 +105,12 @@ services: tags: - { name: oro_filter.extension.orm_filter.filter, type: boolean } + oro_filter.duplicate_filter: + class: Oro\Bundle\FilterBundle\Filter\DuplicateFilter + parent: oro_filter.boolean_filter + tags: + - { name: oro_filter.extension.orm_filter.filter, type: duplicate, datasource: orm } + oro_filter.date_filter_utility: class: %oro_filter.date_filter_utility.class% arguments: diff --git a/src/Oro/Bundle/FilterBundle/Resources/doc/reference/grid_extension.md b/src/Oro/Bundle/FilterBundle/Resources/doc/reference/grid_extension.md index 2df3b484923..541bdc224ba 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/doc/reference/grid_extension.md +++ b/src/Oro/Bundle/FilterBundle/Resources/doc/reference/grid_extension.md @@ -27,6 +27,7 @@ For example: data_name: g.id enabled: true|false #whether filter enabled or not. If filter is not enabled it will not be displayed in filter list but will be accessible in filter management. disabled: true|false #If filter is disabled it will not be displayed in filter list and will not be available in filter management. + visible: true|false #If set to "false" - filter will not be displayed anywhere in UI. However, one can still set filter's value in backend or via url in frontend ``` diff --git a/src/Oro/Bundle/FilterBundle/Resources/public/js/collection-filters-manager.js b/src/Oro/Bundle/FilterBundle/Resources/public/js/collection-filters-manager.js index 4c3c0cae588..49773afbafe 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/public/js/collection-filters-manager.js +++ b/src/Oro/Bundle/FilterBundle/Resources/public/js/collection-filters-manager.js @@ -105,7 +105,7 @@ define([ * @protected */ _onCollectionReset: function(collection) { - var hasRecords = collection.state.totalRecords > 0; + var hasRecords = collection.length > 0; var hasFiltersState = !_.isEmpty(collection.state.filters); if (hasRecords || hasFiltersState) { if (!this.isVisible) { @@ -156,6 +156,7 @@ define([ _applyState: function(state) { var toEnable = []; var toDisable = []; + var valuesToApply = {}; _.each(this.filters, function(filter, name) { var shortName = '__' + name; @@ -178,7 +179,7 @@ define([ value: filterState }; } - filter.setValue(filterState); + valuesToApply[name] = filterState; toEnable.push(filter); } else if (_.has(state, shortName)) { filter.reset(); @@ -195,6 +196,10 @@ define([ this.enableFilters(toEnable); this.disableFilters(toDisable); + _.each(valuesToApply, function(filterState, name) { + this.filters[name].setValue(filterState); + }, this); + return this; } }); diff --git a/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/abstract-filter.js b/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/abstract-filter.js index f29abe3fed3..dc8e71dc0d9 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/abstract-filter.js +++ b/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/abstract-filter.js @@ -40,6 +40,13 @@ define([ */ enabled: false, + /** + * Is filter visible in UI + * + * @property {Boolean} + */ + visible: true, + /** * Is filter enabled by default * @@ -103,7 +110,7 @@ define([ * @param {Boolean} [options.enabled] */ initialize: function(options) { - var opts = _.pick(options || {}, 'enabled', 'canDisable', 'placeholder', 'showLabel', 'label', + var opts = _.pick(options || {}, 'enabled', 'visible', 'canDisable', 'placeholder', 'showLabel', 'label', 'templateSelector', 'templateTheme'); _.extend(this, opts); @@ -185,7 +192,9 @@ define([ * @return {*} */ show: function() { - this.$el.css('display', 'inline-block'); + if (this.visible) { + this.$el.css('display', 'inline-block'); + } return this; }, diff --git a/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/empty-filter.js b/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/empty-filter.js index 53fb75b77b3..eab292974ce 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/empty-filter.js +++ b/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/empty-filter.js @@ -158,7 +158,6 @@ define([ _onClickCriteriaSelector: function(e) { e.stopPropagation(); e.preventDefault(); - $('body').trigger('click'); if (!this.popupCriteriaShowed) { this._showCriteria(); } else { diff --git a/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/none-filter.js b/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/none-filter.js index 9cbc43e53b8..b68a1761bc2 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/none-filter.js +++ b/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/none-filter.js @@ -94,7 +94,6 @@ define([ */ _onClickCriteriaSelector: function(e) { e.stopPropagation(); - $('body').trigger('click'); if (!this.popupCriteriaShowed) { this._showCriteria(); } else { @@ -159,6 +158,7 @@ define([ * @protected */ _showCriteria: function() { + this.trigger('showCriteria', this); this.$(this.criteriaSelector).show(); this._setButtonPressed(this.$(this.criteriaSelector), true); setTimeout(_.bind(function() { diff --git a/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/select-filter.js b/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/select-filter.js index 184cc971115..7e94bc2f3cd 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/select-filter.js +++ b/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/select-filter.js @@ -378,7 +378,9 @@ define([ */ _onValueUpdated: function(newValue, oldValue) { SelectFilter.__super__._onValueUpdated.apply(this, arguments); - this.selectWidget.multiselect('refresh'); + if (this.selectWidget) { + this.selectWidget.multiselect('refresh'); + } }, /** diff --git a/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/text-filter.js b/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/text-filter.js index 014f3f6c2d7..75c01e606d4 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/text-filter.js +++ b/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/text-filter.js @@ -201,6 +201,7 @@ define([ * @protected */ _showCriteria: function() { + this.trigger('showCriteria', this); this.$(this.criteriaSelector).css('visibility', 'visible'); this._alignCriteria(); this._focusCriteria(); diff --git a/src/Oro/Bundle/FilterBundle/Resources/public/js/filters-manager.js b/src/Oro/Bundle/FilterBundle/Resources/public/js/filters-manager.js index cd20697d440..f7e7dff7a15 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/public/js/filters-manager.js +++ b/src/Oro/Bundle/FilterBundle/Resources/public/js/filters-manager.js @@ -2,7 +2,6 @@ define(function(require) { 'use strict'; var FiltersManager; - var DROPDOWN_TOGGLE_SELECTOR = '[data-toggle=dropdown]'; var $ = require('jquery'); var _ = require('underscore'); var __ = require('orotranslation/js/translator'); @@ -81,7 +80,7 @@ define(function(require) { events: { 'change [data-action=add-filter-select]': '_onChangeFilterSelect', 'click .reset-filter-button': '_onReset', - 'click a.dropdown-toggle': '_onDropdownToggle' + 'click a[data-name="filters-dropdown"]': '_onDropdownToggle' }, /** @@ -106,12 +105,14 @@ define(function(require) { filterListeners = { 'update': this._onFilterUpdated, - 'disable': this._onFilterDisabled + 'disable': this._onFilterDisabled, + 'showCriteria': this._onFilterShowCriteria }; if (tools.isMobile()) { + var outsideActionEvents = 'click.' + this.cid + ' shown.bs.dropdown.' + this.cid; filterListeners.updateCriteriaClick = this._onUpdateCriteriaClick; - $('body').on('click.' + this.cid, DROPDOWN_TOGGLE_SELECTOR, _.bind(this._onBodyClick, this)); + $('body').on(outsideActionEvents, this._onOutsideActionEvent.bind(this)); } _.each(this.filters, function(filter) { @@ -167,6 +168,14 @@ define(function(require) { this.trigger('afterDisableFilter', filter); }, + _onFilterShowCriteria: function(shownFilter) { + _.each(this.filters, function(filter) { + if (filter !== shownFilter) { + _.result(filter, 'ensurePopupCriteriaClosed'); + } + }); + }, + /** * Returns list of filter raw values */ @@ -236,7 +245,7 @@ define(function(require) { var optionsSelectors = []; _.each(filters, function(filter) { - if (!filter.isRendered()) { + if (filter.visible && !filter.isRendered()) { var oldEl = filter.$el; // filter rendering process replaces $el filter.render(); @@ -305,7 +314,7 @@ define(function(require) { if (_.isFunction(filter.setDropdownContainer)) { filter.setDropdownContainer(this.dropdownContainer); } - if (!filter.enabled) { + if (!filter.enabled || !filter.visible) { // append element to reserve space // empty elements are hidden by default $filterItems.append(filter.$el); @@ -419,13 +428,8 @@ define(function(require) { * @private */ _onDropdownToggle: function(e) { - var $dropdown = this.$('.dropdown'); e.preventDefault(); - e.stopPropagation(); - if (!$dropdown.hasClass('oro-open')) { - $(DROPDOWN_TOGGLE_SELECTOR).trigger('tohide.bs.dropdown'); - } - $dropdown.toggleClass('oro-open'); + this.$('.filter-box > .dropdown').toggleClass('open'); }, /** @@ -435,7 +439,7 @@ define(function(require) { * @param {jQuery.Event} e * @protected */ - _onBodyClick: function(e) { + _onOutsideActionEvent: function(e) { if (!_.contains($(e.target).parents(), this.el)) { this.closeDropdown(); } diff --git a/src/Oro/Bundle/FilterBundle/Resources/public/js/map-filter-module-name.js b/src/Oro/Bundle/FilterBundle/Resources/public/js/map-filter-module-name.js index f8cbd6105e2..0019a2a4235 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/public/js/map-filter-module-name.js +++ b/src/Oro/Bundle/FilterBundle/Resources/public/js/map-filter-module-name.js @@ -9,6 +9,7 @@ define(function() { selectrow: 'select-row', multichoice: 'multiselect', boolean: 'select', + duplicate: 'select', dictionary: 'dictionary' }; diff --git a/src/Oro/Bundle/FilterBundle/Resources/public/js/multiselect-decorator.js b/src/Oro/Bundle/FilterBundle/Resources/public/js/multiselect-decorator.js index 967f97b5d4c..2a5e2a9ef97 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/public/js/multiselect-decorator.js +++ b/src/Oro/Bundle/FilterBundle/Resources/public/js/multiselect-decorator.js @@ -115,7 +115,6 @@ define([ onOpenDropdown: function() { this._setDropdownDesign(); this.getWidget().find('input[type="search"]').focus(); - $('body').trigger('click'); }, /** diff --git a/src/Oro/Bundle/FilterBundle/Resources/views/Js/container.js.twig b/src/Oro/Bundle/FilterBundle/Resources/views/Js/container.js.twig index 27d39981297..ff5042d08d9 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/views/Js/container.js.twig +++ b/src/Oro/Bundle/FilterBundle/Resources/views/Js/container.js.twig @@ -3,16 +3,18 @@ {% if isMobileVersion() %}