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() %}