diff --git a/UPGRADE-1.9.md b/UPGRADE-1.9.md index 6dd26054a16..0d051ef0127 100644 --- a/UPGRADE-1.9.md +++ b/UPGRADE-1.9.md @@ -13,6 +13,7 @@ UPGRADE FROM 1.8 to 1.9 ####AddressBundle - `oro_address.address.manager` service was marked as private +- Validation `AbstractAddress::isRegionValid` was moved to `Oro\Bundle\AddressBundle\Validator\Constraints\ValidRegion` constraint ####CalendarBundle - `oro_calendar.calendar_provider.user` service was marked as private @@ -29,11 +30,92 @@ UPGRADE FROM 1.8 to 1.9 ####DataAuditBundle - `Oro\Bundle\DataAuditBundle\EventListener\KernelListener` added to the class cache and constructor have container as performance improvement +- `Oro\Bundle\DataAuditBundle\Entity\AbstractAudit` has `@InheritanceType("SINGLE_TABLE")` +- `audit-grid` and `audit-history-grid` based on `Oro\Bundle\DataAuditBundle\Entity\AbstractAudit` now. Make join to get your entity on grid ####DataGridBundle +- `Oro\Bundle\DataGridBundle\Datagrid\Builder::DATASOURCE_PATH` marked as deprecated. Use `Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration::DATASOURCE_PATH`. +- `Oro\Bundle\DataGridBundle\Datagrid\Builder::DATASOURCE_TYPE_PATH` marked as deprecated. Use `Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration::getAclResource`. +- `Oro\Bundle\DataGridBundle\Datagrid\Builder::DATASOURCE_ACL_PATH` marked as deprecated. Use `Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration::getAclResource`. +- `Oro\Bundle\DataGridBundle\Datagrid\Builder::BASE_DATAGRID_CLASS_PATH` marked as deprecated. Use `Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration::BASE_DATAGRID_CLASS_PATH`. +- `Oro\Bundle\DataGridBundle\Datagrid\Builder::DATASOURCE_SKIP_ACL_CHECK` marked as deprecated. Use `Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration::isDatasourceSkipAclApply`. +- `Oro\Bundle\DataGridBundle\Datagrid\Builder::DATASOURCE_SKIP_COUNT_WALKER_PATH` marked as deprecated. Use `Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration::DATASOURCE_SKIP_COUNT_WALKER_PATH`. +- Option "acl_resource" moved from option "source" to root node of datagrid configuration: + +Before + +``` +datagrid: + acme-demo-grid: + ... # some configuration + source: + acl_resource: 'acme_demo_entity_view' + ... # some configuration +``` + +Now + +``` +datagrid: + acme-demo-grid: + acl_resource: 'acme_demo_entity_view' + ... # some configuration +``` + +- Option of datagrid "skip_acl_check" is deprecated, use option "skip_acl_apply" instead. Logic of this option was also changed. Before this option caused ignorance of option "acl_resource". Now it is responsible only for indication whether or not ACL should be applied to source query of the grid. See [advanced_grid_configuration.md](.src/Oro/Bundle/DataGridBundle/Resources/doc/backend/advanced_grid_configuration.md) for use cases. + +Before + +``` +datagrid: + acme-demo-grid: + ... # some configuration + options: + skip_acl_check: true +``` + +Now + +``` +datagrid: + acme-demo-grid: + ... # some configuration + source: + skip_acl_apply: true + ... # some configuration +``` + - Services with tag `oro_datagrid.extension.formatter.property` was marked as private - JS collection models format changed to maintain compatibility with Backbone collections: now it is always list of models, and additional parameters are passed through the options - +- Grid merge uses distinct policy + +``` +grid-name: + source: + value: 1 +grid-name: + source: + value: 2 +``` + +will result + +``` +grid-name: + source: + value: 2 +``` + +instead of + +``` +grid-name: + source: + value: + - 1 + - 2 +``` + ####DistributionBundle: - Fix `priority` attribute handling for `routing.options_resolver` tag to be conform Symfony standards. New behaviour: the higher the priority, the sooner the resolver gets executed. @@ -54,6 +136,7 @@ UPGRADE FROM 1.8 to 1.9 - `oro_entity.entity_hierarchy_provider` service was marked as private. - `oro_entity.entity_hierarchy_provider.class` parameter was removed. - `oro_entity.entity_hierarchy_provider.all` service was added. It can be used if you need a hierarchy of all entities but not only configurable ones. +- Class `Oro\Bundle\EntityBundle\Provider\EntityContextProvider` was moved to `Oro\Bundle\ActivityBundle\Provider\ContextGridProvider` and `oro_entity.entity_context_provider` service was moved to `oro_activity.provider.context_grid`. ####EntityConfigBundle - Removed `optionSet` field type deprecated since v1.4. Existing options sets are converted to `Select` or `Multi-Select` automatically during the Platform update. @@ -133,12 +216,16 @@ after: - Added `Oro\Bundle\ImportExportBundle\Formatter\ExcelDateTimeTypeFormatter` as default formatter for the date, time and datetime types in `Oro\Bundle\ImportExportBundle\Serializer\Normalizer\DateTimeNormalizer`. This types exported/imported depends on the application locale and timezone and recognized as dates in Microsoft Excel. - `Oro\Bundle\ImportExportBundle\Field\DatabaseHelper::getRegistry` is deprecated. Use class methods instead of disposed registry - Services with tag `oro_importexport.normalizer` was marked as private +- Allow to omit empty identity fields. To use this feature set `Use As Identity Field` option to `Only when not empty +` (-1 or `Oro\Bundle\ImportExportBundle\Field\FieldHelper::IDENTITY_ONLY_WHEN_NOT_EMPTY` in a code) ####InstallerBundle - `Oro\Bundle\InstallerBundle\EventListener\RequestListener` added to the class cache as performance improvement ####LayoutBundle - `Oro\Bundle\LayoutBundle\EventListener\ThemeListener` added to the class cache as performance improvement +- The theme definition should be placed at theme folder and named `theme.yml`, for example `DemoBundle/Resources/views/layouts/first_theme/theme.yml` +- Deprecated method: placed at `Resources/config/oro/` and named `layout.yml`, for example `DemoBundle/Resources/config/oro/layout.yml` ####LocaleBundle - `Oro\Bundle\LocaleBundle\EventListener\LocaleListener` added to the class cache and constructor have container as performance improvement @@ -150,6 +237,9 @@ after: - `Oro\Bundle\NavigationBundle\Event\AddMasterRequestRouteListener` added to the class cache as performance improvement - `Oro\Bundle\NavigationBundle\Event\RequestTitleListener` added to the class cache as performance improvement +####NoteBundle + - Added parameter `DoctrineHelper $doctrineHelper` to constructor of `\Oro\Bundle\NoteBundle\Placeholder\PlaceholderFilter` + ####PlatformBundle - Bundle now has priority `-200` and it is loaded right after main Symfony bundles - Services with tag `doctrine.event_listener` was marked as private @@ -161,10 +251,15 @@ after: - `Oro\Bundle\SecurityBundle\Owner\OwnerTreeInterface` is changed. New method `buildTree` added (due to performance issues). It should be called once after all `addDeepEntity` calls. See [OwnerTreeProvider](./src/Oro/Bundle/SecurityBundle/Owner/OwnerTreeProvider.php) method `fillTree`. Implementation example [OwnerTree](./src/Oro/Bundle/SecurityBundle/Owner/OwnerTree.php). - Bundle now contains part of Symfony security configuration (ACL configuration and access decision manager strategy) - `Oro\Bundle\SecurityBundle\Http\Firewall\ContextListener` added to the class cache and constructor have container as performance improvement +- `Oro\Bundle\SecurityBundle\Authentication\Token\UsernamePasswordOrganizationTokenFactoryInterface` and its implementation `Oro\Bundle\SecurityBundle\Authentication\Token\UsernamePasswordOrganizationTokenFactory` were introduced to encapsulate creation of `UsernamePasswordOrganizationToken` in `Oro\Bundle\SecurityBundle\Authentication\Provider\UsernamePasswordOrganizationAuthenticationProvider` and `Oro\Bundle\SecurityBundle\Http\Firewall\OrganizationBasicAuthenticationListener` +- `Oro\Bundle\SecurityBundle\Authentication\Token\OrganizationRememberMeTokenFactoryInterface` and its implementation `Oro\Bundle\SecurityBundle\Authentication\Token\OrganizationRememberMeTokenFactory` were introduced to encapsulate creation of `OrganizationRememberMeToken` in `Oro\Bundle\SecurityBundle\Authentication\Provider\UsernamePasswordOrganizationAuthenticationProvider` ####SidebarBundle - `Oro\Bundle\SidebarBundle\EventListener\RequestHandler` added to the class cache as performance improvement +####SSOBundle +- `Oro\Bundle\SSOBundle\Security\OAuthTokenFactoryInterface` and its implementation `Oro\Bundle\SSOBundle\Security\OAuthTokenFactory` were introduced to encapsulate creation of `OAuthToken` in `Oro\Bundle\SSOBundle\Security\OAuthProvider` + ####SoapBundle - Bundle now contains configuration of security firewall `wsse_secured` - `Oro\Bundle\SoapBundle\EventListener\LocaleListener` added to the class cache and constructor have container as performance improvement @@ -176,15 +271,22 @@ after: - `/Resources/translations/tooltips.*.yml` deprecated since 1.9.0. Will be removed in 1.11.0. Use `/Resources/translations/messages.*.yml` instead ####UiBundle +- Added `assets_version_strategy` parameter which can be used to automatically update `assets_version` parameter. Possible values are: + - null - the assets version stays unchanged + - time_hash - a hash of the current time (default strategy) + - incremental - the next assets version is the previous version is incremented by one (e.g. 'ver1' -> 'ver2' or '1' -> '2') +- Removed `assets_version` global variable from TWIG. Use `asset_version` or `asset` TWIG functions instead - Added possibility to group tabs in dropdown for tabs panel. Added options to tabPanel function. Example: `{{ tabPanel(tabs, {useDropdown: true}) }}` - Added possibility to set content for specific tab. Example: `{{ tabPanel([{label: 'Tab', content: 'Tab content'}]) }}` - `Oro\Bundle\UIBundle\EventListener\ContentProviderListener` added to the class cache and constructor have container as performance improvement - Services with tag `oro_ui.content_provider` was marked as private - Services with tag `oro_formatter` was marked as private +- Class `Oro\Bundle\UIBundle\Tools\ArrayUtils` marked as deprecated. Use `Oro\Component\PhpUtils\ArrayUtil` instead. ####UserBundle - Bundle now contains configuration of security providers (`chain_provider`, `oro_user`, `in_memory`), encoders and security firewalls (`login`, `reset_password`, `main`) - Bundle DI extension `OroUserExtension` has been updated to make sure that `main` security firewall is always the last in list +- `Oro\Bundle\UserBundle\Security\WsseTokenFactoryInterface` and its implementation `Oro\Bundle\UserBundle\Security\WsseTokenFactory` were introduced to encapsulate creation of `WsseToken` in `Oro\Bundle\UserBundle\Security\WsseAuthProvider` ####WorkflowBundle - Constructor of `Oro\Bundle\WorkflowBundle\Model\Process` changed. New argument: `ConditionFactory $conditionFactory` diff --git a/composer.json b/composer.json index 6045b5d6073..4a7c481469e 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "symfony/icu": "~1.1", "symfony/swiftmailer-bundle": "2.3.*", "symfony/monolog-bundle": "2.7.*", - "sensio/distribution-bundle": "2.3.14", + "sensio/distribution-bundle": "4.0.3", "sensio/framework-extra-bundle": "2.3.4", "incenteev/composer-parameter-handler": "2.1.0", "jms/job-queue-bundle": "1.2.*", @@ -75,7 +75,8 @@ "components/font-awesome": "~4.3.0", "piwik/device-detector": "~3.0", "oro/jsplumb": "~1.7", - "oro/moment-timezone": "0.3.*" + "oro/moment-timezone": "0.3.*", + "vakata/jstree": "^3.2" }, "require-dev": { "sensio/generator-bundle": "2.5.3" diff --git a/src/Oro/Bundle/ActivityBundle/Autocomplete/ContextSearchHandler.php b/src/Oro/Bundle/ActivityBundle/Autocomplete/ContextSearchHandler.php index 6eacb7c800b..7f5dfb00f0b 100644 --- a/src/Oro/Bundle/ActivityBundle/Autocomplete/ContextSearchHandler.php +++ b/src/Oro/Bundle/ActivityBundle/Autocomplete/ContextSearchHandler.php @@ -19,6 +19,7 @@ use Oro\Bundle\EntityBundle\Tools\EntityClassNameHelper; use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; use Oro\Bundle\ActivityBundle\Manager\ActivityManager; +use Oro\Bundle\ActivityBundle\Event\SearchAliasesEvent; use Oro\Bundle\FormBundle\Autocomplete\ConverterInterface; use Oro\Bundle\SearchBundle\Engine\Indexer; use Oro\Bundle\SearchBundle\Query\Result\Item; @@ -183,17 +184,14 @@ protected function searchById($targetsString) */ public function convertItem($item) { + $this->dispatcher->dispatch(PrepareResultItemEvent::EVENT_NAME, new PrepareResultItemEvent($item)); + /** @var Item $item */ $text = $item->getRecordTitle(); $className = $item->getEntityName(); $entityMapParameter = $this->mapper->getEntityMapParameter($className, 'title_fields'); - if ($text === null && $entityMapParameter) { - $this->dispatcher->dispatch(PrepareResultItemEvent::EVENT_NAME, new PrepareResultItemEvent($item)); - $text = $item->getRecordTitle(); - } - if (strlen($text) === 0 && !$entityMapParameter) { $text = $this->translator->trans('oro.entity.item', ['%id%' => $item->getRecordId()]); } @@ -406,6 +404,10 @@ protected function getSearchAliases() $aliases[] = $alias; } } + /** dispatch oro_activity.search_aliases event */ + $event = new SearchAliasesEvent($aliases, $targetEntityClasses); + $this->dispatcher->dispatch(SearchAliasesEvent::EVENT_NAME, $event); + $aliases = $event->getAliases(); return $aliases; } diff --git a/src/Oro/Bundle/ActivityBundle/Controller/ActivityController.php b/src/Oro/Bundle/ActivityBundle/Controller/ActivityController.php index 6b4f8dbfe73..b188061a1d5 100644 --- a/src/Oro/Bundle/ActivityBundle/Controller/ActivityController.php +++ b/src/Oro/Bundle/ActivityBundle/Controller/ActivityController.php @@ -63,7 +63,7 @@ public function contextAction($activity, $id) throw new AccessDeniedException(); } - $entityTargets = $this->get('oro_entity.entity_context_provider')->getSupportedTargets($entity); + $entityTargets = $this->get('oro_activity.provider.context_grid')->getSupportedTargets($entity); $entityClassAlias = $this->get('oro_entity.entity_alias_resolver')->getPluralAlias($entityClass); return [ @@ -93,12 +93,14 @@ public function contextAction($activity, $id) */ public function contextGridAction($activity, $id, $entityClass = null) { - $gridName = $this->get('oro_entity.entity_context_provider')->getContextGridByEntity($entityClass); + $entityClass = $this->get('oro_entity.routing_helper')->resolveEntityClass($entityClass); + $gridName = $this->get('oro_activity.provider.context_grid')->getContextGridByEntity($entityClass); // Need to specify parameters for Oro\Bundle\ActivityBundle\EventListener\Datagrid\ContextGridListener $params = [ 'activityClass' => $activity, - 'activityId' => $id + 'activityId' => $id, + 'class_name' => $entityClass, ]; return [ diff --git a/src/Oro/Bundle/ActivityBundle/Entity/Manager/ActivityContextApiEntityManager.php b/src/Oro/Bundle/ActivityBundle/Entity/Manager/ActivityContextApiEntityManager.php index 5712efe1246..c2704256e8f 100644 --- a/src/Oro/Bundle/ActivityBundle/Entity/Manager/ActivityContextApiEntityManager.php +++ b/src/Oro/Bundle/ActivityBundle/Entity/Manager/ActivityContextApiEntityManager.php @@ -5,11 +5,12 @@ use Doctrine\Common\Util\ClassUtils; use Doctrine\Common\Persistence\ObjectManager; -use Oro\Bundle\ActivityBundle\Model\ActivityInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Oro\Bundle\ActivityBundle\Model\ActivityInterface; use Oro\Bundle\ActivityBundle\Manager\ActivityManager; use Oro\Bundle\EntityBundle\ORM\DoctrineHelper; use Oro\Bundle\EntityBundle\ORM\EntityAliasResolver; @@ -17,6 +18,7 @@ use Oro\Bundle\EntityExtendBundle\Tools\ExtendHelper; use Oro\Bundle\SearchBundle\Engine\ObjectMapper; use Oro\Bundle\SoapBundle\Entity\Manager\ApiEntityManager; +use Oro\Bundle\ActivityBundle\Event\PrepareContextTitleEvent; class ActivityContextApiEntityManager extends ApiEntityManager { @@ -44,6 +46,9 @@ class ActivityContextApiEntityManager extends ApiEntityManager /** @var DoctrineHelper */ protected $doctrineHelper; + /** @var EventDispatcherInterface */ + protected $dispatcher; + /** * @param ObjectManager $om * @param ActivityManager $activityManager @@ -54,6 +59,7 @@ class ActivityContextApiEntityManager extends ApiEntityManager * @param ObjectMapper $objectMapper * @param TranslatorInterface $translator * @param DoctrineHelper $doctrineHelper + * @param EventDispatcherInterface $dispatcher */ public function __construct( ObjectManager $om, @@ -64,7 +70,8 @@ public function __construct( EntityAliasResolver $entityAliasResolver, ObjectMapper $objectMapper, TranslatorInterface $translator, - DoctrineHelper $doctrineHelper + DoctrineHelper $doctrineHelper, + EventDispatcherInterface $dispatcher ) { parent::__construct(null, $om); @@ -76,6 +83,7 @@ public function __construct( $this->mapper = $objectMapper; $this->translator = $translator; $this->doctrineHelper = $doctrineHelper; + $this->dispatcher = $dispatcher; } /** @@ -110,32 +118,18 @@ public function getActivityContext($class, $id) $item = []; $config = $entityProvider->getConfig($targetClass); - $metadata = $this->configManager->getEntityMetadata($targetClass); $safeClassName = $this->entityClassNameHelper->getUrlSafeClassName($targetClass); - $link = null; - if ($metadata) { - $link = $this->router->generate($metadata->getRoute(), ['id' => $targetId]); - } elseif ($link === null && ExtendHelper::isCustomEntity($targetClass)) { - // Generate view link for the custom entity - $link = $this->router->generate( - 'oro_entity_view', - [ - 'id' => $targetId, - 'entityName' => $safeClassName - - ] - ); - } - - if ($fields = $this->mapper->getEntityMapParameter($targetClass, 'title_fields')) { - $text = []; - foreach ($fields as $field) { - $text[] = $this->mapper->getFieldValue($target, $field); + if (!array_key_exists('title', $item) || !$item['title']) { + if ($fields = $this->mapper->getEntityMapParameter($targetClass, 'title_fields')) { + $text = []; + foreach ($fields as $field) { + $text[] = $this->mapper->getFieldValue($target, $field); + } + $item['title'] = implode(' ', $text); + } else { + $item['title'] = $this->translator->trans('oro.entity.item', ['%id%' => $targetId]); } - $item['title'] = implode(' ', $text); - } else { - $item['title'] = $this->translator->trans('oro.entity.item', ['%id%' => $targetId]); } $item['activityClassAlias'] = $this->entityAliasResolver->getPluralAlias($class); @@ -145,11 +139,49 @@ public function getActivityContext($class, $id) $item['targetClassName'] = $safeClassName; $item['icon'] = $config->get('icon'); - $item['link'] = $link; + $item['link'] = $this->getContextLink($targetClass, $targetId); + + $event = new PrepareContextTitleEvent($item, $targetClass); + $this->dispatcher->dispatch(PrepareContextTitleEvent::EVENT_NAME, $event); + $item = $event->getItem(); $result[] = $item; } return $result; } + + /** + * @param string $targetClass The FQCN of the activity target entity + * @param int $targetId The identifier of the activity target entity + * + * @return string|null + */ + protected function getContextLink($targetClass, $targetId) + { + $metadata = $this->configManager->getEntityMetadata($targetClass); + $link = null; + if ($metadata) { + try { + $route = $metadata->getRoute('view', true); + } catch (\LogicException $exception) { + // Need for cases when entity does not have route. + return null; + } + $link = $this->router->generate($route, ['id' => $targetId]); + } elseif (ExtendHelper::isCustomEntity($targetClass)) { + $safeClassName = $this->entityClassNameHelper->getUrlSafeClassName($targetClass); + // Generate view link for the custom entity + $link = $this->router->generate( + 'oro_entity_view', + [ + 'id' => $targetId, + 'entityName' => $safeClassName + + ] + ); + } + + return $link; + } } diff --git a/src/Oro/Bundle/ActivityBundle/Entity/Manager/ActivityEntityDeleteHandler.php b/src/Oro/Bundle/ActivityBundle/Entity/Manager/ActivityEntityDeleteHandler.php index 67e4c648540..b6fa0385a09 100644 --- a/src/Oro/Bundle/ActivityBundle/Entity/Manager/ActivityEntityDeleteHandler.php +++ b/src/Oro/Bundle/ActivityBundle/Entity/Manager/ActivityEntityDeleteHandler.php @@ -4,6 +4,7 @@ use Doctrine\ORM\EntityNotFoundException; +use Oro\Bundle\ActivityBundle\Manager\ActivityManager; use Oro\Bundle\ActivityBundle\Model\ActivityInterface; use Oro\Bundle\SecurityBundle\Exception\ForbiddenException; use Oro\Bundle\SecurityBundle\SecurityFacade; @@ -16,6 +17,9 @@ class ActivityEntityDeleteHandler extends DeleteHandler /** @var SecurityFacade */ protected $securityFacade; + /** @var ActivityManager */ + protected $activityManager; + /** * @param SecurityFacade $securityFacade */ @@ -24,6 +28,14 @@ public function setSecurityFacade(SecurityFacade $securityFacade) $this->securityFacade = $securityFacade; } + /** + * @param ActivityManager $activityManager + */ + public function setActivityManager(ActivityManager $activityManager) + { + $this->activityManager = $activityManager; + } + /** * Handle delete entity object. * @@ -54,7 +66,7 @@ public function handleDelete($id, ApiEntityManager $manager) throw new ForbiddenException('has no view permissions for related entity'); } - $entity->removeActivityTarget($targetEntity); + $this->activityManager->removeActivityTarget($entity, $targetEntity); $em->flush(); } diff --git a/src/Oro/Bundle/ActivityBundle/Event/PrepareContextTitleEvent.php b/src/Oro/Bundle/ActivityBundle/Event/PrepareContextTitleEvent.php new file mode 100644 index 00000000000..c259e92adc4 --- /dev/null +++ b/src/Oro/Bundle/ActivityBundle/Event/PrepareContextTitleEvent.php @@ -0,0 +1,59 @@ +item = $item; + $this->targetClass = $targetClass; + } + + /** + * Return item array + * + * @return array + */ + public function getItem() + { + return $this->item; + } + + /** + * Set the item array + * + * @param array $item + */ + public function setItem($item) + { + $this->item = $item; + } + + /** + * Return target class + * + * @return string + */ + public function getTargetClass() + { + return $this->targetClass; + } +} diff --git a/src/Oro/Bundle/ActivityBundle/Event/SearchAliasesEvent.php b/src/Oro/Bundle/ActivityBundle/Event/SearchAliasesEvent.php new file mode 100644 index 00000000000..cd1c2e78314 --- /dev/null +++ b/src/Oro/Bundle/ActivityBundle/Event/SearchAliasesEvent.php @@ -0,0 +1,59 @@ +aliases = $aliases; + $this->targetClasses = $targetClasses; + } + + /** + * Return aliases config array + * + * @return array + */ + public function getAliases() + { + return $this->aliases; + } + + /** + * Set the aliases config array + * + * @param array $aliases + */ + public function setAliases($aliases) + { + $this->aliases = $aliases; + } + + /** + * Return target classes array + * + * @return array + */ + public function getTargetClasses() + { + return $this->targetClasses; + } +} diff --git a/src/Oro/Bundle/ActivityBundle/EventListener/Datagrid/ContextGridListener.php b/src/Oro/Bundle/ActivityBundle/EventListener/Datagrid/ContextGridListener.php index 0281dbd1cd8..a4e404c0b34 100644 --- a/src/Oro/Bundle/ActivityBundle/EventListener/Datagrid/ContextGridListener.php +++ b/src/Oro/Bundle/ActivityBundle/EventListener/Datagrid/ContextGridListener.php @@ -60,10 +60,9 @@ public function onBuildAfter(BuildAfter $event) $class = $parameters->get('activityClass'); $entityClass = $this->entityClassNameHelper->resolveEntityClass($class, true); - /** @var ActivityInterface $entity */ $entity = $this->doctrineHelper->getEntity($entityClass, $id); - if ($entity) { + if ($entity && $entity instanceof ActivityInterface) { $targetsArray = $entity->getActivityTargets($targetClass); $targetIds = []; @@ -72,7 +71,7 @@ public function onBuildAfter(BuildAfter $event) } if ($targetIds) { - $queryBuilder->andWhere($queryBuilder->expr()->notIn("$alias.id", $targetIds)); + $queryBuilder->andWhere($queryBuilder->expr()->notIn(sprintf('%s.id', $alias), $targetIds)); } } } diff --git a/src/Oro/Bundle/ActivityBundle/Form/DataTransformer/ContextsToViewTransformer.php b/src/Oro/Bundle/ActivityBundle/Form/DataTransformer/ContextsToViewTransformer.php index bf74880d7ad..f534bb32f77 100644 --- a/src/Oro/Bundle/ActivityBundle/Form/DataTransformer/ContextsToViewTransformer.php +++ b/src/Oro/Bundle/ActivityBundle/Form/DataTransformer/ContextsToViewTransformer.php @@ -5,10 +5,12 @@ use Doctrine\Common\Util\ClassUtils; use Doctrine\ORM\EntityManager; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Oro\Bundle\ActivityBundle\Event\PrepareContextTitleEvent; use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; use Oro\Bundle\SearchBundle\Engine\ObjectMapper; @@ -29,25 +31,31 @@ class ContextsToViewTransformer implements DataTransformerInterface /* @var TokenStorageInterface */ protected $securityTokenStorage; + /** @var EventDispatcherInterface */ + protected $dispatcher; + /** * @param EntityManager $entityManager * @param ConfigManager $configManager * @param TranslatorInterface $translator * @param ObjectMapper $mapper * @param TokenStorageInterface $securityTokenStorage + * @param EventDispatcherInterface $dispatcher */ public function __construct( EntityManager $entityManager, ConfigManager $configManager, TranslatorInterface $translator, ObjectMapper $mapper, - TokenStorageInterface $securityTokenStorage + TokenStorageInterface $securityTokenStorage, + EventDispatcherInterface $dispatcher ) { $this->entityManager = $entityManager; $this->configManager = $configManager; $this->translator = $translator; $this->mapper = $mapper; $this->securityTokenStorage = $securityTokenStorage; + $this->dispatcher = $dispatcher; } /** @@ -64,13 +72,14 @@ public function transform($value) $user = $this->securityTokenStorage->getToken()->getUser(); foreach ($value as $target) { // Exclude current user - if (ClassUtils::getClass($user) === ClassUtils::getClass($target) && + $targetClass = ClassUtils::getClass($target); + if (ClassUtils::getClass($user) === $targetClass && $user->getId() === $target->getId() ) { continue; } - if ($fields = $this->mapper->getEntityMapParameter(ClassUtils::getClass($target), 'title_fields')) { + if ($fields = $this->mapper->getEntityMapParameter($targetClass, 'title_fields')) { $text = []; foreach ($fields as $field) { $text[] = $this->mapper->getFieldValue($target, $field); @@ -79,10 +88,17 @@ public function transform($value) $text = [$this->translator->trans('oro.entity.item', ['%id%' => $target->getId()])]; } $text = implode(' ', $text); - if ($label = $this->getClassLabel(ClassUtils::getClass($target))) { + if ($label = $this->getClassLabel($targetClass)) { $text .= ' (' . $label . ')'; } + $item['title'] = $text; + $item['targetId'] = $target->getId(); + $event = new PrepareContextTitleEvent($item, $targetClass); + $this->dispatcher->dispatch(PrepareContextTitleEvent::EVENT_NAME, $event); + $item = $event->getItem(); + $text = $item['title']; + $result[] = json_encode( [ 'text' => $text, diff --git a/src/Oro/Bundle/ActivityBundle/Form/Type/ContextsSelectType.php b/src/Oro/Bundle/ActivityBundle/Form/Type/ContextsSelectType.php index 4bbc7deee86..0522b96a998 100644 --- a/src/Oro/Bundle/ActivityBundle/Form/Type/ContextsSelectType.php +++ b/src/Oro/Bundle/ActivityBundle/Form/Type/ContextsSelectType.php @@ -12,6 +12,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Oro\Bundle\ActivityBundle\Form\DataTransformer\ContextsToViewTransformer; use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; @@ -36,25 +37,31 @@ class ContextsSelectType extends AbstractType /* @var TokenStorageInterface */ protected $securityTokenStorage; + /** @var EventDispatcherInterface */ + protected $dispatcher; + /** * @param EntityManager $entityManager * @param ConfigManager $configManager * @param TranslatorInterface $translator * @param ObjectMapper $mapper * @param TokenStorageInterface $securityTokenStorage + * @param EventDispatcherInterface $dispatcher */ public function __construct( EntityManager $entityManager, ConfigManager $configManager, TranslatorInterface $translator, ObjectMapper $mapper, - TokenStorageInterface $securityTokenStorage + TokenStorageInterface $securityTokenStorage, + EventDispatcherInterface $dispatcher ) { $this->entityManager = $entityManager; $this->configManager = $configManager; $this->translator = $translator; $this->mapper = $mapper; $this->securityTokenStorage = $securityTokenStorage; + $this->dispatcher = $dispatcher; } /** @@ -69,7 +76,8 @@ public function buildForm(FormBuilderInterface $builder, array $options) $this->configManager, $this->translator, $this->mapper, - $this->securityTokenStorage + $this->securityTokenStorage, + $this->dispatcher ) ); } diff --git a/src/Oro/Bundle/ActivityBundle/Form/Type/MultipleAssociationChoiceType.php b/src/Oro/Bundle/ActivityBundle/Form/Type/MultipleAssociationChoiceType.php new file mode 100644 index 00000000000..346ac062b9c --- /dev/null +++ b/src/Oro/Bundle/ActivityBundle/Form/Type/MultipleAssociationChoiceType.php @@ -0,0 +1,51 @@ +getClassName(); + + /** @var FormView $choiceView */ + foreach ($view->children as $choiceView) { + // disable activity with same class as target entity + if ((isset($view->vars['disabled']) && $view->vars['disabled']) + || ($choiceView->vars['value'] === $targetClassName) + ) { + $choiceView->vars['disabled'] = true; + } + } + + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_activity_multiple_association_choice'; + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return 'oro_entity_extend_multiple_association_choice'; + } +} diff --git a/src/Oro/Bundle/ActivityBundle/Migrations/Schema/OroActivityBundleInstaller.php b/src/Oro/Bundle/ActivityBundle/Migrations/Schema/OroActivityBundleInstaller.php index d12ffde18a3..6512e5c8899 100644 --- a/src/Oro/Bundle/ActivityBundle/Migrations/Schema/OroActivityBundleInstaller.php +++ b/src/Oro/Bundle/ActivityBundle/Migrations/Schema/OroActivityBundleInstaller.php @@ -29,7 +29,7 @@ public function setContainer(ContainerInterface $container = null) */ public function getMigrationVersion() { - return 'v1_0'; + return 'v1_1'; } /** diff --git a/src/Oro/Bundle/ActivityBundle/Migrations/Schema/v1_0/OroActivityBundle.php b/src/Oro/Bundle/ActivityBundle/Migrations/Schema/v1_0/OroActivityBundle.php index 003e4115dc0..5eb16e5c06f 100644 --- a/src/Oro/Bundle/ActivityBundle/Migrations/Schema/v1_0/OroActivityBundle.php +++ b/src/Oro/Bundle/ActivityBundle/Migrations/Schema/v1_0/OroActivityBundle.php @@ -7,9 +7,6 @@ use Oro\Bundle\MigrationBundle\Migration\Migration; use Oro\Bundle\MigrationBundle\Migration\QueryBag; -/** - * - */ class OroActivityBundle implements Migration { /** diff --git a/src/Oro/Bundle/ActivityBundle/Migrations/Schema/v1_0/UpdateActivityButtonConfigQuery.php b/src/Oro/Bundle/ActivityBundle/Migrations/Schema/v1_0/UpdateActivityButtonConfigQuery.php index 1731aec1fb3..a93150a204a 100644 --- a/src/Oro/Bundle/ActivityBundle/Migrations/Schema/v1_0/UpdateActivityButtonConfigQuery.php +++ b/src/Oro/Bundle/ActivityBundle/Migrations/Schema/v1_0/UpdateActivityButtonConfigQuery.php @@ -10,7 +10,7 @@ class UpdateActivityButtonConfigQuery extends ParametrizedMigrationQuery { /** - * {inheritdoc} + * {@inheritdoc} */ public function getDescription() { @@ -21,7 +21,7 @@ public function getDescription() } /** - * {inheritdoc} + * {@inheritdoc} */ public function execute(LoggerInterface $logger) { diff --git a/src/Oro/Bundle/ActivityBundle/Migrations/Schema/v1_1/OroActivityBundle.php b/src/Oro/Bundle/ActivityBundle/Migrations/Schema/v1_1/OroActivityBundle.php new file mode 100644 index 00000000000..cb0ebc1c1d9 --- /dev/null +++ b/src/Oro/Bundle/ActivityBundle/Migrations/Schema/v1_1/OroActivityBundle.php @@ -0,0 +1,19 @@ +addQuery(new RemoveUnusedContextConfigQuery()); + } +} diff --git a/src/Oro/Bundle/ActivityBundle/Migrations/Schema/v1_1/RemoveUnusedContextConfigQuery.php b/src/Oro/Bundle/ActivityBundle/Migrations/Schema/v1_1/RemoveUnusedContextConfigQuery.php new file mode 100644 index 00000000000..2840c2c2a20 --- /dev/null +++ b/src/Oro/Bundle/ActivityBundle/Migrations/Schema/v1_1/RemoveUnusedContextConfigQuery.php @@ -0,0 +1,73 @@ +removeContextConfigs($logger, true); + + return $logger->getMessages(); + } + + /** + * {@inheritdoc} + */ + public function execute(LoggerInterface $logger) + { + $this->removeContextConfigs($logger); + } + + /** + * @param LoggerInterface $logger + * @param bool $dryRun + */ + protected function removeContextConfigs(LoggerInterface $logger, $dryRun = false) + { + $configs = $this->loadConfigs($logger); + foreach ($configs as $id => $data) { + if (!isset($data['entity']['context-grid'])) { + continue; + } + unset($data['entity']['context-grid']); + + $query = 'UPDATE oro_entity_config SET data = :data WHERE id = :id'; + $params = ['data' => $data, 'id' => $id]; + $types = ['data' => 'array', 'id' => 'integer']; + $this->logQuery($logger, $query, $params, $types); + if (!$dryRun) { + $this->connection->executeUpdate($query, $params, $types); + } + } + } + + /** + * @param LoggerInterface $logger + * + * @return array key = {config id}, value = data + */ + protected function loadConfigs(LoggerInterface $logger) + { + $sql = 'SELECT id, data FROM oro_entity_config'; + $this->logQuery($logger, $sql); + + $result = []; + + $rows = $this->connection->fetchAll($sql); + foreach ($rows as $row) { + $result[$row['id']] = $this->connection->convertToPHPValue($row['data'], 'array'); + } + + return $result; + } +} diff --git a/src/Oro/Bundle/EntityBundle/Provider/EntityContextProvider.php b/src/Oro/Bundle/ActivityBundle/Provider/ContextGridProvider.php similarity index 60% rename from src/Oro/Bundle/EntityBundle/Provider/EntityContextProvider.php rename to src/Oro/Bundle/ActivityBundle/Provider/ContextGridProvider.php index 9a7a8ae5c39..234ff536310 100644 --- a/src/Oro/Bundle/EntityBundle/Provider/EntityContextProvider.php +++ b/src/Oro/Bundle/ActivityBundle/Provider/ContextGridProvider.php @@ -1,67 +1,63 @@ routingHelper = $routingHelper; - $this->entityProvider = $entityProvider; + $this->routingHelper = $routingHelper; + $this->entityProvider = $entityProvider; $this->entityConfigProvider = $entityConfigProvider; } /** * @param object $entity + * * @return array */ public function getSupportedTargets($entity) { $targetEntities = $this->entityProvider->getEntities(); - $entityTargets = []; + $entityTargets = []; if (!is_object($entity) || !method_exists($entity, 'supportActivityTarget')) { return $entityTargets; } $count = count($targetEntities); - for ($i=0; $i < $count; $i++) { + for ($i = 0; $i < $count; $i++) { $targetEntity = $targetEntities[$i]; - $className = $targetEntity['name']; - $gridName = $this->getContextGridByEntity($className); + $className = $targetEntity['name']; + $gridName = $this->getContextGridByEntity($className); if ($gridName && !empty($className) && $entity->supportActivityTarget($className)) { $entityTargets[] = [ - 'label' => $targetEntity['label'], + 'label' => $targetEntity['label'], 'className' => $this->routingHelper->getUrlSafeClassName($targetEntity['name']), - 'first' => count($entityTargets) === 0, - 'gridName' => $gridName + 'first' => count($entityTargets) === 0, + 'gridName' => $gridName ]; $i++; @@ -73,16 +69,22 @@ public function getSupportedTargets($entity) /** * @param string $entityClass + * * @return string|null */ public function getContextGridByEntity($entityClass) { if (!empty($entityClass)) { $entityClass = $this->routingHelper->resolveEntityClass($entityClass); + if (ExtendHelper::isCustomEntity($entityClass)) { + return 'custom-entity-grid'; + } $config = $this->entityConfigProvider->getConfig($entityClass); - $gridName = $config->get('context-grid'); - if ($gridName) { - return $gridName; + if ($config->has('context')) { + return $config->get('context'); + } + if ($config->has('default')) { + return $config->get('default'); } } diff --git a/src/Oro/Bundle/ActivityBundle/README.md b/src/Oro/Bundle/ActivityBundle/README.md index a574f392756..be7e5b1d776 100644 --- a/src/Oro/Bundle/ActivityBundle/README.md +++ b/src/Oro/Bundle/ActivityBundle/README.md @@ -273,3 +273,26 @@ Bind items declared in *placeholders.yml* to the activity entity using `action_b */ class Email extends ExtendEmail ``` + +How to configure custom grid for activity context dialog +-------------------------------------------------------- + +If you want to define context grid for entity(e.g User) in the activity context dialog you need to add the +`context` option in entity class `@Config` annotation, e.g: + +``` php +/** + * @Config( + * defaultValues={ + * "grid"={ + * default="default-grid", + * context="default-context-grid" + * } + * } + * ) + */ +class User extends ExtendUser +``` + +This option is used to recognize grid for entity with higher priority than `default` option. +In cases if these options (`context` or `default`) are not defined for entity, it won`t appear in the context dialog. diff --git a/src/Oro/Bundle/ActivityBundle/Resources/config/assets.yml b/src/Oro/Bundle/ActivityBundle/Resources/config/assets.yml index fca15891bf9..1882abb6b81 100644 --- a/src/Oro/Bundle/ActivityBundle/Resources/config/assets.yml +++ b/src/Oro/Bundle/ActivityBundle/Resources/config/assets.yml @@ -1,3 +1,3 @@ css: 'oroactivity': - - 'bundles/oroactivity/css/less/style.less' + - 'bundles/oroactivity/css/less/main.less' diff --git a/src/Oro/Bundle/ActivityBundle/Resources/config/entity_config.yml b/src/Oro/Bundle/ActivityBundle/Resources/config/entity_config.yml index 84ba0b116c3..a9270e25b5c 100644 --- a/src/Oro/Bundle/ActivityBundle/Resources/config/entity_config.yml +++ b/src/Oro/Bundle/ActivityBundle/Resources/config/entity_config.yml @@ -8,7 +8,7 @@ oro_entity_config: require_schema_update: true priority: 250 form: - type: oro_entity_extend_multiple_association_choice + type: oro_activity_multiple_association_choice options: block: associations required: false @@ -74,3 +74,11 @@ oro_entity_config: action_link_widget: # string options: auditable: false + + grid: + entity: + items: + # grid name that used for rendering entity context + context: # string + options: + auditable: false diff --git a/src/Oro/Bundle/ActivityBundle/Resources/config/form.yml b/src/Oro/Bundle/ActivityBundle/Resources/config/form.yml index 2c45598d918..02ae491ca4d 100644 --- a/src/Oro/Bundle/ActivityBundle/Resources/config/form.yml +++ b/src/Oro/Bundle/ActivityBundle/Resources/config/form.yml @@ -1,3 +1,6 @@ +parameters: + oro_activity.type.multiple_association_choice.class: Oro\Bundle\ActivityBundle\Form\Type\MultipleAssociationChoiceType + services: oro_activity.form.type.contexts_select: class: Oro\Bundle\ActivityBundle\Form\Type\ContextsSelectType @@ -7,6 +10,7 @@ services: - @translator - @oro_search.mapper - @security.token_storage + - @event_dispatcher tags: - { name: form.type, alias: oro_activity_contexts_select } @@ -21,3 +25,11 @@ services: - [setRequest, [@?request=]] tags: - { name: form.type_extension, alias: form } + + oro_activity.type.multiple_association_choice: + class: %oro_activity.type.multiple_association_choice.class% + arguments: + - @oro_entity_extend.association_type_helper + - @oro_entity_config.config_manager + tags: + - { name: form.type, alias: oro_activity_multiple_association_choice } diff --git a/src/Oro/Bundle/ActivityBundle/Resources/config/services.yml b/src/Oro/Bundle/ActivityBundle/Resources/config/services.yml index a621ac871c2..7b40325dc1c 100644 --- a/src/Oro/Bundle/ActivityBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/ActivityBundle/Resources/config/services.yml @@ -13,7 +13,7 @@ parameters: oro_activity.handler.delete.activity_entity.class: Oro\Bundle\ActivityBundle\Entity\Manager\ActivityEntityDeleteHandler oro_activity.form.handler.activity_entity.api.class: Oro\Bundle\ActivityBundle\Form\Handler\ActivityEntityApiHandler oro_activity.manager.activity_target.api.class: Oro\Bundle\ActivityBundle\Entity\Manager\ActivityTargetApiEntityManager - oro_activity.listener.datagrid.context.class: Oro\Bundle\ActivityBundle\EventListener\Datagrid\ContextGridListener + services: oro_activity.manager: class: %oro_activity.manager.class% @@ -60,6 +60,21 @@ services: tags: - { name: oro_activity.activity_widget_provider } + oro_activity.provider.context_grid: + class: Oro\Bundle\ActivityBundle\Provider\ContextGridProvider + arguments: + - @oro_entity.routing_helper + - @oro_entity.entity_provider + - @oro_entity_config.provider.grid + + oro_activity.listener.context_grid: + class: Oro\Bundle\ActivityBundle\EventListener\Datagrid\ContextGridListener + arguments: + - @oro_entity.doctrine_helper + - @oro_entity.entity_class_name_helper + tags: + - { name: kernel.event_listener, event: oro_datagrid.datagrid.build.after, method: onBuildAfter } + oro_activity.widget_provider.actions: class: %oro_activity.widget_provider.actions.class% arguments: @@ -113,6 +128,7 @@ services: parent: oro_soap.handler.delete.abstract calls: - [setSecurityFacade, [@oro_security.security_facade]] + - [setActivityManager, [@oro_activity.manager]] oro_activity.form.handler.autocomplete: class: Oro\Bundle\ActivityBundle\Autocomplete\ContextSearchHandler @@ -169,3 +185,4 @@ services: - @oro_search.mapper - @translator - @oro_entity.doctrine_helper + - @event_dispatcher diff --git a/src/Oro/Bundle/ActivityBundle/Resources/public/css/less/style.less b/src/Oro/Bundle/ActivityBundle/Resources/public/css/less/activity-context.less similarity index 71% rename from src/Oro/Bundle/ActivityBundle/Resources/public/css/less/style.less rename to src/Oro/Bundle/ActivityBundle/Resources/public/css/less/activity-context.less index 506f6ce89eb..6acf5ec67be 100644 --- a/src/Oro/Bundle/ActivityBundle/Resources/public/css/less/style.less +++ b/src/Oro/Bundle/ActivityBundle/Resources/public/css/less/activity-context.less @@ -1,11 +1,13 @@ -@import "oroui/css/less/mixins"; - .activity-list-widget { .activity-context-activity-list { padding: 0 @horizontalPadding; } } +.activity-context-activity { + display: none; +} + .activity-context-activity-list { margin-bottom: 5px; } @@ -19,11 +21,10 @@ .context-item { list-style: none; cursor: pointer; - padding: 0 7px 0 7px; white-space: nowrap; } -.context-item:hover, .context-item.active { +.context-item.active { background-color: #ebebeb; } @@ -52,8 +53,13 @@ } .context-items-dropdown { - top: 35px; - left: 15px; + top: 38px; + left: 14px; + border-color: #d3d3d3; + .context-item { + padding: 2px 8px; + font-size: 13px; + } } .activity-context-current-block { @@ -63,6 +69,9 @@ outline: none; margin: 10px 10px 15px 10px; box-shadow: none; + .btn-group.open & { + box-shadow: none; + } } .activity-context-current-item { @@ -70,3 +79,14 @@ font-weight: bold; color: #666666; } + +.control-group .controls .activity-context-activity-items { + padding-top: 5px; + margin-left: 0; + .context-item { + color: #777; + font-size: 13px; + line-height: 20px; + padding: 0 2px; + } +} diff --git a/src/Oro/Bundle/ActivityBundle/Resources/public/css/less/main.less b/src/Oro/Bundle/ActivityBundle/Resources/public/css/less/main.less new file mode 100644 index 00000000000..d46d55814c3 --- /dev/null +++ b/src/Oro/Bundle/ActivityBundle/Resources/public/css/less/main.less @@ -0,0 +1,3 @@ +@import "oroui/css/less/mixins"; +@import "./activity-context"; +@import "./mobile/main"; diff --git a/src/Oro/Bundle/ActivityBundle/Resources/public/css/less/mobile/activity-context.less b/src/Oro/Bundle/ActivityBundle/Resources/public/css/less/mobile/activity-context.less new file mode 100644 index 00000000000..e7a7cd2cd41 --- /dev/null +++ b/src/Oro/Bundle/ActivityBundle/Resources/public/css/less/mobile/activity-context.less @@ -0,0 +1,49 @@ +// e.g. inside a page header +.activity-context-activity-block { + .activity-context-activity { + width: 100%; + .activity-context-activity-label, + .activity-context-activity-items { + font-size: inherit; + line-height: inherit; + } + .activity-context-activity-items .context-item { + font-size: inherit; + } + } +} + +// e.g. inside activity list widget +.activity-context-activity-list { + margin-bottom: 7px; + .activity-context-activity-label, + .activity-context-activity-items { + font-size: 12px; + line-height: 1.2em; + } + .activity-context-activity-label { + margin-bottom: 5px; + color: #777; + float: none; + text-align: left; + } + .activity-context-activity-items { + margin-left: 0; + padding-left: 0; + .context-item { + font-size: inherit; + line-height: inherit; + } + } +} + +.activity-list-widget { + .activity-context-activity-list { + padding: 0 @contentPadding; + } + .responsive-cell { + .activity-context-activity-list { + padding: 0; + } + } +} diff --git a/src/Oro/Bundle/ActivityBundle/Resources/public/css/less/mobile/main.less b/src/Oro/Bundle/ActivityBundle/Resources/public/css/less/mobile/main.less new file mode 100644 index 00000000000..83096550294 --- /dev/null +++ b/src/Oro/Bundle/ActivityBundle/Resources/public/css/less/mobile/main.less @@ -0,0 +1,4 @@ +.mobile-version { + @import "oroui/css/less/mobile/variables"; + @import "./activity-context"; +} diff --git a/src/Oro/Bundle/ActivityBundle/Resources/public/js/app/components/activity-context-activity-component.js b/src/Oro/Bundle/ActivityBundle/Resources/public/js/app/components/activity-context-activity-component.js index 910436f147c..babd5d660b0 100644 --- a/src/Oro/Bundle/ActivityBundle/Resources/public/js/app/components/activity-context-activity-component.js +++ b/src/Oro/Bundle/ActivityBundle/Resources/public/js/app/components/activity-context-activity-component.js @@ -12,12 +12,7 @@ define(function(require) { initialize: function(options) { this.options = options; - this.init(); - }, - - init: function() { this.initView(); - this.contextsView.render(); }, initView: function() { diff --git a/src/Oro/Bundle/ActivityBundle/Resources/public/js/app/views/activity-context-activity-view.js b/src/Oro/Bundle/ActivityBundle/Resources/public/js/app/views/activity-context-activity-view.js index a2f3161e7f3..e9b3eec5fb8 100644 --- a/src/Oro/Bundle/ActivityBundle/Resources/public/js/app/views/activity-context-activity-view.js +++ b/src/Oro/Bundle/ActivityBundle/Resources/public/js/app/views/activity-context-activity-view.js @@ -24,7 +24,7 @@ define([ this.template = _.template($('#activity-context-activity-list').html()); this.$containerContextTargets = $(options.el).find('.activity-context-activity-items'); - this.collection = new ActivityContextActivityCollection('oro_api_delete_activity_relation'); + this.collection = new ActivityContextActivityCollection(); this.editable = options.editable; this.initEvents(); @@ -74,11 +74,7 @@ define([ }, render: function() { - if (this.collection.length === 0) { - this.$el.hide(); - } else { - this.$el.show(); - } + this.$el.toggle(this.collection.length > 0); }, initEvents: function() { @@ -99,6 +95,7 @@ define([ self.$containerContextTargets.append($view); $view.find('i.icon-remove').click(function() { + $view.fadeOut(); model.destroy({ success: function(model, response) { messenger.notificationFlashMessage('success', __('oro.activity.contexts.removed')); @@ -112,6 +109,7 @@ define([ } }, error: function(model, response) { + $view.show(); messenger.showErrorMessage(__('oro.ui.item_delete_error'), response.responseJSON || {}); } }); diff --git a/src/Oro/Bundle/ActivityBundle/Tests/Unit/Event/SearchAliasesEventTest.php b/src/Oro/Bundle/ActivityBundle/Tests/Unit/Event/SearchAliasesEventTest.php new file mode 100644 index 00000000000..a73fa873400 --- /dev/null +++ b/src/Oro/Bundle/ActivityBundle/Tests/Unit/Event/SearchAliasesEventTest.php @@ -0,0 +1,18 @@ +assertSame($aliases, $event->getAliases()); + $updatedAliases = array_merge($aliases, ['customEntity']); + $event->setAliases($updatedAliases); + $this->assertSame($updatedAliases, $event->getAliases()); + } +} diff --git a/src/Oro/Bundle/ActivityBundle/Tests/Unit/Form/Type/ContextsSelectTypeTest.php b/src/Oro/Bundle/ActivityBundle/Tests/Unit/Form/Type/ContextsSelectTypeTest.php index 60b9720aaf7..42b3fff444b 100644 --- a/src/Oro/Bundle/ActivityBundle/Tests/Unit/Form/Type/ContextsSelectTypeTest.php +++ b/src/Oro/Bundle/ActivityBundle/Tests/Unit/Form/Type/ContextsSelectTypeTest.php @@ -26,6 +26,9 @@ class ContextsSelectTypeTest extends TypeTestCase /* @var \PHPUnit_Framework_MockObject_MockObject */ protected $securityTokenStorage; + /* @var \PHPUnit_Framework_MockObject_MockObject */ + protected $dispatcher; + protected function setUp() { parent::setUp(); @@ -45,6 +48,10 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); + $this->dispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface') + ->disableOriginalConstructor() + ->getMock(); + $this->securityTokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface') ->disableOriginalConstructor() @@ -73,7 +80,8 @@ public function testBuildForm() $this->configManager, $this->translator, $this->mapper, - $this->securityTokenStorage + $this->securityTokenStorage, + $this->dispatcher ); $type->buildForm($builder, []); } @@ -102,7 +110,8 @@ public function testSetDefaultOptions() $this->configManager, $this->translator, $this->mapper, - $this->securityTokenStorage + $this->securityTokenStorage, + $this->dispatcher ); $type->setDefaultOptions($resolver); } @@ -114,7 +123,8 @@ public function testGetParent() $this->configManager, $this->translator, $this->mapper, - $this->securityTokenStorage + $this->securityTokenStorage, + $this->dispatcher ); $this->assertEquals('genemu_jqueryselect2_hidden', $type->getParent()); @@ -127,7 +137,8 @@ public function testGetName() $this->configManager, $this->translator, $this->mapper, - $this->securityTokenStorage + $this->securityTokenStorage, + $this->dispatcher ); $this->assertEquals('oro_activity_contexts_select', $type->getName()); } diff --git a/src/Oro/Bundle/ActivityBundle/Tests/Unit/Form/Type/MultipleAssociationChoiceTypeTest.php b/src/Oro/Bundle/ActivityBundle/Tests/Unit/Form/Type/MultipleAssociationChoiceTypeTest.php new file mode 100644 index 00000000000..07ffcae8ccc --- /dev/null +++ b/src/Oro/Bundle/ActivityBundle/Tests/Unit/Form/Type/MultipleAssociationChoiceTypeTest.php @@ -0,0 +1,97 @@ +getMockBuilder('Oro\Bundle\EntityBundle\ORM\EntityClassResolver') + ->disableOriginalConstructor() + ->getMock(); + + $this->type = new MultipleAssociationChoiceType( + new AssociationTypeHelper($this->configManager, $entityClassResolver), + $this->configManager + ); + } + + public function testFinishViewForDisabled() + { + $this->configManager->expects($this->any()) + ->method('getProvider') + ->will( + $this->returnValueMap( + [ + ['test', $this->testConfigProvider], + ] + ) + ); + + $this->testConfigProvider->expects($this->once()) + ->method('hasConfig') + ->with('Test\Entity2') + ->will($this->returnValue(false)); + $this->testConfigProvider->expects($this->never()) + ->method('getConfig'); + + $view = new FormView(); + $form = new Form($this->getMock('Symfony\Component\Form\FormConfigInterface')); + $options = [ + 'config_id' => new EntityConfigId('test', 'Test\Entity2'), + 'association_class' => 'test' + ]; + + $view->vars['disabled'] = false; + + $view->children[0] = new FormView($view); + $view->children[1] = new FormView($view); + + $view->children[0]->vars['value'] = 'Test\Entity1'; + $view->children[1]->vars['value'] = 'Test\Entity2'; + + $this->type->finishView($view, $form, $options); + + $this->assertEquals( + [ + 'attr' => [], + 'value' => 'Test\Entity1' + ], + $view->children[0]->vars + ); + $this->assertEquals( + [ + 'attr' => [], + 'disabled' => true, + 'value' => 'Test\Entity2' + ], + $view->children[1]->vars + ); + } + + public function testGetName() + { + $this->assertEquals('oro_activity_multiple_association_choice', $this->type->getName()); + } + + public function testGetParent() + { + $this->assertEquals('oro_entity_extend_multiple_association_choice', $this->type->getParent()); + } +} diff --git a/src/Oro/Bundle/EntityBundle/Tests/Unit/Provider/EntityContextProviderTest.php b/src/Oro/Bundle/ActivityBundle/Tests/Unit/Provider/ContextGridProviderTest.php similarity index 85% rename from src/Oro/Bundle/EntityBundle/Tests/Unit/Provider/EntityContextProviderTest.php rename to src/Oro/Bundle/ActivityBundle/Tests/Unit/Provider/ContextGridProviderTest.php index c5da4ce6fb7..628d50538a5 100644 --- a/src/Oro/Bundle/EntityBundle/Tests/Unit/Provider/EntityContextProviderTest.php +++ b/src/Oro/Bundle/ActivityBundle/Tests/Unit/Provider/ContextGridProviderTest.php @@ -1,10 +1,10 @@ configProvider = $this ->getMockBuilder('Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider') ->disableOriginalConstructor() - ->setMethods(['getConfig', 'get']) + ->setMethods(['getConfig', 'has', 'get']) ->getMock(); $this->configProvider->expects($this->any()) @@ -86,12 +86,17 @@ protected function setUp() ->with($this->routingHelper->getUrlSafeClassName($this->entityClass)) ->will($this->returnValue($this->configProvider)); + $this->configProvider->expects($this->any()) + ->method('has') + ->with('context') + ->willReturn(true); + $this->configProvider->expects($this->any()) ->method('get') - ->with('context-grid') + ->with('context') ->will($this->returnValue($this->expectedGridName)); - $this->provider = new EntityContextProvider( + $this->provider = new ContextGridProvider( $this->routingHelper, $this->entityProvider, $this->configProvider diff --git a/src/Oro/Bundle/ActivityListBundle/Entity/Manager/ActivityListManager.php b/src/Oro/Bundle/ActivityListBundle/Entity/Manager/ActivityListManager.php index 48feaa073c1..8a9c276663f 100644 --- a/src/Oro/Bundle/ActivityListBundle/Entity/Manager/ActivityListManager.php +++ b/src/Oro/Bundle/ActivityListBundle/Entity/Manager/ActivityListManager.php @@ -2,12 +2,12 @@ namespace Oro\Bundle\ActivityListBundle\Entity\Manager; -use Doctrine\Bundle\DoctrineBundle\Registry; -use Doctrine\ORM\EntityManager; use Doctrine\ORM\QueryBuilder; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Security\Core\Util\ClassUtils; +use Oro\Bundle\ActivityListBundle\Event\ActivityListPreQueryBuildEvent; use Oro\Bundle\ActivityListBundle\Helper\ActivityInheritanceTargetsHelper; use Oro\Bundle\ActivityBundle\EntityConfig\ActivityScope; use Oro\Bundle\ActivityListBundle\Model\ActivityListGroupProviderInterface; @@ -55,6 +55,9 @@ class ActivityListManager /** @var ActivityInheritanceTargetsHelper */ protected $activityInheritanceTargetsHelper; + /** @var EventDispatcherInterface */ + protected $eventDispatcher; + /** * @param SecurityFacade $securityFacade * @param EntityNameResolver $entityNameResolver @@ -66,6 +69,7 @@ class ActivityListManager * @param DoctrineHelper $doctrineHelper * @param ActivityListAclCriteriaHelper $aclHelper * @param ActivityInheritanceTargetsHelper $activityInheritanceTargetsHelper + * @param EventDispatcherInterface $eventDispatcher * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -79,7 +83,8 @@ public function __construct( CommentApiManager $commentManager, DoctrineHelper $doctrineHelper, ActivityListAclCriteriaHelper $aclHelper, - ActivityInheritanceTargetsHelper $activityInheritanceTargetsHelper + ActivityInheritanceTargetsHelper $activityInheritanceTargetsHelper, + EventDispatcherInterface $eventDispatcher ) { $this->securityFacade = $securityFacade; $this->entityNameResolver = $entityNameResolver; @@ -91,6 +96,7 @@ public function __construct( $this->doctrineHelper = $doctrineHelper; $this->activityListAclHelper = $aclHelper; $this->activityInheritanceTargetsHelper = $activityInheritanceTargetsHelper; + $this->eventDispatcher = $eventDispatcher; } /** @@ -260,15 +266,18 @@ public function getEntityViewModel(ActivityList $entity, $targetEntityData = []) /** * @param string $entityClass - * @param string $entityId + * @param int $entityId * * @return QueryBuilder */ protected function getBaseQB($entityClass, $entityId) { + $event = new ActivityListPreQueryBuildEvent($entityClass, $entityId); + $this->eventDispatcher->dispatch(ActivityListPreQueryBuildEvent::EVENT_NAME, $event); + $entityIds = $event->getTargetIds(); return $this->getRepository()->getBaseActivityListQueryBuilder( $entityClass, - $entityId, + $entityIds, $this->config->get('oro_activity_list.sorting_field'), $this->config->get('oro_activity_list.sorting_direction'), $this->config->get('oro_activity_list.grouping') diff --git a/src/Oro/Bundle/ActivityListBundle/Entity/Repository/ActivityListRepository.php b/src/Oro/Bundle/ActivityListBundle/Entity/Repository/ActivityListRepository.php index 9732ad34c46..7b1100d6208 100644 --- a/src/Oro/Bundle/ActivityListBundle/Entity/Repository/ActivityListRepository.php +++ b/src/Oro/Bundle/ActivityListBundle/Entity/Repository/ActivityListRepository.php @@ -54,7 +54,7 @@ public function getActivityListQueryBuilder( /** * @param string $entityClass - * @param integer $entityId + * @param integer|integer[] $entityIds * @param string $orderField * @param string $orderDirection * @param boolean $grouping @@ -63,16 +63,27 @@ public function getActivityListQueryBuilder( */ public function getBaseActivityListQueryBuilder( $entityClass, - $entityId, + $entityIds, $orderField = 'updatedAt', $orderDirection = 'DESC', $grouping = false ) { + if (is_scalar($entityIds)) { + $entityIds = [$entityIds]; + } $queryBuilder = $this->createQueryBuilder('activity') ->leftJoin('activity.' . $this->getAssociationName($entityClass), 'r') - ->leftJoin('activity.activityOwners', 'ao') - ->where('r.id = :entityId') - ->setParameter('entityId', $entityId) + ->leftJoin('activity.activityOwners', 'ao'); + if (count($entityIds) > 1) { + $queryBuilder + ->where('r.id IN (:entityIds)') + ->setParameter('entityIds', $entityIds); + } else { + $queryBuilder + ->where('r.id = :entityId') + ->setParameter('entityId', reset($entityIds)); + } + $queryBuilder ->orderBy('activity.' . $orderField, $orderDirection) ->groupBy('activity.id'); diff --git a/src/Oro/Bundle/ActivityListBundle/Event/ActivityListPreQueryBuildEvent.php b/src/Oro/Bundle/ActivityListBundle/Event/ActivityListPreQueryBuildEvent.php new file mode 100644 index 00000000000..ea1ed789a5d --- /dev/null +++ b/src/Oro/Bundle/ActivityListBundle/Event/ActivityListPreQueryBuildEvent.php @@ -0,0 +1,68 @@ +targetClass = $targetClass; + $this->targetId = $targetId; + } + + /** + * @return string + */ + public function getTargetClass() + { + return $this->targetClass; + } + + /** + * @return integer[] + */ + public function getTargetIds() + { + if (!$this->targetIds) { + $this->targetIds = [$this->targetId]; + } + + return $this->targetIds; + } + + /** + * @return integer + */ + public function getTargetId() + { + return $this->targetId; + } + + /** + * @param integer[] $targetIds + */ + public function setTargetIds(array $targetIds) + { + $this->targetIds = $targetIds; + } +} diff --git a/src/Oro/Bundle/ActivityListBundle/Model/Strategy/ReplaceStrategy.php b/src/Oro/Bundle/ActivityListBundle/Model/Strategy/ReplaceStrategy.php index 31104106d0c..dd96f740ae4 100644 --- a/src/Oro/Bundle/ActivityListBundle/Model/Strategy/ReplaceStrategy.php +++ b/src/Oro/Bundle/ActivityListBundle/Model/Strategy/ReplaceStrategy.php @@ -3,6 +3,9 @@ namespace Oro\Bundle\ActivityListBundle\Model\Strategy; use Symfony\Component\Security\Core\Util\ClassUtils; + +use Oro\Component\PhpUtils\ArrayUtil; + use Oro\Bundle\ActivityBundle\Manager\ActivityManager; use Oro\Bundle\ActivityListBundle\Entity\Manager\ActivityListManager; use Oro\Bundle\ActivityListBundle\Entity\ActivityList; @@ -10,12 +13,7 @@ use Oro\Bundle\EntityBundle\ORM\DoctrineHelper; use Oro\Bundle\EntityMergeBundle\Data\FieldData; use Oro\Bundle\EntityMergeBundle\Model\Strategy\StrategyInterface; -use Oro\Bundle\UIBundle\Tools\ArrayUtils; -/** - * Class ReplaceStrategy - * @package Oro\Bundle\ActivityListBundle\Model\Strategy - */ class ReplaceStrategy implements StrategyInterface { /** @var ActivityListManager */ @@ -56,7 +54,7 @@ public function merge(FieldData $fieldData) $activityClass = $fieldMetadata->get('type'); $activityListItems = $this->getActivitiesByEntity($masterEntity, $activityClass); - $activityIds = ArrayUtils::arrayColumn($activityListItems, 'relatedActivityId'); + $activityIds = ArrayUtil::arrayColumn($activityListItems, 'relatedActivityId'); $activities = $this->doctrineHelper->getEntityRepository($activityClass)->findBy(['id' => $activityIds]); foreach ($activities as $activity) { @@ -65,7 +63,7 @@ public function merge(FieldData $fieldData) $activityListItems = $this->getActivitiesByEntity($sourceEntity, $activityClass); - $activityIds = ArrayUtils::arrayColumn($activityListItems, 'id'); + $activityIds = ArrayUtil::arrayColumn($activityListItems, 'id'); $entityClass = ClassUtils::getRealClass($masterEntity); $this->activityListManager ->replaceActivityTargetWithPlainQuery( @@ -75,7 +73,7 @@ public function merge(FieldData $fieldData) $masterEntity->getId() ); - $activityIds = ArrayUtils::arrayColumn($activityListItems, 'relatedActivityId'); + $activityIds = ArrayUtil::arrayColumn($activityListItems, 'relatedActivityId'); $this->activityListManager ->replaceActivityTargetWithPlainQuery( $activityIds, diff --git a/src/Oro/Bundle/ActivityListBundle/Model/Strategy/UniteStrategy.php b/src/Oro/Bundle/ActivityListBundle/Model/Strategy/UniteStrategy.php index 44621d1dc0f..24ee39db5b7 100644 --- a/src/Oro/Bundle/ActivityListBundle/Model/Strategy/UniteStrategy.php +++ b/src/Oro/Bundle/ActivityListBundle/Model/Strategy/UniteStrategy.php @@ -3,18 +3,16 @@ namespace Oro\Bundle\ActivityListBundle\Model\Strategy; use Symfony\Component\Security\Core\Util\ClassUtils; + +use Oro\Component\PhpUtils\ArrayUtil; + use Oro\Bundle\ActivityListBundle\Entity\Manager\ActivityListManager; use Oro\Bundle\ActivityListBundle\Entity\ActivityList; use Oro\Bundle\ActivityListBundle\Model\MergeModes; use Oro\Bundle\EntityBundle\ORM\DoctrineHelper; use Oro\Bundle\EntityMergeBundle\Model\Strategy\StrategyInterface; use Oro\Bundle\EntityMergeBundle\Data\FieldData; -use Oro\Bundle\UIBundle\Tools\ArrayUtils; -/** - * Class UniteStrategy - * @package Oro\Bundle\ActivityListBundle\Model\Strategy - */ class UniteStrategy implements StrategyInterface { /** @var ActivityListManager */ @@ -53,7 +51,7 @@ public function merge(FieldData $fieldData) $activityListItems = $queryBuilder->getQuery()->getResult(); - $activityIds = ArrayUtils::arrayColumn($activityListItems, 'id'); + $activityIds = ArrayUtil::arrayColumn($activityListItems, 'id'); $this->activityListManager ->replaceActivityTargetWithPlainQuery( $activityIds, @@ -62,7 +60,7 @@ public function merge(FieldData $fieldData) $masterEntity->getId() ); - $activityIds = ArrayUtils::arrayColumn($activityListItems, 'relatedActivityId'); + $activityIds = ArrayUtil::arrayColumn($activityListItems, 'relatedActivityId'); $this->activityListManager ->replaceActivityTargetWithPlainQuery( $activityIds, diff --git a/src/Oro/Bundle/ActivityListBundle/Placeholder/PlaceholderFilter.php b/src/Oro/Bundle/ActivityListBundle/Placeholder/PlaceholderFilter.php index c7b2931ecca..9e4b8c3a5fa 100644 --- a/src/Oro/Bundle/ActivityListBundle/Placeholder/PlaceholderFilter.php +++ b/src/Oro/Bundle/ActivityListBundle/Placeholder/PlaceholderFilter.php @@ -66,29 +66,11 @@ public function isApplicable($entity = null, $pageType = null) } $entityClass = $this->doctrineHelper->getEntityClass($entity); - if (!$this->configProvider->hasConfig($entityClass)) { - return false; - } - - $hasAppliedActivityAssociation = false; - $activityAssociations = $this->activityManager->getActivityAssociations($entityClass); - foreach ($activityAssociations as $activityAssociation) { - $isAssociationAccessible = ExtendHelper::isFieldAccessible( - $this->configProvider->getConfig( - $activityAssociation['className'], - $activityAssociation['associationName'] - ) - ); - if ($isAssociationAccessible) { - $hasAppliedActivityAssociation = true; - break; - } - } /** * If at least one activity is accessible we can continue otherwise no. */ - if (!$hasAppliedActivityAssociation) { + if (!$this->hasApplicableActivityAssociations($entityClass)) { return false; } @@ -102,6 +84,36 @@ public function isApplicable($entity = null, $pageType = null) ); } + /** + * @param string $entityClass + * @return bool + */ + protected function hasApplicableActivityAssociations($entityClass) + { + if (!$this->configProvider->hasConfig($entityClass)) { + return false; + } + $supportedActivities = $this->activityListProvider->getSupportedActivities(); + foreach ($supportedActivities as $supportedActivity) { + if ($this->activityListProvider->isApplicableTarget($entityClass, $supportedActivity)) { + return true; + } + } + $activityAssociations = $this->activityManager->getActivityAssociations($entityClass); + foreach ($activityAssociations as $activityAssociation) { + $isAssociationAccessible = ExtendHelper::isFieldAccessible( + $this->configProvider->getConfig( + $activityAssociation['className'], + $activityAssociation['associationName'] + ) + ); + if ($isAssociationAccessible) { + return true; + } + } + return false; + } + /** * @param string $entityClass * @param int $pageType diff --git a/src/Oro/Bundle/ActivityListBundle/Provider/ActivityListChainProvider.php b/src/Oro/Bundle/ActivityListBundle/Provider/ActivityListChainProvider.php index 2cf79f7df5f..8847ede7b4b 100644 --- a/src/Oro/Bundle/ActivityListBundle/Provider/ActivityListChainProvider.php +++ b/src/Oro/Bundle/ActivityListBundle/Provider/ActivityListChainProvider.php @@ -20,8 +20,6 @@ use Oro\Bundle\UIBundle\Tools\HtmlTagHelper; /** - * Class ActivityListChainProvider - * @package Oro\Bundle\ActivityListBundle\Provider * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ @@ -90,9 +88,9 @@ public function addProvider(ActivityListProviderInterface $provider) } /** - * Get array providers + * Get all registered providers * - * @return \Oro\Bundle\ActivityListBundle\Model\ActivityListProviderInterface[] + * @return ActivityListProviderInterface[] [activity class => provider, ...] */ public function getProviders() { @@ -133,19 +131,16 @@ public function getTargetEntityClasses() */ public function isApplicableTarget($targetClassName, $activityClassName) { - /** @var ConfigIdInterface[] $configIds */ - $configIds = $this->configManager->getIds('entity', null, false); - foreach ($configIds as $configId) { - if (array_key_exists($activityClassName, $this->providers)) { - $provider = $this->getProviderByClass($activityClassName); - if ($provider->isApplicableTarget($configId, $this->configManager) - && $configId->getClassName() === $targetClassName) { - return true; - } - } + if (!isset($this->providers[$activityClassName]) + || !$this->configManager->hasConfig($targetClassName) + ) { + return false; } - return false; + return $this->providers[$activityClassName]->isApplicableTarget( + $this->configManager->getId('entity', $targetClassName), + $this->configManager + ); } /** diff --git a/src/Oro/Bundle/ActivityListBundle/Resources/config/assets.yml b/src/Oro/Bundle/ActivityListBundle/Resources/config/assets.yml index 7831d3d2c3b..a3cc25040ef 100644 --- a/src/Oro/Bundle/ActivityListBundle/Resources/config/assets.yml +++ b/src/Oro/Bundle/ActivityListBundle/Resources/config/assets.yml @@ -1,3 +1,3 @@ css: oroactivitylist: - - 'bundles/oroactivitylist/css/less/activity-list.less' + - 'bundles/oroactivitylist/css/less/main.less' diff --git a/src/Oro/Bundle/ActivityListBundle/Resources/config/services.yml b/src/Oro/Bundle/ActivityListBundle/Resources/config/services.yml index e6b445e63dc..8e46369cb82 100644 --- a/src/Oro/Bundle/ActivityListBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/ActivityListBundle/Resources/config/services.yml @@ -50,6 +50,7 @@ services: - @oro_entity.doctrine_helper - @oro_activity_list.helper.acl_criteria - @oro_activity_list.helper.activity_inheritance_targets + - @event_dispatcher oro_activity_list.collect_manager: class: %oro_activity_list.collect_manager.class% diff --git a/src/Oro/Bundle/ActivityListBundle/Resources/public/css/less/activity-list.less b/src/Oro/Bundle/ActivityListBundle/Resources/public/css/less/activity-list.less index b8329d31588..749770af82f 100644 --- a/src/Oro/Bundle/ActivityListBundle/Resources/public/css/less/activity-list.less +++ b/src/Oro/Bundle/ActivityListBundle/Resources/public/css/less/activity-list.less @@ -131,7 +131,7 @@ width: 27px; font-size: 18px; color: #999; - margin: 8px 0 0 10px; + margin: 8px 6px 0 8px; } > .actions{ @@ -141,7 +141,7 @@ .horizontal-icon-menu; } > .details { - margin: 7px 7px 0 0; + margin: 9px 7px 0 0; max-width: 247px; min-width: 247px; padding-bottom: 1px; @@ -154,7 +154,7 @@ } > .created-at { float: right; - margin: 7px 0 -1px 8px; + margin: 8px 0 -1px 8px; color: #888; } > .comment-count { @@ -162,7 +162,7 @@ margin-top: 8px; } > .message-item { - margin: 7px 0 0 0; + margin: 9px 0 0 0; float: left; width: calc(~"100% - 510px"); color: #888; @@ -220,7 +220,7 @@ .accordion > .items { .message-item { clear: left; - margin-left: 4px; + margin: 7px 0 7px 4px; width: calc(~"100% - 186px"); } .details { diff --git a/src/Oro/Bundle/ActivityListBundle/Resources/public/css/less/main.less b/src/Oro/Bundle/ActivityListBundle/Resources/public/css/less/main.less new file mode 100644 index 00000000000..e70f1ca72b7 --- /dev/null +++ b/src/Oro/Bundle/ActivityListBundle/Resources/public/css/less/main.less @@ -0,0 +1,4 @@ +@import "./activity-list"; + +// mobile +@import "./mobile/main"; diff --git a/src/Oro/Bundle/ActivityListBundle/Resources/public/css/less/mobile/activity-list.less b/src/Oro/Bundle/ActivityListBundle/Resources/public/css/less/mobile/activity-list.less new file mode 100644 index 00000000000..326f32a09eb --- /dev/null +++ b/src/Oro/Bundle/ActivityListBundle/Resources/public/css/less/mobile/activity-list.less @@ -0,0 +1,18 @@ +.activity-list-widget { + .activity-list { + .info { + margin-bottom: @contentPadding; + } + } + .grid-toolbar { + padding: 0; + margin-bottom: @contentPadding; + .filter-container { + padding: 0; + margin-bottom: @contentPadding; + } + } + .comments-view-footer { + margin-bottom: 0; + } +} diff --git a/src/Oro/Bundle/ActivityListBundle/Resources/public/css/less/mobile/main.less b/src/Oro/Bundle/ActivityListBundle/Resources/public/css/less/mobile/main.less new file mode 100644 index 00000000000..4e3b4d898b3 --- /dev/null +++ b/src/Oro/Bundle/ActivityListBundle/Resources/public/css/less/mobile/main.less @@ -0,0 +1,4 @@ +.mobile-version { + @import "oroui/css/less/mobile/variables"; + @import "./activity-list"; +} diff --git a/src/Oro/Bundle/ActivityListBundle/Resources/public/js/app/views/activity-view.js b/src/Oro/Bundle/ActivityListBundle/Resources/public/js/app/views/activity-view.js index 851bbfd02ed..04443c681aa 100644 --- a/src/Oro/Bundle/ActivityListBundle/Resources/public/js/app/views/activity-view.js +++ b/src/Oro/Bundle/ActivityListBundle/Resources/public/js/app/views/activity-view.js @@ -68,7 +68,8 @@ define(function(require) { data.collapsed = this.collapsed; data.createdAt = dateTimeFormatter.formatSmartDateTime(data.createdAt); data.updatedAt = dateTimeFormatter.formatSmartDateTime(data.updatedAt); - data.relatedActivityClass = _.escape(data.relatedActivityClass); + // use special model's method to get activity class name with replaced slashes + data.relatedActivityClass = _.escape(this.model.getRelatedActivityClass()); if (data.owner_id) { data.owner_url = routing.generate('oro_user_view', {'id': data.owner_id}); } else { diff --git a/src/Oro/Bundle/ActivityListBundle/Resources/views/ActivityList/widget/activities.html.twig b/src/Oro/Bundle/ActivityListBundle/Resources/views/ActivityList/widget/activities.html.twig index 85d81461b60..cf0bfa1cee3 100644 --- a/src/Oro/Bundle/ActivityListBundle/Resources/views/ActivityList/widget/activities.html.twig +++ b/src/Oro/Bundle/ActivityListBundle/Resources/views/ActivityList/widget/activities.html.twig @@ -20,7 +20,7 @@
-
+
{{ UI.clientLink({ 'aCss': 'action btn', 'iCss': 'icon-refresh', diff --git a/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Entity/Manager/ActivityListManagerTest.php b/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Entity/Manager/ActivityListManagerTest.php index c3124e3743a..bf988d14099 100644 --- a/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Entity/Manager/ActivityListManagerTest.php +++ b/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Entity/Manager/ActivityListManagerTest.php @@ -51,6 +51,9 @@ class ActivityListManagerTest extends \PHPUnit_Framework_TestCase /** @var \PHPUnit_Framework_MockObject_MockObject */ protected $inheritanceHelper; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $eventDispatcher; + public function setUp() { $this->securityFacade = $this->getMockBuilder('Oro\Bundle\SecurityBundle\SecurityFacade') @@ -78,6 +81,9 @@ public function setUp() $this->inheritanceHelper = $this ->getMockBuilder('Oro\Bundle\ActivityListBundle\Helper\ActivityInheritanceTargetsHelper') ->disableOriginalConstructor()->getMock(); + $this->eventDispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcher') + ->disableOriginalConstructor() + ->getMock(); $this->activityListManager = new ActivityListManager( $this->securityFacade, @@ -89,7 +95,8 @@ public function setUp() $this->commentManager, $this->doctrineHelper, $this->aclHelper, - $this->inheritanceHelper + $this->inheritanceHelper, + $this->eventDispatcher ); } diff --git a/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Event/ActivityListPreQueryBuildEventTest.php b/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Event/ActivityListPreQueryBuildEventTest.php new file mode 100644 index 00000000000..8746530ae42 --- /dev/null +++ b/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Event/ActivityListPreQueryBuildEventTest.php @@ -0,0 +1,24 @@ +assertEquals($targetId, $event->getTargetId()); + $this->assertEquals([$targetId], $event->getTargetIds()); + $this->assertEquals($targetClass, $event->getTargetClass()); + + $event = new ActivityListPreQueryBuildEvent($targetClass, $targetId); + $event->setTargetIds($targetIds); + $this->assertEquals($targetIds, $event->getTargetIds()); + } +} diff --git a/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Placeholder/PlaceholderFilterTest.php b/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Placeholder/PlaceholderFilterTest.php index 60db6fd0b71..7be178c53b8 100644 --- a/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Placeholder/PlaceholderFilterTest.php +++ b/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Placeholder/PlaceholderFilterTest.php @@ -5,12 +5,14 @@ use Doctrine\Common\Persistence\ManagerRegistry; use Oro\Bundle\ActivityBundle\EntityConfig\ActivityScope; +use Oro\Bundle\ActivityBundle\Manager\ActivityManager; use Oro\Bundle\ActivityListBundle\Placeholder\PlaceholderFilter; use Oro\Bundle\ActivityListBundle\Provider\ActivityListChainProvider; use Oro\Bundle\ActivityListBundle\Tests\Unit\Placeholder\Fixture\TestNonActiveTarget; use Oro\Bundle\ActivityListBundle\Tests\Unit\Placeholder\Fixture\TestNonManagedTarget; use Oro\Bundle\ActivityListBundle\Tests\Unit\Placeholder\Fixture\TestTarget; use Oro\Bundle\EntityBundle\ORM\DoctrineHelper; +use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; use Oro\Bundle\EntityConfigBundle\Tests\Unit\ConfigProviderMock; use Oro\Bundle\UIBundle\Event\BeforeGroupingChainWidgetEvent; @@ -19,7 +21,7 @@ class PlaceholderFilterTest extends \PHPUnit_Framework_TestCase /** @var \PHPUnit_Framework_MockObject_MockObject|ActivityListChainProvider */ protected $activityListProvider; - /** @var \PHPUnit_Framework_MockObject_MockObject|ActivityListChainProvider */ + /** @var \PHPUnit_Framework_MockObject_MockObject|ActivityManager */ protected $activityManager; /** @var \PHPUnit_Framework_MockObject_MockObject|ManagerRegistry */ @@ -80,6 +82,7 @@ public function setUp() return !$entity instanceof TestNonManagedTarget; }); + /** @var ConfigManager $configManager */ $configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Config\ConfigManager') ->disableOriginalConstructor() ->getMock(); @@ -105,6 +108,10 @@ public function testIsApplicable() ); $this->configProvider->addFieldConfig($entityClass, 'associationField'); + $this->activityListProvider->expects($this->once()) + ->method('getSupportedActivities') + ->willReturn([]); + $this->activityManager ->expects($this->once()) ->method('getActivityAssociations') @@ -119,6 +126,35 @@ public function testIsApplicable() $this->assertFalse($this->filter->isApplicable(null, ActivityScope::VIEW_PAGE)); } + public function testIsApplicableWithSupportedActivityList() + { + $testTarget = new TestTarget(1); + + $entityClass = get_class($testTarget); + $this->configProvider->addEntityConfig( + $entityClass, + [ActivityScope::SHOW_ON_PAGE => '\Oro\Bundle\ActivityBundle\EntityConfig\ActivityScope::VIEW_PAGE'] + ); + + $supportedActivity = 'Class/Name'; + $this->activityListProvider->expects($this->once()) + ->method('getSupportedActivities') + ->willReturn([$supportedActivity]); + + $this->activityListProvider->expects($this->exactly(1)) + ->method('isApplicableTarget') + ->with($entityClass, $supportedActivity) + ->willReturn(true); + + $this->activityManager + ->expects($this->never()) + ->method('getActivityAssociations') + ->with($entityClass) + ->willReturn([]); + + $this->assertTrue($this->filter->isApplicable($testTarget, ActivityScope::VIEW_PAGE)); + } + public function testIsApplicableWithNonManagedEntity() { $testTarget = new TestNonManagedTarget(1); @@ -134,6 +170,11 @@ public function testIsApplicableWithShowOnPageConfiguration() $entityClass, [ActivityScope::SHOW_ON_PAGE => '\Oro\Bundle\ActivityBundle\EntityConfig\ActivityScope::UPDATE_PAGE'] ); + + $this->activityListProvider->expects($this->exactly(2)) + ->method('getSupportedActivities') + ->willReturn([]); + $this->configProvider->addFieldConfig($entityClass, 'associationField'); $this->activityManager->expects($this->exactly(2)) ->method('getActivityAssociations') @@ -174,6 +215,10 @@ public function testIsApplicableOnNonSupportedTarget() ); $this->configProvider->addFieldConfig($entityClass, 'associationField'); + $this->activityListProvider->expects($this->once()) + ->method('getSupportedActivities') + ->willReturn([]); + $this->activityManager ->expects($this->once()) ->method('getActivityAssociations') diff --git a/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Provider/ActivityListChainProviderTest.php b/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Provider/ActivityListChainProviderTest.php index b9e4fe5a4cb..ca31bc941ff 100644 --- a/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Provider/ActivityListChainProviderTest.php +++ b/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Provider/ActivityListChainProviderTest.php @@ -62,6 +62,74 @@ public function setUp() $this->provider->addProvider($this->testActivityProvider); } + public function testIsApplicableTarget() + { + $targetClassName = TestActivityProvider::SUPPORTED_TARGET_CLASS_NAME; + $activityClassName = TestActivityProvider::ACTIVITY_CLASS_NAME; + + $this->configManager->expects($this->any()) + ->method('hasConfig') + ->with($targetClassName) + ->willReturn(true); + $this->configManager->expects($this->any()) + ->method('getId') + ->with('entity', $targetClassName) + ->willReturn(new EntityConfigId('entity', $targetClassName)); + + $this->assertTrue( + $this->provider->isApplicableTarget($targetClassName, $activityClassName) + ); + } + + public function testIsApplicableTargetForNotSupportedTargetEntity() + { + $targetClassName = 'Test\NotSupportedTargetEntity'; + $activityClassName = TestActivityProvider::ACTIVITY_CLASS_NAME; + + $this->configManager->expects($this->any()) + ->method('hasConfig') + ->with($targetClassName) + ->willReturn(true); + $this->configManager->expects($this->any()) + ->method('getId') + ->with('entity', $targetClassName) + ->willReturn(new EntityConfigId('entity', $targetClassName)); + + $this->assertFalse( + $this->provider->isApplicableTarget($targetClassName, $activityClassName) + ); + } + + public function testIsApplicableTargetForNotRegisteredActivityEntity() + { + $targetClassName = TestActivityProvider::SUPPORTED_TARGET_CLASS_NAME; + $activityClassName = 'Test\NotRegisteredActivityEntity'; + + $this->configManager->expects($this->never()) + ->method('hasConfig'); + + $this->assertFalse( + $this->provider->isApplicableTarget($targetClassName, $activityClassName) + ); + } + + public function testIsApplicableTargetForNotConfigurableTargetEntity() + { + $targetClassName = 'Test\NotConfigurableTargetEntity'; + $activityClassName = TestActivityProvider::ACTIVITY_CLASS_NAME; + + $this->configManager->expects($this->any()) + ->method('hasConfig') + ->with($targetClassName) + ->willReturn(false); + $this->configManager->expects($this->never()) + ->method('getId'); + + $this->assertFalse( + $this->provider->isApplicableTarget($targetClassName, $activityClassName) + ); + } + public function testGetSupportedActivities() { $this->assertEquals( diff --git a/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Provider/Fixture/TestActivityProvider.php b/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Provider/Fixture/TestActivityProvider.php index 6320476594b..fb26017bb50 100644 --- a/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Provider/Fixture/TestActivityProvider.php +++ b/src/Oro/Bundle/ActivityListBundle/Tests/Unit/Provider/Fixture/TestActivityProvider.php @@ -17,6 +17,8 @@ class TestActivityProvider implements const ACTIVITY_CLASS_NAME = 'Test\Entity'; const ACL_CLASS = 'Test\Entity'; + const SUPPORTED_TARGET_CLASS_NAME = 'Acme\DemoBundle\Entity\CorrectEntity'; + protected $targets; /** @@ -24,7 +26,7 @@ class TestActivityProvider implements */ public function isApplicableTarget(ConfigIdInterface $configId, ConfigManager $configManager) { - if ($configId->getClassName() === 'Acme\\DemoBundle\\Entity\\CorrectEntity') { + if ($configId->getClassName() === self::SUPPORTED_TARGET_CLASS_NAME) { return true; } diff --git a/src/Oro/Bundle/AddressBundle/Entity/AbstractAddress.php b/src/Oro/Bundle/AddressBundle/Entity/AbstractAddress.php index 71a6375a1a7..19c575706ee 100644 --- a/src/Oro/Bundle/AddressBundle/Entity/AbstractAddress.php +++ b/src/Oro/Bundle/AddressBundle/Entity/AbstractAddress.php @@ -9,6 +9,8 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Oro\Bundle\AddressBundle\Validator\Constraints\ValidRegion; +use Oro\Bundle\AddressBundle\Validator\Constraints\ValidRegionValidator; use Oro\Bundle\EntityConfigBundle\Metadata\Annotation\ConfigField; use Oro\Bundle\FormBundle\Entity\EmptyItem; use Oro\Bundle\LocaleBundle\Model\AddressInterface; @@ -764,19 +766,17 @@ public function beforeSave() $this->updated = new \DateTime('now', new \DateTimeZone('UTC')); } + /** + * @param ExecutionContextInterface $context + * @deprecated Use \Oro\Bundle\AddressBundle\Validator\Constraints\ValidRegionValidator instead + */ public function isRegionValid(ExecutionContextInterface $context) { - if ($this->getCountry() && $this->getCountry()->hasRegions() && !$this->region && !$this->regionText) { - // do not allow saving text region in case when region was checked from list - // except when in base data region text existed - // another way region_text field will be null, logic are placed in form listener - $propertyPath = $context->getPropertyPath() . '.region'; - $context->addViolationAt( - $propertyPath, - 'State is required for country {{ country }}', - ['{{ country }}' => $this->getCountry()->getName()] - ); - } + // Use validator instead of duplicate code + $constraint = new ValidRegion(); + $validator = new ValidRegionValidator(); + $validator->initialize($context); + $validator->validate($this, $constraint); } /** diff --git a/src/Oro/Bundle/AddressBundle/Resources/config/services.yml b/src/Oro/Bundle/AddressBundle/Resources/config/services.yml index 2bf7a1373b3..229d0463416 100644 --- a/src/Oro/Bundle/AddressBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/AddressBundle/Resources/config/services.yml @@ -4,6 +4,8 @@ parameters: oro_address.address.manager.class: Oro\Bundle\AddressBundle\Entity\Manager\AddressManager oro_address.provider.address.class: Oro\Bundle\AddressBundle\Provider\AddressProvider oro_address.provider.phone.class: Oro\Bundle\AddressBundle\Provider\PhoneProvider + oro_address.validator.valid_region.class: Oro\Bundle\AddressBundle\Validator\Constraints\ValidRegionValidator + services: oro_address.address.manager: @@ -22,3 +24,8 @@ services: class: %oro_address.provider.phone.class% arguments: - @oro_entity_config.provider.extend + + oro_address.validator.valid_region: + class: %oro_address.validator.valid_region.class% + tags: + - { name: validator.constraint_validator, alias: oro_address_valid_region } diff --git a/src/Oro/Bundle/AddressBundle/Resources/config/validation.yml b/src/Oro/Bundle/AddressBundle/Resources/config/validation.yml index c73577ce9c4..f02c6b1a9b7 100644 --- a/src/Oro/Bundle/AddressBundle/Resources/config/validation.yml +++ b/src/Oro/Bundle/AddressBundle/Resources/config/validation.yml @@ -57,8 +57,7 @@ Oro\Bundle\AddressBundle\Entity\Address: postalCode: - NotBlank: ~ constraints: - - Callback: - methods: [isRegionValid] + - Oro\Bundle\AddressBundle\Validator\Constraints\ValidRegion: ~ Oro\Bundle\AddressBundle\Entity\AbstractEmail: properties: diff --git a/src/Oro/Bundle/AddressBundle/Resources/public/css/less/address.less b/src/Oro/Bundle/AddressBundle/Resources/public/css/less/address.less index 7cca723fd0d..cfe69bda89a 100644 --- a/src/Oro/Bundle/AddressBundle/Resources/public/css/less/address.less +++ b/src/Oro/Bundle/AddressBundle/Resources/public/css/less/address.less @@ -36,7 +36,9 @@ text-align: center; } .map-item { - cursor:pointer; + &:not(.active) { + cursor: pointer; + } address { margin-bottom:0; line-height:1.5em; diff --git a/src/Oro/Bundle/AddressBundle/Resources/public/js/region/view.js b/src/Oro/Bundle/AddressBundle/Resources/public/js/region/view.js index 9ff0c7ec1cb..d082c0afa3f 100644 --- a/src/Oro/Bundle/AddressBundle/Resources/public/js/region/view.js +++ b/src/Oro/Bundle/AddressBundle/Resources/public/js/region/view.js @@ -32,6 +32,7 @@ define([ this.$simpleEl.attr('type', 'text'); this.showSelect = options.showSelect; + this.regionRequired = options.regionRequired; this.template = _.template($('#region-chooser-template').html()); @@ -50,11 +51,15 @@ define([ */ displaySelect2: function(display) { if (display) { - this.addRequiredFlag(this.$simpleEl); + if (this.regionRequired) { + this.addRequiredFlag(this.$simpleEl); + } this.target.select2('container').show(); } else { this.target.select2('container').hide(); - this.removeRequiredFlag(this.$simpleEl); + if (this.regionRequired) { + this.removeRequiredFlag(this.$simpleEl); + } this.target.validate().hideElementErrors(this.target); } }, diff --git a/src/Oro/Bundle/AddressBundle/Resources/views/Include/fields.html.twig b/src/Oro/Bundle/AddressBundle/Resources/views/Include/fields.html.twig index 61512ac33a2..805b50c8926 100644 --- a/src/Oro/Bundle/AddressBundle/Resources/views/Include/fields.html.twig +++ b/src/Oro/Bundle/AddressBundle/Resources/views/Include/fields.html.twig @@ -16,7 +16,13 @@ {% endif %} {% set region_text_field = form.parent[region_text_field] %} - {{ form_widget(form, {'attr': {'data-validation': { NotBlank: null}|json_encode} }) }} + {% set attr = {} %} + + {% if required %} + {% set attr = attr|merge({'attr': {'data-validation': { NotBlank: null}|json_encode} }) %} + {% endif %} + + {{ form_widget(form, attr) }} {% set showSelect = (choices is not empty and region_text_field.vars.value is empty) %} diff --git a/src/Oro/Bundle/DataAuditBundle/Entity/AbstractAudit.php b/src/Oro/Bundle/DataAuditBundle/Entity/AbstractAudit.php index cf72f0cb974..215ad0ae863 100644 --- a/src/Oro/Bundle/DataAuditBundle/Entity/AbstractAudit.php +++ b/src/Oro/Bundle/DataAuditBundle/Entity/AbstractAudit.php @@ -12,7 +12,14 @@ use Oro\Bundle\UserBundle\Entity\AbstractUser; /** - * @ORM\MappedSuperclass + * @ORM\Entity() + * @ORM\Table(name="oro_audit", indexes={ + * @ORM\Index(name="idx_oro_audit_logged_at", columns={"logged_at"}), + * @ORM\Index(name="idx_oro_audit_type", columns={"type"}) + * }) + * @ORM\InheritanceType("SINGLE_TABLE") + * @ORM\DiscriminatorColumn(name="type", type="string") + * @ORM\DiscriminatorMap({"audit" = "Oro\Bundle\DataAuditBundle\Entity\Audit"}) */ abstract class AbstractAudit extends AbstractLogEntry { @@ -40,7 +47,11 @@ abstract class AbstractAudit extends AbstractLogEntry /** * @var AbstractAuditField[]|Collection * - * @ORM\OneToMany(targetEntity="AbstractAuditField", mappedBy="audit", cascade={"persist"}) + * @ORM\OneToMany( + * targetEntity="Oro\Bundle\DataAuditBundle\Entity\AuditField", + * mappedBy="audit", + * cascade={"persist"} + * ) */ protected $fields; @@ -80,7 +91,10 @@ abstract public function getUser(); * @param mixed $oldValue * @return AbstractAuditField */ - abstract protected function getAuditFieldInstance(AbstractAudit $audit, $field, $dataType, $newValue, $oldValue); + protected function getAuditFieldInstance(AbstractAudit $audit, $field, $dataType, $newValue, $oldValue) + { + return new AuditField($audit, $field, $dataType, $newValue, $oldValue); + } /** * Constructor diff --git a/src/Oro/Bundle/DataAuditBundle/Entity/AbstractAuditField.php b/src/Oro/Bundle/DataAuditBundle/Entity/AbstractAuditField.php index 8ffed588692..9dd1e449c45 100644 --- a/src/Oro/Bundle/DataAuditBundle/Entity/AbstractAuditField.php +++ b/src/Oro/Bundle/DataAuditBundle/Entity/AbstractAuditField.php @@ -9,7 +9,7 @@ use Oro\Bundle\DataAuditBundle\Model\AuditFieldTypeRegistry; /** - * @ORM\MappedSuperclass + * @ORM\MappedSuperclass() */ abstract class AbstractAuditField { diff --git a/src/Oro/Bundle/DataAuditBundle/Entity/Audit.php b/src/Oro/Bundle/DataAuditBundle/Entity/Audit.php index 9f14e64c274..3a7bbc13a25 100644 --- a/src/Oro/Bundle/DataAuditBundle/Entity/Audit.php +++ b/src/Oro/Bundle/DataAuditBundle/Entity/Audit.php @@ -2,7 +2,6 @@ namespace Oro\Bundle\DataAuditBundle\Entity; -use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use JMS\Serializer\Annotation\Type; @@ -14,9 +13,6 @@ /** * @ORM\Entity(repositoryClass="Oro\Bundle\DataAuditBundle\Entity\Repository\AuditRepository") - * @ORM\Table(name="oro_audit", indexes={ - * @ORM\Index(name="idx_oro_audit_logged_at", columns={"logged_at"}) - * }) */ class Audit extends AbstractAudit { @@ -70,13 +66,6 @@ class Audit extends AbstractAudit */ protected $version; - /** - * @var AuditField[]|Collection - * - * @ORM\OneToMany(targetEntity="AuditField", mappedBy="audit", cascade={"persist"}) - */ - protected $fields; - /** * @var string $username * @@ -94,14 +83,6 @@ class Audit extends AbstractAudit */ protected $user; - /** - * {@inheritdoc} - */ - protected function getAuditFieldInstance(AbstractAudit $audit, $field, $dataType, $newValue, $oldValue) - { - return new AuditField($audit, $field, $dataType, $newValue, $oldValue); - } - /** * {@inheritdoc} */ diff --git a/src/Oro/Bundle/DataAuditBundle/Entity/AuditField.php b/src/Oro/Bundle/DataAuditBundle/Entity/AuditField.php index 8321a2c8474..d0caae079fe 100644 --- a/src/Oro/Bundle/DataAuditBundle/Entity/AuditField.php +++ b/src/Oro/Bundle/DataAuditBundle/Entity/AuditField.php @@ -8,18 +8,20 @@ use Oro\Bundle\EntityConfigBundle\Metadata\Annotation\Config; /** - * @ORM\Entity + * @ORM\Entity() * @ORM\Table(name="oro_audit_field") - * @Config( - * mode="hidden" - * ) + * @Config(mode="hidden") */ class AuditField extends ExtendAuditField { /** * @var Audit * - * @ORM\ManyToOne(targetEntity="Audit", inversedBy="fields", cascade={"persist"}) + * @ORM\ManyToOne( + * targetEntity="Oro\Bundle\DataAuditBundle\Entity\AbstractAudit", + * inversedBy="fields", + * cascade={"persist"} + * ) * @ORM\JoinColumn(name="audit_id", referencedColumnName="id", nullable=false, onDelete="CASCADE") */ protected $audit; diff --git a/src/Oro/Bundle/DataAuditBundle/EventListener/AuditGridListener.php b/src/Oro/Bundle/DataAuditBundle/EventListener/AuditGridListener.php index 9e64668f3a2..9369178778d 100644 --- a/src/Oro/Bundle/DataAuditBundle/EventListener/AuditGridListener.php +++ b/src/Oro/Bundle/DataAuditBundle/EventListener/AuditGridListener.php @@ -16,7 +16,7 @@ class AuditGridListener protected $em; /** @var null|array */ - protected $objectClassChoices = null; + protected $objectClassChoices; /** * @param EntityManager $em @@ -34,11 +34,11 @@ public function __construct(EntityManager $em) public function getObjectClassOptions() { if (is_null($this->objectClassChoices)) { - $options = array(); + $options = []; $result = $this->em->createQueryBuilder() - ->add('select', 'a.objectClass') - ->add('from', 'Oro\Bundle\DataAuditBundle\Entity\Audit a') + ->select('a.objectClass') + ->from('Oro\Bundle\DataAuditBundle\Entity\AbstractAudit', 'a') ->distinct('a.objectClass') ->getQuery() ->getArrayResult(); diff --git a/src/Oro/Bundle/DataAuditBundle/Loggable/LoggableManager.php b/src/Oro/Bundle/DataAuditBundle/Loggable/LoggableManager.php index 4b15444cb3d..5f7013ac220 100644 --- a/src/Oro/Bundle/DataAuditBundle/Loggable/LoggableManager.php +++ b/src/Oro/Bundle/DataAuditBundle/Loggable/LoggableManager.php @@ -5,6 +5,7 @@ use Symfony\Component\Routing\Exception\InvalidParameterException; use Symfony\Component\Security\Core\SecurityContextInterface; +use Doctrine\ORM\UnitOfWork; use Doctrine\ORM\PersistentCollection; use Doctrine\ORM\EntityManager; use Doctrine\Common\Util\ClassUtils; @@ -167,7 +168,7 @@ public function handleLoggable(EntityManager $em) $collections = array_merge($uow->getScheduledCollectionUpdates(), $uow->getScheduledCollectionDeletions()); foreach ($collections as $collection) { - $this->calculateCollectionData($collection); + $this->calculateActualCollectionData($collection); } $entities = array_merge( @@ -286,7 +287,7 @@ protected function calculateManyToOneData(DoctrineClassMetadata $entityMeta, $en } $entityIdentifier = $this->getEntityIdentifierString($owner); - $this->calculateCollectionData($collection, $entityIdentifier); + $this->calculateActualCollectionData($collection, $entityIdentifier); $entities[$entityIdentifier] = $owner; } @@ -294,6 +295,21 @@ protected function calculateManyToOneData(DoctrineClassMetadata $entityMeta, $en return $entities; } + /** + * @param PersistentCollection $collection + * @param string $entityIdentifier + */ + protected function calculateActualCollectionData(PersistentCollection $collection, $entityIdentifier = null) + { + $ownerEntity = $collection->getOwner(); + $entityState = $this->em->getUnitOfWork()->getEntityState($ownerEntity, UnitOfWork::STATE_NEW); + if ($entityState === UnitOfWork::STATE_REMOVED) { + return; + } + + $this->calculateCollectionData($collection, $entityIdentifier); + } + /** * @param PersistentCollection $collection * @param string $entityIdentifier diff --git a/src/Oro/Bundle/DataAuditBundle/Migrations/Schema/OroDataAuditBundleInstaller.php b/src/Oro/Bundle/DataAuditBundle/Migrations/Schema/OroDataAuditBundleInstaller.php index 8afbf51ff61..f80c60f5788 100644 --- a/src/Oro/Bundle/DataAuditBundle/Migrations/Schema/OroDataAuditBundleInstaller.php +++ b/src/Oro/Bundle/DataAuditBundle/Migrations/Schema/OroDataAuditBundleInstaller.php @@ -14,7 +14,7 @@ class OroDataAuditBundleInstaller implements Installation */ public function getMigrationVersion() { - return 'v1_5'; + return 'v1_6'; } /** @@ -41,9 +41,13 @@ private function createAudit(Schema $schema) $auditTable->addColumn('object_name', 'string', ['length' => 255]); $auditTable->addColumn('version', 'integer', []); $auditTable->addColumn('organization_id', 'integer', ['notnull' => false]); + $auditTable->addColumn('type', 'string', ['length' => 255]); + $auditTable->setPrimaryKey(['id']); $auditTable->addIndex(['user_id'], 'IDX_5FBA427CA76ED395', []); + $auditTable->addIndex(['type'], 'idx_oro_audit_type'); + $auditTable->addForeignKeyConstraint( $schema->getTable('oro_user'), ['user_id'], diff --git a/src/Oro/Bundle/DataAuditBundle/Migrations/Schema/v1_6/AddColumn.php b/src/Oro/Bundle/DataAuditBundle/Migrations/Schema/v1_6/AddColumn.php new file mode 100644 index 00000000000..5f5ed5f4a8e --- /dev/null +++ b/src/Oro/Bundle/DataAuditBundle/Migrations/Schema/v1_6/AddColumn.php @@ -0,0 +1,27 @@ +getTable('oro_audit'); + $auditTable->addColumn('type', 'string', ['length' => 255, 'notnull' => false]); + } +} diff --git a/src/Oro/Bundle/DataAuditBundle/Migrations/Schema/v1_6/SetNotNullable.php b/src/Oro/Bundle/DataAuditBundle/Migrations/Schema/v1_6/SetNotNullable.php new file mode 100644 index 00000000000..365b6a485bd --- /dev/null +++ b/src/Oro/Bundle/DataAuditBundle/Migrations/Schema/v1_6/SetNotNullable.php @@ -0,0 +1,31 @@ +getTable('oro_audit'); + $auditTable->getColumn('type') + ->setType(Type::getType(Type::STRING)) + ->setOptions(['length' => 255, 'notnull' => true]); + $auditTable->addIndex(['type'], 'idx_oro_audit_type'); + } +} diff --git a/src/Oro/Bundle/DataAuditBundle/Migrations/Schema/v1_6/SetValue.php b/src/Oro/Bundle/DataAuditBundle/Migrations/Schema/v1_6/SetValue.php new file mode 100644 index 00000000000..642674c05c0 --- /dev/null +++ b/src/Oro/Bundle/DataAuditBundle/Migrations/Schema/v1_6/SetValue.php @@ -0,0 +1,34 @@ +addPreQuery( + new ParametrizedSqlMigrationQuery( + 'UPDATE oro_audit SET type = :type', + ['type' => 'audit'], + ['type' => Type::STRING] + ) + ); + } +} diff --git a/src/Oro/Bundle/DataAuditBundle/Resources/config/datagrid.yml b/src/Oro/Bundle/DataAuditBundle/Resources/config/datagrid.yml index d49bde388fd..7b38ace447b 100644 --- a/src/Oro/Bundle/DataAuditBundle/Resources/config/datagrid.yml +++ b/src/Oro/Bundle/DataAuditBundle/Resources/config/datagrid.yml @@ -1,43 +1,40 @@ datagrid: audit-grid: + acl_resource: oro_dataaudit_history options: entityHint: audit source: - acl_resource: oro_dataaudit_history type: orm query: select: - - a - - a.id - - a.action - - a.version - - a.objectClass - - a.objectName - - a.objectId - - a.loggedAt - - > - CONCAT( - CONCAT( - CONCAT(u.firstName, ' '), - CONCAT(u.lastName, ' ') - ), - CONCAT(' - ', u.email) - ) as author - - o.name as organization + audit: a + id: a.id + section: a.action + version: a.version + objectClass: a.objectClass + objectName: a.objectName + objectId: a.objectId + loggedAt: a.loggedAt + author: CONCAT(u.firstName, ' ', u.lastName, ' - ', u.email) as author + organization: o.name as organization from: - - { table: OroDataAuditBundle:Audit, alias: a } + - { table: OroDataAuditBundle:AbstractAudit, alias: a } join: left: - user: - join: a.user - alias: u organization: join: a.organization alias: o fields: join: a.fields alias: f - + audit: + join: OroDataAuditBundle:Audit + alias: ua + conditionType: WITH + condition: ua.id = a.id + user: + join: ua.user + alias: u where: and: - o.id = @oro_security.security_facade->getOrganizationId @@ -125,32 +122,30 @@ datagrid: enabled: false audit-history-grid: + acl_resource: oro_dataaudit_history source: - acl_resource: oro_dataaudit_history type: orm query: select: - - a - - a.id - - a.loggedAt - - > - CONCAT( - CONCAT( - CONCAT(u.firstName, ' '), - CONCAT(u.lastName, ' ') - ), - CONCAT(' - ', u.email) - ) as author + audit: a + id: a.id + loggedAt: a.loggedAt + author: CONCAT(u.firstName, ' ', u.lastName, ' - ', u.email) as author from: - - { table: OroDataAuditBundle:Audit, alias: a } + - { table: OroDataAuditBundle:AbstractAudit, alias: a } join: left: - user: - join: a.user - alias: u fields: join: a.fields alias: f + audit: + join: OroDataAuditBundle:Audit + alias: ua + conditionType: WITH + condition: ua.id = a.id + user: + join: ua.user + alias: u where: and: - a.objectClass = :objectClass AND a.objectId = :objectId diff --git a/src/Oro/Bundle/DataAuditBundle/Resources/config/placeholders.yml b/src/Oro/Bundle/DataAuditBundle/Resources/config/placeholders.yml index b208a31289a..6df62f1c029 100644 --- a/src/Oro/Bundle/DataAuditBundle/Resources/config/placeholders.yml +++ b/src/Oro/Bundle/DataAuditBundle/Resources/config/placeholders.yml @@ -12,7 +12,9 @@ placeholders: items: change_history_link: template: OroDataAuditBundle::change_history_link.html.twig - applicable: @oro_dataaudit.placeholder.filter->isEntityAuditable($entity$, $audit_show_change_history$) + applicable: + - @oro_dataaudit.placeholder.filter->isEntityAuditable($entity$, $audit_show_change_history$) + - @oro_user.placeholder.filter->isUserApplicable() acl: oro_dataaudit_history template_audit_condition_type_select: template: OroDataAuditBundle:js:audit-condition-type-select.html.twig diff --git a/src/Oro/Bundle/DataAuditBundle/Tests/Functional/ControllersTest.php b/src/Oro/Bundle/DataAuditBundle/Tests/Functional/ControllersTest.php index 005886c106f..a0bf586b3f6 100644 --- a/src/Oro/Bundle/DataAuditBundle/Tests/Functional/ControllersTest.php +++ b/src/Oro/Bundle/DataAuditBundle/Tests/Functional/ControllersTest.php @@ -129,7 +129,7 @@ public function testAuditHistory($result) $this->assertEquals($this->userData[$key], $value); } - $this->assertEquals('John Doe - admin@example.com', $result['author']); + $this->assertEquals('John Doe - admin@example.com', $result['author']); } protected function clearResult($result) diff --git a/src/Oro/Bundle/DataGridBundle/Common/Object.php b/src/Oro/Bundle/DataGridBundle/Common/Object.php index e6ad3a01e23..7d7c5526a5e 100644 --- a/src/Oro/Bundle/DataGridBundle/Common/Object.php +++ b/src/Oro/Bundle/DataGridBundle/Common/Object.php @@ -6,6 +6,7 @@ use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyAccess\PropertyPathInterface; use Oro\Component\PropertyAccess\PropertyAccessor; @@ -135,8 +136,8 @@ public function offsetGetOr($offset, $default = null) /** * Try to get property using PropertyAccessor * - * @param string $path - * @param null $default + * @param string|PropertyPathInterface $path + * @param null $default * * @return mixed */ @@ -155,6 +156,25 @@ public function offsetGetByPath($path, $default = null) return $value ? : $default; } + /** + * Check property existence using PropertyAccessor + * + * @param string|PropertyPathInterface $path + * + * @return mixed + */ + public function offsetExistByPath($path) + { + try { + $value = $this->accessor->getValue($this, $path); + } catch (NoSuchPropertyException $e) { + return false; + } + + // If NULL then result is FALSE, same behavior as function isset() has + return $value !== null; + } + /** * {@inheritDoc} */ @@ -168,8 +188,8 @@ public function offsetSet($offset, $value) /** * Set property using PropertyAccessor * - * @param string $path - * @param mixed $value + * @param string|PropertyPathInterface $path + * @param mixed $value * * @return $this */ @@ -191,9 +211,11 @@ public function offsetUnset($offset) } /** - * Expects data format for array data + * Unset property using PropertyAccessor * - * {@inheritDoc} + * @param string|PropertyPathInterface $path + * + * @return $this */ public function offsetUnsetByPath($path) { @@ -220,7 +242,7 @@ public function offsetUnsetByPath($path) } /** - * @param string $path + * @param string|PropertyPathInterface $path * @return array */ protected function explodeArrayPath($path) @@ -270,8 +292,8 @@ public function offsetAddToArray($offset, array $value) /** * Merge value to array property, if property not isset creates new one * - * @param string $path - * @param array $value + * @param string|PropertyPathInterface $path + * @param array $value * * @return $this */ diff --git a/src/Oro/Bundle/DataGridBundle/Controller/Api/Rest/GridViewController.php b/src/Oro/Bundle/DataGridBundle/Controller/Api/Rest/GridViewController.php index efab9eacf64..d533417d7e6 100644 --- a/src/Oro/Bundle/DataGridBundle/Controller/Api/Rest/GridViewController.php +++ b/src/Oro/Bundle/DataGridBundle/Controller/Api/Rest/GridViewController.php @@ -6,6 +6,7 @@ use FOS\RestBundle\Controller\Annotations\Post; use FOS\RestBundle\Controller\Annotations\Put; use FOS\RestBundle\Controller\Annotations as Rest; +use FOS\RestBundle\Util\Codes; use Nelmio\ApiDocBundle\Annotation\ApiDoc; @@ -98,6 +99,50 @@ public function deleteAction($id) return $this->handleDeleteRequest($id); } + + /** + * Set/unset grid view as default for current user. + * + * @param int $id + * @param bool $default + * + * @return Response + * @Post( + * "/gridviews/{id}/default/{default}", + * requirements={"id"="\d+", "default"="\d+"}, + * defaults={"default"=false} + *) + * @ApiDoc( + * description="Set/unset grid view as default for current user", + * resource=true, + * requirements={ + * {"name"="id", "dataType"="integer"}, + * {"name"="default", "dataType"="boolean"}, + * }, + * defaults={"default"="false"} + * ) + * @Acl( + * id="oro_datagrid_gridview_view", + * type="entity", + * class="OroDataGridBundle:GridView", + * permission="VIEW" + * ) + */ + public function defaultAction($id, $default = false) + { + /** @var GridView $gridView */ + $manager = $this->getManager(); + $gridView = $manager->find($id); + if ($gridView) { + $manager->setDefaultGridView($this->getUser(), $gridView, $default); + $view = $this->view(null, Codes::HTTP_NO_CONTENT); + } else { + $view = $this->view(null, Codes::HTTP_NOT_FOUND); + } + + return $this->buildResponse($view, self::ACTION_UPDATE, ['id' => $id, 'entity' => $gridView]); + } + /** * @param GridView $gridView * diff --git a/src/Oro/Bundle/DataGridBundle/Controller/GridController.php b/src/Oro/Bundle/DataGridBundle/Controller/GridController.php index 69975475499..15baf3f377a 100644 --- a/src/Oro/Bundle/DataGridBundle/Controller/GridController.php +++ b/src/Oro/Bundle/DataGridBundle/Controller/GridController.php @@ -58,10 +58,9 @@ public function getAction($gridName) { $gridManager = $this->get('oro_datagrid.datagrid.manager'); $gridConfig = $gridManager->getConfigurationForGrid($gridName); - $acl = $gridConfig->offsetGetByPath(Builder::DATASOURCE_ACL_PATH); - $aclSkip = $gridConfig->offsetGetByPath(Builder::DATASOURCE_SKIP_ACL_CHECK, false); + $acl = $gridConfig->getAclResource(); - if (!$aclSkip && $acl && !$this->get('oro_security.security_facade')->isGranted($acl)) { + if ($acl && !$this->get('oro_security.security_facade')->isGranted($acl)) { throw new AccessDeniedException('Access denied.'); } diff --git a/src/Oro/Bundle/DataGridBundle/Datagrid/Builder.php b/src/Oro/Bundle/DataGridBundle/Datagrid/Builder.php index 41c34cb0736..5f207f5bfa7 100644 --- a/src/Oro/Bundle/DataGridBundle/Datagrid/Builder.php +++ b/src/Oro/Bundle/DataGridBundle/Datagrid/Builder.php @@ -15,13 +15,40 @@ class Builder { + /** + * @deprecated Since 1.9, will be removed after 1.11. + * @see DatagridConfiguration::DATASOURCE_PATH + */ const DATASOURCE_PATH = '[source]'; + + /** + * @deprecated Since 1.9, will be removed after 1.11. + * @see DatagridConfiguration::DATASOURCE_TYPE_PATH, DatagridConfiguration::getDatasourceType + */ const DATASOURCE_TYPE_PATH = '[source][type]'; + + /** + * @deprecated Since 1.9, will be removed after 1.11. + * @see DatagridConfiguration::ACL_RESOURCE_PATH, DatagridConfiguration::getAclResource + */ const DATASOURCE_ACL_PATH = '[source][acl_resource]'; + + /** + * @deprecated Since 1.9, will be removed after 1.11. + * @see DatagridConfiguration::BASE_DATAGRID_CLASS_PATH + */ const BASE_DATAGRID_CLASS_PATH = '[options][base_datagrid_class]'; + + /** + * @deprecated Since 1.9, will be removed after 1.11. + * @see DatagridConfiguration::DATASOURCE_SKIP_ACL_APPLY_PATH, DatagridConfiguration::isDatasourceSkipAclApply + */ const DATASOURCE_SKIP_ACL_CHECK = '[options][skip_acl_check]'; - // Use this option as workaround for http://www.doctrine-project.org/jira/browse/DDC-2794 + /** + * @deprecated Since 1.9, will be removed after 1.11. + * @see DatagridConfiguration::DATASOURCE_SKIP_COUNT_WALKER_PATH + */ const DATASOURCE_SKIP_COUNT_WALKER_PATH = '[options][skip_count_walker]'; /** @var string */ @@ -79,7 +106,7 @@ public function build(DatagridConfiguration $config, ParameterBag $parameters) $event = new PreBuild($config, $parameters); $this->eventDispatcher->dispatch(PreBuild::NAME, $event); - $class = $config->offsetGetByPath(self::BASE_DATAGRID_CLASS_PATH, $this->baseDatagridClass); + $class = $config->offsetGetByPath(DatagridConfiguration::BASE_DATAGRID_CLASS_PATH, $this->baseDatagridClass); $name = $config->getName(); /** @var DatagridInterface $datagrid */ @@ -170,7 +197,7 @@ protected function createAcceptor(DatagridConfiguration $config, ParameterBag $p */ protected function buildDataSource(DatagridInterface $grid, DatagridConfiguration $config) { - $sourceType = $config->offsetGetByPath(self::DATASOURCE_TYPE_PATH, false); + $sourceType = $config->offsetGetByPath(DatagridConfiguration::DATASOURCE_TYPE_PATH, false); if (!$sourceType) { throw new RuntimeException('Datagrid source does not configured'); } @@ -179,6 +206,9 @@ protected function buildDataSource(DatagridInterface $grid, DatagridConfiguratio throw new RuntimeException(sprintf('Datagrid source "%s" does not exist', $sourceType)); } - $this->dataSources[$sourceType]->process($grid, $config->offsetGetByPath(self::DATASOURCE_PATH, [])); + $this->dataSources[$sourceType]->process( + $grid, + $config->offsetGetByPath(DatagridConfiguration::DATASOURCE_PATH, []) + ); } } diff --git a/src/Oro/Bundle/DataGridBundle/Datagrid/Common/DatagridConfiguration.php b/src/Oro/Bundle/DataGridBundle/Datagrid/Common/DatagridConfiguration.php index 261ceb16a7d..ea220b9bcf9 100644 --- a/src/Oro/Bundle/DataGridBundle/Datagrid/Common/DatagridConfiguration.php +++ b/src/Oro/Bundle/DataGridBundle/Datagrid/Common/DatagridConfiguration.php @@ -2,8 +2,321 @@ namespace Oro\Bundle\DataGridBundle\Datagrid\Common; +use Doctrine\ORM\EntityRepository; + use Oro\Bundle\DataGridBundle\Common\Object; +use Oro\Bundle\EntityExtendBundle\Tools\ExtendHelper; +use Oro\Bundle\DataGridBundle\Datagrid\Builder; +/** + * @SuppressWarnings(PHPMD.TooManyPublicMethods) + */ class DatagridConfiguration extends Object { + const COLUMN_PATH = '[columns][%s]'; + const SORTER_PATH = '[sorters][columns][%s]'; + const FILTER_PATH = '[filters][columns][%s]'; + const DATASOURCE_PATH = '[source]'; + const DATASOURCE_TYPE_PATH = '[source][type]'; + const BASE_DATAGRID_CLASS_PATH = '[options][base_datagrid_class]'; + + // Use this option as workaround for http://www.doctrine-project.org/jira/browse/DDC-2794 + const DATASOURCE_SKIP_COUNT_WALKER_PATH = '[options][skip_count_walker]'; + + /** + * This option refers to ACL resource that will be checked before datagrid is loaded. + */ + const ACL_RESOURCE_PATH = '[acl_resource]'; + + /** + * This option makes possible to skip apply of ACL adjustment to source query of datagrid. + */ + const DATASOURCE_SKIP_ACL_APPLY_PATH = '[source][skip_acl_apply]'; + + /** + * @return string + */ + public function getDatasourceType() + { + return $this->offsetGetByPath(self::DATASOURCE_TYPE_PATH); + } + + /** + * Get value of "acl_resource" option from datagrid configuration. + * + * @return string|null + */ + public function getAclResource() + { + if ($this->offsetExistByPath(self::ACL_RESOURCE_PATH)) { + $result = $this->offsetGetByPath(self::ACL_RESOURCE_PATH); + } else { + // Support backward compatibility until 1.11 to get this option from deprecated path. + $result = $this->offsetGetByPath(Builder::DATASOURCE_ACL_PATH, false); + } + + return $result; + } + + /** + * Check if ACL apply to source query of datagrid should be skipped + * + * @return bool + */ + public function isDatasourceSkipAclApply() + { + if ($this->offsetExistByPath(self::DATASOURCE_SKIP_ACL_APPLY_PATH)) { + $result = $this->offsetGetByPath(self::DATASOURCE_SKIP_ACL_APPLY_PATH); + } else { + // Support backward compatibility until 1.11 to get this option from deprecated path. + $result = $this->offsetGetByPath(Builder::DATASOURCE_SKIP_ACL_CHECK, false); + } + + return (bool)$result; + } + + /** + * @param string $name + * @param string $label + * + * @return DatagridConfiguration + */ + public function updateLabel($name, $label) + { + if (empty($name)) { + throw new \BadMethodCallException('DatagridConfiguration::updateLabel: name should not be empty'); + } + + $this->offsetSetByPath(sprintf(self::COLUMN_PATH.'[label]', $name), $label); + + return $this; + } + + /** + * @param string $name column name + * @param array $definition definition array as in datagrid.yml + * @param null|string $select select part for the column + * @param array $sorter sorter definition + * @param array $filter filter definition + * + * @return DatagridConfiguration + */ + public function addColumn($name, array $definition, $select = null, array $sorter = [], array $filter = []) + { + if (empty($name)) { + throw new \BadMethodCallException('DatagridConfiguration::addColumn: name should not be empty'); + } + + $this->offsetSetByPath( + sprintf(self::COLUMN_PATH, $name), + $definition + ); + + if (!is_null($select)) { + $this->addSelect($select); + } + + if (!empty($sorter)) { + $this->addSorter($name, $sorter); + } + + if (!empty($filter)) { + $this->addFilter($name, $filter); + } + + return $this; + } + + /** + * @param string $select + * + * @return DatagridConfiguration + */ + public function addSelect($select) + { + if (empty($select)) { + throw new \BadMethodCallException('DatagridConfiguration::addSelect: select should not be empty'); + } + + $this->offsetAddToArrayByPath( + '[source][query][select]', + [$select] + ); + + return $this; + } + + /** + * @param string $type + * @param array $definition + * + * @return DatagridConfiguration + */ + public function joinTable($type, array $definition) + { + $this + ->offsetAddToArrayByPath( + sprintf('[source][query][join][%s]', $type), + [$definition] + ); + + return $this; + } + + /** + * @param string $name + * @param array $definition + * + * @return DatagridConfiguration + */ + public function addFilter($name, array $definition) + { + $this->offsetSetByPath( + sprintf(self::FILTER_PATH, $name), + $definition + ); + + return $this; + } + + /** + * @param string $name + * @param array $definition + * + * @return DatagridConfiguration + */ + public function addSorter($name, array $definition) + { + $this->offsetSetByPath( + sprintf(self::SORTER_PATH, $name), + $definition + ); + + return $this; + } + + /** + * Remove column definition + * should remove sorters as well and optionally filters + * + * @param string $name column name from grid definition + * @param bool $removeFilter whether remove filter or not, true by default + * + * @return DatagridConfiguration + */ + public function removeColumn($name, $removeFilter = true) + { + $this->offsetUnsetByPath( + sprintf(self::COLUMN_PATH, $name) + ); + + $this->removeSorter($name); + if ($removeFilter) { + $this->removeFilter($name); + } + + return $this; + } + + /** + * @param string $name column name + */ + public function removeSorter($name) + { + $this->offsetUnsetByPath( + sprintf(self::SORTER_PATH, $name) + ); + } + + /** + * Remove filter definition + * + * @param string $name column name + * + * @return DatagridConfiguration + */ + public function removeFilter($name) + { + $this->offsetUnsetByPath( + sprintf(self::FILTER_PATH, $name) + ); + + return $this; + } + + /** + * @param string $columnName column name + * @param string $dataName property path of the field, e.g. entity.enum_field + * @param string $enumCode enum code + * @param bool $isMultiple allow to filter by several values + * + * @return DatagridConfiguration + */ + public function addEnumFilter($columnName, $dataName, $enumCode, $isMultiple = false) + { + $this->addFilter( + $columnName, + [ + 'type' => 'entity', + 'data_name' => $dataName, + 'options' => [ + 'field_options' => [ + 'class' => ExtendHelper::buildEnumValueClassName($enumCode), + 'property' => 'name', + 'query_builder' => function (EntityRepository $entityRepository) { + return $entityRepository->createQueryBuilder('c') + ->orderBy('c.name', 'ASC'); + }, + 'multiple' => $isMultiple, + ], + ], + ] + ); + + return $this; + } + + /** + * @param string $name + * @param string $label + * @param string $templatePath + * @param null|string $select select part for the column + * @param array $sorter sorter definition + * @param array $filter filter definitio + * + * @return DatagridConfiguration + */ + public function addTwigColumn($name, $label, $templatePath, $select = null, array $sorter = [], array $filter = []) + { + $this->addColumn( + $name, + [ + 'label' => $label, + 'type' => 'twig', + 'frontend_type' => 'html', + 'template' => $templatePath, + ], + $select, + $sorter, + $filter + ); + + return $this; + } + + /** + * @param string $name + * @param array $options + * + * @return DatagridConfiguration + */ + public function addMassAction($name, array $options) + { + $this->offsetSetByPath( + sprintf('[mass_actions][%s]', $name), + $options + ); + + return $this; + } } diff --git a/src/Oro/Bundle/DataGridBundle/Datasource/ResultRecord.php b/src/Oro/Bundle/DataGridBundle/Datasource/ResultRecord.php index 818e4d95260..c715e7f8538 100644 --- a/src/Oro/Bundle/DataGridBundle/Datasource/ResultRecord.php +++ b/src/Oro/Bundle/DataGridBundle/Datasource/ResultRecord.php @@ -76,8 +76,10 @@ public function getValue($name) */ public function getRootEntity() { - if (array_key_exists(0, $this->valueContainers) && is_object($this->valueContainers[0])) { - return $this->valueContainers[0]; + foreach ($this->valueContainers as $value) { + if (is_object($value)) { + return $value; + } } return null; diff --git a/src/Oro/Bundle/DataGridBundle/DependencyInjection/CompilerPass/ConfigurationPass.php b/src/Oro/Bundle/DataGridBundle/DependencyInjection/CompilerPass/ConfigurationPass.php index 04860ed3947..127a68c4424 100644 --- a/src/Oro/Bundle/DataGridBundle/DependencyInjection/CompilerPass/ConfigurationPass.php +++ b/src/Oro/Bundle/DataGridBundle/DependencyInjection/CompilerPass/ConfigurationPass.php @@ -8,6 +8,7 @@ use Oro\Component\Config\Loader\CumulativeConfigLoader; use Oro\Component\Config\Loader\YamlCumulativeFileLoader; +use Oro\Component\PhpUtils\ArrayUtil; class ConfigurationPass implements CompilerPassInterface { @@ -48,7 +49,7 @@ protected function registerConfigFiles(ContainerBuilder $container) $resources = $configLoader->load($container); foreach ($resources as $resource) { if (isset($resource->data[self::ROOT_PARAMETER]) && is_array($resource->data[self::ROOT_PARAMETER])) { - $config = array_merge_recursive($config, $resource->data[self::ROOT_PARAMETER]); + $config = ArrayUtil::arrayMergeRecursiveDistinct($config, $resource->data[self::ROOT_PARAMETER]); } } @@ -65,7 +66,7 @@ protected function registerConfigFiles(ContainerBuilder $container) protected function registerConfigProviders(ContainerBuilder $container) { if ($container->hasDefinition(self::CHAIN_PROVIDER_SERVICE_ID)) { - $providers = array(); + $providers = []; foreach ($container->findTaggedServiceIds(self::PROVIDER_TAG_NAME) as $id => $attributes) { $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; $providers[$priority][] = new Reference($id); diff --git a/src/Oro/Bundle/DataGridBundle/Entity/GridView.php b/src/Oro/Bundle/DataGridBundle/Entity/GridView.php index 3d90134dd3e..856d7fa1872 100644 --- a/src/Oro/Bundle/DataGridBundle/Entity/GridView.php +++ b/src/Oro/Bundle/DataGridBundle/Entity/GridView.php @@ -2,6 +2,7 @@ namespace Oro\Bundle\DataGridBundle\Entity; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; @@ -39,12 +40,12 @@ class GridView { const TYPE_PRIVATE = 'private'; - const TYPE_PUBLIC = 'public'; + const TYPE_PUBLIC = 'public'; /** @var array */ protected static $types = [ self::TYPE_PRIVATE => self::TYPE_PRIVATE, - self::TYPE_PUBLIC => self::TYPE_PUBLIC, + self::TYPE_PUBLIC => self::TYPE_PUBLIC, ]; /** @@ -81,16 +82,18 @@ class GridView protected $filtersData = []; /** - * @var array + * @var array of ['column name' => -1|1, ... ]. + * Contains information about sorters ('-1' for 'ASC', '1' for 'DESC'). * * @ORM\Column(type="array") */ protected $sortersData = []; /** - * @var array + * @var array of ['column name' => ['renderable' => true|false, 'order' = int(0)], ... ]. + * Contains information about columns orders in the grid. * - * @ORM\Column(type="array") + * @ORM\Column(type="array", nullable=true) */ protected $columnsData = []; @@ -119,6 +122,29 @@ class GridView */ protected $organization; + /** + * Collection of users who have chosen this grid view as default. + * + * @var ArrayCollection|User[] + * + * @ORM\ManyToMany( + * targetEntity="Oro\Bundle\UserBundle\Entity\User" + * ) + * @ORM\JoinTable(name="oro_grid_view_user", + * joinColumns={@ORM\JoinColumn(name="grid_view_id", referencedColumnName="id", onDelete="CASCADE")}, + * inverseJoinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE")}, + * ) + */ + protected $users; + + /** + * GridView constructor. + */ + public function __construct() + { + $this->users = new ArrayCollection(); + } + /** * @return int */ @@ -178,7 +204,7 @@ public function getOwner() /** * @param int $id * - * @return this + * @return $this */ public function setId($id) { @@ -190,7 +216,7 @@ public function setId($id) /** * @param string $name * - * @return this + * @return $this */ public function setName($name) { @@ -202,7 +228,7 @@ public function setName($name) /** * @param string $type * - * @return this + * @return $this */ public function setType($type) { @@ -214,7 +240,7 @@ public function setType($type) /** * @param array $filtersData * - * @return this + * @return $this */ public function setFiltersData(array $filtersData = []) { @@ -226,7 +252,7 @@ public function setFiltersData(array $filtersData = []) /** * @param array $sortersData * - * @return this + * @return $this */ public function setSortersData(array $sortersData = []) { @@ -240,6 +266,10 @@ public function setSortersData(array $sortersData = []) */ public function getColumnsData() { + if ($this->columnsData === null) { + $this->columnsData = []; + } + return $this->columnsData; } @@ -254,7 +284,7 @@ public function setColumnsData(array $columnsData = []) /** * @param string $gridName * - * @return this + * @return $this */ public function setGridName($gridName) { @@ -266,7 +296,7 @@ public function setGridName($gridName) /** * @param User $owner * - * @return this + * @return $this */ public function setOwner(User $owner = null) { @@ -280,7 +310,7 @@ public function setOwner(User $owner = null) */ public function createView() { - $view = new View($this->id, $this->filtersData, $this->sortersData, $this->type, $this->columnsData); + $view = new View($this->id, $this->filtersData, $this->sortersData, $this->type, $this->getColumnsData()); $view->setLabel($this->name); return $view; @@ -298,6 +328,7 @@ public static function getTypes() * Set organization * * @param OrganizationInterface $organization + * * @return User */ public function setOrganization(OrganizationInterface $organization = null) @@ -316,4 +347,34 @@ public function getOrganization() { return $this->organization; } + + /** + * @return ArrayCollection|User[] + */ + public function getUsers() + { + return $this->users; + } + + /** + * @param User $user + * + * @return $this + */ + public function addUser(User $user) + { + if (!$this->users->contains($user)) { + $this->users->add($user); + } + + return $this; + } + + /** + * @param User $user + */ + public function removeUser(User $user) + { + $this->users->removeElement($user); + } } diff --git a/src/Oro/Bundle/DataGridBundle/Entity/Manager/GridViewApiEntityManager.php b/src/Oro/Bundle/DataGridBundle/Entity/Manager/GridViewApiEntityManager.php new file mode 100644 index 00000000000..71e57ef9f82 --- /dev/null +++ b/src/Oro/Bundle/DataGridBundle/Entity/Manager/GridViewApiEntityManager.php @@ -0,0 +1,39 @@ +gridViewManager = $gridViewManager; + } + + /** + * @param User $user + * @param GridView $gridView + * @param bool $default + */ + public function setDefaultGridView(User $user, GridView $gridView, $default) + { + $this->gridViewManager->setDefaultGridView($user, $gridView, $default); + + $this->getObjectManager()->flush(); + } +} diff --git a/src/Oro/Bundle/DataGridBundle/Entity/Manager/GridViewManager.php b/src/Oro/Bundle/DataGridBundle/Entity/Manager/GridViewManager.php new file mode 100644 index 00000000000..e27b7a2231b --- /dev/null +++ b/src/Oro/Bundle/DataGridBundle/Entity/Manager/GridViewManager.php @@ -0,0 +1,53 @@ +aclHelper = $aclHelper; + $this->registry = $registry; + } + + /** + * @param User $user + * @param GridView $gridView + * @param bool $default + */ + public function setDefaultGridView(User $user, GridView $gridView, $default) + { + $isGridViewDefault = $gridView->getUsers()->contains($user); + // Checks if default grid view changed + if ($isGridViewDefault !== $default) { + $om = $this->registry->getManagerForClass('OroDataGridBundle:GridView'); + /** @var GridViewRepository $repository */ + $repository = $om->getRepository('OroDataGridBundle:GridView'); + $gridViews = $repository->findDefaultGridViews($this->aclHelper, $user, $gridView, false); + foreach ($gridViews as $view) { + $view->removeUser($user); + } + + if ($default) { + $gridView->addUser($user); + } + } + } +} diff --git a/src/Oro/Bundle/DataGridBundle/Entity/Repository/GridViewRepository.php b/src/Oro/Bundle/DataGridBundle/Entity/Repository/GridViewRepository.php index 9d174a3a755..e6f72db6c2b 100644 --- a/src/Oro/Bundle/DataGridBundle/Entity/Repository/GridViewRepository.php +++ b/src/Oro/Bundle/DataGridBundle/Entity/Repository/GridViewRepository.php @@ -2,19 +2,20 @@ namespace Oro\Bundle\DataGridBundle\Entity\Repository; -use Symfony\Component\Security\Core\User\UserInterface; - +use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\EntityRepository; +use Symfony\Component\Security\Core\User\UserInterface; + use Oro\Bundle\DataGridBundle\Entity\GridView; use Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper; class GridViewRepository extends EntityRepository { /** - * @param AclHelper $aclHelper + * @param AclHelper $aclHelper * @param UserInterface $user - * @param string $gridName + * @param string $gridName * * @return GridView[] */ @@ -23,17 +24,97 @@ public function findGridViews(AclHelper $aclHelper, UserInterface $user, $gridNa $qb = $this->createQueryBuilder('gv'); $qb ->andWhere('gv.gridName = :gridName') - ->andWhere($qb->expr()->orX( - 'gv.owner = :owner', - 'gv.type = :public' - )) - ->setParameters([ - 'gridName' => $gridName, - 'owner' => $user, - 'public' => GridView::TYPE_PUBLIC, - ]) + ->andWhere( + $qb->expr()->orX( + 'gv.owner = :owner', + 'gv.type = :public' + ) + ) + ->setParameters( + [ + 'gridName' => $gridName, + 'owner' => $user, + 'public' => GridView::TYPE_PUBLIC, + ] + ) ->orderBy('gv.gridName'); return $aclHelper->apply($qb)->getResult(); } + + /** + * @param AclHelper $aclHelper + * @param UserInterface $user + * @param string $gridName + * + * @return GridView|null + */ + public function findDefaultGridView(AclHelper $aclHelper, UserInterface $user, $gridName) + { + $qb = $this->getFindDefaultGridViewQb($user, $gridName); + $qb->setMaxResults(1); + + return $aclHelper->apply($qb)->getOneOrNullResult(); + } + + /** + * @param AclHelper $aclHelper + * @param UserInterface $user + * @param GridView $gridView + * @param bool $checkOwner + * + * @return GridView[] + */ + public function findDefaultGridViews( + AclHelper $aclHelper, + UserInterface $user, + GridView $gridView, + $checkOwner = true + ) { + /** @var GridView[] $defaultGridViews */ + $qb = $this->getFindDefaultGridViewQb($user, $gridView->getGridName(), $checkOwner); + + return $aclHelper->apply($qb)->getResult(); + } + + /** + * @param UserInterface $user + * @param string $gridName + * @param bool $checkOwner + * + * @return QueryBuilder + */ + protected function getFindDefaultGridViewQb(UserInterface $user, $gridName, $checkOwner = true) + { + $parameters = [ + 'gridName' => $gridName, + 'user' => $user, + ]; + + $qb = $this->createQueryBuilder('gv'); + $qb->innerJoin('gv.users', 'u') + ->where('gv.gridName = :gridName') + ->andWhere('u = :user'); + + if ($checkOwner) { + $qb->andWhere( + $qb->expr()->orX( + 'gv.owner = :owner', + 'gv.type = :public' + ) + ); + + $parameters = array_merge( + $parameters, + [ + 'owner' => $user, + 'public' => GridView::TYPE_PUBLIC + ] + ); + } + + $qb->setParameters($parameters); + + return $qb; + } } diff --git a/src/Oro/Bundle/DataGridBundle/EventListener/GridViewsLoadListener.php b/src/Oro/Bundle/DataGridBundle/EventListener/GridViewsLoadListener.php index c84e52b0670..cbb1f3f3336 100644 --- a/src/Oro/Bundle/DataGridBundle/EventListener/GridViewsLoadListener.php +++ b/src/Oro/Bundle/DataGridBundle/EventListener/GridViewsLoadListener.php @@ -49,35 +49,37 @@ public function __construct( */ public function onViewsLoad(GridViewsLoadEvent $event) { - $gridName = $event->getGridName(); + $gridName = $event->getGridName(); $currentUser = $this->getCurrentUser(); if (!$currentUser) { return; } - $gridViews = $this->getGridViewRepository()->findGridViews($this->aclHelper, $currentUser, $gridName); + $gridViewRepository = $this->getGridViewRepository(); + $gridViews = $gridViewRepository->findGridViews($this->aclHelper, $currentUser, $gridName); + $defaultGridView = $gridViewRepository->findDefaultGridView($this->aclHelper, $currentUser, $gridName); if (!$gridViews) { return; } $choices = []; - $views = []; + $views = []; foreach ($gridViews as $gridView) { $view = $gridView->createView(); $view->setEditable($this->securityFacade->isGranted('EDIT', $gridView)); $view->setDeletable($this->securityFacade->isGranted('DELETE', $gridView)); - - $views[] = $view->getMetadata(); + $view->setDefault($defaultGridView === $gridView); + $views[] = $view->getMetadata(); $choices[] = [ 'label' => $this->createGridViewLabel($currentUser, $gridView), 'value' => $gridView->getId(), ]; } - $newGridViews = $event->getGridViews(); + /** @var array $newGridViews */ + $newGridViews = $event->getGridViews(); $newGridViews['choices'] = array_merge($newGridViews['choices'], $choices); - $newGridViews['views'] = array_merge($newGridViews['views'], $views); - + $newGridViews['views'] = array_merge($newGridViews['views'], $views); $event->setGridViews($newGridViews); } diff --git a/src/Oro/Bundle/DataGridBundle/EventListener/OrmDatasourceAclListener.php b/src/Oro/Bundle/DataGridBundle/EventListener/OrmDatasourceAclListener.php index 3fec80548df..5ffc52d86db 100644 --- a/src/Oro/Bundle/DataGridBundle/EventListener/OrmDatasourceAclListener.php +++ b/src/Oro/Bundle/DataGridBundle/EventListener/OrmDatasourceAclListener.php @@ -2,7 +2,6 @@ namespace Oro\Bundle\DataGridBundle\EventListener; -use Oro\Bundle\DataGridBundle\Datagrid\Builder; use Oro\Bundle\DataGridBundle\Event\OrmResultBefore; use Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper; @@ -26,7 +25,7 @@ public function __construct(AclHelper $aclHelper) public function onResultBefore(OrmResultBefore $event) { $config = $event->getDatagrid()->getConfig(); - if (!$config->offsetGetByPath(Builder::DATASOURCE_SKIP_ACL_CHECK, false)) { + if (!$config->isDatasourceSkipAclApply()) { $this->aclHelper->apply($event->getQuery()); } } diff --git a/src/Oro/Bundle/DataGridBundle/Extension/Action/ActionExtension.php b/src/Oro/Bundle/DataGridBundle/Extension/Action/ActionExtension.php index 0edd7e920f8..e4cf9e50b52 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/Action/ActionExtension.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/Action/ActionExtension.php @@ -99,6 +99,15 @@ public function getPriority() * {@inheritDoc} */ public function visitMetadata(DatagridConfiguration $config, MetadataObject $data) + { + $data->offsetAddToArray(static::METADATA_ACTION_KEY, $this->getActionsMetadata($config)); + } + + /** + * @param DatagridConfiguration $config + * @return array + */ + protected function getActionsMetadata(DatagridConfiguration $config) { $actionsMetadata = []; $actions = $config->offsetGetOr(static::ACTION_KEY, []); @@ -114,7 +123,7 @@ public function visitMetadata(DatagridConfiguration $config, MetadataObject $dat } } - $data->offsetAddToArray(static::METADATA_ACTION_KEY, $actionsMetadata); + return $actionsMetadata; } /** diff --git a/src/Oro/Bundle/DataGridBundle/Extension/Action/Actions/AbstractAction.php b/src/Oro/Bundle/DataGridBundle/Extension/Action/Actions/AbstractAction.php index 8a3bcbc836f..e1babb5aea7 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/Action/Actions/AbstractAction.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/Action/Actions/AbstractAction.php @@ -59,7 +59,7 @@ public function setOptions(ActionConfiguration $options) } /** - * Accert required options array + * Assert required options array */ protected function assertHasRequiredOptions() { diff --git a/src/Oro/Bundle/DataGridBundle/Extension/Columns/ColumnsExtension.php b/src/Oro/Bundle/DataGridBundle/Extension/Columns/ColumnsExtension.php index ef707eb2b58..8273f1ab8a7 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/Columns/ColumnsExtension.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/Columns/ColumnsExtension.php @@ -16,7 +16,11 @@ use Oro\Bundle\DataGridBundle\Tools\ColumnsHelper; use Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper; use Oro\Bundle\SecurityBundle\SecurityFacade; +use Oro\Bundle\DataGridBundle\Entity\GridView; +/** + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + */ class ColumnsExtension extends AbstractExtension { /** @@ -40,6 +44,9 @@ class ColumnsExtension extends AbstractExtension /** @var ColumnsHelper */ protected $columnsHelper; + /** @var GridView|null|bool */ + protected $defaultGridView = false; + /** * @param Registry $registry * @param SecurityFacade $securityFacade @@ -259,11 +266,20 @@ protected function setColumnsOrder(DatagridConfiguration $config, MetadataObject /** * @param DatagridConfiguration $config + * @param bool $default * * @return array */ - protected function getColumnsWithOrder(DatagridConfiguration $config) + protected function getColumnsWithOrder(DatagridConfiguration $config, $default = false) { + if (!$default) { + $params = $this->getParameters()->get(ParameterBag::ADDITIONAL_PARAMETERS, []); + $defaultGridView = $this->getDefaultGridView($config->getName()); + if (isset($params['view']) && $defaultGridView && $params['view'] === $defaultGridView->getId()) { + return $defaultGridView->getColumnsData(); + } + } + $columnsData = $config->offsetGet(self::COLUMNS_PATH); $columnsOrder = $this->columnsHelper->buildColumnsOrder($columnsData); $columns = $this->applyColumnsOrderAndRender($columnsData, $columnsOrder); @@ -271,6 +287,26 @@ protected function getColumnsWithOrder(DatagridConfiguration $config) return $columns; } + /** + * @param string $gridName + * + * @return GridView|null + */ + protected function getDefaultGridView($gridName) + { + if ($this->defaultGridView === false) { + $defaultGridView = $this->getGridViewRepository()->findDefaultGridView( + $this->aclHelper, + $this->getCurrentUser(), + $gridName + ); + + $this->defaultGridView = $defaultGridView; + } + + return $this->defaultGridView; + } + /** * Create grid view for default grid state __all__ * @@ -282,7 +318,7 @@ protected function getColumnsWithOrder(DatagridConfiguration $config) protected function createNewGridView(DatagridConfiguration $config, MetadataObject $data) { $newGridView = new View(GridViewsExtension::DEFAULT_VIEW_ID); - $columns = $this->getColumnsWithOrder($config); + $columns = $this->getColumnsWithOrder($config, true); /** Set config columns state to __all__ grid view */ $newGridView->setColumnsData($columns); diff --git a/src/Oro/Bundle/DataGridBundle/Extension/GridParams/GridParamsExtension.php b/src/Oro/Bundle/DataGridBundle/Extension/GridParams/GridParamsExtension.php index 9c841347635..69ff37d3994 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/GridParams/GridParamsExtension.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/GridParams/GridParamsExtension.php @@ -2,7 +2,6 @@ namespace Oro\Bundle\DataGridBundle\Extension\GridParams; -use Oro\Bundle\DataGridBundle\Datagrid\Builder; use Oro\Bundle\DataGridBundle\Datasource\Orm\OrmDatasource; use Oro\Bundle\DataGridBundle\Extension\AbstractExtension; use Oro\Bundle\DataGridBundle\Datagrid\Common\MetadataObject; @@ -18,7 +17,7 @@ class GridParamsExtension extends AbstractExtension */ public function isApplicable(DatagridConfiguration $config) { - return $config->offsetGetByPath(Builder::DATASOURCE_TYPE_PATH) == OrmDatasource::TYPE; + return $config->getDatasourceType() == OrmDatasource::TYPE; } /** diff --git a/src/Oro/Bundle/DataGridBundle/Extension/GridViews/GridViewsExtension.php b/src/Oro/Bundle/DataGridBundle/Extension/GridViews/GridViewsExtension.php index 560112ff2f9..33fd0eb8ab6 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/GridViews/GridViewsExtension.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/GridViews/GridViewsExtension.php @@ -2,9 +2,12 @@ namespace Oro\Bundle\DataGridBundle\Extension\GridViews; +use Doctrine\Common\Persistence\ManagerRegistry; + use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Translation\TranslatorInterface; +use Oro\Bundle\DataGridBundle\Entity\GridView; use Oro\Bundle\DataGridBundle\Event\GridViewsLoadEvent; use Oro\Bundle\DataGridBundle\Extension\AbstractExtension; use Oro\Bundle\DataGridBundle\Datagrid\ParameterBag; @@ -12,9 +15,13 @@ use Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration; use Oro\Bundle\SecurityBundle\SecurityFacade; +use Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper; class GridViewsExtension extends AbstractExtension { + const GRID_VIEW_ROOT_PARAM = '_grid_view'; + const DISABLED_PARAM = '_disabled'; + const VIEWS_LIST_KEY = 'views_list'; const VIEWS_PARAM_KEY = 'view'; const MINIFIED_VIEWS_PARAM_KEY = 'v'; @@ -29,54 +36,83 @@ class GridViewsExtension extends AbstractExtension /** @var TranslatorInterface */ protected $translator; + /** @var ManagerRegistry */ + protected $registry; + + /** @var AclHelper */ + protected $aclHelper; + + /** @var GridView|null|bool */ + protected $defaultGridView = false; + /** * @param EventDispatcherInterface $eventDispatcher - * @param SecurityFacade $securityFacade - * @param TranslatorInterface $translator + * @param SecurityFacade $securityFacade + * @param TranslatorInterface $translator + * @param ManagerRegistry $registry + * @param AclHelper $aclHelper */ public function __construct( EventDispatcherInterface $eventDispatcher, SecurityFacade $securityFacade, - TranslatorInterface $translator + TranslatorInterface $translator, + ManagerRegistry $registry, + AclHelper $aclHelper ) { $this->eventDispatcher = $eventDispatcher; - $this->securityFacade = $securityFacade; - $this->translator = $translator; + $this->securityFacade = $securityFacade; + $this->translator = $translator; + $this->registry = $registry; + $this->aclHelper = $aclHelper; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function isApplicable(DatagridConfiguration $config) { - return true; + return !$this->isDisabled(); + } + + /** + * {@inheritdoc} + */ + public function getPriority() + { + return 10; + } + + /** + * @return bool + */ + protected function isDisabled() + { + $parameters = $this->getParameters()->get(self::GRID_VIEW_ROOT_PARAM, []); + + return !empty($parameters[self::DISABLED_PARAM]); } /** - * {@inheritDoc} + * {@inheritdoc} */ public function visitMetadata(DatagridConfiguration $config, MetadataObject $data) { - $params = $this->getParameters()->get(ParameterBag::ADDITIONAL_PARAMETERS, []); - if (isset($params[self::VIEWS_PARAM_KEY])) { - $currentView = (int)$params[self::VIEWS_PARAM_KEY]; - } else { - $currentView = self::DEFAULT_VIEW_ID; - } + $currentViewId = $this->getCurrentViewId($config->getName()); + $this->setDefaultParams($config->getName()); $data->offsetAddToArray('initialState', ['gridView' => self::DEFAULT_VIEW_ID]); - $data->offsetAddToArray('state', ['gridView' => $currentView]); + $data->offsetAddToArray('state', ['gridView' => $currentViewId]); $allLabel = null; - if (isset($config['options']) - &&isset($config['options']['gridViews']) - && isset($config['options']['gridViews']['allLabel']) - ) { + if (isset($config['options'], $config['options']['gridViews'], $config['options']['gridViews']['allLabel'])) { $allLabel = $this->translator->trans($config['options']['gridViews']['allLabel']); } /** @var AbstractViewsList $list */ - $list = $config->offsetGetOr(self::VIEWS_LIST_KEY, false); + $list = $config->offsetGetOr(self::VIEWS_LIST_KEY, false); + $systemGridView = new View(self::DEFAULT_VIEW_ID); + $systemGridView->setDefault($this->getDefaultViewId($config->getName()) === null); + $gridViews = [ 'choices' => [ [ @@ -84,15 +120,15 @@ public function visitMetadata(DatagridConfiguration $config, MetadataObject $dat 'value' => self::DEFAULT_VIEW_ID, ], ], - 'views' => [ - (new View(self::DEFAULT_VIEW_ID))->getMetadata(), + 'views' => [ + $systemGridView->getMetadata() ], ]; if ($list !== false) { - $configuredGridViews = $list->getMetadata(); - $configuredGridViews['views'] = array_merge($gridViews['views'], $configuredGridViews['views']); + $configuredGridViews = $list->getMetadata(); + $configuredGridViews['views'] = array_merge($gridViews['views'], $configuredGridViews['views']); $configuredGridViews['choices'] = array_merge($gridViews['choices'], $configuredGridViews['choices']); - $gridViews = $configuredGridViews; + $gridViews = $configuredGridViews; } if ($this->eventDispatcher->hasListeners(GridViewsLoadEvent::EVENT_NAME)) { @@ -101,11 +137,89 @@ public function visitMetadata(DatagridConfiguration $config, MetadataObject $dat $gridViews = $event->getGridViews(); } - $gridViews['gridName'] = $config->getName(); + $gridViews['gridName'] = $config->getName(); $gridViews['permissions'] = $this->getPermissions(); $data->offsetAddToArray('gridViews', $gridViews); } + /** + * Gets id for current grid view + * + * @param string $gridName + * + * @return int|string + */ + protected function getCurrentViewId($gridName) + { + $params = $this->getParameters()->get(ParameterBag::ADDITIONAL_PARAMETERS, []); + if (isset($params[self::VIEWS_PARAM_KEY])) { + return (int)$params[self::VIEWS_PARAM_KEY]; + } else { + $defaultViewId = $this->getDefaultViewId($gridName); + + return $defaultViewId ? $defaultViewId : self::DEFAULT_VIEW_ID; + } + } + + /** + * Gets id for defined as default grid view for current logged user. + * + * @param string $gridName + * + * @return int|null + */ + protected function getDefaultViewId($gridName) + { + $defaultGridView = $this->getDefaultView($gridName); + + return $defaultGridView ? $defaultGridView->getId() : null; + } + + /** + * Gets defined as default grid view for current logged user. + * + * @param string $gridName + * + * @return GridView|null + */ + protected function getDefaultView($gridName) + { + if ($this->defaultGridView === false) { + $repository = $this->registry->getRepository('OroDataGridBundle:GridView'); + $defaultGridView = $repository->findDefaultGridView( + $this->aclHelper, + $this->securityFacade->getLoggedUser(), + $gridName + ); + + $this->defaultGridView = $defaultGridView; + } + + return $this->defaultGridView; + } + + /** + * Sets default parameters. + * Added filters and sorters for defined as default grid view for current logged user. + * + * @param string $gridName + */ + protected function setDefaultParams($gridName) + { + $params = $this->getParameters()->get(ParameterBag::ADDITIONAL_PARAMETERS, []); + if (!isset($params[self::VIEWS_PARAM_KEY])) { + $currentViewId = $this->getCurrentViewId($gridName); + $params[self::VIEWS_PARAM_KEY] = $currentViewId; + + $defaultGridView = $this->getDefaultView($gridName); + if ($defaultGridView) { + $this->getParameters()->mergeKey('_filter', $defaultGridView->getFiltersData()); + $this->getParameters()->mergeKey('_sort_by', $defaultGridView->getSortersData()); + } + } + $this->getParameters()->set(ParameterBag::ADDITIONAL_PARAMETERS, $params); + } + /** * @return array */ diff --git a/src/Oro/Bundle/DataGridBundle/Extension/GridViews/View.php b/src/Oro/Bundle/DataGridBundle/Extension/GridViews/View.php index ccd78efe42f..434d4bcca31 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/GridViews/View.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/GridViews/View.php @@ -25,6 +25,9 @@ class View /** @var bool */ protected $deletable = false; + /** @var bool */ + protected $default = false; + /** * @var array * @@ -179,6 +182,10 @@ public function setDeletable($deletable = true) */ public function getColumnsData() { + if ($this->columnsData === null) { + $this->columnsData = []; + } + return $this->columnsData; } @@ -190,6 +197,26 @@ public function setColumnsData(array $columnsData = []) $this->columnsData = $columnsData; } + /** + * @return boolean + */ + public function isDefault() + { + return $this->default; + } + + /** + * @param boolean $default + * + * @return $this + */ + public function setDefault($default) + { + $this->default = $default; + + return $this; + } + /** * Convert to view data * @@ -198,14 +225,15 @@ public function setColumnsData(array $columnsData = []) public function getMetadata() { return [ - 'name' => $this->getName(), - 'label' => $this->label, - 'type' => $this->getType(), - 'filters' => $this->getFiltersData(), - 'sorters' => $this->getSortersData(), - 'columns' => $this->columnsData, - 'editable' => $this->editable, - 'deletable' => $this->deletable, + 'name' => $this->getName(), + 'label' => $this->label, + 'type' => $this->getType(), + 'filters' => $this->getFiltersData(), + 'sorters' => $this->getSortersData(), + 'columns' => $this->columnsData, + 'editable' => $this->editable, + 'deletable' => $this->deletable, + 'is_default' => $this->default ]; } } diff --git a/src/Oro/Bundle/DataGridBundle/Extension/MassAction/Actions/Ajax/MassDelete/MassDeleteLimitResult.php b/src/Oro/Bundle/DataGridBundle/Extension/MassAction/Actions/Ajax/MassDelete/MassDeleteLimitResult.php new file mode 100644 index 00000000000..ac9c94ed759 --- /dev/null +++ b/src/Oro/Bundle/DataGridBundle/Extension/MassAction/Actions/Ajax/MassDelete/MassDeleteLimitResult.php @@ -0,0 +1,59 @@ +selected = (int)$selected; + $this->deletable = (int)$deletable; + $this->maxLimit = (int)$maxLimit; + } + + /** + * Returns max amount of records which can be remove at once. + * + * @return int + */ + public function getMaxLimit() + { + return $this->maxLimit; + } + + /** + * Returns amount of selected records. + * + * @return int + */ + public function getSelected() + { + return $this->selected; + } + + /** + * Returns amount of records which current user able to remove. + * + * @return int + */ + public function getDeletable() + { + return $this->deletable; + } +} diff --git a/src/Oro/Bundle/DataGridBundle/Extension/MassAction/Actions/Ajax/MassDelete/MassDeleteLimiter.php b/src/Oro/Bundle/DataGridBundle/Extension/MassAction/Actions/Ajax/MassDelete/MassDeleteLimiter.php new file mode 100644 index 00000000000..b555d6912c3 --- /dev/null +++ b/src/Oro/Bundle/DataGridBundle/Extension/MassAction/Actions/Ajax/MassDelete/MassDeleteLimiter.php @@ -0,0 +1,108 @@ +aclHelper = $helper; + } + + /** + * Returns limitation code from MassDeleteLimitResult parameters. + * + * @param MassDeleteLimitResult $result + * + * @return int + */ + public function getLimitationCode(MassDeleteLimitResult $result) + { + $selected = $result->getSelected(); + $deletable = $result->getDeletable(); + $maxLimit = $result->getMaxLimit(); + + if ($deletable <= $maxLimit) { + return $selected === $deletable + ? self::NO_LIMIT + : self::LIMIT_ACCESS; + } else { + return $selected === $deletable + ? self::LIMIT_MAX_RECORDS + : self::LIMIT_ACCESS_MAX_RECORDS; + } + } + + /** + * Limits query for deletion with access and/or performance restrictions. + * + * @param MassDeleteLimitResult $result + * @param MassActionHandlerArgs $args + */ + public function limitQuery(MassDeleteLimitResult $result, MassActionHandlerArgs $args) + { + $code = $this->getLimitationCode($result); + $queryBuilder = $args->getResults()->getSource(); + if (in_array($code, [self::LIMIT_ACCESS, self::LIMIT_ACCESS_MAX_RECORDS])) { + $this->aclHelper->apply($queryBuilder, 'DELETE'); + } + if (in_array($code, [self::LIMIT_MAX_RECORDS, self::LIMIT_ACCESS_MAX_RECORDS])) { + $queryBuilder->setMaxResults($result->getMaxLimit()); + } + } + + /** + * @param MassActionHandlerArgs $args + * + * @return MassDeleteLimitResult + */ + public function getLimitResult(MassActionHandlerArgs $args) + { + $query = $args->getResults()->getSource(); + $resultsForSelected = new DeletionIterableResult($query); + $deletableQuery = $this->cloneQuery($query); + + $accessLimitedQuery = $this->aclHelper->apply($deletableQuery, 'DELETE'); + $resultsForDelete = new DeletionIterableResult($accessLimitedQuery); + + return new MassDeleteLimitResult($resultsForSelected->count(), $resultsForDelete->count()); + } + + /** + * Clones $query. Also clones parameters for Doctrine\ORM\Query case. + * + * @param Query|QueryBuilder $query + * + * @return Query|QueryBuilder + */ + protected function cloneQuery($query) + { + $cloned = clone $query; + if ($query instanceof Query) { + $cloned->setParameters(clone $query->getParameters()); + } + + return $cloned; + } +} diff --git a/src/Oro/Bundle/DataGridBundle/Extension/MassAction/DeleteMassActionExtension.php b/src/Oro/Bundle/DataGridBundle/Extension/MassAction/DeleteMassActionExtension.php new file mode 100644 index 00000000000..49370442719 --- /dev/null +++ b/src/Oro/Bundle/DataGridBundle/Extension/MassAction/DeleteMassActionExtension.php @@ -0,0 +1,158 @@ +doctrineHelper = $doctrineHelper; + $this->gridConfigurationHelper = $gridConfigurationHelper; + } + + /** + * {@inheritdoc} + */ + public function isApplicable(DatagridConfiguration $config) + { + return + // Checks if mass delete action does not exists + !$this->isDeleteActionExists($config, static::MASS_ACTION_KEY) && + // Checks if delete action exists + $this->isDeleteActionExists($config, static::ACTION_KEY) && + $this->isApplicableForEntity($config); + } + + /** + * Checks if we can apply mass delete action for the entity: + * - extract entity class + * - extract entity single identifier + * - extract alias for this entity + * + * @param DatagridConfiguration $config + * + * @return bool + */ + protected function isApplicableForEntity(DatagridConfiguration $config) + { + $entity = $this->getEntity($config); + + return + $entity && + $this->doctrineHelper->getSingleEntityIdentifierFieldName($entity, false) && + $this->gridConfigurationHelper->getEntityRootAlias($config); + } + + /** + * @param DatagridConfiguration $config + * @param string $key + * + * @return bool + */ + protected function isDeleteActionExists(DatagridConfiguration $config, $key) + { + $actions = $config->offsetGetOr($key, []); + foreach ($actions as $action) { + if ($action[static::ACTION_TYPE_KEY] == static::ACTION_TYPE_DELETE) { + return true; + } + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function processConfigs(DatagridConfiguration $config) + { + $actions = $config->offsetGetOr(static::MASS_ACTION_KEY, []); + + $actions['delete'] = $this->getMassDeleteActionConfig($config); + + $config->offsetSet(static::MASS_ACTION_KEY, $actions); + } + + /** + * @param DatagridConfiguration $config + * + * @return array + */ + protected function getMassDeleteActionConfig(DatagridConfiguration $config) + { + $entity = $this->getEntity($config); + + return [ + 'type' => 'delete', + 'icon' => 'trash', + 'label' => 'oro.grid.action.delete', + 'entity_name' => $entity, + 'data_identifier' => $this->getDataIdentifier($config), + 'acl_resource' => sprintf('DELETE;entity:%s', $entity), + ]; + } + + /** + * @param DatagridConfiguration $config + * + * @return string|null + */ + protected function getEntity(DatagridConfiguration $config) + { + if ($this->entityClassName === null) { + $this->entityClassName = $this->gridConfigurationHelper->getEntity($config); + } + + return $this->entityClassName; + } + + /** + * @param DatagridConfiguration $config + * + * @return string + */ + protected function getDataIdentifier(DatagridConfiguration $config) + { + $entity = $this->getEntity($config); + $identifier = $this->doctrineHelper->getSingleEntityIdentifierFieldName($entity); + $rootAlias = $this->gridConfigurationHelper->getEntityRootAlias($config); + + return sprintf('%s.%s', $rootAlias, $identifier); + } + + + /** + * {@inheritdoc} + */ + public function getPriority() + { + // should be applied before mass action extension + return 210; + } +} diff --git a/src/Oro/Bundle/DataGridBundle/Extension/MassAction/DeleteMassActionHandler.php b/src/Oro/Bundle/DataGridBundle/Extension/MassAction/DeleteMassActionHandler.php index 68a46faf313..ec8d2feb9aa 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/MassAction/DeleteMassActionHandler.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/MassAction/DeleteMassActionHandler.php @@ -4,30 +4,36 @@ use Doctrine\ORM\EntityManager; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Translation\TranslatorInterface; +use Oro\Bundle\DataGridBundle\Extension\MassAction\Actions\Ajax\MassDelete\MassDeleteLimiter; +use Oro\Bundle\DataGridBundle\Extension\MassAction\Actions\Ajax\MassDelete\MassDeleteLimitResult; use Oro\Bundle\DataGridBundle\Exception\LogicException; use Oro\Bundle\DataGridBundle\Datasource\ResultRecordInterface; use Oro\Bundle\DataGridBundle\Datasource\Orm\DeletionIterableResult; + use Oro\Bundle\SecurityBundle\SecurityFacade; class DeleteMassActionHandler implements MassActionHandlerInterface { const FLUSH_BATCH_SIZE = 100; - /** - * @var EntityManager - */ + /** @var EntityManager */ protected $entityManager; - /** - * @var TranslatorInterface - */ + /** @var TranslatorInterface */ protected $translator; /** @var SecurityFacade */ protected $securityFacade; + /** @var MassDeleteLimiter */ + protected $limiter; + + /** @var RequestStack */ + protected $requestStack; + /** * @var string */ @@ -37,15 +43,21 @@ class DeleteMassActionHandler implements MassActionHandlerInterface * @param EntityManager $entityManager * @param TranslatorInterface $translator * @param SecurityFacade $securityFacade + * @param MassDeleteLimiter $limiter + * @param RequestStack $requestStack */ public function __construct( EntityManager $entityManager, TranslatorInterface $translator, - SecurityFacade $securityFacade + SecurityFacade $securityFacade, + MassDeleteLimiter $limiter, + RequestStack $requestStack ) { $this->entityManager = $entityManager; $this->translator = $translator; $this->securityFacade = $securityFacade; + $this->limiter = $limiter; + $this->requestStack = $requestStack; } /** @@ -53,56 +65,18 @@ public function __construct( */ public function handle(MassActionHandlerArgs $args) { - $iteration = 0; - $entityName = null; - $entityIdentifiedField = null; - - $results = new DeletionIterableResult($args->getResults()->getSource()); - $results->setBufferSize(self::FLUSH_BATCH_SIZE); - - // batch remove should be processed in transaction - $this->entityManager->beginTransaction(); - try { - // if huge amount data must be deleted - set_time_limit(0); - - foreach ($results as $result) { - /** @var $result ResultRecordInterface */ - $entity = $result->getRootEntity(); - if (!$entity) { - // no entity in result record, it should be extracted from DB - if (!$entityName) { - $entityName = $this->getEntityName($args); - } - if (!$entityIdentifiedField) { - $entityIdentifiedField = $this->getEntityIdentifierField($args); - } - $entity = $this->getEntity($entityName, $result->getValue($entityIdentifiedField)); - } - - if ($entity) { - if ($this->securityFacade->isGranted('DELETE', $entity)) { - $this->entityManager->remove($entity); - $iteration++; - } - - if ($iteration % self::FLUSH_BATCH_SIZE == 0) { - $this->finishBatch(); - } - } - } - - if ($iteration % self::FLUSH_BATCH_SIZE > 0) { - $this->finishBatch(); - } - - $this->entityManager->commit(); - } catch (\Exception $e) { - $this->entityManager->rollback(); - throw $e; + $limitResult = $this->limiter->getLimitResult($args); + $method = $this->requestStack->getMasterRequest()->getMethod(); + if ($method === 'POST') { + $result = $this->getPostResponse($limitResult); + } elseif ($method === 'DELETE') { + $this->limiter->limitQuery($limitResult, $args); + $result = $this->doDelete($args); + } else { + $result = $this->getNotSupportedResponse($method); } - return $this->getResponse($args, $iteration); + return $result; } /** @@ -122,7 +96,7 @@ protected function finishBatch() * * @return MassActionResponse */ - protected function getResponse(MassActionHandlerArgs $args, $entitiesCount = 0) + protected function getDeleteResponse(MassActionHandlerArgs $args, $entitiesCount = 0) { $massAction = $args->getMassAction(); $responseMessage = $massAction->getOptions()->offsetGetByPath('[messages][success]', $this->responseMessage); @@ -141,6 +115,37 @@ protected function getResponse(MassActionHandlerArgs $args, $entitiesCount = 0) ); } + /** + * @param MassDeleteLimitResult $limitResult + * + * @return MassActionResponse + */ + protected function getPostResponse(MassDeleteLimitResult $limitResult) + { + return new MassActionResponse( + true, + 'OK', + [ + 'selected' => $limitResult->getSelected(), + 'deletable' => $limitResult->getDeletable(), + 'max_limit' => $limitResult->getMaxLimit() + ] + ); + } + + /** + * @param $method + * + * @return MassActionResponse + */ + protected function getNotSupportedResponse($method) + { + return new MassActionResponse( + false, + sprintf('Method "%s" is not supported', $method) + ); + } + /** * @param MassActionHandlerArgs $args * @@ -192,4 +197,63 @@ protected function getEntity($entityName, $identifierValue) { return $this->entityManager->getReference($entityName, $identifierValue); } + + /** + * @param MassActionHandlerArgs $args + * + * @return MassActionResponse + * @throws \Exception + */ + protected function doDelete(MassActionHandlerArgs $args) + { + $iteration = 0; + $entityName = null; + $entityIdentifiedField = null; + $queryBuilder = $args->getResults()->getSource(); + + $results = new DeletionIterableResult($queryBuilder); + $results->setBufferSize(self::FLUSH_BATCH_SIZE); + + // batch remove should be processed in transaction + $this->entityManager->beginTransaction(); + try { + // if huge amount data must be deleted + set_time_limit(0); + + foreach ($results as $result) { + /** @var $result ResultRecordInterface */ + $entity = $result->getRootEntity(); + if (!$entity) { + // no entity in result record, it should be extracted from DB + if (!$entityName) { + $entityName = $this->getEntityName($args); + } + if (!$entityIdentifiedField) { + $entityIdentifiedField = $this->getEntityIdentifierField($args); + } + $entity = $this->getEntity($entityName, $result->getValue($entityIdentifiedField)); + } + + if ($entity) { + $this->entityManager->remove($entity); + $iteration++; + + if ($iteration % self::FLUSH_BATCH_SIZE == 0) { + $this->finishBatch(); + } + } + } + + if ($iteration % self::FLUSH_BATCH_SIZE > 0) { + $this->finishBatch(); + } + + $this->entityManager->commit(); + } catch (\Exception $e) { + $this->entityManager->rollback(); + throw $e; + } + + return $this->getDeleteResponse($args, $iteration); + } } diff --git a/src/Oro/Bundle/DataGridBundle/Extension/MassAction/MassActionDispatcher.php b/src/Oro/Bundle/DataGridBundle/Extension/MassAction/MassActionDispatcher.php index b07c2254204..922deb9950f 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/MassAction/MassActionDispatcher.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/MassAction/MassActionDispatcher.php @@ -9,7 +9,6 @@ use Symfony\Component\HttpFoundation\File\Exception\UnexpectedTypeException; use Symfony\Component\HttpFoundation\Request; -use Oro\Bundle\DataGridBundle\Datagrid\Builder; use Oro\Bundle\DataGridBundle\Datagrid\Manager; use Oro\Bundle\DataGridBundle\Datagrid\DatagridInterface; use Oro\Bundle\DataGridBundle\Datasource\Orm\IterableResult; @@ -110,7 +109,7 @@ public function dispatch($datagridName, $actionName, array $parameters, array $d //prepare query builder $qb->setMaxResults(null); - if (!$datagrid->getConfig()->offsetGetByPath(Builder::DATASOURCE_SKIP_ACL_CHECK, false)) { + if (!$datagrid->getConfig()->isDatasourceSkipAclApply()) { $qb = $this->aclHelper->apply($qb); } diff --git a/src/Oro/Bundle/DataGridBundle/Extension/MassAction/MassActionExtension.php b/src/Oro/Bundle/DataGridBundle/Extension/MassAction/MassActionExtension.php index 95c6f78aa90..70d64c5fb4b 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/MassAction/MassActionExtension.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/MassAction/MassActionExtension.php @@ -3,6 +3,7 @@ namespace Oro\Bundle\DataGridBundle\Extension\MassAction; use Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration; +use Oro\Bundle\DataGridBundle\Datagrid\Common\ResultsObject; use Oro\Bundle\DataGridBundle\Datagrid\DatagridInterface; use Oro\Bundle\DataGridBundle\Extension\Action\ActionExtension; use Oro\Bundle\DataGridBundle\Extension\Action\Actions\ActionInterface; @@ -16,17 +17,29 @@ class MassActionExtension extends ActionExtension protected $actions = []; /** - * {@inheritDoc} + * {@inheritdoc} */ public function isApplicable(DatagridConfiguration $config) { - $massActions = $config->offsetGetOr(static::ACTION_KEY, []); + // Applicable due to the possibility of dynamically add mass action + return true; + } - return !empty($massActions); + /** + * {@inheritDoc} + */ + public function visitResult(DatagridConfiguration $config, ResultsObject $result) + { + $result->offsetAddToArray( + 'metadata', + [ + static::METADATA_ACTION_KEY => $this->getActionsMetadata($config) + ] + ); } /** - * Get grid massaction by name + * Get grid mass action by name * * @param string $name * @param DatagridInterface $datagrid diff --git a/src/Oro/Bundle/DataGridBundle/Extension/Pager/OrmPagerExtension.php b/src/Oro/Bundle/DataGridBundle/Extension/Pager/OrmPagerExtension.php index b3cb16fc04b..6e551f0426d 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/Pager/OrmPagerExtension.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/Pager/OrmPagerExtension.php @@ -2,7 +2,6 @@ namespace Oro\Bundle\DataGridBundle\Extension\Pager; -use Oro\Bundle\DataGridBundle\Datagrid\Builder; use Oro\Bundle\DataGridBundle\Datagrid\Common\MetadataObject; use Oro\Bundle\DataGridBundle\Datagrid\Common\ResultsObject; use Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration; @@ -50,7 +49,7 @@ public function isApplicable(DatagridConfiguration $config) $disabled = $this->getOr(PagerInterface::DISABLED_PARAM, false) || $config->offsetGetByPath(ToolbarExtension::TOOLBAR_PAGINATION_HIDE_OPTION_PATH, false); - return !$disabled && $config->offsetGetByPath(Builder::DATASOURCE_TYPE_PATH) == OrmDatasource::TYPE; + return !$disabled && $config->getDatasourceType() == OrmDatasource::TYPE; } /** @@ -62,11 +61,9 @@ public function visitDatasource(DatagridConfiguration $config, DatasourceInterfa if ($datasource instanceof OrmDatasource) { $this->pager->setQueryBuilder($datasource->getQueryBuilder()); - $this->pager->setSkipAclCheck( - $config->offsetGetByPath(Builder::DATASOURCE_SKIP_ACL_CHECK, false) - ); + $this->pager->setSkipAclCheck($config->isDatasourceSkipAclApply()); $this->pager->setSkipCountWalker( - $config->offsetGetByPath(Builder::DATASOURCE_SKIP_COUNT_WALKER_PATH) + $config->offsetGetByPath(DatagridConfiguration::DATASOURCE_SKIP_COUNT_WALKER_PATH) ); } diff --git a/src/Oro/Bundle/DataGridBundle/Extension/Sorter/OrmSorterExtension.php b/src/Oro/Bundle/DataGridBundle/Extension/Sorter/OrmSorterExtension.php index 4a08cd7ec9e..3580d888c56 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/Sorter/OrmSorterExtension.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/Sorter/OrmSorterExtension.php @@ -2,7 +2,6 @@ namespace Oro\Bundle\DataGridBundle\Extension\Sorter; -use Oro\Bundle\DataGridBundle\Datagrid\Builder; use Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration; use Oro\Bundle\DataGridBundle\Datagrid\Common\MetadataObject; use Oro\Bundle\DataGridBundle\Datagrid\ParameterBag; @@ -36,7 +35,7 @@ class OrmSorterExtension extends AbstractExtension public function isApplicable(DatagridConfiguration $config) { $columns = $config->offsetGetByPath(Configuration::COLUMNS_PATH); - $isApplicable = $config->offsetGetByPath(Builder::DATASOURCE_TYPE_PATH) === OrmDatasource::TYPE + $isApplicable = $config->getDatasourceType() === OrmDatasource::TYPE && is_array($columns); return $isApplicable; diff --git a/src/Oro/Bundle/DataGridBundle/Extension/Totals/OrmTotalsExtension.php b/src/Oro/Bundle/DataGridBundle/Extension/Totals/OrmTotalsExtension.php index d139d5fb719..c7efbb3769c 100644 --- a/src/Oro/Bundle/DataGridBundle/Extension/Totals/OrmTotalsExtension.php +++ b/src/Oro/Bundle/DataGridBundle/Extension/Totals/OrmTotalsExtension.php @@ -8,7 +8,8 @@ use Symfony\Component\Translation\TranslatorInterface; -use Oro\Bundle\DataGridBundle\Datagrid\Builder; +use Oro\Component\PhpUtils\ArrayUtil; + use Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration; use Oro\Bundle\DataGridBundle\Datagrid\Common\MetadataObject; use Oro\Bundle\DataGridBundle\Datagrid\Common\ResultsObject; @@ -22,7 +23,6 @@ use Oro\Bundle\LocaleBundle\Formatter\NumberFormatter; use Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper; -use Oro\Bundle\UIBundle\Tools\ArrayUtils; /** * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) @@ -70,7 +70,7 @@ public function __construct( */ public function isApplicable(DatagridConfiguration $config) { - return $config->offsetGetByPath(Builder::DATASOURCE_TYPE_PATH) === OrmDatasource::TYPE; + return $config->getDatasourceType() === OrmDatasource::TYPE; } /** @@ -123,7 +123,7 @@ public function visitResult(DatagridConfiguration $config, ResultsObject $result $result, $rowConfig['columns'], $rowConfig[Configuration::TOTALS_PER_PAGE_ROW_KEY], - $config->offsetGetByPath(Builder::DATASOURCE_SKIP_ACL_CHECK, false) + $config->isDatasourceSkipAclApply() ) ); } @@ -347,7 +347,7 @@ protected function addPageLimits(QueryBuilder $dataQueryBuilder, $pageData, $per $data = $pageData['data']; } foreach ($rootIdentifiers as $identifier) { - $ids = ArrayUtils::arrayColumn($data, $identifier['alias']); + $ids = ArrayUtil::arrayColumn($data, $identifier['alias']); $field = isset($identifier['entityAlias']) ? $identifier['entityAlias'] . '.' . $identifier['fieldAlias'] diff --git a/src/Oro/Bundle/DataGridBundle/Form/Handler/GridViewApiHandler.php b/src/Oro/Bundle/DataGridBundle/Form/Handler/GridViewApiHandler.php index e81be763ed5..09e7530f6d9 100644 --- a/src/Oro/Bundle/DataGridBundle/Form/Handler/GridViewApiHandler.php +++ b/src/Oro/Bundle/DataGridBundle/Form/Handler/GridViewApiHandler.php @@ -6,8 +6,10 @@ use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Oro\Bundle\DataGridBundle\Entity\GridView; +use Oro\Bundle\DataGridBundle\Entity\Manager\GridViewManager; class GridViewApiHandler { @@ -20,16 +22,31 @@ class GridViewApiHandler /** @var Registry */ protected $registry; + /** @var GridViewManager */ + protected $gridViewManager; + + /** @var TokenStorageInterface */ + protected $tokenStorage; + /** - * @param FormInterface $form - * @param Request $request - * @param Registry $registry + * @param FormInterface $form + * @param Request $request + * @param Registry $registry + * @param GridViewManager $gridViewManager + * @param TokenStorageInterface $tokenStorage */ - public function __construct(FormInterface $form, Request $request, Registry $registry) - { - $this->form = $form; - $this->request = $request; - $this->registry = $registry; + public function __construct( + FormInterface $form, + Request $request, + Registry $registry, + GridViewManager $gridViewManager, + TokenStorageInterface $tokenStorage + ) { + $this->form = $form; + $this->request = $request; + $this->registry = $registry; + $this->gridViewManager = $gridViewManager; + $this->tokenStorage = $tokenStorage; } /** @@ -67,12 +84,25 @@ public function process(GridView $entity) */ protected function onSuccess(GridView $entity) { + $default = $this->form->get('is_default')->getData(); + $this->setDefaultGridView($entity, $default); + $this->fixFilters($entity); $om = $this->registry->getManagerForClass('OroDataGridBundle:GridView'); $om->persist($entity); $om->flush(); } + /** + * @param GridView $gridView + * @param bool $default + */ + protected function setDefaultGridView(GridView $gridView, $default) + { + $user = $this->tokenStorage->getToken()->getUser(); + $this->gridViewManager->setDefaultGridView($user, $gridView, $default); + } + /** * @todo Remove once https://github.com/symfony/symfony/issues/5906 is fixed * diff --git a/src/Oro/Bundle/DataGridBundle/Form/Type/GridViewType.php b/src/Oro/Bundle/DataGridBundle/Form/Type/GridViewType.php index e2fc70a69fe..64954391bd4 100644 --- a/src/Oro/Bundle/DataGridBundle/Form/Type/GridViewType.php +++ b/src/Oro/Bundle/DataGridBundle/Form/Type/GridViewType.php @@ -24,6 +24,10 @@ public function buildForm(FormBuilderInterface $builder, array $options) ->add('label', 'text', [ 'property_path' => 'name', ]) + ->add('is_default', 'checkbox', [ + 'required' => false, + 'mapped' => false, + ]) ->add('type', 'choice', [ 'choices' => GridView::getTypes(), ]) diff --git a/src/Oro/Bundle/DataGridBundle/Migrations/Schema/OroDataGridBundleInstaller.php b/src/Oro/Bundle/DataGridBundle/Migrations/Schema/OroDataGridBundleInstaller.php index 1c82bb136df..26685157220 100644 --- a/src/Oro/Bundle/DataGridBundle/Migrations/Schema/OroDataGridBundleInstaller.php +++ b/src/Oro/Bundle/DataGridBundle/Migrations/Schema/OroDataGridBundleInstaller.php @@ -6,6 +6,7 @@ use Oro\Bundle\MigrationBundle\Migration\Installation; use Oro\Bundle\MigrationBundle\Migration\QueryBag; +use Oro\Bundle\DataGridBundle\Migrations\Schema\v1_2\DefaultGridViewUsersRelation; class OroDataGridBundleInstaller implements Installation { @@ -14,7 +15,7 @@ class OroDataGridBundleInstaller implements Installation */ public function getMigrationVersion() { - return 'v1_1'; + return 'v1_2'; } /** @@ -23,6 +24,7 @@ public function getMigrationVersion() public function up(Schema $schema, QueryBag $queries) { $this->createOroGridViewTable($schema); + DefaultGridViewUsersRelation::createOroDefaultGridViewUsersTable($schema); } /** @@ -38,7 +40,7 @@ protected function createOroGridViewTable(Schema $schema) $table->addColumn('type', 'string', ['length' => 255]); $table->addColumn('filtersData', 'array', ['comment' => '(DC2Type:array)']); $table->addColumn('sortersData', 'array', ['comment' => '(DC2Type:array)']); - $table->addColumn('columnsData', 'array', ['comment' => '(DC2Type:array)']); + $table->addColumn('columnsData', 'array', ['comment' => '(DC2Type:array)', 'notnull' => false]); $table->addColumn('gridName', 'string', ['length' => 255]); $table->setPrimaryKey(['id']); $table->addIndex(['user_owner_id'], 'IDX_5B73FBCB9EB185F9', []); diff --git a/src/Oro/Bundle/DataGridBundle/Migrations/Schema/v1_1/OroDataGridBundle.php b/src/Oro/Bundle/DataGridBundle/Migrations/Schema/v1_1/OroDataGridBundle.php index 39ba41f404c..126cdcab418 100644 --- a/src/Oro/Bundle/DataGridBundle/Migrations/Schema/v1_1/OroDataGridBundle.php +++ b/src/Oro/Bundle/DataGridBundle/Migrations/Schema/v1_1/OroDataGridBundle.php @@ -6,6 +6,7 @@ use Oro\Bundle\MigrationBundle\Migration\Migration; use Oro\Bundle\MigrationBundle\Migration\QueryBag; +use Oro\Bundle\MigrationBundle\Migration\ParametrizedSqlMigrationQuery; class OroDataGridBundle implements Migration { @@ -16,6 +17,13 @@ public function up(Schema $schema, QueryBag $queries) { $table = $schema->getTable('oro_grid_view'); - $table->addColumn('columnsData', 'array', ['comment' => '(DC2Type:array)']); + $table->addColumn('columnsData', 'array', ['comment' => '(DC2Type:array)', 'notnull' => false]); + + $queries->addPostQuery( + new ParametrizedSqlMigrationQuery( + 'UPDATE oro_grid_view SET columnsData = :columnsData WHERE columnsData is NULL; ', + ['columnsData' => serialize([])] + ) + ); } } diff --git a/src/Oro/Bundle/DataGridBundle/Migrations/Schema/v1_2/DefaultGridViewUsersRelation.php b/src/Oro/Bundle/DataGridBundle/Migrations/Schema/v1_2/DefaultGridViewUsersRelation.php new file mode 100644 index 00000000000..241af2abd43 --- /dev/null +++ b/src/Oro/Bundle/DataGridBundle/Migrations/Schema/v1_2/DefaultGridViewUsersRelation.php @@ -0,0 +1,44 @@ +createTable('oro_grid_view_user'); + $table->addColumn('grid_view_id', 'integer', []); + $table->addColumn('user_id', 'integer', []); + $table->setPrimaryKey(['grid_view_id', 'user_id']); + $table->addIndex(['grid_view_id'], 'IDX_80CFBA3FBF53711B', []); + $table->addIndex(['user_id'], 'IDX_80CFBA3FA76ED395', []); + $table->addForeignKeyConstraint( + $schema->getTable('oro_grid_view'), + ['grid_view_id'], + ['id'], + ['onDelete' => 'CASCADE', 'onUpdate' => null] + ); + $table->addForeignKeyConstraint( + $schema->getTable('oro_user'), + ['user_id'], + ['id'], + ['onDelete' => 'CASCADE', 'onUpdate' => null] + ); + } +} diff --git a/src/Oro/Bundle/DataGridBundle/Provider/SystemAwareResolver.php b/src/Oro/Bundle/DataGridBundle/Provider/SystemAwareResolver.php index 6076d7c87a4..fea26ce3003 100644 --- a/src/Oro/Bundle/DataGridBundle/Provider/SystemAwareResolver.php +++ b/src/Oro/Bundle/DataGridBundle/Provider/SystemAwareResolver.php @@ -5,7 +5,7 @@ use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerInterface; -use Oro\Bundle\UIBundle\Tools\ArrayUtils; +use Oro\Component\PhpUtils\ArrayUtil; class SystemAwareResolver implements ContainerAwareInterface { @@ -51,7 +51,7 @@ public function resolve($datagridName, $datagridDefinition, $recursion = false) ->getConfigurationForGrid($val); // merge them and remove extend directive - $datagridDefinition = ArrayUtils::arrayMergeRecursiveDistinct( + $datagridDefinition = ArrayUtil::arrayMergeRecursiveDistinct( $definition->toArray(), $datagridDefinition ); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/config/assets.yml b/src/Oro/Bundle/DataGridBundle/Resources/config/assets.yml index 30050d0e412..6f4b05d1e39 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/config/assets.yml +++ b/src/Oro/Bundle/DataGridBundle/Resources/config/assets.yml @@ -3,3 +3,4 @@ css: - 'bundles/orodatagrid/lib/backgrid/backgrid.css' - 'bundles/orodatagrid/lib/backgrid/extensions/paginator/backgrid-paginator.css' - 'bundles/orodatagrid/css/less/main.less' + - 'bundles/orodatagrid/css/less/inline-editing-help.less' diff --git a/src/Oro/Bundle/DataGridBundle/Resources/config/entity_config.yml b/src/Oro/Bundle/DataGridBundle/Resources/config/entity_config.yml new file mode 100644 index 00000000000..cc64e6d51ac --- /dev/null +++ b/src/Oro/Bundle/DataGridBundle/Resources/config/entity_config.yml @@ -0,0 +1,8 @@ +oro_entity_config: + grid: + entity: + items: + # default grid name for the entity + default: # string + options: + auditable: false diff --git a/src/Oro/Bundle/DataGridBundle/Resources/config/extensions.yml b/src/Oro/Bundle/DataGridBundle/Resources/config/extensions.yml index 1647a0e069d..e3e81770057 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/config/extensions.yml +++ b/src/Oro/Bundle/DataGridBundle/Resources/config/extensions.yml @@ -59,6 +59,14 @@ services: tags: - { name: oro_datagrid.extension } + oro_datagrid.extension.mass_delete_action: + class: Oro\Bundle\DataGridBundle\Extension\MassAction\DeleteMassActionExtension + arguments: + - @oro_entity.doctrine_helper + - @oro_datagrid.grid_configuration.helper + tags: + - { name: oro_datagrid.extension } + oro_datagrid.extension.mass_action: class: %oro_datagrid.extension.mass_action.class% arguments: @@ -81,6 +89,8 @@ services: - @event_dispatcher - @oro_security.security_facade - @translator + - @doctrine + - @oro_security.acl_helper tags: - { name: oro_datagrid.extension } diff --git a/src/Oro/Bundle/DataGridBundle/Resources/config/mass_actions.yml b/src/Oro/Bundle/DataGridBundle/Resources/config/mass_actions.yml index debe802e43c..983f087034e 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/config/mass_actions.yml +++ b/src/Oro/Bundle/DataGridBundle/Resources/config/mass_actions.yml @@ -5,6 +5,7 @@ parameters: oro_datagrid.extension.mass_action.type.ajax.class: Oro\Bundle\DataGridBundle\Extension\MassAction\Actions\Ajax\AjaxMassAction oro_datagrid.extension.mass_action.type.delete.class: Oro\Bundle\DataGridBundle\Extension\MassAction\Actions\Ajax\DeleteMassAction + oro_datagrid.extension.mass_action.type.redirect.class: Oro\Bundle\DataGridBundle\Extension\MassAction\Actions\Redirect\RedirectMassAction oro_datagrid.extension.mass_action.type.widget.class: Oro\Bundle\DataGridBundle\Extension\MassAction\Actions\Widget\WidgetMassAction oro_datagrid.extension.mass_action.type.window.class: Oro\Bundle\DataGridBundle\Extension\MassAction\Actions\Widget\WindowMassAction @@ -16,6 +17,8 @@ services: - @doctrine.orm.entity_manager - @translator - @oro_security.security_facade + - @oro_datagrid.extension.mass_action.actions.ajax.mass_delete_limiter + - @request_stack oro_datagrid.mass_action.parameters_parser: class: %oro_datagrid.mass_action.parameters_parser.class% @@ -62,3 +65,8 @@ services: scope: prototype tags: - { name: oro_datagrid.extension.mass_action.type, type: frontend } + + oro_datagrid.extension.mass_action.actions.ajax.mass_delete_limiter: + class: Oro\Bundle\DataGridBundle\Extension\MassAction\Actions\Ajax\MassDelete\MassDeleteLimiter + arguments: + - @oro_security.acl_helper diff --git a/src/Oro/Bundle/DataGridBundle/Resources/config/requirejs.yml b/src/Oro/Bundle/DataGridBundle/Resources/config/requirejs.yml index 36b20c2b332..a091ec04b29 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/config/requirejs.yml +++ b/src/Oro/Bundle/DataGridBundle/Resources/config/requirejs.yml @@ -67,6 +67,7 @@ config: 'orodatagrid/js/datagrid/grid-views/view': 'bundles/orodatagrid/js/datagrid/grid-views/view.js' 'orodatagrid/js/datagrid/grid-views/model': 'bundles/orodatagrid/js/datagrid/grid-views/model.js' 'orodatagrid/js/datagrid/grid-views/collection': 'bundles/orodatagrid/js/datagrid/grid-views/collection.js' + 'orodatagrid/js/datagrid/metadata-model': 'bundles/orodatagrid/js/datagrid/metadata-model.js' 'orodatagrid/js/datagrid/pagination-input': 'bundles/orodatagrid/js/datagrid/pagination-input.js' 'orodatagrid/js/datagrid/pagination': 'bundles/orodatagrid/js/datagrid/pagination.js' 'orodatagrid/js/datagrid/row': 'bundles/orodatagrid/js/datagrid/row.js' diff --git a/src/Oro/Bundle/DataGridBundle/Resources/config/services.yml b/src/Oro/Bundle/DataGridBundle/Resources/config/services.yml index 06667c8eae1..db42de59ea2 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/DataGridBundle/Resources/config/services.yml @@ -24,7 +24,7 @@ parameters: oro_datagrid.event_listener.grid_views_load.class: Oro\Bundle\DataGridBundle\EventListener\GridViewsLoadListener oro_datagrid.form.type.grid_view_type.class: Oro\Bundle\DataGridBundle\Form\Type\GridViewType oro_datagrid.form.type.sorting_type.class: Oro\Bundle\DataGridBundle\Form\Type\GridSortingType - oro_datagrid.grid_view.manager.api.class: Oro\Bundle\SoapBundle\Entity\Manager\ApiEntityManager + oro_datagrid.grid_view.manager.api.class: Oro\Bundle\DataGridBundle\Entity\Manager\GridViewApiEntityManager oro_datagrid.grid_view.entity.class: Oro\Bundle\DataGridBundle\Entity\GridView oro_datagrid.grid_view.form.handler.api.class: Oro\Bundle\DataGridBundle\Form\Handler\GridViewApiHandler oro_datagrid.columns.helper.class: Oro\Bundle\DataGridBundle\Tools\ColumnsHelper @@ -149,12 +149,19 @@ services: tags: - { name: form.type, alias: oro_datagrid_sorting } + oro_datagrid.grid_view.manager: + class: Oro\Bundle\DataGridBundle\Entity\Manager\GridViewManager + arguments: + - @oro_security.acl_helper + - @doctrine + oro_datagrid.grid_view.manager.api: class: %oro_datagrid.grid_view.manager.api.class% parent: oro_soap.manager.entity_manager.abstract arguments: - %oro_datagrid.grid_view.entity.class% - @doctrine.orm.entity_manager + - @oro_datagrid.grid_view.manager oro_datagrid.form.grid_view.api: class: Symfony\Component\Form\Form @@ -170,6 +177,8 @@ services: - @oro_datagrid.form.grid_view.api - @request - @doctrine + - @oro_datagrid.grid_view.manager + - @security.token_storage scope: request oro_datagrid.datagrid.inline_edit_column_options_guesser: @@ -217,3 +226,8 @@ services: oro_datagrid.columns.helper: class: %oro_datagrid.columns.helper.class% + + oro_datagrid.grid_configuration.helper: + class: Oro\Bundle\DataGridBundle\Tools\GridConfigurationHelper + arguments: + - @oro_entity.orm.entity_class_resolver diff --git a/src/Oro/Bundle/DataGridBundle/Resources/doc/backend/advanced_grid_configuration.md b/src/Oro/Bundle/DataGridBundle/Resources/doc/backend/advanced_grid_configuration.md index 958c8f36cab..fa02c01182e 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/doc/backend/advanced_grid_configuration.md +++ b/src/Oro/Bundle/DataGridBundle/Resources/doc/backend/advanced_grid_configuration.md @@ -238,28 +238,32 @@ datagrid: #### Problem: *I'm developing grid that should not be under ACL control* #### Solution: -- set option 'skip_acl_check' to TRUE +- set option 'skip_acl_apply' to TRUE Example: ``` yml datagrid: acme-demo-grid: ... # some configuration - options: - skip_acl_check: true + source: + skip_acl_apply: true + ... # some configuration of source ``` #### Problem: -*I want to implement some custom security verification/logic without any default acl, even if some "acl_resource" have been defined * +*I want to implement some custom security verification/logic without any default ACL, even if some "acl_resource" have been defined * *e.g. i'm extending some existing grid but with custom acl logic* #### Solution: -- configure grid (set option 'skip_acl_check' to TRUE) +- configure grid (set option 'skip_acl_apply' to TRUE) +- override option 'acl_resource' and to make it false ``` yml datagrid: acme-demo-grid: ... # some configuration - options: - skip_acl_check: true + acl_resource: false + source: + skip_acl_apply: true + ... # some configuration of source ``` - declare own grid listener ``` @@ -273,3 +277,18 @@ my_bundle.event_listener.my_grid_listener: - as an example see: - Oro/Bundle/UserBundle/Resources/config/datagrid.yml (owner-users-select-grid) - Oro/Bundle/UserBundle/EventListener/OwnerUserGridListener.php (service name: "oro_user.event_listener.owner_user_grid_listener") + +#### Problem: +*I want to have a grid secured by ACL resource but skip applying of ACL to DQL query of the grid.* +#### Solution: +- configure grid with option 'skip_acl_apply' set to TRUE, it will ignore applying of ACL to source query of the grid +- configure grid with option 'acl_resource' set to name of some ACL resource, it will check permission to this ACL resouce before datagrid data will be loaded +``` yml +datagrid: + acme-demo-grid: + ... # some configuration + acl_resource: 'acme_demo_entity_view' + source: + skip_acl_apply: true + +``` diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/column-manager.less b/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/column-manager.less index fa46b3b6fa8..005c3673b39 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/column-manager.less +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/column-manager.less @@ -17,7 +17,7 @@ left: auto; padding: 20px; border-color: #ccc; - min-width: 270px; + min-width: 268px; &:before, &:after { content: ''; display: inline-block; diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/inline-editing-help.less b/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/inline-editing-help.less new file mode 100644 index 00000000000..7bb0cd709de --- /dev/null +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/inline-editing-help.less @@ -0,0 +1,40 @@ +.inline-editing-help-wrapper{ + z-index: 10000; + border-radius: 50px; + background: white; + width: 38px; + height: 38px; + position: absolute; + top: 30px; + right: 30px; + padding: 4px; + box-shadow: 0px 3px 16px rgba(0,0,0,0.4), 0 0 6px rgba(0,0,0,0.4); + .inline-editing-help-content { + width: 474px; + background: #FFF; + top: 39px; + margin-left: -415px !important; + max-width: 457px; + z-index: 10000; + color: #444; + font-size: 13px; + padding: 15px; + > .arrow { + left: 433px; + } + } + &:hover { + box-shadow: 0px 4px 20px rgba(0,0,0,0.4), 0 0 8px rgba(0,0,0,0.4); + } +} +.inline-editing-help{ + border-radius: 50px; + width: 30px; + height: 30px; + background: rgb(108, 146, 209); + color: white; + font-size: 24px; + text-align: center; + line-height: 31px; + font-weight: normal; +} diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/mobile/column-manager.less b/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/mobile/column-manager.less index 59768ec6ab0..c93945cdc6b 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/mobile/column-manager.less +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/mobile/column-manager.less @@ -8,22 +8,66 @@ line-height: @baseLineHeight; } .dropdown-menu { + right: 0; + top: 100%; + margin-top: 7px; + min-width: 300px; + padding: 18px 12px 12px; + .close { + right: 12px; + } &:before { - top: 9px; + top: -8px; + right: 9px; + border-top: 0 none; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid rgba(0,0,0,0.2); } &:after { - top: 10px; + top: -6px; + right: 10px; + border-top: 0 none; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid #fff; + } + } + .table-header-wrapper th:nth-child(2) { + text-align: center; + } + td.title-cell { + white-space: normal; + } + td.sort-cell { + width: 40px; + padding-left: 0px; + padding-right: 0px; + .btn { + height: 20px; + line-height: 16px; } } + td.visibility-cell { + width: 40px; + } } +/* changes position of bubble point of dropdown when it's shown above */ .dropup .column-manager .dropdown-menu { + top: auto; + margin-top: 0; + margin-bottom: 40px; &:before { top: auto; - bottom: 9px; + bottom: -7px; + border-bottom: 0 none; + border-top: 7px solid rgba(0,0,0,0.1); } &:after { top: auto; - bottom: 10px; + bottom: -6px; + border-bottom: 0 none; + border-top: 6px solid #fff; } } diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/mobile/grid-toolbar.less b/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/mobile/grid-toolbar.less index 23be254a816..3ebf11a7081 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/mobile/grid-toolbar.less +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/css/less/mobile/grid-toolbar.less @@ -25,8 +25,8 @@ a.action.btn { .forScreen(400px, display, none); i { - font-size: 14px; &:before { + font-size: 14px; line-height: 30px; } } 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 82ea344ea28..7a0c9e3907c 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 @@ -1,3 +1,6 @@ +.grid-views { + margin: 0; +} .grid-toolbar { padding: @contentPadding 10px; margin-bottom: -@contentPadding; @@ -71,6 +74,11 @@ .other-scroll-container { margin: 10px 10px 10px 10px; } +.responsive-cell { + .other-scroll-container { + margin: 10px 0; + } +} .grid-scrollable-container { -webkit-overflow-scrolling: touch; overflow-scrolling: touch; 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 fecc7048145..45d297c015b 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 @@ -12,6 +12,9 @@ th.renderable, td.renderable { display: table-cell; } + .loader-mask { + z-index: @oroZindexDropdown - 1;// to show under grid's filter + } } .grid-toolbar .pagination ul li input { @@ -328,6 +331,9 @@ td > .nowrap-ellipsis { padding: 0; } } +.select-all-header-cell { + width: 40px; +} .action-cell .more-bar-holder .dropdown-toggle { margin-left: 5px; } diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/components/datagrid-component.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/components/datagrid-component.js index 09095977a9b..63b57c063fd 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/components/datagrid-component.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/components/datagrid-component.js @@ -7,6 +7,7 @@ define(function(require) { var _ = require('underscore'); var tools = require('oroui/js/tools'); var mediator = require('oroui/js/mediator'); + var Backbone = require('backbone'); var BaseComponent = require('oroui/js/app/components/base/component'); var PageableCollection = require('orodatagrid/js/pageable-collection'); var Grid = require('orodatagrid/js/datagrid/grid'); @@ -16,6 +17,7 @@ define(function(require) { var FloatingHeaderPlugin = require('orodatagrid/js/app/plugins/grid/floating-header-plugin'); var FullscreenPlugin = require('orodatagrid/js/app/plugins/grid/fullscreen-plugin'); var ColumnManagerPlugin = require('orodatagrid/js/app/plugins/grid/column-manager-plugin'); + var MetadataModel = require('orodatagrid/js/datagrid/metadata-model'); helpers = { cellType: function(type) { @@ -112,12 +114,17 @@ define(function(require) { rowActions: {}, massActions: {} }); + this.metadataModel = new MetadataModel(this.metadata); this.modules = {}; this.collectModules(); // load all dependencies and build grid tools.loadModules(this.modules, this.build, this); + + this.listenTo(this.metadataModel, 'change:massActions', function(model, massActions) { + this.grid.massActions.reset(this.buildMassActionsOptions(massActions)); + }, this); }, /** @@ -167,9 +174,6 @@ define(function(require) { if (!collection) { // otherwise, create collection from metadata collection = new PageableCollection(collectionModels, collectionOptions); - } else if (this.data) { - _.extend(collectionOptions, {parse: true}); - collection.reset(collectionModels, collectionOptions); } // create grid @@ -205,7 +209,11 @@ define(function(require) { */ combineCollectionOptions: function() { var options = _.extend({ - inputName: this.inputName, + /* + * gridName contains extended information "inputName + scopeName" + * (allows to differentiate grid instances) + */ + inputName: this.gridName, parse: true, url: '\/user\/json', state: _.extend({ @@ -227,7 +235,6 @@ define(function(require) { combineGridOptions: function() { var columns; var rowActions = {}; - var massActions = {}; var defaultOptions = { sortable: false }; @@ -255,9 +262,7 @@ define(function(require) { }); // mass actions - _.each(metadata.massActions, function(options, action) { - massActions[action] = modules[helpers.actionType(options.frontend_type)].extend(options); - }); + var massActions = this.buildMassActionsOptions(this.metadata.massActions); if (tools.isMobile()) { plugins.push(FloatingHeaderPlugin); @@ -275,18 +280,37 @@ define(function(require) { name: this.gridName, columns: columns, rowActions: rowActions, - massActions: massActions, + massActions: new Backbone.Collection(massActions), toolbarOptions: metadata.options.toolbarOptions || {}, multipleSorting: metadata.options.multipleSorting || false, entityHint: metadata.options.entityHint, exportOptions: metadata.options.export || {}, routerEnabled: _.isUndefined(metadata.options.routerEnabled) ? true : metadata.options.routerEnabled, - multiSelectRowEnabled: metadata.options.multiSelectRowEnabled || !_.isEmpty(massActions), + multiSelectRowEnabled: metadata.options.multiSelectRowEnabled || massActions.length, metadata: this.metadata, + metadataModel: this.metadataModel, plugins: plugins }; }, + /** + * @param {Object} actions + * @returns {Array} + */ + buildMassActionsOptions: function(actions) { + var modules = this.modules; + var massActions = []; + + _.each(actions, function(options, action) { + massActions.push({ + action: action, + module: modules[helpers.actionType(options.frontend_type)].extend(options) + }); + }); + + return massActions; + }, + fixStates: function(options) { if (options.metadata) { this.fixState(options.metadata.state); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/plugins/grid/inline-editing-help-plugin.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/plugins/grid/inline-editing-help-plugin.js new file mode 100644 index 00000000000..ca326396f85 --- /dev/null +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/plugins/grid/inline-editing-help-plugin.js @@ -0,0 +1,39 @@ +define(function(require) { + 'use strict'; + + var InlineEditingHelpPlugin; + var __ = require('orotranslation/js/translator'); + var BasePlugin = require('oroui/js/app/plugins/base/plugin'); + var $ = require('jquery'); + + InlineEditingHelpPlugin = BasePlugin.extend({ + enable: function() { + this.listenTo(this.main, 'holdInlineEditingBackdrop', this.onHoldInlineEditingBackdrop); + this.listenTo(this.main, 'releaseInlineEditingBackdrop', this.onReleaseInlineEditingBackdrop); + InlineEditingHelpPlugin.__super__.enable.call(this); + }, + + onHoldInlineEditingBackdrop: function() { + $(document.body).append( + '
' + + '
' + + '' + + '
' + + '
' + + '
' + + __('oro.datagrid.inline_editing.help') + + '
' + + '
' + ); + $('.inline-editing-help-wrapper').click(function() { + $('.inline-editing-help-content').toggle(); + }); + }, + + onReleaseInlineEditingBackdrop: function() { + $('body > .inline-editing-help-wrapper').remove(); + } + }); + + return InlineEditingHelpPlugin; +}); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/plugins/grid/inline-editing-plugin.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/plugins/grid/inline-editing-plugin.js index da71dde22a1..cf995f426da 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/plugins/grid/inline-editing-plugin.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/plugins/grid/inline-editing-plugin.js @@ -55,17 +55,17 @@ define(function(require) { constructor: function() { this.onKeyDown = _.bind(this.onKeyDown, this); - this.hidePopover = _.bind(this.hidePopover, this); InlineEditingPlugin.__super__.constructor.apply(this, arguments); }, enable: function() { + this.main.$el.addClass('grid-editable'); this.listenTo(this.main, { afterMakeCell: this.onAfterMakeCell, - shown: this.onGridShown, - rowClicked: this.onGridRowClicked + shown: this.onGridShown }); this.listenTo(mediator, 'page:beforeChange', function() { + this.hidePopover(); if (this.editModeEnabled) { this.exitEditMode(true); } @@ -84,17 +84,10 @@ define(function(require) { if (this.editModeEnabled) { this.exitEditMode(true); } - this.hidePopover(); + this.main.$el.removeClass('grid-editable'); this.main.body.refresh(); - InlineEditingPlugin.__super__.disable.call(this); - }, - - dispose: function() { - if (this.disposed) { - return; - } this.destroyPopover(); - return InlineEditingPlugin.__super__.dispose.call(this); + InlineEditingPlugin.__super__.disable.call(this); }, onAfterMakeCell: function(row, cell) { @@ -108,21 +101,21 @@ define(function(require) { } var originalRender = cell.render; cell.render = function() { - originalRender.apply(this, arguments); - var originalEvents = this.events; - if (_this.isEditable(this)) { - this.$el.addClass('editable view-mode prevent-text-selection-on-dblclick'); - this.$el.append('Edit'); - this.events = _.extend(Object.create(this.events), { + var cell = this; + originalRender.apply(cell, arguments); + var originalEvents = cell.events; + if (_this.isEditable(cell)) { + cell.$el.addClass('editable view-mode prevent-text-selection-on-dblclick'); + cell.$el.append('Edit'); + cell.events = _.extend(Object.create(cell.events), { 'dblclick': enterEditModeIfNeeded, - 'mouseleave': _this.hidePopover, 'mousedown .icon-edit': enterEditModeIfNeeded, 'click': _.noop }); } - this.delegateEvents(); - this.events = originalEvents; - return this; + cell.delegateEvents(); + cell.events = originalEvents; + return cell; }; }, @@ -130,15 +123,10 @@ define(function(require) { this.initPopover(); }, - onGridRowClicked: function(grid, row) { - row.$('.editable').removeClass('editable'); - this.hidePopover(); - }, - initPopover: function() { this.main.$el.popover({ content: __('oro.form.inlineEditing.helpMessage'), - container: document.body, + container: this.main.$el, selector: 'td.editable', placement: 'bottom', delay: {show: 1400, hide: 0}, @@ -155,6 +143,7 @@ define(function(require) { destroyPopover: function() { if (this.main.$el.data('popover')) { + this.hidePopover(); this.main.$el.popover('destroy'); } }, @@ -242,8 +231,10 @@ define(function(require) { this.exitEditMode(false); } else { if (backdropManager.isReleased(this.backdropId)) { + this.hidePopover(); // before adding backdrop this.backdropId = backdropManager.hold(); $(document).on('keydown', this.onKeyDown); + this.main.trigger('holdInlineEditingBackdrop'); } } this.editModeEnabled = true; @@ -348,11 +339,17 @@ define(function(require) { newData[this.editor.save_api_accessor.initialOptions.field_name] = serverUpdateData[keys[0]]; serverUpdateData = newData; } - this.editor.save_api_accessor.send(cell.model.toJSON(), serverUpdateData, {}, { - processingMessage: __('oro.form.inlineEditing.saving_progress'), - preventWindowUnload: __('oro.form.inlineEditing.inline_edits') - }) - .done(_.bind(InlineEditingPlugin.onSaveSuccess, ctx)) + var savePromise = this.editor.save_api_accessor.send(cell.model.toJSON(), serverUpdateData, {}, { + processingMessage: __('oro.form.inlineEditing.saving_progress'), + preventWindowUnload: __('oro.form.inlineEditing.inline_edits') + }); + if (this.editor.component.processSavePromise) { + savePromise = this.editor.component.processSavePromise(savePromise, cell.column.get('metadata')); + } + if (this.editor.view.processSavePromise) { + savePromise = this.editor.view.processSavePromise(savePromise, cell.column.get('metadata')); + } + savePromise.done(_.bind(InlineEditingPlugin.onSaveSuccess, ctx)) .fail(_.bind(InlineEditingPlugin.onSaveError, ctx)) .always(function() { cell.$el.removeClass('loading'); @@ -392,6 +389,7 @@ define(function(require) { if (releaseBackdrop !== false) { backdropManager.release(this.backdropId); $(document).off('keydown', this.onKeyDown); + this.main.trigger('releaseInlineEditingBackdrop'); } delete this.editorComponent; }, @@ -573,20 +571,16 @@ define(function(require) { onSaveSuccess: function(response) { if (!this.cell.disposed && this.cell.$el) { if (response) { - var routeParametersRenameMap - = this.cell.column.get('metadata').inline_editing.save_api_accessor.routeParametersRenameMap; - for (var i in routeParametersRenameMap) { - if (typeof response[routeParametersRenameMap[i]] !== 'undefined') { - this.cell.model.set(i, response[routeParametersRenameMap[i]]); + var routeParametersRenameMap = _.invert(this.cell.column.get('metadata').inline_editing. + save_api_accessor.routeParametersRenameMap); + _.each(response, function(item, i) { + var propName = routeParametersRenameMap.hasOwnProperty(i) ? routeParametersRenameMap[i] : i; + if (this.cell.model.has(propName)) { + this.cell.model.set(propName, item); } - } + }, this); } - - var _this = this; - this.cell.$el.addClass('save-success'); - _.delay(function() { - _this.cell.$el.removeClass('save-success'); - }, 2000); + this.cell.$el.addClassTemporarily('save-success', 2000); } mediator.execute('showFlashMessage', 'success', __('oro.form.inlineEditing.successMessage')); }, @@ -594,11 +588,7 @@ define(function(require) { onSaveError: function(jqXHR) { var errorCode = 'responseJSON' in jqXHR ? jqXHR.responseJSON.code : jqXHR.status; if (!this.cell.disposed && this.cell.$el) { - var _this = this; - this.cell.$el.addClass('save-fail'); - _.delay(function() { - _this.cell.$el.removeClass('save-fail'); - }, 2000); + this.cell.$el.addClassTemporarily('save-fail', 2000); } this.cell.model.set(this.oldState); this.main.trigger('content:update'); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/views/column-manager/column-manager-view.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/views/column-manager/column-manager-view.js index 88b186da723..d8e511e19fc 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/views/column-manager/column-manager-view.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/app/views/column-manager/column-manager-view.js @@ -10,7 +10,8 @@ define(function(require) { autoRender: true, className: 'dropdown-menu', events: { - 'click [data-role="column-manager-select-all"]': 'onSelectAll' + 'click [data-role="column-manager-select-all"]': 'onSelectAll', + 'shown.bs.dropdown': 'onOpen' }, listen: { @@ -55,6 +56,13 @@ define(function(require) { }); }, + onOpen: function() { + var rect = this.el.getBoundingClientRect(); + this.$el.css({ + maxWidth: rect.right + 'px' + }); + }, + _getFilteredModels: function() { return _.filter(this.collection.filter(this.filterer), function(model) { return !model.get('disabledVisibilityChange'); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/content-manager.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/content-manager.js index 56bc92c32eb..37291b20998 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/content-manager.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/content-manager.js @@ -1,12 +1,12 @@ -define([ - 'underscore', - 'backbone', - 'oroui/js/mediator', - 'orodatagrid/js/pageable-collection' -], function(_, Backbone, mediator, PageableCollection) { +define(function(require) { 'use strict'; var contentManager; + var _ = require('underscore'); + var Backbone = require('backbone'); + var mediator = require('oroui/js/mediator'); + var PageableCollection = require('orodatagrid/js/pageable-collection'); + var GridViewsCollection = require('orodatagrid/js/datagrid/grid-views/collection'); function updateState(collection) { var key = collection.stateHashKey(); @@ -15,6 +15,11 @@ define([ } contentManager = { + /** + * Fetches grid collection from page cache storage + * + * @param {string} gridName + */ get: function(gridName) { var hash; var isActual; @@ -29,15 +34,45 @@ define([ return collection; }, + /** + * Trace grid collection changes and update it's state in page cache + * + * @param {PageableCollection} collection + */ trace: function(collection) { updateState(collection); contentManager.listenTo(collection, { updateState: updateState, - beforeReset: updateState + reset: updateState }); mediator.once('page:beforeChange', function() { contentManager.stopListening(collection); }); + }, + + /** + * Fetches grid views collection from page cache storage + * + * @param {string} gridName + */ + getViewsCollection: function(gridName) { + var key = GridViewsCollection.stateHashKey(gridName); + return mediator.execute('pageCache:state:fetch', key); + }, + + /** + * Trace grid views collection changes and update it's state in page cache + * + * @param {GridViewsCollection} collection + */ + traceViewsCollection: function(collection) { + updateState(collection); + contentManager.listenTo(collection, {'reset add remove': function() { + updateState(collection); + }}); + mediator.once('page:beforeChange', function() { + contentManager.stopListening(collection); + }); } }; _.extend(contentManager, Backbone.Events); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/action/abstract-action.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/action/abstract-action.js index e6a4993ea4e..d3cfac04bc9 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/action/abstract-action.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/action/abstract-action.js @@ -71,6 +71,9 @@ define([ /** @property {Object} */ launcherOptions: null, + /** @property {String} */ + requestType: 'GET', + /** @property {Object} */ defaultMessages: { confirm_title: 'Execution Confirmation', @@ -228,6 +231,7 @@ define([ data: this.getActionParameters(), context: this, dataType: 'json', + type: this.requestType, error: this._onAjaxError, success: this._onAjaxSuccess }); @@ -303,7 +307,7 @@ define([ if (!this.confirmModal) { this.confirmModal = (new this.confirmModalConstructor({ title: __(this.messages.confirm_title), - content: __(this.messages.confirm_content), + content: this.getConfirmContentMessage(), okText: __(this.messages.confirm_ok), cancelText: __(this.messages.confirm_cancel) })); @@ -312,7 +316,26 @@ define([ this.subviews.push(this.confirmModal); } return this.confirmModal; + }, + + /** + * Get confirm content message + * + * @return {String} + */ + getConfirmContentMessage: function() { + return __(this.messages.confirm_content); + }, + + /** + * Get ajax request type + * + * @return {String} + */ + getRequestType: function() { + return this.requestType; } + }); return AbstractAction; diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/action/delete-mass-action.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/action/delete-mass-action.js index 6cca0a491b7..906a8813cf5 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/action/delete-mass-action.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/action/delete-mass-action.js @@ -1,7 +1,9 @@ define([ 'oroui/js/delete-confirmation', + 'orotranslation/js/translator', + 'underscore', './mass-action' -], function(DeleteConfirmation, MassAction) { +], function(DeleteConfirmation, __, _, MassAction) { 'use strict'; var DeleteMassAction; @@ -20,12 +22,144 @@ define([ /** @property {Object} */ defaultMessages: { confirm_title: 'Delete Confirmation', - confirm_content: 'Are you sure you want to do remove selected items?', + confirm_content: 'Are you sure you want to remove these items?', confirm_ok: 'Yes, Delete', confirm_cancel: 'Cancel', success: 'Selected items were removed.', error: 'Selected items were not removed.', empty_selection: 'Please, select items to remove.' + }, + + /** @property {Object} */ + confirmMessages: { + selected_message: 'oro.datagrid.mass_action.delete.selected_message', + max_limit_message: 'oro.datagrid.mass_action.delete.max_limit_message', + restricted_access_message: 'oro.datagrid.mass_action.delete.restricted_access_message', + restricted_access_empty_message: 'oro.datagrid.mass_action.delete.restricted_access_empty_message' + }, + + /** @property {String} */ + confirmMessage: null, + + /** @property {Boolean} */ + allowOk: true, + + /** + * As in this action we need to send POST request to get data for confirm message + * we set this.confirmation = false at initialization to prevent opening confirm window. + * + * @param options + */ + initialize: function(options) { + DeleteMassAction.__super__.initialize.apply(this, arguments); + this.confirmMessage = __(this.defaultMessages.confirm_content); + this.confirmation = false; + }, + + /** + * Get view for confirm modal + * + * @return {oroui.Modal} + */ + getConfirmDialog: function(callback) { + if (!this.confirmModal) { + this.confirmModal = (new this.confirmModalConstructor({ + title: __(this.messages.confirm_title), + content: this.getConfirmContentMessage(), + okText: __(this.messages.confirm_ok), + cancelText: __(this.messages.confirm_cancel), + allowOk: this.allowOk + })); + this.listenTo(this.confirmModal, 'ok', callback); + + this.subviews.push(this.confirmModal); + } + return this.confirmModal; + }, + + /** + * Need to handle POST and DELETE requests differently. + * + * - POST request: we prepare confirm message from result data + * and set requestType = DELETE and this.confirmation = true, to prepare actually request for deleting. + * - DELETE request: handled as ordinary mass action request. + * + */ + _onAjaxSuccess: function(data, textStatus, jqXHR) { + if (this.requestType === 'POST') { + this.requestType = 'DELETE'; + this.setConfirmMessage(data); + if (this.reloadData) { + this.datagrid.hideLoading(); + } + this.confirmation = true; + return DeleteMassAction.__super__.execute.call(this); + } else { + MassAction.__super__._onAjaxSuccess.apply(this, arguments); + } + }, + + /** + * Sends POST request to prepare confirm message. + * Normal action request will be send manually after handling POST request. + * Sets this.confirmModal = null to rebuild confirm window after each request. + */ + execute: function() { + this.requestType = 'POST'; + this.confirmModal = null; + if (this.checkSelectionState()) { + DeleteMassAction.__super__.executeConfiguredAction.call(this); + } + }, + + /** + * @inheritDoc + */ + getConfirmContentMessage: function() { + return this.confirmMessage; + }, + + /** + * Sets confirm message from received data. + * + * @param data + */ + setConfirmMessage: function(data) { + this.allowOk = true; + if (this.isDefined(data.selected) && this.isDefined(data.deletable) && this.isDefined(data.max_limit)) { + if (data.deletable === 0) { + this.confirmMessage = __(this.confirmMessages.restricted_access_empty_message); + this.allowOk = false; + } else if (data.deletable <= data.max_limit) { + if (data.deletable >= data.selected) { + this.confirmMessage = __(this.confirmMessages.selected_message, {selected: data.selected}); + } else { + this.confirmMessage = __(this.confirmMessages.restricted_access_message, { + deletable: data.deletable, + selected: data.selected + }); + } + } else { + this.confirmMessage = __(this.confirmMessages.max_limit_message, {max_limit: data.max_limit}); + } + + } + }, + + isDefined: function(value) { + return !_.isUndefined(value); + }, + + getLink: function(parameters) { + if (this.requestType === 'DELETE') { + var actionParameters = this.getActionParameters(); + if (_.isUndefined(parameters)) { + parameters = {}; + } + parameters = _.extend(actionParameters, parameters); + } + + return DeleteMassAction.__super__.getLink.call(this, parameters); } }); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/action/mass-action.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/action/mass-action.js index f446387b39a..9d01b4579c4 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/action/mass-action.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/action/mass-action.js @@ -44,12 +44,24 @@ define([ * Ask a confirmation and execute mass action. */ execute: function() { + if (this.checkSelectionState()) { + MassAction.__super__.execute.call(this); + } + }, + + /** + * Checks if any records are selected. + * + * @returns {boolean} + */ + checkSelectionState: function() { var selectionState = this.datagrid.getSelectionState(); if (_.isEmpty(selectionState.selectedModels) && selectionState.inset) { messenger.notificationFlashMessage('warning', __(this.messages.empty_selection)); - } else { - MassAction.__super__.execute.call(this); + return false; } + + return true; }, /** diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/column/action-column.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/column/action-column.js index 4eb3b479aaf..9ecaa01c2fc 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/column/action-column.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/column/action-column.js @@ -1,9 +1,10 @@ define([ + 'backbone', 'underscore', 'backgrid', 'oro/datagrid/cell/action-cell', '../header-cell/action-header-cell' -], function(_, Backgrid, ActionCell, ActionHeaderCell) { +], function(Backbone, _, Backgrid, ActionCell, ActionHeaderCell) { 'use strict'; var ActionColumn; @@ -26,7 +27,7 @@ define([ headerCell: ActionHeaderCell, datagrid: null, actions: [], - massActions: [] + massActions: new Backbone.Collection() }, Backgrid.Column.prototype.defaults), /** @@ -40,7 +41,7 @@ define([ if (!attrs.name) { attrs.name = this.defaults.name; } - if (_.isEmpty(attrs.actions) && _.isEmpty(attrs.massActions)) { + if (_.isEmpty(attrs.actions) && attrs.massActions.length) { this.set('renderable', false); } ActionColumn.__super__.initialize.apply(this, arguments); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid-views/collection.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid-views/collection.js index 84f01da2d59..4ff7f110c59 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid-views/collection.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid-views/collection.js @@ -1,15 +1,77 @@ -define([ - 'backbone', - './model' -], function(Backbone, GridViewsModel) { +define(function(require) { 'use strict'; var GridViewsCollection; + var _ = require('underscore'); + var Backbone = require('backbone'); + var BaseCollection = require('oroui/js/app/models/base/collection'); + var GridViewsModel = require('./model'); - GridViewsCollection = Backbone.Collection.extend({ + GridViewsCollection = BaseCollection.extend({ /** @property */ - model: GridViewsModel + model: GridViewsModel, + + /** @type {string} */ + gridName: '', + + /** + * @inheritDoc + */ + initialize: function(models, options) { + _.extend(this, _.pick(options, ['gridName'])); + GridViewsCollection.__super__.initialize.call(this, models, options); + }, + + /** + * @inheritDoc + */ + _prepareModel: function(attrs, options) { + if (attrs instanceof Backbone.Model) { + attrs.set('grid_name', this.gridName, {silent: true}); + } else { + attrs.grid_name = this.gridName; + } + return GridViewsCollection.__super__._prepareModel.call(this, attrs, options); + }, + + /** + * @inheritDoc + */ + clone: function() { + return new this.constructor(this.toJSON(), {gridName: this.gridName}); + }, + + /** + * Fetches key for a state hash of GridViewsCollection + * + * @returns {string} + */ + stateHashKey: function() { + return GridViewsCollection.stateHashKey(this.gridName); + }, + + /** + * Fetches value for a state hash of GridViewsCollection + * + * @param {boolean=} purge If true, clears value from initial state + * @returns {string|null} + */ + stateHashValue: function(purge) { + // selected grid view is already preserved in URL, no need extra value + return null; + } }); + /** + * Generates name of URL parameter for collection state + * + * @static + * @param {string} gridName + * @returns {string} + */ + GridViewsCollection.stateHashKey = function(gridName) { + return 'gridViews[' + gridName + ']'; + }; + return GridViewsCollection; }); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid-views/model.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid-views/model.js index 2ea4878c4e7..276c6c97577 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid-views/model.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid-views/model.js @@ -20,7 +20,8 @@ define([ sorters: [], columns: {}, deletable: false, - editable: false + editable: false, + is_default: false }, /** @property */ diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid-views/view-name-modal.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid-views/view-name-modal.js index 322561ff251..45dd572085d 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid-views/view-name-modal.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid-views/view-name-modal.js @@ -20,7 +20,9 @@ define([ options.title = options.title || __('oro.datagrid.name_modal.title'); options.content = options.content || this.contentTemplate({ value: options.defaultValue || '', - label: __('oro.datagrid.gridView.name') + label: __('oro.datagrid.gridView.name'), + defaultLabel: __('oro.datagrid.action.use_as_default_grid_view'), + defaultChecked: options.defaultChecked || false }); options.okText = __('oro.datagrid.gridView.save_name'); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid-views/view.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid-views/view.js index a13a47423a1..61ca45cd253 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid-views/view.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid-views/view.js @@ -1,17 +1,16 @@ -define([ - 'backbone', - 'underscore', - 'orotranslation/js/translator', - './collection', - './model', - './view-name-modal', - 'oroui/js/mediator', - 'oroui/js/delete-confirmation' -], function(Backbone, _, __, GridViewsCollection, GridViewModel, ViewNameModal, mediator, DeleteConfirmation) { +define(function(require) { 'use strict'; var GridViewsView; + var Backbone = require('backbone'); var $ = Backbone.$; + var _ = require('underscore'); + var __ = require('orotranslation/js/translator'); + var GridViewModel = require('./model'); + var ViewNameModal = require('./view-name-modal'); + var mediator = require('oroui/js/mediator'); + var DeleteConfirmation = require('oroui/js/delete-confirmation'); + var routing = require('routing'); /** * Datagrid views widget @@ -34,7 +33,8 @@ define([ 'click a.unshare': 'onUnshare', 'click a.delete': 'onDelete', 'click a.rename': 'onRename', - 'click a.discard_changes': 'onDiscardChanges' + 'click a.discard_changes': 'onDiscardChanges', + 'click a.use_as_default': 'onUseAsDefault' }, /** @property */ @@ -49,9 +49,6 @@ define([ /** @property */ enabled: true, - /** @property */ - choices: [], - /** @property */ permissions: { CREATE: false, @@ -67,8 +64,8 @@ define([ /** @property */ gridName: {}, - /** @property */ - viewsCollection: GridViewsCollection, + /** @type {GridViewsCollection} */ + viewsCollection: null, /** @property */ originalTitle: null, @@ -79,8 +76,8 @@ define([ * @param {Object} options * @param {Backbone.Collection} options.collection * @param {Boolean} [options.enable] - * @param {Array} [options.choices] - * @param {Array} [options.views] + * @param {string} [options.title] + * @param {GridViewsCollection} [options.viewsCollection] */ initialize: function(options) { options = options || {}; @@ -89,37 +86,25 @@ define([ throw new TypeError('"collection" is required'); } + if (!options.viewsCollection) { + throw new TypeError('"viewsCollection" is required'); + } + + _.extend(this, _.pick(options, ['viewsCollection', 'title'])); + this.template = _.template($('#template-datagrid-grid-view').html()); this.titleTemplate = _.template($('#template-datagrid-grid-view-label').html()); - if (options.choices) { - this.choices = _.union(this.choices, options.choices); - if (!this._getView(this.DEFAULT_GRID_VIEW_ID).label) { - this._getView(this.DEFAULT_GRID_VIEW_ID).label = - __('oro.datagrid.gridView.all') + (options.title || ''); - } - } - if (options.permissions) { this.permissions = _.extend(this.permissions, options.permissions); } - if (options.title) { - this.title = options.title; - } - this.originalTitle = $('head title').text(); this.gridName = options.gridName; this.collection = options.collection; this.enabled = options.enable !== false; - options.views = options.views || []; - _.each(options.views, function(view) { - view.grid_name = this.gridName; - }, this); - - this.viewsCollection = new this.viewsCollection(options.views); if (!this.collection.state.gridView) { this.collection.state.gridView = this.DEFAULT_GRID_VIEW_ID; } @@ -247,6 +232,7 @@ define([ modal.on('ok', function(e) { var model = self._createViewModel({ label: this.$('input[name=name]').val(), + is_default: this.$('input[name=is_default]').is(':checked'), type: 'private', grid_name: self.gridName, filters: self.collection.state.filters, @@ -267,6 +253,10 @@ define([ self._updateTitle(); self._showFlashMessage('success', __('oro.datagrid.gridView.created')); mediator.trigger('datagrid:' + self.gridName + ':views:add', model); + + if (model.get('is_default')) { + self._getCurrentDefaultViewModel().set({is_default: false}); + } }, error: function(model, response, options) { modal.open(); @@ -344,14 +334,21 @@ define([ var self = this; var modal = new ViewNameModal({ - defaultValue: model.get('label') + defaultValue: model.get('label'), + defaultChecked: model.get('is_default'), }); modal.on('ok', function() { model.save({ - label: this.$('input[name=name]').val() + label: this.$('input[name=name]').val(), + is_default: this.$('input[name=is_default]').is(':checked') }, { wait: true, success: function() { + if (model.get('is_default')) { + self._getCurrentDefaultViewModel().set({is_default: false}); + } else { + self._getDefaultSystemViewModel().set({is_default: true}); + } self._showFlashMessage('success', __('oro.datagrid.gridView.updated')); }, error: function(model, response, options) { @@ -371,16 +368,69 @@ define([ this.changeView(this.collection.state.gridView); }, + /** + * Prepares choice items for grid view dropdown + * + * @return {Array<{label:{string},value:{*}}>} + */ + getViewChoices: function() { + var choices = this.viewsCollection.map(function(model) { + return { + label: model.get('label'), + value: model.get('name') + }; + }); + + var defaultItem = _.findWhere(choices, {value: this.DEFAULT_GRID_VIEW_ID}); + if (defaultItem.label === this.DEFAULT_GRID_VIEW_ID) { + defaultItem.label = __('oro.datagrid.gridView.all') + (this.title || ''); + } + + return choices; + }, + + /** + * @param {Event} e + */ + onUseAsDefault: function(e) { + var self = this; + var isDefault = 1; + var defaultModel = this._getCurrentDefaultViewModel(); + var currentViewModel = this._getCurrentViewModel(); + var id = currentViewModel.id; + if (this._isCurrentViewSystem()) { + // in this case we need to set default to false on current default view + isDefault = 0; + id = defaultModel.id; + } + return $.post( + routing.generate('oro_datagrid_api_rest_gridview_default', { + id: id, + default: isDefault + }), + {}, + function(response) { + defaultModel.set({is_default: false}); + currentViewModel.set({is_default: true}); + self._showFlashMessage('success', __('oro.datagrid.gridView.updated')); + } + ).fail( + function(response) { + if (response.status === 404) { + self._showFlashMessage('error', __('oro.datagrid.gridView.error.not_found')); + } else { + self._showFlashMessage('error', __('oro.ui.unexpected_error')); + } + } + ); + }, + /** * @private * * @param {GridViewModel} model */ - _onModelAdd: function(model) { - this.choices.push({ - label: model.get('label'), - value: model.get('name') - }); + _onModelAdd: function() { this.render(); }, @@ -393,15 +443,16 @@ define([ var viewId = this.collection.state.gridView; viewId = viewId === this.DEFAULT_GRID_VIEW_ID ? viewId : parseInt(viewId, 10); - this.choices = _.reject(this.choices, function(item) { - return item.value === viewId; - }, this); - if (model.id === viewId) { this.collection.state.gridView = this.DEFAULT_GRID_VIEW_ID; this.viewDirty = !this._isCurrentStateSynchronized(); } + if (model.get('is_default')) { + var systemModel = this._getDefaultSystemViewModel(); + systemModel.set({is_default: true}); + } + this.render(); }, @@ -462,7 +513,7 @@ define([ title: title, titleLabel: this.title, disabled: !this.enabled, - choices: this.choices, + choices: this.getViewChoices(), dirty: this.viewDirty, editedLabel: __('oro.datagrid.gridView.data_edited'), actionsLabel: __('oro.datagrid.gridView.actions'), @@ -543,6 +594,11 @@ define([ name: 'delete', enabled: typeof currentView !== 'undefined' && currentView.get('deletable') + }, + { + label: __('oro.datagrid.action.use_as_default_grid_view'), + name: 'use_as_default', + enabled: typeof currentView !== 'undefined' && !currentView.get('is_default') } ]; }, @@ -572,6 +628,41 @@ define([ }); }, + /** + * @private + * + * @returns {undefined|GridViewModel} + */ + _getCurrentDefaultViewModel: function() { + if (!this._hasActiveView()) { + return; + } + + return this.viewsCollection.findWhere({ + is_default: true + }); + }, + + /** + * @private + * + * @returns {boolean} + */ + _isCurrentViewSystem: function() { + return this._getCurrentView().value === this.DEFAULT_GRID_VIEW_ID; + }, + + /** + * @private + * + * @returns {undefined|GridViewModel} + */ + _getDefaultSystemViewModel: function() { + return this.viewsCollection.findWhere({ + name: this.DEFAULT_GRID_VIEW_ID + }); + }, + /** * @private * @@ -590,24 +681,20 @@ define([ var currentView = this._getCurrentView(); if (typeof currentView === 'undefined') { - return this.title ? this.title : __('Please select view'); + return this.title ? $.trim(this.title) : __('Please select view'); } - return currentView.label; + return $.trim(currentView.label); }, /** * @private * - * @param {String} name + * @param {string|number} name * @returns {undefined|Object} */ _getView: function(name) { - var currentViews = _.filter(this.choices, function(item) { - return item.value === name; - }, this); - - return _.first(currentViews); + return _.findWhere(this.getViewChoices(), {value: name}); }, /** diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid.js index 2ae3fc45f59..3a649eec977 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/grid.js @@ -4,6 +4,7 @@ define(function(require) { var Grid; var $ = require('jquery'); var _ = require('underscore'); + var Backbone = require('backbone'); var Backgrid = require('backgrid'); var __ = require('orotranslation/js/translator'); var mediator = require('oroui/js/mediator'); @@ -84,6 +85,9 @@ define(function(require) { /** @property {orodatagrid.datagrid.Toolbar} */ toolbar: Toolbar, + /** @property {orodatagrid.datagrid.MetadataModel} */ + metadataModel: null, + /** @property {LoadingMaskView|null} */ loadingMask: null, @@ -108,7 +112,7 @@ define(function(require) { rowClickAction: undefined, multipleSorting: true, rowActions: [], - massActions: [], + massActions: new Backbone.Collection(), enableFullScreenLayout: false }, @@ -129,7 +133,7 @@ define(function(require) { * @param {Object} [options.toolbarOptions] Options for toolbar * @param {Object} [options.exportOptions] Options for export * @param {Array} [options.rowActions] Array of row actions prototypes - * @param {Array} [options.massActions] Array of mass actions prototypes + * @param {Backbone.Collection} [options.massActions] Collection of mass actions prototypes * @param {Boolean} [options.multiSelectRowEnabled] Option for enabling multi select row * @param {oro.datagrid.action.AbstractAction} [options.rowClickAction] Prototype for * action that handles row click @@ -162,6 +166,10 @@ define(function(require) { this.noColumnsFlag = true; } + if (!opts.metadataModel) { + throw new TypeError('"metadataModel" is required'); + } + // Init properties values based on options and defaults _.extend(this, this.defaults, opts); this.toolbarOptions = {}; @@ -485,7 +493,7 @@ define(function(require) { xhr.always = function() { always.apply(this, arguments); if (!self.disposed) { - self._afterRequest(); + self._afterRequest(this); } }; }); @@ -676,7 +684,12 @@ define(function(require) { * * @private */ - _afterRequest: function() { + _afterRequest: function(jqXHR) { + var json = jqXHR.responseJSON || {}; + if (json.metadata) { + this._processLoadedMetadata(json.metadata); + } + this.requestsCount -= 1; if (this.requestsCount === 0) { this.hideLoading(); @@ -690,6 +703,15 @@ define(function(require) { } }, + /** + * @param {Object} metadata + * @private + */ + _processLoadedMetadata: function(metadata) { + _.extend(this.metadata, metadata); + this.metadataModel.set(metadata); + }, + /** * Show loading mask and disable toolbar */ diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/header-cell/action-header-cell.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/header-cell/action-header-cell.js index 299862d8856..f1c31e8ffe7 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/header-cell/action-header-cell.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/header-cell/action-header-cell.js @@ -2,8 +2,9 @@ define([ 'underscore', 'backbone', 'backgrid', - '../actions-panel' -], function(_, Backbone, Backgrid, ActionsPanel) { + '../actions-panel', + 'oroui/js/app/views/base/view' +], function(_, Backbone, Backgrid, ActionsPanel, BaseView) { 'use strict'; var ActionHeaderCell; @@ -14,9 +15,9 @@ define([ * * @export orodatagrid/js/datagrid/header-cell/action-header-cell * @class orodatagrid.datagrid.headerCell.ActionHeaderCell - * @extends Backbone.View + * @extends oroui/js/app/views/base/view */ - ActionHeaderCell = Backbone.View.extend({ + ActionHeaderCell = BaseView.extend({ /** @property */ className: 'action-column renderable', @@ -31,19 +32,19 @@ define([ }, initialize: function(options) { - var datagrid; + ActionHeaderCell.__super__.initialize.apply(this, arguments); this.column = options.column; if (!(this.column instanceof Backgrid.Column)) { this.column = new Backgrid.Column(this.column); } - this.subviews = []; this.createActionsPanel(); - datagrid = this.column.get('datagrid'); + var datagrid = this.column.get('datagrid'); this.listenTo(datagrid, 'enable', this.enable); this.listenTo(datagrid, 'disable', this.disable); + this.listenTo(datagrid.massActions, 'reset', this.rebuildAndRender); }, /** @@ -53,7 +54,6 @@ define([ if (this.disposed) { return; } - delete this.actionsPanel; delete this.column; ActionHeaderCell.__super__.dispose.apply(this, arguments); }, @@ -62,21 +62,21 @@ define([ var actions = []; var datagrid = this.column.get('datagrid'); - _.each(this.column.get('massActions'), function(Action) { - var action = new Action({ - datagrid: datagrid - }); - actions.push(action); - }); + this.column.get('massActions').each(function(Action) { + var ActionModule = Action.get('module'); - this.actionsPanel = new ActionsPanel(); - this.actionsPanel.setActions(actions); + actions.push( + new ActionModule({ + datagrid: datagrid + }) + ); + }); - this.subviews.push(this.actionsPanel); + this.subview('actionsPanel', new ActionsPanel({'actions': actions})); }, render: function() { - var panel = this.actionsPanel; + var panel = this.subview('actionsPanel'); this.$el.empty(); if (panel.haveActions()) { this.$el.append($(this.template).text()); @@ -87,13 +87,20 @@ define([ return this; }, + rebuildAndRender: function(massActions) { + this.column.set('massActions', massActions); + + this.createActionsPanel(); + this.render(); + }, + enable: function() { - this.actionsPanel.enable(); + this.subview('actionsPanel').enable(); this.$(this.options.controls).removeClass('disabled'); }, disable: function() { - this.actionsPanel.disable(); + this.subview('actionsPanel').disable(); this.$(this.options.controls).addClass('disabled'); } }); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/metadata-model.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/metadata-model.js new file mode 100644 index 00000000000..7b7b88625b9 --- /dev/null +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/datagrid/metadata-model.js @@ -0,0 +1,25 @@ +define(['backbone'], function(Backbone) { + 'use strict'; + + var MetadataModel; + + /** + * Datagrid metadata model + * + * @export orodatagrid/js/datagrid/metadata-model + * @class orodatagrid.datagrid.MetadataModel + * @extends Backbone.Model + */ + MetadataModel = Backbone.Model.extend({ + defaults: { + columns: [], + options: {}, + state: {}, + initialState: {}, + rowActions: {}, + massActions: {} + } + }); + + return MetadataModel; +}); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/grid-views-builder.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/grid-views-builder.js index 11ea6f1ee60..741ab846e78 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/grid-views-builder.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/grid-views-builder.js @@ -5,6 +5,8 @@ define(function(require) { var _ = require('underscore'); var mediator = require('oroui/js/mediator'); var GridViewsView = require('orodatagrid/js/datagrid/grid-views/view'); + var GridViewsCollection = require('orodatagrid/js/datagrid/grid-views/collection'); + var gridContentManager = require('orodatagrid/js/content-manager'); var gridGridViewsSelector = '.page-title > .navbar-extra .pull-left-extra > .pull-left'; var gridViewsBuilder = { /** @@ -22,7 +24,8 @@ define(function(require) { init: function(deferred, options) { var self = { metadata: _.defaults(options.metadata, { - gridViews: {} + gridViews: {}, + options: {} }), enableViews: options.enableViews, $gridEl: options.$el, @@ -89,7 +92,21 @@ define(function(require) { * @returns {Object} */ combineGridViewsOptions: function() { - return this.metadata.gridViews; + var options = this.metadata.gridViews; + // check is grid views collection is stored in content manager + var collection = gridContentManager.getViewsCollection(options.gridName); + + if (!collection) { + collection = new GridViewsCollection(options.views, {gridName: options.gridName}); + } + + if (this.metadata.options.routerEnabled !== false) { + // trace grid views collection changes + gridContentManager.traceViewsCollection(collection); + } + + options.viewsCollection = collection; + return _.omit(options, ['choices', 'views']); } }; diff --git a/src/Oro/Bundle/DataGridBundle/Resources/public/js/inline-editing/builder.js b/src/Oro/Bundle/DataGridBundle/Resources/public/js/inline-editing/builder.js index c0312fa4e49..843ff5a0405 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/public/js/inline-editing/builder.js +++ b/src/Oro/Bundle/DataGridBundle/Resources/public/js/inline-editing/builder.js @@ -4,6 +4,7 @@ define(function(require) { var $ = require('jquery'); var _ = require('underscore'); var tools = require('oroui/js/tools'); + var InlineEditingHelpPlugin = require('../app/plugins/grid/inline-editing-help-plugin'); var gridViewsBuilder = { /** @@ -29,6 +30,9 @@ define(function(require) { options.gridPromise.done(function(grid) { grid.pluginManager.create(options.metadata.inline_editing.plugin, options); grid.pluginManager.enable(options.metadata.inline_editing.plugin); + if (options.metadata.inline_editing.disable_help !== false) { + grid.pluginManager.enable(InlineEditingHelpPlugin); + } deferred.resolve(); }); }); diff --git a/src/Oro/Bundle/DataGridBundle/Resources/translations/jsmessages.en.yml b/src/Oro/Bundle/DataGridBundle/Resources/translations/jsmessages.en.yml index 2b934e7c14d..116883e8aa9 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/translations/jsmessages.en.yml +++ b/src/Oro/Bundle/DataGridBundle/Resources/translations/jsmessages.en.yml @@ -1,7 +1,7 @@ "Item deleted": "Item deleted" "Are you sure you want to delete this item?": "Are you sure you want to delete this item?" "Delete Error": "Delete Error" -"Are you sure you want to do remove selected items?": "Are you sure you want to do remove selected items?" +"Are you sure you want to remove these items?": "Are you sure you want to remove these items?" "Are you sure you want to remove this item?": "Are you sure you want to remove this item?" "Selected items were removed.": "Selected items were removed." "Selected items were not removed.": "Selected items were removed." @@ -53,6 +53,7 @@ oro_datagrid.action.refresh: "Refresh" "oro.datagrid.action.share_grid_view": "Share With Others" "oro.datagrid.action.unshare_grid_view": "Unshare" "oro.datagrid.action.delete_grid_view": "Delete" +"oro.datagrid.action.use_as_default_grid_view": "Use as default" "oro.datagrid.gridView.all": "All" "oro.datagrid.gridView.default": "Default" "oro.datagrid.gridView.created": "View has been successfully created" @@ -62,6 +63,7 @@ oro_datagrid.action.refresh: "Refresh" "oro.datagrid.gridView.actions": "Options" "oro.datagrid.gridView.data_edited": "Edited" "oro.datagrid.gridView.save_name": "Save" +"oro.datagrid.gridView.error.not_found": "View not found" "Unexpected format": Unexpected format oro: datagrid: @@ -82,3 +84,25 @@ oro: close_tooltip: "Close Column Manager" empty_list: "No columns found" not_number: Not a number + inline_editing: + help: | +
    +
  • Enter, Shift + Enter - Save and edit next/previous cell in column
  • +
  • Tab, Shift + Tab - Save and edit next/previous cell in row
  • +
  • Alt + ←/↑/→/↓ - Navigate between cells
  • +
  • Ctrl + Enter - Save and close
  • +
  • Escape - Quit
  • +
+ mass_action: + delete: + selected_message: > + You have selected {{ selected }} records.
+ Are you sure you want to delete them? + max_limit_message: > + The maximum number of records that can be deleted at once is {{ max_limit }}.
+ Are you sure you want to delete first {{ max_limit }} records in the selection? + restricted_access_message: > + You have permissions to delete {{ deletable }} records out of {{ selected }} selected.
+ Are you sure you want to delete them? + restricted_access_empty_message: > + You don't have permissions to delete any of the selected records. diff --git a/src/Oro/Bundle/DataGridBundle/Resources/translations/messages.en.yml b/src/Oro/Bundle/DataGridBundle/Resources/translations/messages.en.yml index ef7d2c28631..c6eaf5e1ce3 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/translations/messages.en.yml +++ b/src/Oro/Bundle/DataGridBundle/Resources/translations/messages.en.yml @@ -15,6 +15,7 @@ oro: id.label: Id name.label: Name owner.label: Owner + users.label: Users organization.label: Organization sorters_data.label: Sorters type.label: Type @@ -32,7 +33,7 @@ oro: delete: label: Delete confirm_title: Mass Delete Confirmation - confirm_content: Are you sure you want to do delete selected items? + confirm_content: Are you sure you want to remove these items? success_message: "{0} No entities were removed|{1} One entity was removed|]1,Inf[ %count% entities were removed" datagrid.page_size.all: All export.csv: CSV diff --git a/src/Oro/Bundle/DataGridBundle/Resources/views/js/grid-view.html.twig b/src/Oro/Bundle/DataGridBundle/Resources/views/js/grid-view.html.twig index cdd2f8bde69..1012f86d539 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/views/js/grid-view.html.twig +++ b/src/Oro/Bundle/DataGridBundle/Resources/views/js/grid-view.html.twig @@ -2,9 +2,9 @@
<% if (choices.length) { %>
- +
- <% if (dirty) { %> + <% if (dirty) { %>
 - <%= editedLabel %>
- <% } %> + <% } %> +
<% } %>
diff --git a/src/Oro/Bundle/DataGridBundle/Resources/views/js/view-name-modal.html.twig b/src/Oro/Bundle/DataGridBundle/Resources/views/js/view-name-modal.html.twig index 27d3923b466..c2b7b943946 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/views/js/view-name-modal.html.twig +++ b/src/Oro/Bundle/DataGridBundle/Resources/views/js/view-name-modal.html.twig @@ -5,6 +5,11 @@
+ + +
+ checked <% } %> > +
diff --git a/src/Oro/Bundle/DataGridBundle/Resources/views/macros.html.twig b/src/Oro/Bundle/DataGridBundle/Resources/views/macros.html.twig index c791ae50649..fa166de08e1 100644 --- a/src/Oro/Bundle/DataGridBundle/Resources/views/macros.html.twig +++ b/src/Oro/Bundle/DataGridBundle/Resources/views/macros.html.twig @@ -12,6 +12,9 @@ {% if app.request.get('_widgetContainer') == 'dialog' %} {% set renderParams = {'enableViews': false}|merge(renderParams) %} {% endif %} + {% if params._grid_view._disabled is defined and params._grid_view._disabled %} + {% set renderParams = renderParams|merge({'enableViews': false}) %} + {% endif %} {% set metaData = oro_datagrid_metadata(datagrid, params) %} {% if renderParams.routerEnabled is defined %} {% set metadataOptions = metaData.options is defined ? metaData.options : {} %} diff --git a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Common/ObjectTest.php b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Common/ObjectTest.php new file mode 100644 index 00000000000..04f43a27d42 --- /dev/null +++ b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Common/ObjectTest.php @@ -0,0 +1,76 @@ +assertEquals($expected, $object->offsetExistByPath($path)); + } + + public function getOffsetGetByPathDataProvider() + { + $params = [ + 'true' => true, + 'false' => false, + 'null' => null, + 'array' => [ + 'true' => true, + 'false' => false, + 'null' => null, + ], + ]; + return [ + [ + 'params' => $params, + 'path' => '[true]', + 'expected' => true, + ], + [ + 'params' => $params, + 'path' => '[false]', + 'expected' => true, + ], + [ + 'params' => $params, + 'path' => '[null]', + 'expected' => false, + ], + [ + 'params' => $params, + 'path' => '[unknown]', + 'expected' => false, + ], + [ + 'params' => $params, + 'path' => '[array][false]', + 'expected' => true, + ], + [ + 'params' => $params, + 'path' => '[array][true]', + 'expected' => true, + ], + [ + 'params' => $params, + 'path' => '[array][null]', + 'expected' => false, + ], + [ + 'params' => $params, + 'path' => '[array][unknown]', + 'expected' => false, + ], + ]; + } +} diff --git a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Datagrid/Common/DatagridConfigurationTest.php b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Datagrid/Common/DatagridConfigurationTest.php new file mode 100644 index 00000000000..0266428d513 --- /dev/null +++ b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Datagrid/Common/DatagridConfigurationTest.php @@ -0,0 +1,327 @@ +configuration = DatagridConfiguration::create([]); + } + + /** + * @param array $params + * @param bool $expected + * @dataProvider getAclResourceDataProvider + */ + public function testGetAclResource(array $params, $expected) + { + $this->configuration->merge($params); + $this->assertEquals($expected, $this->configuration->getAclResource()); + } + + public function getAclResourceDataProvider() + { + return [ + [ + 'params' => [ + 'acl_resource' => false, + 'source' => ['acl_resource' => false], + ], + 'expected' => false, + ], + [ + 'params' => [ + 'acl_resource' => false, + 'source' => ['acl_resource' => true], + ], + 'expected' => false, + ], + [ + 'params' => [ + 'acl_resource' => true, + 'source' => ['acl_resource' => false], + ], + 'expected' => true, + ], + [ + 'params' => [ + 'acl_resource' => true, + 'source' => ['acl_resource' => true], + ], + 'expected' => true, + ], + [ + 'params' => ['acl_resource' => true], + 'expected' => true, + ], + [ + 'params' => [ + 'acl_resource' => false, + ], + 'expected' => false, + ], + [ + 'params' => [ + 'source' => ['acl_resource' => false], + ], + 'expected' => false, + ], + [ + 'params' => [ + 'source' => ['acl_resource' => true], + ], + 'expected' => true, + ], + ]; + } + + /** + * @param array $params + * @param bool $expected + * @dataProvider isDatasourceSkipAclApplyDataProvider + */ + public function testIsDatasourceSkipAclApply(array $params, $expected) + { + $this->configuration->merge($params); + $this->assertEquals($expected, $this->configuration->isDatasourceSkipAclApply()); + } + + public function isDatasourceSkipAclApplyDataProvider() + { + return [ + [ + 'params' => [ + 'source' => [ + 'skip_acl_apply' => false, + 'skip_acl_check' => false, + ], + ], + 'expected' => false, + ], + [ + 'params' => [ + 'source' => [ + 'skip_acl_apply' => false, + 'skip_acl_check' => true, + ], + ], + 'expected' => false, + ], + [ + 'params' => [ + 'source' => [ + 'skip_acl_apply' => true, + 'skip_acl_check' => false, + ], + ], + 'expected' => true, + ], + [ + 'params' => [ + 'source' => [ + 'skip_acl_apply' => true, + 'skip_acl_check' => true, + ], + ], + 'expected' => true, + ], + ]; + } + + /** + * @dataProvider addColumnDataProvider + * + * @param array $expected + * @param string $name + * @param string $select + * @param array $definition + * @param array $sorter + * @param array $filter + */ + public function testAddColumn( + $expected, + $name, + $definition, + $select = null, + $sorter = [], + $filter = [] + ) { + $this->configuration->addColumn( + $name, + $definition, + $select, + $sorter, + $filter + ); + + $configArray = $this->configuration->toArray(); + $this->assertEquals($expected, $configArray); + } + + public function testExceptions() + { + $this->setExpectedException( + 'BadMethodCallException', + 'DatagridConfiguration::addColumn: name should not be empty' + ); + $this->configuration->addColumn(null, []); + + $this->setExpectedException( + 'BadMethodCallException', + 'DatagridConfiguration::updateLabel: name should not be empty' + ); + $this->configuration->updateLabel(null, []); + + $this->setExpectedException( + 'BadMethodCallException', + 'DatagridConfiguration::addSelect: select should not be empty' + ); + $this->configuration->addSelect(null); + } + + public function testUpdateLabel() + { + $this->configuration->updateLabel('testColumn', 'label1'); + + $configArray = $this->configuration->toArray(); + $this->assertEquals( + ['columns' => ['testColumn' => ['label' => 'label1']]], + $configArray + ); + + $this->configuration->updateLabel('testColumn1', null); + $configArray = $this->configuration->toArray(); + $this->assertEquals( + [ + 'columns' => [ + 'testColumn' => ['label' => 'label1'], + 'testColumn1' => ['label' => null], + ] + ], + $configArray + ); + + $this->configuration->updateLabel('testColumn', 'label2'); + $configArray = $this->configuration->toArray(); + $this->assertEquals( + [ + 'columns' => [ + 'testColumn' => ['label' => 'label2'], + 'testColumn1' => ['label' => null], + ] + ], + $configArray + ); + } + + public function testAddSelect() + { + $this->configuration->addSelect('testColumn'); + + $configArray = $this->configuration->toArray(); + $this->assertEquals( + [ + 'source' => [ + 'query' => ['select' => ['testColumn']], + ] + ], + $configArray + ); + } + + public function testJoinTable() + { + $this->configuration->joinTable('left', ['param' => 'value']); + + $configArray = $this->configuration->toArray(); + $this->assertEquals( + [ + 'source' => [ + 'query' => ['join' => ['left' => [['param' => 'value']]]], + ] + ], + $configArray + ); + } + + public function testRemoveColumn() + { + $this->configuration->addColumn('testColumn', ['param' => 123], null, ['param' => 123], ['param' => 123]); + + $configArray = $this->configuration->toArray(); + $this->assertTrue(isset($configArray['columns']['testColumn'])); + + $this->configuration->removeColumn('testColumn'); + $configArray = $this->configuration->toArray(); + + $this->assertEmpty($configArray['columns']); + $this->assertEmpty($configArray['sorters']['columns']); + $this->assertEmpty($configArray['filters']['columns']); + } + + /** + * @return array + */ + public function addColumnDataProvider() + { + return [ + 'all data supplied' => [ + 'expected' => [ + 'source' => [ + 'query' => ['select' => ['entity.testColumn1',]], + ], + 'columns' => ['testColumn1' => ['testParam1' => 'abc', 'testParam2' => 123,]], + 'sorters' => ['columns' => ['testColumn1' => ['data_name' => 'testColumn1']]], + 'filters' => ['columns' => ['testColumn1' => ['data_name' => 'testColumn1', 'type' => 'string']]], + ], + 'name' => 'testColumn1', + 'definition' => ['testParam1' => 'abc', 'testParam2' => 123,], + 'select' => 'entity.testColumn1', + 'sorter' => ['data_name' => 'testColumn1'], + 'filter' => ['data_name' => 'testColumn1', 'type' => 'string'], + ], + 'without sorter and filter' => [ + 'expected' => [ + 'source' => [ + 'query' => ['select' => ['entity.testColumn2',]], + ], + 'columns' => ['testColumn2' => ['testParam1' => 'abc', 'testParam2' => 123,]], + ], + 'name' => 'testColumn2', + 'definition' => ['testParam1' => 'abc', 'testParam2' => 123,], + 'select' => 'entity.testColumn2', + ], + 'without select part' => [ + 'expected' => [ + 'columns' => ['testColumn2' => ['testParam1' => 'abc', 'testParam2' => 123,]], + ], + 'name' => 'testColumn2', + 'definition' => ['testParam1' => 'abc', 'testParam2' => 123,], + ], + 'without sorter and select' => [ + 'expected' => [ + 'columns' => ['testColumn1' => ['testParam1' => 'abc', 'testParam2' => 123,]], + 'filters' => ['columns' => ['testColumn1' => ['data_name' => 'testColumn1', 'type' => 'string']]], + ], + 'name' => 'testColumn1', + 'definition' => ['testParam1' => 'abc', 'testParam2' => 123,], + 'select' => null, + 'sorter' => [], + 'filter' => ['data_name' => 'testColumn1', 'type' => 'string'], + ], + 'with empty definition' => [ + 'expected' => [ + 'columns' => ['testColumn1' => []], + ], + 'name' => 'testColumn1', + 'definition' => [], + ], + ]; + } +} diff --git a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Datasource/ResultRecordTest.php b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Datasource/ResultRecordTest.php index 47c8b273331..479d01989ea 100644 --- a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Datasource/ResultRecordTest.php +++ b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Datasource/ResultRecordTest.php @@ -74,4 +74,39 @@ public function getValueProvider() ], ]; } + + /** + * @dataProvider getRootEntityProvider + */ + public function testGetRootEntity($data, $expectedValue) + { + $resultRecord = new ResultRecord($data); + + $this->assertEquals($expectedValue, $resultRecord->getRootEntity()); + } + + public function getRootEntityProvider() + { + $obj = new \stdClass(); + $obj->item1 = 'val1'; + + return [ + [ + 'data' => [], + 'expectedValue' => null + ], + [ + 'data' => ['item1' => 'val1'], + 'expectedValue' => null + ], + [ + 'data' => $obj, + 'expectedValue' => $obj + ], + [ + 'data' => [['item1' => 'val1'], $obj], + 'expectedValue' => $obj + ], + ]; + } } diff --git a/src/Oro/Bundle/DataGridBundle/Tests/Unit/EventListener/GridViewsLoadListenerTest.php b/src/Oro/Bundle/DataGridBundle/Tests/Unit/EventListener/GridViewsLoadListenerTest.php index 0649675c22e..53c69e5e672 100644 --- a/src/Oro/Bundle/DataGridBundle/Tests/Unit/EventListener/GridViewsLoadListenerTest.php +++ b/src/Oro/Bundle/DataGridBundle/Tests/Unit/EventListener/GridViewsLoadListenerTest.php @@ -128,6 +128,7 @@ public function testListenerShouldAddViewsIntoEvent() 'deletable' => true, 'editable' => true, 'columns' => [], + 'is_default' => false ], [ 'label' => 'view2', @@ -138,6 +139,7 @@ public function testListenerShouldAddViewsIntoEvent() 'deletable' => true, 'editable' => true, 'columns' => [], + 'is_default' => false ], ] ]; diff --git a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/Columns/ColumnsExtensionTest.php b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/Columns/ColumnsExtensionTest.php index 6173b4fb986..a2a03895c50 100644 --- a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/Columns/ColumnsExtensionTest.php +++ b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/Columns/ColumnsExtensionTest.php @@ -118,6 +118,7 @@ public function testVisitMetadata( $dataInitialState, $isGridView = true ) { + $this->extension->setParameters(new ParameterBag([])); $user = $this->getMockBuilder('Oro\Bundle\UserBundle\Entity\User') ->disableOriginalConstructor() ->getMock(); @@ -137,7 +138,7 @@ public function testVisitMetadata( ->with('columns') ->will(static::returnValue($columnsConfigArray)); $config - ->expects(static::once()) + ->expects(static::any()) ->method('getName') ->will(static::returnValue('test-grid')); @@ -155,12 +156,12 @@ public function testVisitMetadata( ); $repository = $this->getMockBuilder('Oro\Bundle\DataGridBundle\Entity\Repository\GridViewRepository') - ->setMethods(['findGridViews']) + ->setMethods(['findGridViews', 'findDefaultGridView']) ->disableOriginalConstructor() ->getMock(); $this->registry - ->expects(static::once()) + ->expects(static::any()) ->method('getRepository') ->with('OroDataGridBundle:GridView') ->will(static::returnValue($repository)); diff --git a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/GridViews/GridViewsExtensionTest.php b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/GridViews/GridViewsExtensionTest.php index b37de7cd588..244e2b63fbf 100644 --- a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/GridViews/GridViewsExtensionTest.php +++ b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/GridViews/GridViewsExtensionTest.php @@ -12,6 +12,8 @@ class GridViewsExtensionTest extends \PHPUnit_Framework_TestCase { private $eventDispatcher; + + /** @var GridViewsExtension */ private $gridViewsExtension; public function setUp() @@ -29,16 +31,41 @@ public function setUp() ->method('isGranted') ->will($this->returnValue(true)); - $this->gridViewsExtension = new GridViewsExtension($this->eventDispatcher, $securityFacade, $translator); + $repo = $this->getMockBuilder('Doctrine\ORM\EntityRepository') + ->disableOriginalConstructor() + ->getMock(); + + $repo->expects($this->any()) + ->method('findDefaultGridView') + ->willReturn(null); + + $registry = $this->getMock('Doctrine\Common\Persistence\ManagerRegistry'); + $registry->expects($this->any()) + ->method('getRepository') + ->willReturn($repo); + + $aclHelper = $this->getMockBuilder('Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper') + ->disableOriginalConstructor() + ->getMock(); + + $this->gridViewsExtension = new GridViewsExtension( + $this->eventDispatcher, + $securityFacade, + $translator, + $registry, + $aclHelper + ); } public function testVisitMetadataShouldAddGridViewsFromEvent() { $this->gridViewsExtension->setParameters(new ParameterBag()); - $data = MetadataObject::create([]); - $config = DatagridConfiguration::create([ - DatagridConfiguration::NAME_KEY => 'grid', - ]); + $data = MetadataObject::create([]); + $config = DatagridConfiguration::create( + [ + DatagridConfiguration::NAME_KEY => 'grid', + ] + ); $this->eventDispatcher ->expects($this->once()) @@ -64,11 +91,15 @@ public function testVisitMetadataShouldAddGridViewsFromEvent() ->expects($this->once()) ->method('dispatch') ->with(GridViewsLoadEvent::EVENT_NAME) - ->will($this->returnCallback(function ($eventName, GridViewsLoadEvent $event) use ($expectedViews) { - $event->setGridViews($expectedViews); + ->will( + $this->returnCallback( + function ($eventName, GridViewsLoadEvent $event) use ($expectedViews) { + $event->setGridViews($expectedViews); - return $event; - })); + return $event; + } + ) + ); $this->assertFalse($data->offsetExists('gridViews')); $this->gridViewsExtension->visitMetadata($config, $data); @@ -76,6 +107,53 @@ public function testVisitMetadataShouldAddGridViewsFromEvent() $this->assertEquals($expectedViews, $data->offsetGet('gridViews')); } + + /** + * @param array $input + * @param bool $expected + * + * @dataProvider isApplicableDataProvider + */ + public function testIsApplicable($input, $expected) + { + $this->gridViewsExtension->setParameters(new ParameterBag($input)); + $config = DatagridConfiguration::create( + [ + DatagridConfiguration::NAME_KEY => 'grid', + ] + ); + $this->assertEquals($expected, $this->gridViewsExtension->isApplicable($config)); + } + + /** + * @return array + */ + public function isApplicableDataProvider() + { + return [ + 'Default' => [ + 'input' => [], + 'expected' => true, + ], + 'Extension disabled' => [ + 'input' => [ + '_grid_view' => [ + '_disabled' => true + ] + ], + 'expected' => false, + ], + 'Extension enabled' => [ + 'input' => [ + '_grid_view' => [ + '_disabled' => false + ] + ], + 'expected' => true, + ], + ]; + } + /** * @param array $input * @param array $expected diff --git a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/MassAction/Actions/Ajax/MassAction/MassDeleteLimiterTest.php b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/MassAction/Actions/Ajax/MassAction/MassDeleteLimiterTest.php new file mode 100644 index 00000000000..542af95c17f --- /dev/null +++ b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/MassAction/Actions/Ajax/MassAction/MassDeleteLimiterTest.php @@ -0,0 +1,154 @@ +helper = $this + ->getMockBuilder('Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper') + ->disableOriginalConstructor() + ->getMock(); + $this->limiter = new MassDeleteLimiter($this->helper); + } + + /** + * @dataProvider getLimitationCodeDataProvider + * + * @param MassDeleteLimitResult $limitResult + * @param int $result + */ + public function testGetLimitationCode(MassDeleteLimitResult $limitResult, $result) + { + $this->assertEquals($result, $this->limiter->getLimitationCode($limitResult)); + } + + /** + * @dataProvider getLimitQueryDataProvider + * + * @param MassDeleteLimitResult $limitResult + * @param bool $accessRestriction + * @param bool $maxLimitRestriction + * + * @internal param $MassDeleteLimitResult $ + */ + public function testLimitQuery( + MassDeleteLimitResult $limitResult, + $accessRestriction = false, + $maxLimitRestriction = false + ) { + /** @var QueryBuilder|\PHPUnit_Framework_MockObject_MockObject $queryBuilder */ + $queryBuilder = $this + ->getMockBuilder('Doctrine\ORM\QueryBuilder') + ->disableOriginalConstructor() + ->getMock(); + + /** @var MassActionHandlerArgs|\PHPUnit_Framework_MockObject_MockObject $args */ + $args = $this + ->getMockBuilder('Oro\Bundle\DataGridBundle\Extension\MassAction\MassActionHandlerArgs') + ->disableOriginalConstructor() + ->getMock(); + + $args + ->expects($this->once()) + ->method('getResults') + ->willReturn(new DeletionIterableResult($queryBuilder)); + + if ($accessRestriction) { + $this->helper + ->expects($this->once()) + ->method('apply') + ->with($queryBuilder, 'DELETE'); + } + + if ($maxLimitRestriction) { + $queryBuilder + ->expects($this->once()) + ->method('setMaxResults') + ->with($limitResult->getMaxLimit()); + } + + $this->limiter->limitQuery($limitResult, $args); + } + + public function getLimitQueryDataProvider() + { + return [ + 'no limits' => [ + $this->getMassDeleteResult(MassDeleteLimiter::NO_LIMIT) + ], + 'limit by access' => [ + $this->getMassDeleteResult(MassDeleteLimiter::LIMIT_ACCESS), + true + ], + 'limit by max records' => [ + $this->getMassDeleteResult(MassDeleteLimiter::LIMIT_MAX_RECORDS), + false, + true + ], + 'limit by access and max records' => [ + $this->getMassDeleteResult(MassDeleteLimiter::LIMIT_ACCESS_MAX_RECORDS), + true, + true + ], + ]; + } + + public function getLimitationCodeDataProvider() + { + return [ + 'no limits code' => [ + $this->getMassDeleteResult(MassDeleteLimiter::NO_LIMIT), + MassDeleteLimiter::NO_LIMIT + ], + 'limit by access code' => [ + $this->getMassDeleteResult(MassDeleteLimiter::LIMIT_ACCESS), + MassDeleteLimiter::LIMIT_ACCESS + ], + 'limit by max records code' => [ + $this->getMassDeleteResult(MassDeleteLimiter::LIMIT_MAX_RECORDS), + MassDeleteLimiter::LIMIT_MAX_RECORDS + ], + 'limit by access and max records code' => [ + $this->getMassDeleteResult(MassDeleteLimiter::LIMIT_ACCESS_MAX_RECORDS), + MassDeleteLimiter::LIMIT_ACCESS_MAX_RECORDS + ], + ]; + } + + protected function getMassDeleteResult($code) + { + switch ($code) { + case MassDeleteLimiter::LIMIT_ACCESS: + $result = new MassDeleteLimitResult(100, 50, 1000); + break; + case MassDeleteLimiter::LIMIT_MAX_RECORDS: + $result = new MassDeleteLimitResult(2000, 2000, 1000); + break; + case MassDeleteLimiter::LIMIT_ACCESS_MAX_RECORDS: + $result = new MassDeleteLimitResult(2000, 1500, 1000); + break; + default: + $result = new MassDeleteLimitResult(100, 100, 1000); + break; + } + + return $result; + } +} diff --git a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/MassAction/MassActionExtensionTest.php b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/MassAction/MassActionExtensionTest.php new file mode 100644 index 00000000000..977675b98f0 --- /dev/null +++ b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/MassAction/MassActionExtensionTest.php @@ -0,0 +1,59 @@ +container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + + $this->securityFacade = $this->getMockBuilder('Oro\Bundle\SecurityBundle\SecurityFacade') + ->disableOriginalConstructor() + ->getMock(); + + $this->translator = $this->getMock('Symfony\Component\Translation\TranslatorInterface'); + + $this->extension = new MassActionExtension($this->container, $this->securityFacade, $this->translator); + } + + protected function tearDown() + { + unset($this->extension, $this->container, $this->securityFacade, $this->translator); + } + + public function testIsApplicable() + { + $this->assertTrue($this->extension->isApplicable(DatagridConfiguration::create([]))); + } + + public function testVisitResult() + { + $result = ResultsObject::create([]); + + $this->extension->visitResult(DatagridConfiguration::create([]), $result); + + $this->assertArrayHasKey('metadata', $result); + $this->assertInternalType('array', $result['metadata']); + } +} diff --git a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/Totals/OrmTotalsExtensionTest.php b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/Totals/OrmTotalsExtensionTest.php index 0b1efe0c112..5e435b346df 100644 --- a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/Totals/OrmTotalsExtensionTest.php +++ b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Extension/Totals/OrmTotalsExtensionTest.php @@ -2,19 +2,12 @@ namespace Oro\Bundle\DataGridBundle\Tests\Unit\Extension\Totals; -use Doctrine\Common\Annotations\AnnotationReader; -use Doctrine\ORM\Mapping\Driver\AnnotationDriver; -use Doctrine\ORM\QueryBuilder; - -use Oro\Bundle\DataGridBundle\Datagrid\Builder; use Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration; use Oro\Bundle\DataGridBundle\Datagrid\Common\MetadataObject; use Oro\Bundle\DataGridBundle\Datagrid\Common\ResultsObject; -use Oro\Bundle\DataGridBundle\Datasource\Orm\OrmDatasource; use Oro\Bundle\DataGridBundle\Extension\Totals\OrmTotalsExtension; use Oro\Bundle\DataGridBundle\Extension\Totals\Configuration; -use Oro\Bundle\TestFrameworkBundle\Test\Doctrine\ORM\Mocks\EntityManagerMock; use Oro\Bundle\TestFrameworkBundle\Test\Doctrine\ORM\OrmTestCase; class OrmTotalsExtensionTest extends OrmTestCase @@ -77,7 +70,7 @@ protected function setUp() public function testIsApplicable() { $this->assertTrue($this->extension->isApplicable($this->config)); - $this->config->offsetSetByPath(Builder::DATASOURCE_TYPE_PATH, 'non_orm'); + $this->config->offsetSetByPath(DatagridConfiguration::DATASOURCE_TYPE_PATH, 'non_orm'); $this->assertFalse($this->extension->isApplicable($this->config)); } diff --git a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Tools/GridConfigurationHelperTest.php b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Tools/GridConfigurationHelperTest.php new file mode 100644 index 00000000000..097d4e5fc18 --- /dev/null +++ b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Tools/GridConfigurationHelperTest.php @@ -0,0 +1,82 @@ +entityClassResolver = $this->getMockBuilder('Oro\Bundle\EntityBundle\ORM\EntityClassResolver') + ->disableOriginalConstructor() + ->getMock(); + $this->entityClassResolver->expects($this->any()) + ->method('getEntityClass') + ->with(self::ENTITY_CLASS) + ->willReturn(self::ENTITY_CLASS); + + $this->gridConfigurationHelper = new GridConfigurationHelper($this->entityClassResolver); + } + + /** + * @dataProvider getDatagridConfigurationDataProvider + * + * @param array $param + */ + public function testGetEntity($param) + { + $config = DatagridConfiguration::create($param); + $this->assertEquals(self::ENTITY_CLASS, $this->gridConfigurationHelper->getEntity($config)); + } + + /** + * @dataProvider getDatagridConfigurationDataProvider + * + * @param array $param + */ + public function testGetRootAlias($param) + { + $config = DatagridConfiguration::create($param); + $this->assertEquals(self::ENTITY_ALIAS, $this->gridConfigurationHelper->getEntityRootAlias($config)); + } + + public function getDatagridConfigurationDataProvider() + { + $source = [ + 'query' => [ + 'from' => [ + [ + 'table' => self::ENTITY_CLASS, + 'alias' => self::ENTITY_ALIAS + ] + ] + ] + + ]; + + return [ + 'with extended entity name' => [ + [ + 'extended_entity_name' => self::ENTITY_CLASS, + 'source' => $source + ] + ], + 'without extended entity name' => [ + [ + 'source' => $source + ] + ], + ]; + } +} diff --git a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Twig/DataGridExtensionTest.php b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Twig/DataGridExtensionTest.php index 5962ffcadd6..a7fc3528cd9 100644 --- a/src/Oro/Bundle/DataGridBundle/Tests/Unit/Twig/DataGridExtensionTest.php +++ b/src/Oro/Bundle/DataGridBundle/Tests/Unit/Twig/DataGridExtensionTest.php @@ -5,7 +5,6 @@ use Oro\Bundle\DataGridBundle\Datagrid\DatagridInterface; use Symfony\Component\Routing\RouterInterface; -use Oro\Bundle\DataGridBundle\Datagrid\Builder; use Oro\Bundle\DataGridBundle\Datagrid\ManagerInterface; use Oro\Bundle\DataGridBundle\Datagrid\NameStrategyInterface; use Oro\Bundle\DataGridBundle\Twig\DataGridExtension; @@ -85,15 +84,11 @@ public function testGetGridWorks() $configuration = $this->getMockBuilder('Oro\\Bundle\\DataGridBundle\\Datagrid\\Common\\DatagridConfiguration') ->disableOriginalConstructor() + ->setMethods(['getAclResource']) ->getMock(); - $configuration->expects($this->at(0)) - ->method('offsetGetByPath') - ->with(Builder::DATASOURCE_ACL_PATH) - ->will($this->returnValue(null)); - $configuration->expects($this->at(1)) - ->method('offsetGetByPath') - ->with(Builder::DATASOURCE_SKIP_ACL_CHECK) + $configuration->expects($this->once()) + ->method('getAclResource') ->will($this->returnValue(null)); $this->manager->expects($this->once()) @@ -131,14 +126,9 @@ public function testGetGridReturnsNullWhenDontHavePermissions() ->disableOriginalConstructor() ->getMock(); - $configuration->expects($this->at(0)) - ->method('offsetGetByPath') - ->with(Builder::DATASOURCE_ACL_PATH) + $configuration->expects($this->once()) + ->method('getAclResource') ->will($this->returnValue($acl)); - $configuration->expects($this->at(1)) - ->method('offsetGetByPath') - ->with(Builder::DATASOURCE_SKIP_ACL_CHECK) - ->will($this->returnValue(false)); $this->manager->expects($this->once()) ->method('getConfigurationForGrid') diff --git a/src/Oro/Bundle/DataGridBundle/Tools/GridConfigurationHelper.php b/src/Oro/Bundle/DataGridBundle/Tools/GridConfigurationHelper.php new file mode 100644 index 00000000000..72e2a692e59 --- /dev/null +++ b/src/Oro/Bundle/DataGridBundle/Tools/GridConfigurationHelper.php @@ -0,0 +1,55 @@ +entityClassResolver = $entityClassResolver; + } + + /** + * @param DatagridConfiguration $config + * + * @return string|null + */ + public function getEntity(DatagridConfiguration $config) + { + $entityClassName = $config->offsetGetByPath('[extended_entity_name]'); + if ($entityClassName) { + return $entityClassName; + } + + $from = $config->offsetGetByPath('[source][query][from]'); + if (!$from) { + return null; + } + + return $this->entityClassResolver->getEntityClass($from[0]['table']); + } + + /** + * @param DatagridConfiguration $config + * + * @return null + */ + public function getEntityRootAlias(DatagridConfiguration $config) + { + $from = $config->offsetGetByPath('[source][query][from]'); + if ($from) { + return $from[0]['alias']; + } + + return null; + } +} diff --git a/src/Oro/Bundle/DataGridBundle/Twig/DataGridExtension.php b/src/Oro/Bundle/DataGridBundle/Twig/DataGridExtension.php index 8bef690d211..5b236cca61a 100644 --- a/src/Oro/Bundle/DataGridBundle/Twig/DataGridExtension.php +++ b/src/Oro/Bundle/DataGridBundle/Twig/DataGridExtension.php @@ -4,7 +4,6 @@ use Symfony\Component\Routing\RouterInterface; -use Oro\Bundle\DataGridBundle\Datagrid\Builder; use Oro\Bundle\DataGridBundle\Datagrid\DatagridInterface; use Oro\Bundle\DataGridBundle\Datagrid\ManagerInterface; use Oro\Bundle\DataGridBundle\Datagrid\NameStrategyInterface; @@ -183,9 +182,8 @@ protected function isAclGrantedForGridName($gridName) $gridConfig = $this->manager->getConfigurationForGrid($gridName); if ($gridConfig) { - $acl = $gridConfig->offsetGetByPath(Builder::DATASOURCE_ACL_PATH); - $aclSKip = $gridConfig->offsetGetByPath(Builder::DATASOURCE_SKIP_ACL_CHECK, false); - if (!$aclSKip && $acl && !$this->securityFacade->isGranted($acl)) { + $aclResource = $gridConfig->getAclResource(); + if ($aclResource && !$this->securityFacade->isGranted($aclResource)) { return false; } else { return true; diff --git a/src/Oro/Bundle/EmailBundle/Acl/Voter/EmailVoter.php b/src/Oro/Bundle/EmailBundle/Acl/Voter/EmailVoter.php index bb725b5eb08..a73d68d83f8 100644 --- a/src/Oro/Bundle/EmailBundle/Acl/Voter/EmailVoter.php +++ b/src/Oro/Bundle/EmailBundle/Acl/Voter/EmailVoter.php @@ -85,8 +85,8 @@ public function vote(TokenInterface $token, $object, array $attributes) if ($mailbox = $emailUser->getMailboxOwner() !== null && $token instanceof UsernamePasswordOrganizationToken ) { - $repo = $this->container->get('doctrine')->getRepository('OroEmailBundle:Mailbox'); - $mailboxes = $repo->findAvailableMailboxes( + $manager = $this->container->get('oro_email.mailbox.manager'); + $mailboxes = $manager->findAvailableMailboxes( $token->getUser(), $token->getOrganizationContext() ); diff --git a/src/Oro/Bundle/EmailBundle/Builder/Helper/EmailModelBuilderHelper.php b/src/Oro/Bundle/EmailBundle/Builder/Helper/EmailModelBuilderHelper.php index f6b04a9a670..03d1524dfce 100644 --- a/src/Oro/Bundle/EmailBundle/Builder/Helper/EmailModelBuilderHelper.php +++ b/src/Oro/Bundle/EmailBundle/Builder/Helper/EmailModelBuilderHelper.php @@ -13,6 +13,7 @@ use Oro\Bundle\EmailBundle\Entity\EmailOwnerInterface; use Oro\Bundle\EmailBundle\Entity\Mailbox; use Oro\Bundle\EmailBundle\Entity\Manager\EmailAddressManager; +use Oro\Bundle\EmailBundle\Entity\Manager\MailboxManager; use Oro\Bundle\EmailBundle\Exception\LoadEmailBodyException; use Oro\Bundle\EmailBundle\Model\EmailHolderInterface; use Oro\Bundle\EmailBundle\Tools\EmailAddressHelper; @@ -67,6 +68,11 @@ class EmailModelBuilderHelper */ protected $templating; + /** + * @var MailboxManager + */ + protected $mailboxManager; + /** * @param EntityRoutingHelper $entityRoutingHelper * @param EmailAddressHelper $emailAddressHelper @@ -76,6 +82,7 @@ class EmailModelBuilderHelper * @param EntityManager $entityManager * @param EmailCacheManager $emailCacheManager * @param EngineInterface $engineInterface + * @param MailboxManager $mailboxManager */ public function __construct( EntityRoutingHelper $entityRoutingHelper, @@ -85,7 +92,8 @@ public function __construct( EmailAddressManager $emailAddressManager, EntityManager $entityManager, EmailCacheManager $emailCacheManager, - EngineInterface $engineInterface + EngineInterface $engineInterface, + MailboxManager $mailboxManager ) { $this->entityRoutingHelper = $entityRoutingHelper; $this->emailAddressHelper = $emailAddressHelper; @@ -95,6 +103,7 @@ public function __construct( $this->entityManager = $entityManager; $this->emailCacheManager = $emailCacheManager; $this->templating = $engineInterface; + $this->mailboxManager = $mailboxManager; } /** @@ -262,7 +271,7 @@ public function getTargetEntity($entityClass, $entityId) */ public function getMailboxes() { - $mailboxes = $this->entityManager->getRepository('OroEmailBundle:Mailbox')->findAvailableMailboxes( + $mailboxes = $this->mailboxManager->findAvailableMailboxes( $this->getUser(), $this->getOrganization() ); diff --git a/src/Oro/Bundle/EmailBundle/Datagrid/EmailQueryFactory.php b/src/Oro/Bundle/EmailBundle/Datagrid/EmailQueryFactory.php index c72eeadcb8a..e13a92b9baf 100644 --- a/src/Oro/Bundle/EmailBundle/Datagrid/EmailQueryFactory.php +++ b/src/Oro/Bundle/EmailBundle/Datagrid/EmailQueryFactory.php @@ -5,8 +5,10 @@ use Doctrine\Bundle\DoctrineBundle\Registry; use Doctrine\ORM\QueryBuilder; -use Oro\Bundle\EntityBundle\Provider\EntityNameResolver; +use Oro\Bundle\EmailBundle\Entity\Manager\MailboxManager; use Oro\Bundle\EmailBundle\Entity\Provider\EmailOwnerProviderStorage; +use Oro\Bundle\EntityBundle\Provider\EntityNameResolver; +use Oro\Bundle\OrganizationBundle\Entity\Organization; use Oro\Bundle\SecurityBundle\SecurityFacade; class EmailQueryFactory @@ -21,7 +23,7 @@ class EmailQueryFactory protected $fromEmailExpression; /** @var Registry */ - protected $doctrine; + protected $mailboxManager; /** @var SecurityFacade */ protected $securityFacade; @@ -29,18 +31,18 @@ class EmailQueryFactory /** * @param EmailOwnerProviderStorage $emailOwnerProviderStorage * @param EntityNameResolver $entityNameResolver - * @param Registry $doctrine + * @param MailboxManager $mailboxManager * @param SecurityFacade $securityFacade */ public function __construct( EmailOwnerProviderStorage $emailOwnerProviderStorage, EntityNameResolver $entityNameResolver, - Registry $doctrine, + MailboxManager $mailboxManager, SecurityFacade $securityFacade ) { $this->emailOwnerProviderStorage = $emailOwnerProviderStorage; $this->entityNameResolver = $entityNameResolver; - $this->doctrine = $doctrine; + $this->mailboxManager = $mailboxManager; $this->securityFacade = $securityFacade; } @@ -66,14 +68,16 @@ public function prepareQuery(QueryBuilder $qb, $emailFromTableAlias = 'a') public function applyAcl(QueryBuilder $qb) { $user = $this->securityFacade->getLoggedUser(); - $organization = $this->securityFacade->getOrganization(); + $organization = $this->getOrganization(); - $mailboxIds = $this->doctrine->getRepository('OroEmailBundle:Mailbox') - ->findAvailableMailboxIds($user, $organization); - $uoCheck = $qb->expr()->andX( - $qb->expr()->eq('eu.owner', ':owner'), - $qb->expr()->eq('eu.organization ', ':organization') - ); + $mailboxIds = $this->mailboxManager->findAvailableMailboxIds($user, $organization); + + $exprs = [$qb->expr()->eq('eu.owner', ':owner')]; + if ($organization) { + $exprs[] = $qb->expr()->eq('eu.organization ', ':organization'); + $qb->setParameter('organization', $organization->getId()); + } + $uoCheck = call_user_func_array([$qb->expr(), 'andX'], $exprs); if (!empty($mailboxIds)) { $qb->andWhere( @@ -87,7 +91,14 @@ public function applyAcl(QueryBuilder $qb) $qb->andWhere($uoCheck); } $qb->setParameter('owner', $user->getId()); - $qb->setParameter('organization', $organization->getId()); + } + + /** + * @return Organization|null + */ + protected function getOrganization() + { + return $this->securityFacade->getOrganization(); } /** diff --git a/src/Oro/Bundle/EmailBundle/Datagrid/MailboxChoiceList.php b/src/Oro/Bundle/EmailBundle/Datagrid/MailboxChoiceList.php index 3e5805b066e..34e8bd02e05 100644 --- a/src/Oro/Bundle/EmailBundle/Datagrid/MailboxChoiceList.php +++ b/src/Oro/Bundle/EmailBundle/Datagrid/MailboxChoiceList.php @@ -6,7 +6,9 @@ use Oro\Bundle\EmailBundle\Entity\EmailOrigin; use Oro\Bundle\EmailBundle\Entity\Mailbox; +use Oro\Bundle\EmailBundle\Entity\Manager\MailboxManager; use Oro\Bundle\SecurityBundle\SecurityFacade; +use Oro\Bundle\OrganizationBundle\Entity\Organization; class MailboxChoiceList { @@ -16,14 +18,19 @@ class MailboxChoiceList /** @var SecurityFacade */ private $securityFacade; + /** @var MailboxManager */ + private $mailboxManager; + /** * @param Registry $doctrine * @param SecurityFacade $securityFacade + * @param MailboxManager $mailboxManager */ - public function __construct(Registry $doctrine, SecurityFacade $securityFacade) + public function __construct(Registry $doctrine, SecurityFacade $securityFacade, MailboxManager $mailboxManager) { $this->doctrine = $doctrine; $this->securityFacade = $securityFacade; + $this->mailboxManager = $mailboxManager; } /** @@ -33,14 +40,15 @@ public function __construct(Registry $doctrine, SecurityFacade $securityFacade) */ public function getChoiceList() { - $repo = $this->doctrine->getRepository('OroEmailBundle:Mailbox'); - /** @var Mailbox[] $systemMailboxes */ - $systemMailboxes = $repo->findAvailableMailboxes( + $systemMailboxes = $this->mailboxManager->findAvailableMailboxes( $this->securityFacade->getLoggedUser(), - $this->securityFacade->getOrganization() + $this->getOrganization() + ); + $origins = $this->mailboxManager->findAvailableOrigins( + $this->securityFacade->getLoggedUser(), + $this->getOrganization() ); - $origins = $this->getOriginsList(); $choiceList = []; foreach ($origins as $origin) { @@ -59,15 +67,10 @@ public function getChoiceList() } /** - * @return EmailOrigin[] + * @return Organization|null */ - protected function getOriginsList() + protected function getOrganization() { - $criteria = [ - 'owner' => $this->securityFacade->getLoggedUser(), - 'isActive' => true, - ]; - - return $this->doctrine->getRepository('OroEmailBundle:EmailOrigin')->findBy($criteria); + return $this->securityFacade->getOrganization(); } } diff --git a/src/Oro/Bundle/EmailBundle/Entity/Email.php b/src/Oro/Bundle/EmailBundle/Entity/Email.php index 2722ecc78a0..441341ebc86 100644 --- a/src/Oro/Bundle/EmailBundle/Entity/Email.php +++ b/src/Oro/Bundle/EmailBundle/Entity/Email.php @@ -41,6 +41,10 @@ * "acl"="oro_email_email_view", * "action_button_widget"="oro_send_email_button", * "action_link_widget"="oro_send_email_link" + * }, + * "grid"={ + * "default"="email-grid", + * "context"="email-for-context-grid" * } * } * ) diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailUser.php b/src/Oro/Bundle/EmailBundle/Entity/EmailUser.php index ab29708b512..430492d706d 100644 --- a/src/Oro/Bundle/EmailBundle/Entity/EmailUser.php +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailUser.php @@ -42,6 +42,8 @@ */ class EmailUser { + const ENTITY_CLASS = 'Oro\Bundle\EmailBundle\Entity\EmailUser'; + /** * @var integer * diff --git a/src/Oro/Bundle/EmailBundle/Entity/Manager/MailboxManager.php b/src/Oro/Bundle/EmailBundle/Entity/Manager/MailboxManager.php new file mode 100644 index 00000000000..43612faae7c --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/Manager/MailboxManager.php @@ -0,0 +1,75 @@ +registry = $registry; + } + + /** + * Returns a list of ids of mailboxes available to user logged under organization. + * + * @param User|integer $user User or user id + * @param Organization $organization + * + * @return array Array of ids + */ + public function findAvailableMailboxIds($user, $organization) + { + $mailboxes = $this->findAvailableMailboxes($user, $organization); + + $ids = []; + foreach ($mailboxes as $mailbox) { + $ids[] = $mailbox->getId(); + } + + return $ids; + } + + /** + * Returns a list of mailboxes available to user logged under organization. + * + * @param User|integer $user User or user id + * @param Organization $organization|null + * + * @return Collection|Mailbox[] Array or collection of Mailboxes + */ + public function findAvailableMailboxes($user, Organization $organization = null) + { + $qb = $this->registry->getRepository('OroEmailBundle:Mailbox') + ->createAvailableMailboxesQuery($user, $organization); + + return $qb->getQuery()->getResult(); + } + + /** + * @param User $user + * @param Organization $organization + * + * @return EmailOrigin + */ + public function findAvailableOrigins(User $user, Organization $organization) + { + return $this->registry->getRepository('OroEmailBundle:EmailOrigin')->findBy([ + 'owner' => $user, + 'organization' => $organization, + 'isActive' => true, + ]); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/Repository/EmailUserRepository.php b/src/Oro/Bundle/EmailBundle/Entity/Repository/EmailUserRepository.php index 451542dda47..8039a674f80 100644 --- a/src/Oro/Bundle/EmailBundle/Entity/Repository/EmailUserRepository.php +++ b/src/Oro/Bundle/EmailBundle/Entity/Repository/EmailUserRepository.php @@ -79,10 +79,11 @@ public function getEmailUserList(User $user, Organization $organization, array $ /** * @param array $ids * @param EmailFolder $folder + * @param \DateTime $date * * @return array */ - public function getInvertedIdsFromFolder(array $ids, EmailFolder $folder) + public function getInvertedIdsFromFolder(array $ids, EmailFolder $folder, $date = null) { $qb = $this->createQueryBuilder('email_user'); @@ -96,6 +97,11 @@ public function getInvertedIdsFromFolder(array $ids, EmailFolder $folder) ->setParameter('ids', $ids); } + if ($date) { + $qb->andWhere($qb->expr()->gt('email_user.receivedAt', ':date')) + ->setParameter('date', $date); + } + $emailUserIds = $qb->getQuery()->getArrayResult(); $ids = []; diff --git a/src/Oro/Bundle/EmailBundle/Entity/Repository/MailboxRepository.php b/src/Oro/Bundle/EmailBundle/Entity/Repository/MailboxRepository.php index e0bec7e20b4..9acd4277551 100644 --- a/src/Oro/Bundle/EmailBundle/Entity/Repository/MailboxRepository.php +++ b/src/Oro/Bundle/EmailBundle/Entity/Repository/MailboxRepository.php @@ -69,17 +69,21 @@ public function findAvailableMailboxIds($user, $organization) } /** + * Creates query for mailboxes available to user logged under organization. + * If no organization is provided, does not filter by it (useful when looking for mailboxes across organizations). + * * @param User|integer $user User or user id - * @param Organization $organization + * @param Organization|integer|null $organization * * @return \Doctrine\ORM\QueryBuilder */ - protected function createAvailableMailboxesQuery($user, $organization) + public function createAvailableMailboxesQuery($user, $organization = null) { if (!$user instanceof User) { $user = $this->getEntityManager()->getRepository('OroUserBundle:User')->find($user); } - if (!$organization instanceof Organization) { + + if ($organization !== null && !$organization instanceof Organization) { $organization = $this->getEntityManager() ->getRepository('OroOrganizationBundle:Organization') ->find($organization); @@ -92,7 +96,6 @@ protected function createAvailableMailboxesQuery($user, $organization) ->from('OroEmailBundle:Mailbox', 'mb') ->leftJoin('mb.authorizedUsers', 'au') ->leftJoin('mb.authorizedRoles', 'ar') - ->andWhere('mb.organization = :organization') ->andWhere( $qb->expr()->orX( 'au = :user', @@ -101,7 +104,12 @@ protected function createAvailableMailboxesQuery($user, $organization) ); $qb->setParameter('user', $user); $qb->setParameter('roles', $roles); - $qb->setParameter('organization', $organization); + + if ($organization) { + $qb + ->andWhere('mb.organization = :organization') + ->setParameter('organization', $organization); + } return $qb; } diff --git a/src/Oro/Bundle/EmailBundle/EventListener/ActivityListPreQueryBuildListener.php b/src/Oro/Bundle/EmailBundle/EventListener/ActivityListPreQueryBuildListener.php new file mode 100644 index 00000000000..51e26af65e7 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/EventListener/ActivityListPreQueryBuildListener.php @@ -0,0 +1,44 @@ +doctrineHelper = $doctrineHelper; + } + + /** + * Add email thread ids to qb params + * + * @param ActivityListPreQueryBuildEvent $event + */ + public function prepareIdsForEmailThreadEvent(ActivityListPreQueryBuildEvent $event) + { + if ($event->getTargetClass() === Email::ENTITY_CLASS) { + /** @var Email $email */ + $email = $this->doctrineHelper->getEntity(Email::ENTITY_CLASS, $event->getTargetId()); + if ($email->getThread()) { + $emailIds = array_map( + function ($emailEntity) { + return $emailEntity->getId(); + }, + $email->getThread()->getEmails()->toArray() + ); + $event->setTargetIds($emailIds); + } + } + } +} diff --git a/src/Oro/Bundle/EmailBundle/EventListener/MailboxProcessTriggerListener.php b/src/Oro/Bundle/EmailBundle/EventListener/MailboxProcessTriggerListener.php index 5876234d132..b91dd1e9ca3 100644 --- a/src/Oro/Bundle/EmailBundle/EventListener/MailboxProcessTriggerListener.php +++ b/src/Oro/Bundle/EmailBundle/EventListener/MailboxProcessTriggerListener.php @@ -4,6 +4,7 @@ use Doctrine\Bundle\DoctrineBundle\Registry; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Event\OnFlushEventArgs; use Doctrine\ORM\Event\PostFlushEventArgs; use Oro\Bundle\EmailBundle\Entity\EmailBody; @@ -38,6 +39,20 @@ public function __construct( $this->doctrine = $doctrine; } + /** + * {@inheritdoc} In addition it filters out emails which are part of thread + */ + public function onFlush(OnFlushEventArgs $args) + { + parent::onFlush($args); + $this->emailBodies = array_filter( + $this->emailBodies, + function (EmailBody $body) { + return $body->getEmail() && !$body->getEmail()->getThread(); + } + ); + } + /** * Processes email bodies using processes provided in MailboxProcessProviders. * Processes are triggered using this listener instead of normal triggers. diff --git a/src/Oro/Bundle/EmailBundle/EventListener/PrepareContextTitleListener.php b/src/Oro/Bundle/EmailBundle/EventListener/PrepareContextTitleListener.php new file mode 100644 index 00000000000..727eafd839d --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/EventListener/PrepareContextTitleListener.php @@ -0,0 +1,47 @@ +router = $router; + $this->doctrineHelper = $doctrineHelper; + } + + /** + * Correct link and title for email context by EmailUser entity index + * + * @param PrepareContextTitleEvent $event + */ + public function prepareEmailContextTitleEvent(PrepareContextTitleEvent $event) + { + if ($event->getTargetClass() === Email::ENTITY_CLASS) { + $item = $event->getItem(); + /** @var Email $email */ + $email = $this->doctrineHelper->getEntity(Email::ENTITY_CLASS, $item['targetId']); + $item['title'] = $email->getSubject(); + $item['link'] = $this->router->generate('oro_email_thread_view', ['id' => $item['targetId']], true); + $event->setItem($item); + } + } +} diff --git a/src/Oro/Bundle/EmailBundle/EventListener/PrepareResultItemListener.php b/src/Oro/Bundle/EmailBundle/EventListener/PrepareResultItemListener.php new file mode 100644 index 00000000000..59a118f4898 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/EventListener/PrepareResultItemListener.php @@ -0,0 +1,39 @@ +router = $router; + } + + /** + * Change search results view for Email entity + * + * @param PrepareResultItemEvent $event + */ + public function prepareEmailItemDataEvent(PrepareResultItemEvent $event) + { + if ($event->getResultItem()->getEntityName() === EmailUser::ENTITY_CLASS) { + $id = $event->getResultItem()->getEntity()->getEmail()->getId(); + $event->getResultItem()->setRecordId($id); + $event->getResultItem()->setEntityName(Email::ENTITY_CLASS); + $route = $this->router->generate('oro_email_thread_view', ['id' => $id], true); + $event->getResultItem()->setRecordUrl($route); + } + } +} diff --git a/src/Oro/Bundle/EmailBundle/EventListener/SearchAliasesListener.php b/src/Oro/Bundle/EmailBundle/EventListener/SearchAliasesListener.php new file mode 100644 index 00000000000..9866b98a442 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/EventListener/SearchAliasesListener.php @@ -0,0 +1,23 @@ +getAliases(); + if (in_array(Email::ENTITY_CLASS, $event->getTargetClasses(), true)) { + $aliases[] = 'oro_email'; + $event->setAliases($aliases); + } + } +} diff --git a/src/Oro/Bundle/EmailBundle/Form/Type/EmailFolderTreeType.php b/src/Oro/Bundle/EmailBundle/Form/Type/EmailFolderTreeType.php index a36a9a7a7bc..7f9f7008dff 100644 --- a/src/Oro/Bundle/EmailBundle/Form/Type/EmailFolderTreeType.php +++ b/src/Oro/Bundle/EmailBundle/Form/Type/EmailFolderTreeType.php @@ -2,28 +2,73 @@ namespace Oro\Bundle\EmailBundle\Form\Type; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; + use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; use Symfony\Component\OptionsResolver\OptionsResolverInterface; +use Oro\Bundle\EmailBundle\Entity\EmailFolder; + class EmailFolderTreeType extends AbstractType { /** - * {@inheritDoc} + * {@inheritdoc} */ - public function setDefaultOptions(OptionsResolverInterface $resolver) + public function buildForm(FormBuilderInterface $builder, array $options) { - $resolver->setDefaults([ - 'type' => 'oro_email_email_folder', - 'allow_add' => true, - ]); + $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) { + $data = $event->getData(); + if ($data !== null) { + return; + } + + $event->setData(new ArrayCollection()); + }); + + $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) { + $collection = $event->getForm()->getData(); + $data = $event->getData(); + if (!$data || !$collection instanceof Collection) { + return; + } + + array_map( + [$collection, 'add'], + array_map([$this, 'createFolder'], $data) + ); + }); + } + + /** + * @param array $data + * + * @return EmailFolder + */ + protected function createFolder(array $data) + { + return (new EmailFolder()) + ->setSyncEnabled(isset($data['syncEnabled'])) + ->setFullName($data['fullName']) + ->setName($data['name']) + ->setType($data['type']) + ->setSubFolders(new ArrayCollection(array_map( + [$this, 'createFolder'], + isset($data['subFolders']) ? $data['subFolders'] : [] + ))); } /** * {@inheritDoc} */ - public function getParent() + public function setDefaultOptions(OptionsResolverInterface $resolver) { - return 'collection'; + $resolver->setDefaults([ + 'allow_extra_fields' => true, + ]); } /** diff --git a/src/Oro/Bundle/EmailBundle/Form/Type/EmailFolderType.php b/src/Oro/Bundle/EmailBundle/Form/Type/EmailFolderType.php deleted file mode 100644 index 021bac3e7bb..00000000000 --- a/src/Oro/Bundle/EmailBundle/Form/Type/EmailFolderType.php +++ /dev/null @@ -1,50 +0,0 @@ -setDefaults([ - 'data_class' => 'Oro\Bundle\EmailBundle\Entity\EmailFolder', - 'nesting_level' => 10, - ]); - } - - /** - * {@inheritDoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) - { - if ($options['nesting_level'] > 0) { - $builder - ->add('syncEnabled', 'checkbox') - ->add('fullName', 'hidden') - ->add('name', 'hidden') - ->add('type', 'hidden') - ->add('subFolders', 'collection', [ - 'type' => 'oro_email_email_folder', - 'allow_add' => true, - 'options' => [ - 'nesting_level' => --$options['nesting_level'], - ], - ]); - } - } - - /** - * {@inheritDoc} - */ - public function getName() - { - return 'oro_email_email_folder'; - } -} diff --git a/src/Oro/Bundle/EmailBundle/Provider/EmailActivityListProvider.php b/src/Oro/Bundle/EmailBundle/Provider/EmailActivityListProvider.php index 5cf767b258d..626cdac2d0f 100644 --- a/src/Oro/Bundle/EmailBundle/Provider/EmailActivityListProvider.php +++ b/src/Oro/Bundle/EmailBundle/Provider/EmailActivityListProvider.php @@ -68,14 +68,14 @@ class EmailActivityListProvider implements /** @var HtmlTagHelper */ protected $htmlTagHelper; - /** @var ServiceLink */ + /** @var ServiceLink */ protected $securityContextLink; /** @var ServiceLink */ protected $securityFacadeLink; - /** @var MailboxProcessStorage */ - protected $mailboxProcessStorage; + /** @var ServiceLink */ + protected $mailboxProcessStorageLink; /** * @param DoctrineHelper $doctrineHelper @@ -86,7 +86,7 @@ class EmailActivityListProvider implements * @param EmailThreadProvider $emailThreadProvider * @param HtmlTagHelper $htmlTagHelper * @param ServiceLink $securityFacadeLink - * @param MailboxProcessStorage $mailboxProcessStorage + * @param ServiceLink $mailboxProcessStorageLink */ public function __construct( DoctrineHelper $doctrineHelper, @@ -97,7 +97,7 @@ public function __construct( EmailThreadProvider $emailThreadProvider, HtmlTagHelper $htmlTagHelper, ServiceLink $securityFacadeLink, - MailboxProcessStorage $mailboxProcessStorage + ServiceLink $mailboxProcessStorageLink ) { $this->doctrineHelper = $doctrineHelper; $this->doctrineRegistryLink = $doctrineRegistryLink; @@ -107,7 +107,7 @@ public function __construct( $this->emailThreadProvider = $emailThreadProvider; $this->htmlTagHelper = $htmlTagHelper; $this->securityFacadeLink = $securityFacadeLink; - $this->mailboxProcessStorage = $mailboxProcessStorage; + $this->mailboxProcessStorageLink = $mailboxProcessStorageLink; } /** @@ -240,7 +240,7 @@ public function getOrganization($activityEntity) return $token->getOrganizationContext(); } - $processes = $this->mailboxProcessStorage->getProcesses(); + $processes = $this->mailboxProcessStorageLink->getService()->getProcesses(); foreach ($processes as $process) { $settingsClass = $process->getSettingsEntityFQCN(); diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/assets.yml b/src/Oro/Bundle/EmailBundle/Resources/config/assets.yml index 924e52487eb..44e0c8b46e4 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/config/assets.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/config/assets.yml @@ -1,3 +1,3 @@ css: 'oroemail': - - 'bundles/oroemail/css/less/style.less' + - 'bundles/oroemail/css/less/main.less' diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml b/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml index 0b3a8d95a29..c29aaf6c827 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml @@ -1,8 +1,8 @@ datagrid: email-auto-response-rules: + acl_resource: oro_email_autoresponserule_view source: type: orm - acl_resource: oro_email_autoresponserule_view query: select: - r.id @@ -68,14 +68,6 @@ datagrid: label: oro.grid.action.delete icon: trash link: delete_link - mass_actions: - delete: - type: delete - entity_name: %oro_email.autoresponserule.entity.class% - data_identifier: r.id - acl_resource: oro_email_autoresponserule_update - label: oro.grid.action.delete - icon: trash options: toolbarOptions: hide: true @@ -85,6 +77,7 @@ datagrid: base-email-grid: source: type: orm + skip_acl_apply: true query: select: - partial eu.{id, email} @@ -159,7 +152,6 @@ datagrid: default: { sentAt: %oro_datagrid.extension.orm_sorter.class%::DIRECTION_DESC } options: entityHint: email - skip_acl_check: true simplified-email-grid: extends: base-email-grid @@ -412,9 +404,9 @@ datagrid: receivedAt: %oro_datagrid.extension.orm_sorter.class%::DIRECTION_DESC email-templates: + acl_resource: oro_email_emailtemplate_index source: type: orm - acl_resource: oro_email_emailtemplate_index query: select: - t.id @@ -520,6 +512,7 @@ datagrid: base-mailboxes-grid: source: type: orm + skip_acl_apply: true query: select: - m @@ -564,7 +557,6 @@ datagrid: params: id: id options: - skip_acl_check: true toolbarOptions: hide: true actions: @@ -579,3 +571,13 @@ datagrid: label: oro.grid.action.delete icon: trash link: delete_link + + email-for-context-grid: + extends: email-grid + options: + entityHint: email + entity_pagination: true + toolbarOptions: + pageSize: + default_per_page: 10 + routerEnabled: false diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/form.yml b/src/Oro/Bundle/EmailBundle/Resources/config/form.yml index 1a7ce1eb9ef..1c4eb3c51fe 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/config/form.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/config/form.yml @@ -25,7 +25,6 @@ parameters: oro_email.form.type.mailbox.class: Oro\Bundle\EmailBundle\Form\Type\MailboxType oro_email.form.handler.mailbox.class: Oro\Bundle\EmailBundle\Form\Handler\MailboxHandler oro_email.form.factory.class: Oro\Bundle\EmailBundle\Form\Model\Factory - oro_email.form.type.email_folder.class: Oro\Bundle\EmailBundle\Form\Type\EmailFolderType oro_email.form.type.email_folder_tree.class: Oro\Bundle\EmailBundle\Form\Type\EmailFolderTreeType oro_email.form.type.filter.originfolder.class: Oro\Bundle\EmailBundle\Form\Type\Filter\ChoiceOriginFolderFilterType oro_email.form.type.email_address_from.class: Oro\Bundle\EmailBundle\Form\Type\EmailAddressFromType @@ -225,11 +224,6 @@ services: oro_email.form.factory: class: %oro_email.form.factory.class% - oro_email.form.type.email_folder: - class: %oro_email.form.type.email_folder.class% - tags: - - { name: form.type, alias: oro_email_email_folder } - oro_email.form.type.email_folder_tree: class: %oro_email.form.type.email_folder_tree.class% tags: diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/services.yml b/src/Oro/Bundle/EmailBundle/Resources/config/services.yml index 4862ae6c177..56a09c1eb37 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/config/services.yml @@ -104,7 +104,6 @@ parameters: oro_email.manager.email_attachment_manager.class: Oro\Bundle\EmailBundle\Manager\EmailAttachmentManager oro_email.provider.email_attachment_provider.class: Oro\Bundle\EmailBundle\Provider\EmailAttachmentProvider oro_email.acl.voter.email_voter.class: Oro\Bundle\EmailBundle\Acl\Voter\EmailVoter - oro_email.acl.voter.email_user_voter.class: Oro\Bundle\EmailBundle\Acl\Voter\EmailUserVoter # Email DataGrid oro_email.datagrid.origin_folder.provider.class: Oro\Bundle\EmailBundle\Datagrid\OriginFolderFilterProvider @@ -120,6 +119,7 @@ parameters: oro_email.listener.mailbox.authorization.class: Oro\Bundle\EmailBundle\EventListener\MailboxAuthorizationListener oro_email.listener.datagrid.mailbox_grid.class: Oro\Bundle\EmailBundle\EventListener\Datagrid\MailboxGridListener oro_email.autocomplete.mailbox_user_search_handler.class: Oro\Bundle\EmailBundle\Autocomplete\MailboxUserSearchHandler + oro_email.mailbox.manager.class: Oro\Bundle\EmailBundle\Entity\Manager\MailboxManager # Workflow conditions oro_email.workflow.condition.instanceof.class: Oro\Bundle\EmailBundle\Model\Condition\IsInstanceOf @@ -132,6 +132,11 @@ parameters: oro_email.workflow.action.strip_html_tags.class: Oro\Bundle\EmailBundle\Model\Action\StripHtmlTags oro_email.workflow.action.add_activity_target.class: Oro\Bundle\EmailBundle\Model\Action\AddActivityTarget + oro_email.listener.search_aliases_listener.class: Oro\Bundle\EmailBundle\EventListener\SearchAliasesListener + oro_email.listener.prepare_result_item_listener.class: Oro\Bundle\EmailBundle\EventListener\PrepareResultItemListener + oro_email.listener.prepare_context_title_listener.class: Oro\Bundle\EmailBundle\EventListener\PrepareContextTitleListener + oro_email.listener.activity_list_pre_query_build_listener.class: Oro\Bundle\EmailBundle\EventListener\ActivityListPreQueryBuildListener + services: oro_email.entity.cache.warmer: class: %oro_email.entity.cache.warmer.class% @@ -211,6 +216,7 @@ services: - @doctrine.orm.entity_manager - @oro_email.email.cache.manager - @templating + - @oro_email.mailbox.manager oro_email.email.entity.builder: class: %oro_email.email.entity.builder.class% @@ -441,7 +447,7 @@ services: arguments: - @oro_email.email.owner.provider.storage - @oro_entity.entity_name_resolver - - @doctrine + - @oro_email.mailbox.manager - @oro_security.security_facade oro_email.emailtemplate.datagrid_view_list: @@ -617,6 +623,10 @@ services: tags: - { name: oro_service_link, service: doctrine.orm.entity_manager } + oro_email.mailbox.process_storage.link: + tags: + - { name: oro_service_link, service: oro_email.mailbox.process_storage } + oro_email.activity_list.provider: class: %oro_email.activity_list.provider.class% arguments: @@ -628,7 +638,7 @@ services: - @oro_email.email.thread.provider - @oro_ui.html_tag_helper - @oro_security.security_facade.link - - @oro_email.mailbox.process_storage + - @oro_email.mailbox.process_storage.link calls: - [ setSecurityContextLink, [@security.context.link] ] tags: @@ -829,6 +839,7 @@ services: arguments: - @doctrine - @oro_security.security_facade + - @oro_email.mailbox.manager oro_email.mailbox.manager.api: class: %oro_email.mailbox.manager.api.class% @@ -881,6 +892,11 @@ services: - { name: kernel.event_listener, event: oro_datagrid.datagrid.build.pre.base-mailboxes-grid, method: onPreBuild } - { name: kernel.event_listener, event: oro_datagrid.datagrid.build.after.base-mailboxes-grid, method: onBuildAfter } + oro_email.mailbox.manager: + class: %oro_email.mailbox.manager.class% + arguments: + - @doctrine + oro_email.workflow.condition.instanceof: class: %oro_email.workflow.condition.instanceof.class% tags: @@ -936,3 +952,30 @@ services: oro_email.placeholder.send_email.filter: class: %oro_email.placeholder.send_email.filter.class% + + oro_email.listener.search_aliases_listener: + class: %oro_email.listener.search_aliases_listener.class% + tags: + - { name: kernel.event_listener, event: oro_activity.search_aliases, method: addEmailAliasEvent } + + oro_email.listener.prepare_result_item_listener: + class: %oro_email.listener.prepare_result_item_listener.class% + arguments: + - @router + tags: + - { name: kernel.event_listener, event: oro_search.prepare_result_item, method: prepareEmailItemDataEvent } + + oro_email.listener.prepare_context_title_listener: + class: %oro_email.listener.prepare_context_title_listener.class% + arguments: + - @router + - @oro_entity.doctrine_helper + tags: + - { name: kernel.event_listener, event: oro_activity.context_title, method: prepareEmailContextTitleEvent } + + oro_email.listener.activity_list_pre_query_build_listener: + class: %oro_email.listener.activity_list_pre_query_build_listener.class% + arguments: + - @oro_entity.doctrine_helper + tags: + - { name: kernel.event_listener, event: oro_activity_list.activity_list_pre_query_build, method: prepareIdsForEmailThreadEvent } diff --git a/src/Oro/Bundle/EmailBundle/Resources/public/css/less/main.less b/src/Oro/Bundle/EmailBundle/Resources/public/css/less/main.less new file mode 100644 index 00000000000..0cb7af53631 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/public/css/less/main.less @@ -0,0 +1,9 @@ +@import "oroui/css/less/mixins"; +@import "./header"; +@import "./email-notification-menu"; +@import "./short-emails-list"; +@import "./sidebar-widget"; +@import "./style"; + +// mobile +@import "./mobile/main"; diff --git a/src/Oro/Bundle/EmailBundle/Resources/public/css/less/mobile/main.less b/src/Oro/Bundle/EmailBundle/Resources/public/css/less/mobile/main.less new file mode 100644 index 00000000000..f8ce0327713 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/public/css/less/mobile/main.less @@ -0,0 +1,4 @@ +.mobile-version { + @import "oroui/css/less/mobile/variables"; + @import "./thread-view"; +} diff --git a/src/Oro/Bundle/EmailBundle/Resources/public/css/less/mobile/thread-view.less b/src/Oro/Bundle/EmailBundle/Resources/public/css/less/mobile/thread-view.less new file mode 100644 index 00000000000..19d5974743b --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/public/css/less/mobile/thread-view.less @@ -0,0 +1,69 @@ +.thread-view { + padding-left: 0; + .email-info, .email-load-more { + margin: 0; + } + .email-info { + .responsive-cell { + padding: 0; + } + .email-participants { + margin-right: 100px; + } + .email-actions { + margin: 16px -10px 0 0; + .btn-group +.btn-group { + margin-left: 5px; + } + } + .email-full { + .email-sent-date { + padding: 0; + height: 18px; + font-size: 12px; + color: #888; + } + } + .email-view-toggle { + padding: @contentPadding; + } + .email-content { + padding: 0 @contentPadding @contentPadding; + } + &:only-child { + .email-view-toggle { + padding: 0 0 10px; + } + .email-content { + padding: 0; + } + } + } + .email-detailed-info-table { + .dropdown-menu { + padding: 0px @contentPadding; + overflow-x: auto; + } + .control-group:last-child { + margin-bottom: 0; + .controls { + margin-bottom: 0; + } + } + } +} + +.activity-list-widget { + .thread-view { + .email-info { + &:last-child, &:only-child { + .email-view-toggle { + padding: @contentPadding; + } + .email-content { + padding: 0 @contentPadding; + } + } + } + } +} diff --git a/src/Oro/Bundle/EmailBundle/Resources/public/css/less/style.less b/src/Oro/Bundle/EmailBundle/Resources/public/css/less/style.less index 180ef92fbc1..3f749f958b8 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/public/css/less/style.less +++ b/src/Oro/Bundle/EmailBundle/Resources/public/css/less/style.less @@ -433,6 +433,7 @@ span.icon.grid .icon-paperclip { .responsive-cell { padding-left: 0; padding-right: 15px; + margin-bottom: 0 !important; /* overrides activity list widget styles */ } .responsive-cell:only-child, .responsive-cell:last-child { @@ -444,6 +445,18 @@ span.icon.grid .icon-paperclip { padding-right: 0; } } + &.responsive-small { + .email-content { + .responsive-cell:not(:last-child) { + margin-bottom: 20px !important; /* overrides activity list widget styles */ + } + } + } +} +.accordion-body .email-info { + &:last-child .email-content { + padding-bottom: 0; + } } .email-detailed-info-table { @@ -708,3 +721,14 @@ a.sync-btn span.dots { .check-connection-messages .alert { margin-top: 10px; } + +#oro_email_mailbox_origin_check_connection, +.check-connection-messages { + width: 294px; +} + +.container-fluid { + .email-activity-widget { + margin-bottom: 20px; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Resources/public/js/app/components/select2-email-recipients-component.js b/src/Oro/Bundle/EmailBundle/Resources/public/js/app/components/select2-email-recipients-component.js index e4d268486b4..261099fdd98 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/public/js/app/components/select2-email-recipients-component.js +++ b/src/Oro/Bundle/EmailBundle/Resources/public/js/app/components/select2-email-recipients-component.js @@ -108,7 +108,7 @@ define([ * Extracts contexts and organizations from data */ _processData: function(data) { - if (typeof data === 'undefined') { + if (typeof data === 'undefined' || this.disposed) { return; } diff --git a/src/Oro/Bundle/EmailBundle/Resources/public/js/app/views/email-notification/mobile-email-notification-view.js b/src/Oro/Bundle/EmailBundle/Resources/public/js/app/views/email-notification/mobile-email-notification-view.js index 6242d83222a..fd146df77e9 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/public/js/app/views/email-notification/mobile-email-notification-view.js +++ b/src/Oro/Bundle/EmailBundle/Resources/public/js/app/views/email-notification/mobile-email-notification-view.js @@ -40,7 +40,7 @@ define(function(require) { if (count === 0) { count = ''; } else { - count = '(' + (count > 10 ? '10+' : count) + ')'; + count = '(' + (count > 99 ? '99+' : count) + ')'; } this.$counter.html(count); $('#user-menu .dropdown-toggle').toggleClass('has-new-emails', Boolean(this.countNewEmail)); diff --git a/src/Oro/Bundle/EmailBundle/Resources/public/js/app/views/email-thread-view.js b/src/Oro/Bundle/EmailBundle/Resources/public/js/app/views/email-thread-view.js index fe4e90e4151..9eb58b62a26 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/public/js/app/views/email-thread-view.js +++ b/src/Oro/Bundle/EmailBundle/Resources/public/js/app/views/email-thread-view.js @@ -15,7 +15,8 @@ define(function(require) { events: { 'click .email-view-toggle-all': 'onToggleAllClick', - 'click .email-load-more': 'onLoadMoreClick' + 'click .email-load-more': 'onLoadMoreClick', + 'shown.bs.dropdown .email-detailed-info-table.mobile .dropdown-menu': 'onDetailedInfoOpen' }, selectors: { @@ -107,6 +108,16 @@ define(function(require) { this.loadEmails(); }, + onDetailedInfoOpen: function(e) { + var rect = e.currentTarget.getBoundingClientRect(); + var parentRect = this.el.getBoundingClientRect(); + var left = parseInt($(e.currentTarget).css('left')); + $(e.currentTarget).css({ + 'left': left + parentRect.left - rect.left + 'px', + 'max-width': parentRect.width + 'px' + }); + }, + /** * Loads emails' html * diff --git a/src/Oro/Bundle/EmailBundle/Resources/translations/messages.en.yml b/src/Oro/Bundle/EmailBundle/Resources/translations/messages.en.yml index 1cc9137f0b3..22879605a13 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/translations/messages.en.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/translations/messages.en.yml @@ -241,6 +241,7 @@ oro: reply: Reply reply_all: Reply All forward: Forward + view: View email forwarded_message: Forwarded message parent_message_header: "On %date% %user% wrote:" load_more_emails: "%quantity% older messages" diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/Configuration/Mailbox/update.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/Configuration/Mailbox/update.html.twig index fc7ac6299a4..886716897f6 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/views/Configuration/Mailbox/update.html.twig +++ b/src/Oro/Bundle/EmailBundle/Resources/views/Configuration/Mailbox/update.html.twig @@ -22,10 +22,14 @@ }, mailboxTitle ] %} +{% set formActionParams = app.request.attributes.get('_route_params')|merge({redirectData: redirectData}) %} +{% if app.request.query.has('form') %} + {% set formActionParams = formActionParams|merge({form: app.request.query.get('form')}) %} +{% endif %} {% set formAction = path( app.request.attributes.get('_route'), - app.request.attributes.get('_route_params')|merge({redirectData: redirectData}) + formActionParams ) %} {% set routeName = 'oro_config_configuration_system' %} diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/Email/Thread/emailItem.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/Email/Thread/emailItem.html.twig index 4d7d2e91ffa..7cd3afb7933 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/views/Email/Thread/emailItem.html.twig +++ b/src/Oro/Bundle/EmailBundle/Resources/views/Email/Thread/emailItem.html.twig @@ -38,23 +38,23 @@
+ {% set actionParameters = routeParameters is defined ? { + routeParameters: routeParameters + } : {} %} + {% set buttonsHtml %} + {% if defaultReplyButton is not defined or defaultReplyButton == 1 %} + {{ Actions.replyButton(email, actionParameters) }} + {{ Actions.replyAllButton(email, actionParameters) }} + {% else %} + {{ Actions.replyAllButton(email, actionParameters) }} + {{ Actions.replyButton(email, actionParameters) }} + {% endif %} + {{ Actions.forwardButton(email, actionParameters ) }} + {% endset %}
- {% set actionParameters = routeParameters is defined ? { - routeParameters: routeParameters - } : {} %} - {% set buttonsHtml %} - {% if defaultReplyButton is not defined or defaultReplyButton == 1 %} - {{ Actions.replyButton(email, actionParameters) }} - {{ Actions.replyAllButton(email, actionParameters) }} - {% else %} - {{ Actions.replyAllButton(email, actionParameters) }} - {{ Actions.replyButton(email, actionParameters) }} - {% endif %} - {{ Actions.forwardButton(email, actionParameters ) }} - {% endset %} - {{ UI.pinnedDropdownButton({ - 'html': buttonsHtml - }) }} + {% if isDesktopVersion() %} + {{ UI.pinnedDropdownButton({'html': buttonsHtml}) }} + {% endif %} {% endblock content_data %} diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/Email/js/groupedActivityItemTemplate.js.twig b/src/Oro/Bundle/EmailBundle/Resources/views/Email/js/groupedActivityItemTemplate.js.twig index b72dc3ac850..722748b6a72 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/views/Email/js/groupedActivityItemTemplate.js.twig +++ b/src/Oro/Bundle/EmailBundle/Resources/views/Email/js/groupedActivityItemTemplate.js.twig @@ -43,7 +43,7 @@ {% set action %} {% if resource_granted('oro_email_email_user_edit') %} {{ 'oro.email.view'|trans|raw }} + {% endset %} + {% set actions = actions|merge([action]) %} + {{ parent() }} {% endblock %} diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/Form/fields.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/Form/fields.html.twig index 4e61a35f22a..6076910bd15 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/views/Form/fields.html.twig +++ b/src/Oro/Bundle/EmailBundle/Resources/views/Form/fields.html.twig @@ -204,22 +204,6 @@ {% endblock %} -{% block oro_email_email_folder_row %} - {% if form.vars.value is not null and form.vars.value.outdatedAt is null %} -
- {{ form_widget(form.syncEnabled) }} - {{ form_widget(form.fullName) }} - {{ form_widget(form.name) }} - {{ form_widget(form.type) }} - {% if form.vars.value.subFolders|length > 0 %} -
- {{ form_widget(form.subFolders) }} -
- {% endif %} -
- {% endif %} -{% endblock %} - {% block oro_email_email_folder_tree_row %} {% if form.vars.value is not null and form.vars.value|length > 0 %} {{ form_row(form) }} @@ -230,10 +214,8 @@
- {% for child in form.children %} - {% if child.vars.value.parentFolder is null and child.vars.value.outdatedAt is null %} - {{ form_row(child) }} - {% endif %} + {% for key, folder in form.vars.value if folder.parentFolder is null and folder.outdatedAt is null %} + {{ _self.renderFolder(key, folder, form.vars.full_name) }} {% endfor %}
@@ -354,3 +336,22 @@ {{ form_widget(form) }}
{% endmacro %} + +{% macro renderFolder(key, folder, namePrefix, maxDepth = 10) %} + {% if maxDepth %} +
+ + + + + + {% if maxDepth > 1 and folder.subFolders|length > 0 %} +
+ {% for subKey, subFolder in folder.subFolders %} + {{ _self.renderFolder(subKey, subFolder, namePrefix ~ '[' ~ key ~ '][subFolders]', maxDepth - 1) }} + {% endfor %} +
+ {% endif %} +
+ {% endif %} +{% endmacro %} diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/macros.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/macros.html.twig index 38919a372aa..ce1c6bc932a 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/views/macros.html.twig +++ b/src/Oro/Bundle/EmailBundle/Resources/views/macros.html.twig @@ -359,10 +359,10 @@ {%- placeholder email_actions with {email: email, entity: entity} -%} {% endset -%} {% set actions = actions|trim %} - - {{ _self.email_address_simple(email) }} + + {{ _self.email_address_simple(email) }} {% if actions is not empty -%} - + {%- endif %} {% endif %} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/Helper/EmailModelBuilderHelperTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/Helper/EmailModelBuilderHelperTest.php index 0a066d5d4ab..9e80c7ffbf9 100644 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/Helper/EmailModelBuilderHelperTest.php +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/Helper/EmailModelBuilderHelperTest.php @@ -95,6 +95,10 @@ protected function setUp() $this->templating = $this->getMock('Symfony\Component\Templating\EngineInterface'); + $mailboxManager = $this->getMockBuilder('Oro\Bundle\EmailBundle\Entity\Manager\MailboxManager') + ->disableOriginalConstructor() + ->getMock(); + $this->helper = new EmailModelBuilderHelper( $this->entityRoutingHelper, $this->emailAddressHelper, @@ -103,7 +107,8 @@ protected function setUp() $this->emailAddressManager, $this->entityManager, $this->emailCacheManager, - $this->templating + $this->templating, + $mailboxManager ); } diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Datagrid/EmailQueryFactoryTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Datagrid/EmailQueryFactoryTest.php index 75ca70423f3..e3124589bbc 100644 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Datagrid/EmailQueryFactoryTest.php +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Datagrid/EmailQueryFactoryTest.php @@ -2,11 +2,9 @@ namespace Oro\Bundle\EmailBundle\Tests\Unit\Datagrid; -use Doctrine\Bundle\DoctrineBundle\Registry; - use Oro\Bundle\EmailBundle\Datagrid\EmailQueryFactory; +use Oro\Bundle\EmailBundle\Entity\Manager\MailboxManager; use Oro\Bundle\EmailBundle\Entity\Provider\EmailOwnerProviderStorage; -use Oro\Bundle\EmailBundle\Entity\Repository\MailboxRepository; use Oro\Bundle\EntityBundle\Provider\EntityNameResolver; use Oro\Bundle\OrganizationBundle\Entity\Organization; use Oro\Bundle\SecurityBundle\SecurityFacade; @@ -28,14 +26,11 @@ class EmailQueryFactoryTest extends OrmTestCase /** @var EmailQueryFactory */ protected $factory; - /** @var Registry */ - protected $doctrine; - /** @var SecurityFacade */ protected $securityFacade; - /** @var MailboxRepository */ - protected $mailboxRepository; + /** @var MailboxManager */ + protected $mailboxManager; public function setUp() { @@ -44,19 +39,10 @@ public function setUp() $this->entityNameResolver = $this->getMockBuilder('Oro\Bundle\EntityBundle\Provider\EntityNameResolver') ->disableOriginalConstructor()->getMock(); - $this->mailboxRepository = $this->getMockBuilder('Oro\Bundle\EmailBundle\Entity\Repository\MailboxRepository') - ->disableOriginalConstructor() - ->getMock(); - - $this->doctrine = $this->getMockBuilder('Doctrine\Bundle\DoctrineBundle\Registry') + $this->mailboxManager = $this->getMockBuilder('Oro\Bundle\EmailBundle\Entity\Manager\MailboxManager') ->disableOriginalConstructor() ->getMock(); - $this->doctrine->expects($this->any()) - ->method('getRepository') - ->with($this->equalTo('OroEmailBundle:Mailbox')) - ->will($this->returnValue($this->mailboxRepository)); - $this->securityFacade = $this->getMockBuilder('Oro\Bundle\SecurityBundle\SecurityFacade') ->disableOriginalConstructor() ->getMock(); @@ -64,7 +50,7 @@ public function setUp() $this->factory = new EmailQueryFactory( $this->providerStorage, $this->entityNameResolver, - $this->doctrine, + $this->mailboxManager, $this->securityFacade ); } @@ -75,7 +61,7 @@ public function tearDown() $this->factory, $this->entityNameResolver, $this->providerStorage, - $this->mailboxRepository, + $this->mailboxManager, $this->doctrine ); } @@ -141,7 +127,7 @@ public function testFilterQueryByUserIdWhenMailboxesAreFound() ->method('getOrganization') ->will($this->returnValue($organization)); - $this->mailboxRepository->expects($this->any()) + $this->mailboxManager->expects($this->any()) ->method('findAvailableMailboxIds') ->with($user, $organization) ->will($this->returnValue([1, 3, 5])); @@ -174,7 +160,7 @@ public function testFilterQueryByUserIdWhenNoMailboxesFound() ->method('getOrganization') ->will($this->returnValue($organization)); - $this->mailboxRepository->expects($this->any()) + $this->mailboxManager->expects($this->any()) ->method('findAvailableMailboxIds') ->with($user, $organization) ->will($this->returnValue([1, 3, 5])); diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/EventListener/ActivityListPreQueryBuildListenerTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/EventListener/ActivityListPreQueryBuildListenerTest.php new file mode 100644 index 00000000000..8fa103b2ba1 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/EventListener/ActivityListPreQueryBuildListenerTest.php @@ -0,0 +1,103 @@ +doctrineHelper = $this->getMockBuilder('Oro\Bundle\EntityBundle\ORM\DoctrineHelper') + ->disableOriginalConstructor() + ->getMock(); + + $this->listener = new ActivityListPreQueryBuildListener($this->doctrineHelper); + } + + public function testPrepareIdsForEmailThreadEventSkippEntity() + { + $targetClass = 'testClass'; + $targetId = 1; + + $event = new ActivityListPreQueryBuildEvent($targetClass, $targetId); + $this->listener->prepareIdsForEmailThreadEvent($event); + $this->doctrineHelper->expects($this->never()) + ->method('getEntity'); + } + + public function testPrepareIdsForEmailThreadWithoutThreadsEvent() + { + $targetClass = Email::ENTITY_CLASS; + $targetId = 1; + + $email = $this->getMockBuilder('Oro\Bundle\EmailBundle\Entity\Email') + ->disableOriginalConstructor() + ->getMock(); + + $this->doctrineHelper->expects($this->once()) + ->method('getEntity') + ->willReturn($email); + + $event = new ActivityListPreQueryBuildEvent($targetClass, $targetId); + $this->listener->prepareIdsForEmailThreadEvent($event); + + $this->assertEquals([$targetId], $event->getTargetIds()); + } + + public function testPrepareIdsForEmailThreadWithThreadsEvent() + { + $targetClass = Email::ENTITY_CLASS; + $targetId = 1; + $expectedResult = [2, 3]; + + $email = $this->getMockBuilder('Oro\Bundle\EmailBundle\Entity\Email') + ->disableOriginalConstructor() + ->getMock(); + $thread = $this->getMockBuilder('Oro\Bundle\EmailBundle\Entity\EmailThread') + ->disableOriginalConstructor() + ->getMock(); + + $email1 = $this->getMockBuilder('Oro\Bundle\EmailBundle\Entity\Email') + ->disableOriginalConstructor() + ->getMock(); + $email2 = $this->getMockBuilder('Oro\Bundle\EmailBundle\Entity\Email') + ->disableOriginalConstructor() + ->getMock(); + + $this->doctrineHelper->expects($this->once()) + ->method('getEntity') + ->willReturn($email); + + $email->expects($this->exactly(2)) + ->method('getThread') + ->willReturn($thread); + + $email1->expects($this->once()) + ->method('getId') + ->willReturn(2); + $email2->expects($this->once()) + ->method('getId') + ->willReturn(3); + + $thread->expects($this->once()) + ->method('getEmails') + ->willReturn(new ArrayCollection([$email1, $email2])); + + $event = new ActivityListPreQueryBuildEvent($targetClass, $targetId); + $this->listener->prepareIdsForEmailThreadEvent($event); + + $this->assertEquals($expectedResult, $event->getTargetIds()); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/EventListener/PrepareContextTitleListenerTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/EventListener/PrepareContextTitleListenerTest.php new file mode 100644 index 00000000000..16011144c7b --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/EventListener/PrepareContextTitleListenerTest.php @@ -0,0 +1,78 @@ +router = $this->getMockBuilder('Symfony\Component\Routing\Router') + ->disableOriginalConstructor() + ->getMock(); + + $this->doctrineHelper = $this->getMockBuilder('Oro\Bundle\EntityBundle\ORM\DoctrineHelper') + ->disableOriginalConstructor() + ->getMock(); + + $this->listener = new PrepareContextTitleListener($this->router, $this->doctrineHelper); + } + + public function testPrepareContextTitleEventSkippEntity() + { + $item = []; + $item['title'] = 'title'; + $targetClass = 'test'; + $expectedItem = ['title' => 'title']; + + $event = new PrepareContextTitleEvent($item, $targetClass); + $this->listener->prepareEmailContextTitleEvent($event); + + $this->assertEquals($expectedItem, $event->getItem()); + } + + public function testPrepareContextTitleDataEvent() + { + $item = []; + $item['title'] = 'title'; + $item['targetId'] = 1; + $targetClass = Email::ENTITY_CLASS; + $expectedItem = ['title' => 'new title', 'link' => 'link', 'targetId' => 1]; + + $entity = $this->getMockBuilder('Oro\Bundle\EmailBundle\Entity\Email') + ->disableOriginalConstructor() + ->getMock(); + + $this->doctrineHelper->expects($this->once()) + ->method('getEntity') + ->willReturn($entity); + + $this->router->expects($this->once()) + ->method('generate') + ->willReturn('link'); + + $entity->expects($this->once()) + ->method('getSubject') + ->willReturn('new title'); + + $event = new PrepareContextTitleEvent($item, $targetClass); + $this->listener->prepareEmailContextTitleEvent($event); + + $this->assertEquals($expectedItem, $event->getItem()); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/EventListener/PrepareResultItemListenerTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/EventListener/PrepareResultItemListenerTest.php new file mode 100644 index 00000000000..854a84d9cd6 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/EventListener/PrepareResultItemListenerTest.php @@ -0,0 +1,72 @@ +router = $this->getMockBuilder('Symfony\Component\Routing\Router') + ->disableOriginalConstructor() + ->getMock(); + + $this->item = $this->getMockBuilder('Oro\Bundle\SearchBundle\Query\Result\Item') + ->disableOriginalConstructor() + ->getMock(); + + $this->listener = new PrepareResultItemListener($this->router); + } + + public function testPrepareEmailItemDataEventSkippEntity() + { + $this->item->expects($this->once()) + ->method('getEntityName') + ->willReturn('test'); + $this->item->expects($this->never()) + ->method('getEntity'); + + $event = new PrepareResultItemEvent($this->item); + $this->listener->prepareEmailItemDataEvent($event); + } + + public function testPrepareEmailItemDataEvent() + { + $entity = $this->getMockBuilder('Oro\Bundle\EmailBundle\Entity\EmailUser') + ->disableOriginalConstructor() + ->getMock(); + $email = $this->getMockBuilder('Oro\Bundle\EmailBundle\Entity\Email') + ->disableOriginalConstructor() + ->getMock(); + + $this->item->expects($this->once()) + ->method('getEntityName') + ->willReturn(EmailUser::ENTITY_CLASS); + $this->item->expects($this->once()) + ->method('getEntity') + ->willReturn($entity); + $entity->expects($this->once()) + ->method('getEmail') + ->willReturn($email); + $email->expects($this->once()) + ->method('getId'); + + $event = new PrepareResultItemEvent($this->item); + $this->listener->prepareEmailItemDataEvent($event); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/EventListener/SearchAliasesListenerTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/EventListener/SearchAliasesListenerTest.php new file mode 100644 index 00000000000..5587fa58bbc --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/EventListener/SearchAliasesListenerTest.php @@ -0,0 +1,38 @@ +listener = new SearchAliasesListener(); + } + + public function testAddEmailAliasEventSkipped() + { + $expectedAliases = []; + $targetClasses = []; + $aliases = []; + $event = new SearchAliasesEvent($aliases, $targetClasses); + $this->listener->addEmailAliasEvent($event); + $this->assertEquals($expectedAliases, $event->getAliases()); + } + + public function testAddEmailAliasEvent() + { + $expectedAliases = ['oro_email']; + $targetClasses = [Email::ENTITY_CLASS]; + $aliases = []; + $event = new SearchAliasesEvent($aliases, $targetClasses); + $this->listener->addEmailAliasEvent($event); + $this->assertEquals($expectedAliases, $event->getAliases()); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailFolderTreeTypeTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailFolderTreeTypeTest.php index 3198b80f534..072e537e7e0 100644 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailFolderTreeTypeTest.php +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailFolderTreeTypeTest.php @@ -32,8 +32,7 @@ public function testSetDefaultOptions() $resolver->expects($this->at(0)) ->method('setDefaults') ->with([ - 'type' => 'oro_email_email_folder', - 'allow_add' => true, + 'allow_extra_fields' => true ]); $this->emailFolderTreeType->setDefaultOptions($resolver); @@ -41,7 +40,7 @@ public function testSetDefaultOptions() public function testGetParent() { - $this->assertEquals('collection', $this->emailFolderTreeType->getParent()); + $this->assertEquals('form', $this->emailFolderTreeType->getParent()); } public function testGetName() diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailFolderTypeTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailFolderTypeTest.php deleted file mode 100644 index 58a05d49118..00000000000 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailFolderTypeTest.php +++ /dev/null @@ -1,86 +0,0 @@ -emailFolderType = new EmailFolderType(); - - parent::setUp(); - } - - public function testSetDefaultOptions() - { - $resolver = $this->getMockBuilder('Symfony\Component\OptionsResolver\OptionsResolver') - ->disableOriginalConstructor() - ->getMock(); - - $resolver->expects($this->at(0)) - ->method('setDefaults') - ->with([ - 'data_class' => 'Oro\Bundle\EmailBundle\Entity\EmailFolder', - 'nesting_level' => 10, - ]); - - $this->emailFolderType->setDefaultOptions($resolver); - } - - public function testBuildForm() - { - $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') - ->disableOriginalConstructor() - ->getMock(); - - $builder->expects($this->at(0)) - ->method('add') - ->with('syncEnabled', 'checkbox') - ->willReturn($builder); - - $builder->expects($this->at(1)) - ->method('add') - ->with('fullName', 'hidden') - ->willReturn($builder); - - $builder->expects($this->at(2)) - ->method('add') - ->with('name', 'hidden') - ->willReturn($builder); - - $builder->expects($this->at(3)) - ->method('add') - ->with('type', 'hidden') - ->willReturn($builder); - - $builder->expects($this->at(4)) - ->method('add') - ->with('subFolders', 'collection', [ - 'type' => 'oro_email_email_folder', - 'allow_add' => true, - 'options' => [ - 'nesting_level' => 4, - ], - ]) - ->willReturn($builder); - - $this->emailFolderType->buildForm($builder, ['nesting_level' => 5]); - } - - public function testGetName() - { - $this->assertEquals('oro_email_email_folder', $this->emailFolderType->getName()); - } -} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTypeTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTypeTest.php index 57de375c929..d1d7312c9fc 100644 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTypeTest.php +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Form/Type/EmailTypeTest.php @@ -156,7 +156,17 @@ protected function getExtensions() $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface') ->disableOriginalConstructor() ->getMock(); - $contextsSelectType = new ContextsSelectType($em, $configManager, $translator, $mapper, $securityTokenStorage); + $eventDispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcher') + ->disableOriginalConstructor() + ->getMock(); + $contextsSelectType = new ContextsSelectType( + $em, + $configManager, + $translator, + $mapper, + $securityTokenStorage, + $eventDispatcher + ); return [ new PreloadedExtension( diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Provider/EmailActivityListProviderTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Provider/EmailActivityListProviderTest.php index 00ed47953f5..07a03c39c92 100644 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Provider/EmailActivityListProviderTest.php +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Provider/EmailActivityListProviderTest.php @@ -27,7 +27,7 @@ class EmailActivityListProviderTest extends \PHPUnit_Framework_TestCase protected $doctrineRegistryLink; /** @var \PHPUnit_Framework_MockObject_MockObject */ - protected $mailboxProcessStorage; + protected $mailboxProcessStorageLink; protected function setUp() { @@ -65,8 +65,8 @@ protected function setUp() ->disableOriginalConstructor() ->getMock(); - $this->mailboxProcessStorage = $this->getMockBuilder( - 'Oro\Bundle\EmailBundle\Mailbox\MailboxProcessStorage' + $this->mailboxProcessStorageLink = $this->getMockBuilder( + 'Oro\Bundle\EntityConfigBundle\DependencyInjection\Utils\ServiceLink' ) ->disableOriginalConstructor() ->getMock(); @@ -80,7 +80,7 @@ protected function setUp() $emailThreadProvider, $htmlTagHelper, $this->securityFacadeLink, - $this->mailboxProcessStorage + $this->mailboxProcessStorageLink ); $this->emailActivityListProvider->setSecurityContextLink($this->securityContextLink); } diff --git a/src/Oro/Bundle/EmbeddedFormBundle/Resources/config/datagrid.yml b/src/Oro/Bundle/EmbeddedFormBundle/Resources/config/datagrid.yml index 4f82b651f62..e4cd22fddd1 100644 --- a/src/Oro/Bundle/EmbeddedFormBundle/Resources/config/datagrid.yml +++ b/src/Oro/Bundle/EmbeddedFormBundle/Resources/config/datagrid.yml @@ -1,7 +1,7 @@ datagrid: embedded-forms-grid: + acl_resource: oro_embedded_form_view source: - acl_resource: oro_embedded_form_view type: orm query: select: diff --git a/src/Oro/Bundle/EmbeddedFormBundle/Resources/config/oro/layout.yml b/src/Oro/Bundle/EmbeddedFormBundle/Resources/config/oro/layout.yml deleted file mode 100644 index fc3c643e37b..00000000000 --- a/src/Oro/Bundle/EmbeddedFormBundle/Resources/config/oro/layout.yml +++ /dev/null @@ -1,5 +0,0 @@ -oro_layout: - themes: - embedded_default: - label: Default theme for embedded forms - groups: [ embedded_forms ] diff --git a/src/Oro/Bundle/EmbeddedFormBundle/Resources/views/layouts/embedded_default/theme.yml b/src/Oro/Bundle/EmbeddedFormBundle/Resources/views/layouts/embedded_default/theme.yml new file mode 100644 index 00000000000..0824e79e6a4 --- /dev/null +++ b/src/Oro/Bundle/EmbeddedFormBundle/Resources/views/layouts/embedded_default/theme.yml @@ -0,0 +1,2 @@ +label: Default theme for embedded forms +groups: [ embedded_forms ] diff --git a/src/Oro/Bundle/EntityBundle/ORM/EntityAliasResolver.php b/src/Oro/Bundle/EntityBundle/ORM/EntityAliasResolver.php index d347ca6f0dc..0632367695a 100644 --- a/src/Oro/Bundle/EntityBundle/ORM/EntityAliasResolver.php +++ b/src/Oro/Bundle/EntityBundle/ORM/EntityAliasResolver.php @@ -3,7 +3,6 @@ namespace Oro\Bundle\EntityBundle\ORM; use Doctrine\Common\Persistence\ManagerRegistry; -use Doctrine\ORM\Mapping\ClassMetadata; use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; @@ -11,6 +10,7 @@ use Oro\Bundle\EntityBundle\Exception\RuntimeException; use Oro\Bundle\EntityBundle\Model\EntityAlias; use Oro\Bundle\EntityBundle\Provider\EntityAliasProviderInterface; +use Oro\Bundle\EntityBundle\Tools\SafeDatabaseChecker; class EntityAliasResolver implements WarmableInterface { @@ -306,8 +306,7 @@ protected function ensureAllAliasesLoaded() return; } - /** @var ClassMetadata[] $allMetadata */ - $allMetadata = $this->doctrine->getManager()->getMetadataFactory()->getAllMetadata(); + $allMetadata = SafeDatabaseChecker::getAllMetadata($this->doctrine->getManager()); foreach ($allMetadata as $metadata) { if (!$metadata->isMappedSuperclass && !isset($this->aliases[$metadata->name])) { $this->findEntityAlias($metadata->name); diff --git a/src/Oro/Bundle/EntityBundle/Provider/AbstractChainProvider.php b/src/Oro/Bundle/EntityBundle/Provider/AbstractChainProvider.php index 49b7de6d41a..bc2589cd307 100644 --- a/src/Oro/Bundle/EntityBundle/Provider/AbstractChainProvider.php +++ b/src/Oro/Bundle/EntityBundle/Provider/AbstractChainProvider.php @@ -36,11 +36,9 @@ protected function getProviders() if (null === $this->sorted) { ksort($this->providers); - if (empty($this->providers)) { - $this->sorted = []; - } else { - $this->sorted = call_user_func_array('array_merge', $this->providers); - } + $this->sorted = $this->providers + ? call_user_func_array('array_merge', $this->providers) + : []; } return $this->sorted; diff --git a/src/Oro/Bundle/EntityBundle/Resources/config/datagrid.yml b/src/Oro/Bundle/EntityBundle/Resources/config/datagrid.yml index 2006ed432bf..b56c95230f5 100644 --- a/src/Oro/Bundle/EntityBundle/Resources/config/datagrid.yml +++ b/src/Oro/Bundle/EntityBundle/Resources/config/datagrid.yml @@ -1,12 +1,12 @@ datagrid: custom-entity-grid: + acl_resource: ~ options: base_datagrid_class: Oro\Bundle\EntityBundle\Grid\CustomEntityDatagrid entityHint: entity export: true entity_pagination: true source: - acl_resource: ~ type: orm query: select: @@ -61,6 +61,7 @@ datagrid: columns: [] entity-relation-grid: + acl_resource: ~ options: entityHint: entity routerEnabled: false @@ -71,7 +72,6 @@ datagrid: included: '#appendRelation' excluded: '#removeRelation' source: - acl_resource: ~ type: orm columns: assigned: diff --git a/src/Oro/Bundle/EntityBundle/Resources/config/services.yml b/src/Oro/Bundle/EntityBundle/Resources/config/services.yml index e26a53fde66..aa6ea58cc6f 100644 --- a/src/Oro/Bundle/EntityBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/EntityBundle/Resources/config/services.yml @@ -30,7 +30,6 @@ parameters: oro_entity.listener.orm.generated_value_strategy_listener.class: Oro\Bundle\EntityBundle\EventListener\ORM\GeneratedValueStrategyListener oro_entity.migration.extension.change_type.class: Oro\Bundle\EntityBundle\Migrations\Extension\ChangeTypeExtension - oro_entity.entity_context_provider.class: Oro\Bundle\EntityBundle\Provider\EntityContextProvider oro_entity.entity_alias_resolver.class: Oro\Bundle\EntityBundle\ORM\EntityAliasResolver oro_entity.entity_alias_config_bag.class: Oro\Bundle\EntityBundle\Provider\EntityAliasConfigBag oro_entity.entity_alias_provider.class: Oro\Bundle\EntityBundle\Provider\EntityAliasProvider @@ -260,13 +259,6 @@ services: tags: - { name: oro_migration.extension, extension_name: change_type } - oro_entity.entity_context_provider: - class: %oro_entity.entity_context_provider.class% - arguments: - - @oro_entity.routing_helper - - @oro_entity.entity_provider - - @oro_entity_config.provider.entity - oro_entity.entity_alias.cache.warmer: class: Oro\Bundle\EntityBundle\Cache\EntityAliasCacheWarmer arguments: diff --git a/src/Oro/Bundle/EntityBundle/Resources/doc/entity_context.md b/src/Oro/Bundle/EntityBundle/Resources/doc/entity_context.md deleted file mode 100644 index 51d4bd5a827..00000000000 --- a/src/Oro/Bundle/EntityBundle/Resources/doc/entity_context.md +++ /dev/null @@ -1,7 +0,0 @@ -## Context ## - -**Adding context entity to email** - -If you want add new context entity class name to email context dialog you need add -"context-grid"="..." option to entity class @config (See CaseEntity.php, for example) -and add datagrid with same name. (See cases-for-context-grid grid in CaseBundle) diff --git a/src/Oro/Bundle/EntityBundle/Resources/public/js/components/select2-entity-field-choice-component.js b/src/Oro/Bundle/EntityBundle/Resources/public/js/components/select2-entity-field-choice-component.js index c2db68fe60b..160303cc003 100644 --- a/src/Oro/Bundle/EntityBundle/Resources/public/js/components/select2-entity-field-choice-component.js +++ b/src/Oro/Bundle/EntityBundle/Resources/public/js/components/select2-entity-field-choice-component.js @@ -1,15 +1,16 @@ define(function(require) { 'use strict'; - var Select2EntityFieldChoiseComponent; + var Select2EntityFieldChoiceComponent; var EntityFieldUtil = require('oroentity/js/entity-field-choice-util'); var Select2EntityFieldComponent = require('oro/select2-entity-field-component'); - Select2EntityFieldChoiseComponent = Select2EntityFieldComponent.extend({ + Select2EntityFieldChoiceComponent = Select2EntityFieldComponent.extend({ initialize: function(options) { this.util = new EntityFieldUtil(options._sourceElement); - Select2EntityFieldChoiseComponent.__super__.initialize.call(this, options); + Select2EntityFieldChoiceComponent.__super__.initialize.call(this, options); } }); - return Select2EntityFieldChoiseComponent; + + return Select2EntityFieldChoiceComponent; }); diff --git a/src/Oro/Bundle/EntityBundle/Tests/Unit/ORM/DoctrineHelperTest.php b/src/Oro/Bundle/EntityBundle/Tests/Unit/ORM/DoctrineHelperTest.php index 5564d464b1d..f970eb842ce 100644 --- a/src/Oro/Bundle/EntityBundle/Tests/Unit/ORM/DoctrineHelperTest.php +++ b/src/Oro/Bundle/EntityBundle/Tests/Unit/ORM/DoctrineHelperTest.php @@ -13,6 +13,7 @@ * @SuppressWarnings(PHPMD.TooManyMethods) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.ExcessivePublicCount) + * @SuppressWarnings(PHPMD.ExcessiveClassLength) */ class DoctrineHelperTest extends \PHPUnit_Framework_TestCase { diff --git a/src/Oro/Bundle/EntityBundle/Tests/Unit/Tools/SafeDatabaseCheckerTest.php b/src/Oro/Bundle/EntityBundle/Tests/Unit/Tools/SafeDatabaseCheckerTest.php new file mode 100644 index 00000000000..4134db8d3af --- /dev/null +++ b/src/Oro/Bundle/EntityBundle/Tests/Unit/Tools/SafeDatabaseCheckerTest.php @@ -0,0 +1,301 @@ +getMockBuilder('Doctrine\DBAL\Connection') + ->disableOriginalConstructor() + ->getMock(); + $schemaManager = $this->getMockBuilder('Doctrine\DBAL\Schema\AbstractSchemaManager') + ->disableOriginalConstructor() + ->setMethods(['tablesExist']) + ->getMockForAbstractClass(); + + $connection->expects($this->once()) + ->method('connect'); + $connection + ->expects($this->once()) + ->method('getSchemaManager') + ->willReturn($schemaManager); + $schemaManager + ->expects($this->once()) + ->method('tablesExist') + ->with($tables) + ->willReturn($tablesExistResult); + + $this->assertSame( + $expectedResult, + SafeDatabaseChecker::tablesExist($connection, $tables) + ); + } + + public function tablesExistProvider() + { + return [ + ['table1'], + [['table1']], + [['table1', 'table2']], + ['table1', false, false], + ]; + } + + /** + * @dataProvider tablesExistWithEmptyTablesParamProvider + */ + public function testTablesExistWithEmptyTablesParam($tables) + { + $connection = $this->getMockBuilder('Doctrine\DBAL\Connection') + ->disableOriginalConstructor() + ->getMock(); + + $connection->expects($this->never()) + ->method('connect'); + + $this->assertFalse( + SafeDatabaseChecker::tablesExist($connection, $tables) + ); + } + + public function tablesExistWithEmptyTablesParamProvider() + { + return [ + [null], + [''], + [[]], + ]; + } + + /** + * @dataProvider expectedExceptionsForTablesExist + */ + public function testTablesExistShouldHandleExpectedExceptions($exception) + { + $connection = $this->getMockBuilder('Doctrine\DBAL\Connection') + ->disableOriginalConstructor() + ->getMock(); + + $connection->expects($this->once()) + ->method('connect') + ->willThrowException($exception); + + $this->assertFalse( + SafeDatabaseChecker::tablesExist($connection, 'table') + ); + } + + public function expectedExceptionsForTablesExist() + { + return [ + [new \PDOException()], + [new DBALException()], + ]; + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage unexpected + */ + public function testTablesExistShouldRethrowUnexpectedException() + { + $connection = $this->getMockBuilder('Doctrine\DBAL\Connection') + ->disableOriginalConstructor() + ->getMock(); + + $connection->expects($this->once()) + ->method('connect') + ->willThrowException(new \Exception('unexpected')); + + SafeDatabaseChecker::tablesExist($connection, 'table'); + } + + public function testGetTableName() + { + $doctrine = $this->getMock('Doctrine\Common\Persistence\ManagerRegistry'); + $em = $this->getMock('Doctrine\ORM\EntityManagerInterface'); + $classMetadata = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadata') + ->disableOriginalConstructor() + ->getMock(); + + $entityName = 'Test\Entity'; + $tableName = 'test_table'; + + $doctrine->expects($this->any()) + ->method('getManagerForClass') + ->with($entityName) + ->willReturn($em); + $em->expects($this->once()) + ->method('getClassMetadata') + ->with($entityName) + ->willReturn($classMetadata); + $classMetadata->expects($this->once()) + ->method('getTableName') + ->willReturn($tableName); + + $this->assertEquals( + $tableName, + SafeDatabaseChecker::getTableName($doctrine, $entityName) + ); + } + + public function testGetTableNameForNotOrmEntity() + { + $doctrine = $this->getMock('Doctrine\Common\Persistence\ManagerRegistry'); + $om = $this->getMock('Doctrine\Common\Persistence\ObjectManager'); + + $entityName = 'Test\Entity'; + + $doctrine->expects($this->any()) + ->method('getManagerForClass') + ->with($entityName) + ->willReturn($om); + $om->expects($this->never()) + ->method('getClassMetadata'); + + $this->assertNull( + SafeDatabaseChecker::getTableName($doctrine, $entityName) + ); + } + + /** + * @dataProvider getTableNameWithEmptyEntityNameParamProvider + */ + public function testGetTableNameWithEmptyEntityNameParam($entityName) + { + $doctrine = $this->getMock('Doctrine\Common\Persistence\ManagerRegistry'); + + $doctrine->expects($this->never()) + ->method('getManagerForClass'); + + $this->assertNull( + SafeDatabaseChecker::getTableName($doctrine, $entityName) + ); + } + + public function getTableNameWithEmptyEntityNameParamProvider() + { + return [ + [null], + [''], + ]; + } + + /** + * @dataProvider expectedExceptionsForGetTableName + */ + public function testGetTableNameShouldHandleExpectedExceptions($exception) + { + $doctrine = $this->getMock('Doctrine\Common\Persistence\ManagerRegistry'); + + $doctrine->expects($this->once()) + ->method('getManagerForClass') + ->willThrowException($exception); + + $this->assertNull( + SafeDatabaseChecker::getTableName($doctrine, 'Test\Entity') + ); + } + + public function expectedExceptionsForGetTableName() + { + return [ + [new \PDOException()], + [new DBALException()], + [new ORMException()], + [new \ReflectionException()], + ]; + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage unexpected + */ + public function testGetTableNameShouldRethrowUnexpectedException() + { + $doctrine = $this->getMock('Doctrine\Common\Persistence\ManagerRegistry'); + + $doctrine->expects($this->once()) + ->method('getManagerForClass') + ->willThrowException(new \Exception('unexpected')); + + SafeDatabaseChecker::getTableName($doctrine, 'Test\Entity'); + } + + public function testGetAllMetadata() + { + $em = $this->getMock('Doctrine\ORM\EntityManagerInterface'); + $metadataFactory = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadataFactory') + ->disableOriginalConstructor() + ->getMock(); + + $classMetadata = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadata') + ->disableOriginalConstructor() + ->getMock(); + + $allMetadata = [$classMetadata]; + + $em->expects($this->once()) + ->method('getMetadataFactory') + ->willReturn($metadataFactory); + $metadataFactory->expects($this->once()) + ->method('getAllMetadata') + ->willReturn($allMetadata); + + $this->assertSame( + $allMetadata, + SafeDatabaseChecker::getAllMetadata($em) + ); + } + + /** + * @dataProvider expectedExceptionsForGetAllMetadata + */ + public function testGetAllMetadataShouldHandleExpectedExceptions($exception) + { + $em = $this->getMock('Doctrine\ORM\EntityManagerInterface'); + + $em->expects($this->once()) + ->method('getMetadataFactory') + ->willThrowException($exception); + + $this->assertSame( + [], + SafeDatabaseChecker::getAllMetadata($em) + ); + } + + public function expectedExceptionsForGetAllMetadata() + { + return [ + [new \PDOException()], + [new DBALException()], + [new ORMException()], + [new \ReflectionException()], + ]; + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage unexpected + */ + public function testGetAllMetadataShouldRethrowUnexpectedException() + { + $em = $this->getMock('Doctrine\ORM\EntityManagerInterface'); + + $em->expects($this->once()) + ->method('getMetadataFactory') + ->willThrowException(new \Exception('unexpected')); + + SafeDatabaseChecker::getAllMetadata($em); + } +} diff --git a/src/Oro/Bundle/EntityBundle/Tools/SafeDatabaseChecker.php b/src/Oro/Bundle/EntityBundle/Tools/SafeDatabaseChecker.php new file mode 100644 index 00000000000..eb0d592757e --- /dev/null +++ b/src/Oro/Bundle/EntityBundle/Tools/SafeDatabaseChecker.php @@ -0,0 +1,90 @@ +connect(); + $result = $connection->getSchemaManager()->tablesExist($tables); + } catch (\PDOException $e) { + } catch (DBALException $e) { + } + } + + return $result; + } + + /** + * Returns the table name for a given entity. + * + * @param ManagerRegistry $doctrine + * @param string $entityName + * + * @return string|null the table name or NULL if it cannot be determined + */ + public static function getTableName(ManagerRegistry $doctrine, $entityName) + { + $result = null; + + if (!empty($entityName)) { + try { + $em = $doctrine->getManagerForClass($entityName); + if ($em instanceof EntityManagerInterface) { + $result = $em->getClassMetadata($entityName)->getTableName(); + } + } catch (\PDOException $e) { + } catch (DBALException $e) { + } catch (ORMException $e) { + } catch (\ReflectionException $e) { + } + } + + return $result; + } + + /** + * Returns metadata of all entities registered in a given entity manager. + * + * @param EntityManagerInterface $em + * + * @return ClassMetadata[] + */ + public static function getAllMetadata(EntityManagerInterface $em) + { + $allMetadata = []; + try { + $allMetadata = $em->getMetadataFactory()->getAllMetadata(); + } catch (\PDOException $e) { + } catch (DBALException $e) { + } catch (ORMException $e) { + } catch (\ReflectionException $e) { + } + + return $allMetadata; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/ConfigModelManager.php b/src/Oro/Bundle/EntityConfigBundle/Config/ConfigModelManager.php index fee0913f2e7..c1890e155e7 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/ConfigModelManager.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/ConfigModelManager.php @@ -6,6 +6,7 @@ use Doctrine\ORM\UnitOfWork; use Oro\Component\DependencyInjection\ServiceLink; +use Oro\Bundle\EntityBundle\Tools\SafeDatabaseChecker; use Oro\Bundle\EntityConfigBundle\Entity\ConfigModel; use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; use Oro\Bundle\EntityConfigBundle\Entity\FieldConfigModel; @@ -80,13 +81,10 @@ public function checkDatabase() return true; } - $this->dbCheck = false; - try { - $conn = $this->getEntityManager()->getConnection(); - $conn->connect(); - $this->dbCheck = $conn->getSchemaManager()->tablesExist($this->requiredTables); - } catch (\PDOException $e) { - } + $this->dbCheck = SafeDatabaseChecker::tablesExist( + $this->getEntityManager()->getConnection(), + $this->requiredTables + ); return $this->dbCheck; } diff --git a/src/Oro/Bundle/EntityConfigBundle/Resources/config/datagrid.yml b/src/Oro/Bundle/EntityConfigBundle/Resources/config/datagrid.yml index 9a7a2fa89d7..c23a3d59b07 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Resources/config/datagrid.yml +++ b/src/Oro/Bundle/EntityConfigBundle/Resources/config/datagrid.yml @@ -6,8 +6,8 @@ datagrid: toolbarOptions: pageSize: default_per_page: 100 + acl_resource: oro_entityconfig_manage source: - acl_resource: oro_entityconfig_manage type: orm query: select: @@ -69,9 +69,9 @@ datagrid: toolbarOptions: pageSize: default_per_page: 50 + # TODO: check oro_entityconfig_view acl (403) right now + acl_resource: oro_entityconfig_manage source: - # TODO: check oro_entityconfig_view acl (403) right now - acl_resource: oro_entityconfig_manage type: orm query: select: @@ -126,8 +126,8 @@ datagrid: entity-audit-grid: options: entityHint: history + acl_resource: oro_entityconfig_manage source: - acl_resource: oro_entityconfig_manage type: orm query: select: diff --git a/src/Oro/Bundle/EntityExtendBundle/Entity/Manager/AssociationManager.php b/src/Oro/Bundle/EntityExtendBundle/Entity/Manager/AssociationManager.php index edce9cfe967..a5a28baacd9 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Entity/Manager/AssociationManager.php +++ b/src/Oro/Bundle/EntityExtendBundle/Entity/Manager/AssociationManager.php @@ -180,6 +180,8 @@ public function getMultiOwnerFilter($scope, $attribute) * function (QueryBuilder $qb, $targetEntityClass) * * @return SqlQueryBuilder + * + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function getMultiAssociationsQueryBuilder( $associationOwnerClass, @@ -198,6 +200,11 @@ public function getMultiAssociationsQueryBuilder( $subQueries = []; foreach ($associationTargets as $entityClass => $fieldName) { $nameExpr = $this->entityNameResolver->getNameDQL($entityClass, 'target'); + // Need to forcibly convert expression to string when the title is different type. + // Example of error: "UNION types text and integer cannot be matched". + if ($nameExpr) { + $nameExpr = sprintf('CONCAT(%s,\'\')', $nameExpr); + } $subQb = $em->getRepository($associationOwnerClass)->createQueryBuilder('e') ->select( sprintf( diff --git a/src/Oro/Bundle/EntityExtendBundle/Form/Type/AbstractAssociationType.php b/src/Oro/Bundle/EntityExtendBundle/Form/Type/AbstractAssociationType.php index bddcd0c57c0..9d6d7ca8c9c 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Form/Type/AbstractAssociationType.php +++ b/src/Oro/Bundle/EntityExtendBundle/Form/Type/AbstractAssociationType.php @@ -54,14 +54,11 @@ protected function isReadOnly($options) $configId = $options['config_id']; $className = $configId->getClassName(); - if (!empty($className)) { - if ($this->typeHelper->isAssociationOwningSideEntity($className, $options['association_class'])) { + if (!empty($className) + && $this->typeHelper->isDictionary($className) + && !$this->typeHelper->isSupportActivityEnabled($className) + ) { return true; - } - if ($this->typeHelper->isDictionary($className) - && !$this->typeHelper->isSupportActivityEnabled($className)) { - return true; - } } return parent::isReadOnly($options); diff --git a/src/Oro/Bundle/EntityExtendBundle/Form/Type/DictionaryFilterType.php b/src/Oro/Bundle/EntityExtendBundle/Form/Type/DictionaryFilterType.php index fe73b686f96..fb8057e8786 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Form/Type/DictionaryFilterType.php +++ b/src/Oro/Bundle/EntityExtendBundle/Form/Type/DictionaryFilterType.php @@ -10,7 +10,6 @@ use Oro\Bundle\EntityExtendBundle\Provider\EnumValueProvider; use Oro\Bundle\EntityExtendBundle\Entity\AbstractEnumValue; use Oro\Bundle\EntityExtendBundle\Tools\ExtendHelper; -use Oro\Bundle\EntityExtendBundle\Form\Type\AbstractMultiChoiceType; use Oro\Bundle\FilterBundle\Form\Type\Filter\ChoiceFilterType; @@ -59,7 +58,7 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) if (empty($options['dictionary_code'])) { throw new InvalidOptionsException( - 'Either "class" or "dictionary_code" must option must be set.' + 'Either "class" or "dictionary_code" option must be set.' ); } diff --git a/src/Oro/Bundle/EntityExtendBundle/Form/Util/AssociationTypeHelper.php b/src/Oro/Bundle/EntityExtendBundle/Form/Util/AssociationTypeHelper.php index 2370bc52fef..1972fb68963 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Form/Util/AssociationTypeHelper.php +++ b/src/Oro/Bundle/EntityExtendBundle/Form/Util/AssociationTypeHelper.php @@ -68,34 +68,6 @@ public function isSupportActivityEnabled($className) return false; } - /** - * Checks if the given entity is an owning side of association - * - * @param string $className - * @param string $associationClass Represents the owning side entity, can be: - * - full class name or entity name for single association - * - a group name for multiple association - * it is supposed that the group name should not contain \ and : characters - * - * @return bool - */ - public function isAssociationOwningSideEntity($className, $associationClass) - { - if (strpos($associationClass, ':') !== false || strpos($associationClass, '\\') !== false) { - // the association class is full class name or entity name - if ($className === $this->entityClassResolver->getEntityClass($associationClass)) { - return true; - } - } else { - // the association class is a group name - if (!empty($className) && in_array($className, $this->getOwningSideEntities($associationClass))) { - return true; - }; - } - - return false; - } - /** * Returns all entities included in the given group * diff --git a/src/Oro/Bundle/EntityExtendBundle/Grid/AbstractFieldsExtension.php b/src/Oro/Bundle/EntityExtendBundle/Grid/AbstractFieldsExtension.php index d53ddf34a7a..070ccca45db 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Grid/AbstractFieldsExtension.php +++ b/src/Oro/Bundle/EntityExtendBundle/Grid/AbstractFieldsExtension.php @@ -5,7 +5,6 @@ use Doctrine\ORM\Query\Expr\From; use Doctrine\ORM\QueryBuilder; -use Oro\Bundle\DataGridBundle\Datagrid\Builder; use Oro\Bundle\DataGridBundle\Datagrid\DatagridGuesser; use Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration; use Oro\Bundle\DataGridBundle\Datasource\DatasourceInterface; @@ -51,7 +50,7 @@ public function __construct( */ public function isApplicable(DatagridConfiguration $config) { - return $config->offsetGetByPath(Builder::DATASOURCE_TYPE_PATH) == OrmDatasource::TYPE; + return $config->getDatasourceType() == OrmDatasource::TYPE; } /** diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/public/js/init-entity-extend-apply.js b/src/Oro/Bundle/EntityExtendBundle/Resources/public/js/init-entity-extend-apply.js index aa4516c1176..f327a8c02d2 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Resources/public/js/init-entity-extend-apply.js +++ b/src/Oro/Bundle/EntityExtendBundle/Resources/public/js/init-entity-extend-apply.js @@ -33,15 +33,17 @@ require([ progress.show(); $.post(url, function() { - mediator.once('page:beforeChange', function() { - modal.close(); - }); - mediator.once('page:afterChange', function() { - mediator.execute('showFlashMessage', 'success', __('oro.entity_extend.schema_updated')); - }); - mediator.execute('redirectTo', { - url: routing.generate('oro_entityconfig_index', {'_enableContentProviders': 'mainMenu'}) - }); + modal.close(); + mediator.execute( + 'showFlashMessage', + 'success', + __('oro.entity_extend.schema_updated'), + {afterReload: true} + ); + mediator.execute('showMessage', 'info', __('Please wait until page will be reloaded...')); + mediator.execute('showLoading'); + // force reload of the application to make sure 'js/routes' is reloaded + window.location.href = routing.generate('oro_entityconfig_index'); }).fail(function() { modal.close(); mediator.execute('showFlashMessage', 'error', __('oro.entity_extend.schema_update_failed')); diff --git a/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Entity/Manager/AssociationManagerTest.php b/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Entity/Manager/AssociationManagerTest.php index 6c04f037dfa..7cd7482d435 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Entity/Manager/AssociationManagerTest.php +++ b/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Entity/Manager/AssociationManagerTest.php @@ -406,7 +406,7 @@ function (QueryBuilder $qb) { . 'FROM (' . 'SELECT DISTINCT t0_.id AS id_0, t1_.id AS id_1, ' . '\'' . $targetClass1 . '\' AS sclr_2, ' - . 't1_.firstName || \' \' || t1_.lastName AS sclr_3 ' + . 't1_.firstName || \' \' || t1_.lastName || \'\' AS sclr_3 ' . 'FROM test_owner1 t0_ ' . 'INNER JOIN test_owner1_to_target1 t2_ ON t0_.id = t2_.owner_id ' . 'INNER JOIN test_target1 t1_ ON t1_.id = t2_.target_id ' @@ -415,7 +415,7 @@ function (QueryBuilder $qb) { . ' UNION ALL ' . 'SELECT DISTINCT t0_.id AS id_0, t1_.id AS id_1, ' . '\'' . $targetClass2 . '\' AS sclr_2, ' - . 't1_.firstName || \' \' || t1_.lastName AS sclr_3 ' + . 't1_.firstName || \' \' || t1_.lastName || \'\' AS sclr_3 ' . 'FROM test_owner1 t0_ ' . 'INNER JOIN test_owner1_to_target2 t2_ ON t0_.id = t2_.owner_id ' . 'INNER JOIN test_target2 t1_ ON t1_.id = t2_.target_id ' diff --git a/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Form/Util/AssociationTypeHelperTest.php b/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Form/Util/AssociationTypeHelperTest.php index 3cb6af2d9d9..cf96a549042 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Form/Util/AssociationTypeHelperTest.php +++ b/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Form/Util/AssociationTypeHelperTest.php @@ -142,81 +142,6 @@ public function isActivitySupport() ]; } - public function testIsAssociationOwningSideEntityForNotOwningSideEntity() - { - $className = 'Test\Entity1'; - $associationClass = 'Test\Entity'; - - $this->entityClassResolver->expects($this->once()) - ->method('getEntityClass') - ->with($associationClass) - ->will($this->returnValue($associationClass)); - - $this->assertFalse( - $this->typeHelper->isAssociationOwningSideEntity($className, $associationClass) - ); - } - - public function testIsAssociationOwningSideEntityWithClassName() - { - $className = 'Test\Entity'; - $associationClass = 'Test\Entity'; - - $this->entityClassResolver->expects($this->once()) - ->method('getEntityClass') - ->with($associationClass) - ->will($this->returnValue($className)); - - $this->assertTrue( - $this->typeHelper->isAssociationOwningSideEntity($className, $associationClass) - ); - } - - public function testIsAssociationOwningSideEntityWithEntityName() - { - $className = 'Test\Entity'; - $associationClass = 'Test:Entity'; - - $this->entityClassResolver->expects($this->once()) - ->method('getEntityClass') - ->with($associationClass) - ->will($this->returnValue($className)); - - $this->assertTrue( - $this->typeHelper->isAssociationOwningSideEntity($className, $associationClass) - ); - } - - public function testIsAssociationOwningSideEntityWithGroupName() - { - $config1 = new Config(new EntityConfigId('grouping', 'Test\Entity1')); - $config1->set('groups', ['some_group', 'another_group']); - $config2 = new Config(new EntityConfigId('grouping', 'Test\Entity2')); - $config2->set('groups', ['another_group']); - $config3 = new Config(new EntityConfigId('grouping', 'Test\Entity3')); - - $configs = [$config1, $config2, $config3]; - - $configProvider = $this->getConfigProviderMock(); - $this->configManager->expects($this->once()) - ->method('getProvider') - ->with('grouping') - ->will($this->returnValue($configProvider)); - $configProvider->expects($this->once()) - ->method('getConfigs') - ->will($this->returnValue($configs)); - - $this->assertTrue( - $this->typeHelper->isAssociationOwningSideEntity('Test\Entity1', 'some_group') - ); - $this->assertFalse( - $this->typeHelper->isAssociationOwningSideEntity('Test\Entity', 'some_group') - ); - $this->assertFalse( - $this->typeHelper->isAssociationOwningSideEntity('Test\Entity2', 'some_group') - ); - } - public function testGetOwningSideEntities() { $config1 = new Config(new EntityConfigId('grouping', 'Test\Entity1')); diff --git a/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Mapping/ExtendClassMetadataFactoryTest.php b/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Mapping/ExtendClassMetadataFactoryTest.php index d0278d7ef4d..bf34cecfcec 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Mapping/ExtendClassMetadataFactoryTest.php +++ b/src/Oro/Bundle/EntityExtendBundle/Tests/Unit/Mapping/ExtendClassMetadataFactoryTest.php @@ -35,12 +35,17 @@ public function testSetMetadataFor() ); $cacheSalt = '$CLASSMETADATA'; - $this->assertAttributeSame( - [ - '[Oro\Bundle\UserBundle\Entity\User'.$cacheSalt .'][1]' => $metadata - ], - 'data', - $this->cmf->getCacheDriver() + $this->assertTrue( + $this->cmf->getCacheDriver()->contains('Oro\Bundle\UserBundle\Entity\User' . $cacheSalt) + ); + } + + public function testSetMetadataForWithoutCacheDriver() + { + $metadata = new ClassMetadata('Oro\Bundle\UserBundle\Entity\User'); + $this->cmf->setMetadataFor( + 'Oro\Bundle\UserBundle\Entity\User', + $metadata ); } } diff --git a/src/Oro/Bundle/EntityExtendBundle/Validator/Constraints/UniqueKeysValidator.php b/src/Oro/Bundle/EntityExtendBundle/Validator/Constraints/UniqueKeysValidator.php index 05f2944c067..fc25d1aef32 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Validator/Constraints/UniqueKeysValidator.php +++ b/src/Oro/Bundle/EntityExtendBundle/Validator/Constraints/UniqueKeysValidator.php @@ -5,7 +5,7 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; -use Oro\Bundle\UIBundle\Tools\ArrayUtils; +use Oro\Component\PhpUtils\ArrayUtil; class UniqueKeysValidator extends ConstraintValidator { @@ -15,7 +15,7 @@ class UniqueKeysValidator extends ConstraintValidator public function validate($value, Constraint $constraint) { /** @var UniqueKeys $constraint */ - $names = ArrayUtils::arrayColumn($value, 'name'); + $names = ArrayUtil::arrayColumn($value, 'name'); if ($names && $names != array_unique($names)) { $this->context->addViolation( $constraint->message @@ -24,7 +24,7 @@ public function validate($value, Constraint $constraint) return; } - $keys = ArrayUtils::arrayColumn($value, 'key'); + $keys = ArrayUtil::arrayColumn($value, 'key'); if ($keys && $keys != array_unique($keys, SORT_REGULAR)) { $this->context->addViolation( $constraint->message diff --git a/src/Oro/Bundle/EntityMergeBundle/Metadata/MetadataBuilder.php b/src/Oro/Bundle/EntityMergeBundle/Metadata/MetadataBuilder.php index 158fd4240bb..47458ca9e4f 100644 --- a/src/Oro/Bundle/EntityMergeBundle/Metadata/MetadataBuilder.php +++ b/src/Oro/Bundle/EntityMergeBundle/Metadata/MetadataBuilder.php @@ -136,10 +136,16 @@ protected function addDoctrineInverseAssociations( $associationMapping['mappedBySourceEntity'] = false; + $mergeModes = [MergeModes::UNITE]; + if ($associationMapping['type'] === ClassMetadataInfo::ONE_TO_ONE) { + // for fields with ONE_TO_ONE relation Unite strategy is impossible, so Replace is used + $mergeModes = [MergeModes::REPLACE]; + } + $fieldMetadata = $this->metadataFactory->createFieldMetadata( array( 'field_name' => $this->createInverseAssociationFieldName($currentClassName, $fieldName), - 'merge_modes' => array(MergeModes::UNITE), + 'merge_modes' => $mergeModes, 'source_field_name' => $fieldName, 'source_class_name' => $currentClassName, ), diff --git a/src/Oro/Bundle/EntityMergeBundle/Tests/Unit/Metadata/MetadataBuilderTest.php b/src/Oro/Bundle/EntityMergeBundle/Tests/Unit/Metadata/MetadataBuilderTest.php index 328ad92988b..00c2b3b51fe 100644 --- a/src/Oro/Bundle/EntityMergeBundle/Tests/Unit/Metadata/MetadataBuilderTest.php +++ b/src/Oro/Bundle/EntityMergeBundle/Tests/Unit/Metadata/MetadataBuilderTest.php @@ -79,15 +79,15 @@ public function testCreateEntityMetadataByClass() $metadataFactoryCallIndex = 0; $this->metadataFactory->expects($this->at($metadataFactoryCallIndex++)) ->method('createEntityMetadata') - ->with(array(), $this->isType('array')) + ->with([], $this->isType('array')) ->will($this->returnValue($entityMetadata)); // Test adding doctrine fields - $doctrineFieldNames = array('id', 'foo_field', 'bar_field'); - $doctrineFieldNamesWithoutId = array('foo_field', 'bar_field'); + $doctrineFieldNames = ['id', 'foo_field', 'bar_field']; + $doctrineFieldNamesWithoutId = ['foo_field', 'bar_field']; $classMetadataCallIndex = 0; - $idFieldNames = array('id'); + $idFieldNames = ['id']; $this->classMetadata->expects($this->at($classMetadataCallIndex++)) ->method('getIdentifierFieldNames') ->will($this->returnValue($idFieldNames)); @@ -97,7 +97,7 @@ public function testCreateEntityMetadataByClass() ->will($this->returnValue($doctrineFieldNames)); foreach ($doctrineFieldNamesWithoutId as $fieldName) { - $fieldMapping = array('fieldName' => $fieldName); + $fieldMapping = ['fieldName' => $fieldName]; $this->classMetadata->expects($this->at($classMetadataCallIndex++)) ->method('getFieldMapping') @@ -108,7 +108,7 @@ public function testCreateEntityMetadataByClass() $this->metadataFactory->expects($this->at($metadataFactoryCallIndex++)) ->method('createFieldMetadata') - ->with(array('field_name' => $fieldName), $fieldMapping) + ->with(['field_name' => $fieldName], $fieldMapping) ->will($this->returnValue($fieldMetadata)); $entityMetadata->expects($this->at($entityMetadataCallIndex++)) @@ -117,10 +117,10 @@ public function testCreateEntityMetadataByClass() } // Test adding doctrine associations - $associationMappings = array( - 'foo_association' => array('foo' => 'bar'), - 'bar_association' => array('bar' => 'baz') - ); + $associationMappings = [ + 'foo_association' => ['foo' => 'bar'], + 'bar_association' => ['bar' => 'baz'] + ]; $this->classMetadata->expects($this->at($classMetadataCallIndex++)) ->method('getAssociationMappings') @@ -131,7 +131,7 @@ public function testCreateEntityMetadataByClass() $this->metadataFactory->expects($this->at($metadataFactoryCallIndex++)) ->method('createFieldMetadata') - ->with(array('field_name' => $fieldName), $associationMapping) + ->with(['field_name' => $fieldName], $associationMapping) ->will($this->returnValue($fieldMetadata)); $entityMetadata->expects($this->at($entityMetadataCallIndex++)) @@ -140,42 +140,56 @@ public function testCreateEntityMetadataByClass() } // Test adding doctrine inverse associations - $allMetadata = array( + $allMetadata = [ self::CLASS_NAME => $this->classMetadata, 'Namespace\\FooEntity' => $fooClassMetadata = $this->createClassMetadata(), 'Namespace\\BarEntity' => $barClassMetadata = $this->createClassMetadata(), - ); - - $expectedClassesData = array( - 'Namespace\\FooEntity' => array( - 'associationMappings' => array( - 'foo_association' => array('foo' => 'bar'), - ), - 'expectedFields' => array( - 'foo_association' => array( + 'Namespace\\FooBarEntity' => $fooBarClassMetadata = $this->createClassMetadata(), + ]; + + $expectedClassesData = [ + 'Namespace\\FooEntity' => [ + 'associationMappings' => [ + 'foo_association' => ['foo' => 'bar', 'type' => ClassMetadataInfo::ONE_TO_MANY], + ], + 'expectedFields' => [ + 'foo_association' => [ 'field_name' => 'Namespace_FooEntity_foo_association', - 'merge_modes' => array(MergeModes::UNITE), + 'merge_modes' => [MergeModes::UNITE], 'source_field_name' => 'foo_association', 'source_class_name' => 'Namespace\\FooEntity', - ), - ) - ), - 'Namespace\\BarEntity' => array( - 'associationMappings' => array( - 'bar_association' => array('bar' => 'baz'), - 'skipped_many_to_many' => array('type' => ClassMetadataInfo::MANY_TO_MANY), - 'skipped_mapped_by' => array('mappedBy' => self::CLASS_NAME), - ), - 'expectedFields' => array( - 'bar_association' => array( + ], + ] + ], + 'Namespace\\BarEntity' => [ + 'associationMappings' => [ + 'bar_association' => ['bar' => 'baz', 'type' => ClassMetadataInfo::ONE_TO_MANY], + 'skipped_many_to_many' => ['type' => ClassMetadataInfo::MANY_TO_MANY], + 'skipped_mapped_by' => ['mappedBy' => self::CLASS_NAME], + ], + 'expectedFields' => [ + 'bar_association' => [ 'field_name' => 'Namespace_BarEntity_bar_association', - 'merge_modes' => array(MergeModes::UNITE), + 'merge_modes' => [MergeModes::UNITE], 'source_field_name' => 'bar_association', 'source_class_name' => 'Namespace\\BarEntity', - ), - ) - ) - ); + ], + ] + ], + 'Namespace\\FooBarEntity' => [ + 'associationMappings' => [ + 'bar_association' => ['bar' => 'baz', 'type' => ClassMetadataInfo::ONE_TO_ONE], + ], + 'expectedFields' => [ + 'bar_association' => [ + 'field_name' => 'Namespace_FooBarEntity_bar_association', + 'merge_modes' => [MergeModes::REPLACE], + 'source_field_name' => 'bar_association', + 'source_class_name' => 'Namespace\\FooBarEntity', + ], + ] + ] + ]; $this->doctrineHelper->expects($this->once()) ->method('getAllMetadata') diff --git a/src/Oro/Bundle/EntityPaginationBundle/Datagrid/EntityPaginationExtension.php b/src/Oro/Bundle/EntityPaginationBundle/Datagrid/EntityPaginationExtension.php index 075a86b3371..762df5ca894 100644 --- a/src/Oro/Bundle/EntityPaginationBundle/Datagrid/EntityPaginationExtension.php +++ b/src/Oro/Bundle/EntityPaginationBundle/Datagrid/EntityPaginationExtension.php @@ -2,7 +2,6 @@ namespace Oro\Bundle\EntityPaginationBundle\Datagrid; -use Oro\Bundle\DataGridBundle\Datagrid\Builder; use Oro\Bundle\DataGridBundle\Extension\AbstractExtension; use Oro\Bundle\DataGridBundle\Datasource\Orm\OrmDatasource; use Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration; @@ -22,7 +21,7 @@ public function isApplicable(DatagridConfiguration $config) return false; } - return $config->offsetGetByPath(Builder::DATASOURCE_TYPE_PATH) == OrmDatasource::TYPE; + return $config->getDatasourceType() == OrmDatasource::TYPE; } /** @@ -36,7 +35,6 @@ public function processConfigs(DatagridConfiguration $config) throw new \LogicException('Entity pagination is not boolean'); } - $pagination = $pagination ? true : false; - $config->offsetSetByPath(self::ENTITY_PAGINATION_PATH, $pagination); + $config->offsetSetByPath(self::ENTITY_PAGINATION_PATH, (bool)$pagination); } } diff --git a/src/Oro/Bundle/EntityPaginationBundle/Manager/EntityPaginationManager.php b/src/Oro/Bundle/EntityPaginationBundle/Manager/EntityPaginationManager.php index 12050a065cb..4b1fcee5575 100644 --- a/src/Oro/Bundle/EntityPaginationBundle/Manager/EntityPaginationManager.php +++ b/src/Oro/Bundle/EntityPaginationBundle/Manager/EntityPaginationManager.php @@ -58,15 +58,11 @@ public static function getPermission($scope) { switch ($scope) { case self::VIEW_SCOPE: - $permission = 'VIEW'; - break; + return 'VIEW'; case self::EDIT_SCOPE: - $permission = 'EDIT'; - break; - default: - throw new \LogicException(sprintf('Scope "%s" is not available.', $scope)); + return 'EDIT'; } - return $permission; + throw new \LogicException(sprintf('Scope "%s" is not available.', $scope)); } } diff --git a/src/Oro/Bundle/EntityPaginationBundle/Manager/MessageManager.php b/src/Oro/Bundle/EntityPaginationBundle/Manager/MessageManager.php index 67c1ec9e014..5e5ec38d5a7 100644 --- a/src/Oro/Bundle/EntityPaginationBundle/Manager/MessageManager.php +++ b/src/Oro/Bundle/EntityPaginationBundle/Manager/MessageManager.php @@ -150,19 +150,15 @@ protected function getStatsMessage($scope) { switch ($scope) { case EntityPaginationManager::VIEW_SCOPE: - $message = + return 'oro.entity_pagination.message.' . 'stats_number_view_%count%_record|stats_number_view_%count%_records'; - break; case EntityPaginationManager::EDIT_SCOPE: - $message = + return 'oro.entity_pagination.message.' . 'stats_number_edit_%count%_record|stats_number_edit_%count%_records'; - break; - default: - throw new \LogicException(sprintf('Scope "%s" is not available.', $scope)); } - return $message; + throw new \LogicException(sprintf('Scope "%s" is not available.', $scope)); } } diff --git a/src/Oro/Bundle/EntityPaginationBundle/Storage/StorageDataCollector.php b/src/Oro/Bundle/EntityPaginationBundle/Storage/StorageDataCollector.php index 8ebc7c46e80..67bad490673 100644 --- a/src/Oro/Bundle/EntityPaginationBundle/Storage/StorageDataCollector.php +++ b/src/Oro/Bundle/EntityPaginationBundle/Storage/StorageDataCollector.php @@ -83,10 +83,19 @@ public function collect(Request $request, $scope) $isDataCollected = false; - $gridNames = array_keys($request->query->get('grid', [])); + $gridNames = array(); + if ($request->query->get('grid')) { + $gridNames = array_keys((array)$request->query->get('grid', [])); + } foreach ($gridNames as $gridName) { - // datagrid manager automatically extracts all required parameters from request - $dataGrid = $this->datagridManager->getDatagridByRequestParams($gridName); + try { + // datagrid manager automatically extracts all required parameters from request + $dataGrid = $this->datagridManager->getDatagridByRequestParams($gridName); + } catch (\RuntimeException $e) { + // processing of invalid grid names + continue; + } + if (!$this->paginationManager->isDatagridApplicable($dataGrid)) { continue; } diff --git a/src/Oro/Bundle/EntityPaginationBundle/Tests/Unit/Storage/StorageDataCollectorTest.php b/src/Oro/Bundle/EntityPaginationBundle/Tests/Unit/Storage/StorageDataCollectorTest.php index 0e282def516..040bb4e53d7 100644 --- a/src/Oro/Bundle/EntityPaginationBundle/Tests/Unit/Storage/StorageDataCollectorTest.php +++ b/src/Oro/Bundle/EntityPaginationBundle/Tests/Unit/Storage/StorageDataCollectorTest.php @@ -96,6 +96,30 @@ public function testCollectWithDisabledPagination() $this->assertFalse($this->collector->collect($this->getGridRequest(), 'test')); } + public function testCollectWithEmptyGridRequest() + { + $this->setPaginationEnabled(true); + $this->datagridManager->expects($this->never()) + ->method('getDatagridByRequestParams'); + + $this->assertFalse($this->collector->collect(new Request(['grid' => '']), 'test')); + } + + public function testCollectWithInvalidGridName() + { + $invalidGridName = 'invalid'; + + $this->setPaginationEnabled(true); + $this->datagridManager->expects($this->once()) + ->method('getDatagridByRequestParams') + ->with($invalidGridName) + ->willThrowException(new \RuntimeException()); + $this->storage->expects($this->never()) + ->method('hasData'); + + $this->assertFalse($this->collector->collect(new Request(['grid' => [$invalidGridName => null]]), 'test')); + } + public function testCollectGridNotApplicable() { $this->setPaginationEnabled(true); diff --git a/src/Oro/Bundle/FilterBundle/Form/EventListener/DateFilterSubscriber.php b/src/Oro/Bundle/FilterBundle/Form/EventListener/DateFilterSubscriber.php index 94ba5a0c051..3822f505694 100644 --- a/src/Oro/Bundle/FilterBundle/Form/EventListener/DateFilterSubscriber.php +++ b/src/Oro/Bundle/FilterBundle/Form/EventListener/DateFilterSubscriber.php @@ -119,9 +119,7 @@ function ($data) { $this->mapValues($children, $data, $this->getDatePartAccessorClosure('Y')); $this->replaceValueFields($form->get('value'), array_flip(range(date('Y') - 50, date('Y') + 50))); break; - // in case we do not need any value to be mapped(converted), in other words - use as is case DateModifierInterface::PART_SOURCE: - return; case DateModifierInterface::PART_VALUE: default: $this->mapValues( diff --git a/src/Oro/Bundle/FilterBundle/Grid/Extension/OrmFilterExtension.php b/src/Oro/Bundle/FilterBundle/Grid/Extension/OrmFilterExtension.php index d341e3872a1..f4fb11ba76c 100644 --- a/src/Oro/Bundle/FilterBundle/Grid/Extension/OrmFilterExtension.php +++ b/src/Oro/Bundle/FilterBundle/Grid/Extension/OrmFilterExtension.php @@ -2,16 +2,15 @@ namespace Oro\Bundle\FilterBundle\Grid\Extension; -use Oro\Bundle\DataGridBundle\Extension\Formatter\Property\PropertyInterface; use Symfony\Component\Translation\TranslatorInterface; -use Oro\Bundle\DataGridBundle\Datagrid\Builder; use Oro\Bundle\DataGridBundle\Datagrid\Common\MetadataObject; use Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration; use Oro\Bundle\DataGridBundle\Datasource\DatasourceInterface; use Oro\Bundle\DataGridBundle\Datasource\Orm\OrmDatasource; use Oro\Bundle\DataGridBundle\Extension\AbstractExtension; use Oro\Bundle\DataGridBundle\Extension\Formatter\Configuration as FormatterConfiguration; +use Oro\Bundle\DataGridBundle\Extension\Formatter\Property\PropertyInterface; use Oro\Bundle\FilterBundle\Filter\FilterUtility; use Oro\Bundle\FilterBundle\Filter\FilterInterface; use Oro\Bundle\FilterBundle\Datasource\Orm\OrmFilterDatasourceAdapter; @@ -52,7 +51,7 @@ public function isApplicable(DatagridConfiguration $config) return false; } - return $config->offsetGetByPath(Builder::DATASOURCE_TYPE_PATH) == OrmDatasource::TYPE; + return $config->getDatasourceType() == OrmDatasource::TYPE; } /** diff --git a/src/Oro/Bundle/FilterBundle/Resources/public/css/less/oro.filter.less b/src/Oro/Bundle/FilterBundle/Resources/public/css/less/oro.filter.less index 10790938465..100a5dacd8c 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/public/css/less/oro.filter.less +++ b/src/Oro/Bundle/FilterBundle/Resources/public/css/less/oro.filter.less @@ -69,7 +69,7 @@ padding: 8px 8px 2px 8px; border: 1px solid #d3d3d3; display: none; - z-index: @zindexPopover; + z-index: @oroZindexDropdown; margin-top: 7px; top: auto; } @@ -115,11 +115,14 @@ margin-bottom: 0; } .filter-select .select-filter-widget { - font-weight: bold; - color: #333333; - padding: 0 0 2px 0; - background: none; - border: none; + &, &.ui-multiselect { + font-weight: bold; + color: #333333; + padding: 0 0 2px 0; + background: none; + border: none; + margin-left: 0; + } } .filter-select .select-filter-widget .ui-icon { @@ -200,7 +203,8 @@ margin: 0 6px 5px 5px; } -.ui-multiselect.filter-list { +.ui-multiselect.filter-list, .ui-multiselect.select-filter-widget { + background: none; border: none; font-size: 13px; @@ -209,15 +213,15 @@ line-height: 1em; } -.ui-multiselect.filter-list:hover .add-filter-button span.caret { +.ui-multiselect.select-filter-widget:hover .add-filter-button span.caret { border-top-color: #005099; /* @linkColorHover; */ } -.ui-multiselect.filter-list:hover a { +.ui-multiselect.select-filter-widget:hover a { color: #005099; /* @linkColorHover; */ } -.ui-multiselect.filter-list.select-filter-widget span.caret { +.ui-multiselect.select-filter-widget span.caret { margin-top: 5px; } @@ -351,9 +355,14 @@ button.ui-multiselect.select-filter-widget.ui-state-hover{ vertical-align: middle; } } -.filter-item.filter-business-unit { +.filter-item.choice-tree-filter { margin-right: 0; margin-bottom: 0; + max-width: 300px; + min-width: 180px; + .choice-tree-filter-search{ + width: calc(~"100% - 14px"); + } .buttons { margin-bottom: 5px; span { @@ -366,7 +375,6 @@ button.ui-multiselect.select-filter-widget.ui-state-hover{ } } .list { - height: 200px; max-height: 200px; overflow: auto; margin-bottom: 10px; @@ -375,14 +383,17 @@ button.ui-multiselect.select-filter-widget.ui-state-hover{ } ul { list-style-type: none; - margin-right: 25px; input { margin-right: 5px; margin-top: 0; + margin-left: -18px; } - label.search-result { - color: #000000; - font-weight: bold; + label { + padding-left: 20px; + &.search-result { + color: #000000; + font-weight: bold; + } } } } diff --git a/src/Oro/Bundle/FilterBundle/Resources/public/js/datafilter-builder.js b/src/Oro/Bundle/FilterBundle/Resources/public/js/datafilter-builder.js index 06a9c37e4c2..cb0da3b532e 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/public/js/datafilter-builder.js +++ b/src/Oro/Bundle/FilterBundle/Resources/public/js/datafilter-builder.js @@ -39,10 +39,12 @@ define([ return; } + var filtersList; var options = methods.combineOptions.call(this); options.collection = this.collection; - var filtersList = new FiltersManager(options); - this.$el.prepend(filtersList.render().$el); + options.el = $('
').prependTo(this.$el); + filtersList = new FiltersManager(options); + filtersList.render(); mediator.trigger('datagrid_filters:rendered', this.collection, this.$el); this.metadata.state.filters = this.metadata.state.filters || []; if (this.collection.length === 0 && this.metadata.state.filters.length === 0) { @@ -74,7 +76,9 @@ define([ filters[options.name] = new Filter(); } }); - return {filters: filters}; + return { + filters: filters + }; } }; 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 420945ac201..8dc2244061a 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 @@ -103,6 +103,13 @@ define([ */ nullLink: '#', + /** + * Element enclosing a criteria dropdown + * + * @property {Array.} + */ + dropdownFitContainers: ['.ui-dialog-content', '#container:visible', 'body'], + /** * Initialize. * @@ -224,6 +231,21 @@ define([ return this; }, + /** + * Find element that dropdown of filter should fit to + * + * @param {string|jQuery|HTMLElement} element + * @return {*} + */ + _findDropdownFitContainer: function(element) { + element = element || this.$el; + var $container = $(); + for (var i = 0; i < this.dropdownFitContainers.length && $container.length === 0; i += 1) { + $container = $(element).closest(this.dropdownFitContainers[i]); + } + return $container.length === 0 ? null : $container; + }, + /** * Converts a display value to raw format, e.g. decimal value can be displayed as "5,000,000.00" * but raw value is 5000000.0 diff --git a/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/choice-tree-filter.js b/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/choice-tree-filter.js index df43b8a974e..48d95399ca9 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/choice-tree-filter.js +++ b/src/Oro/Bundle/FilterBundle/Resources/public/js/filter/choice-tree-filter.js @@ -192,7 +192,7 @@ define(function(require) { items = this._getSelectedItems(items); var temp = []; _.each(items, function(value) { - temp .push({ + temp.push({ value: value, children: [] }); @@ -249,19 +249,27 @@ define(function(require) { }, _convertToTree: function(data) { - var self = this; var response = []; + var idToNodeMap = {}; + var element = {}; + _.each(data, function(value) { - if (!value.owner_id) { - response.push({ - value: value, - children: [] - }); - } - }); + element = {}; + element.value = value; + element.children = []; - _.each(response, function(value, key) { - response[key].children = self.searchEngine.findChild(value, data); + idToNodeMap[element.value.id] = element; + + if (!element.value.owner_id) { + response.push(element); + } else { + var parentNode = idToNodeMap[element.value.owner_id]; + if (parentNode) { + parentNode.children.push(element); + } else { + response.push(element); + } + } }); return response; @@ -298,12 +306,12 @@ define(function(require) { var id = self.name + '-' + value.value.id; template += '
  • ' + - ''; if (value.children.length > 0) { template += self.getListTemplate(value.children); } @@ -333,12 +341,12 @@ define(function(require) { }, /** - * Set raw value to filter - * - * @param value - * @param skipRefresh - * @return {*} - */ + * Set raw value to filter + * + * @param value + * @param skipRefresh + * @return {*} + */ setValue: function(value, skipRefresh) { if (!tools.isEqualsLoosely(this.value, value)) { var oldValue = this.value; 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 fa7ef12edd8..6d82e6bc2c3 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 @@ -157,6 +157,7 @@ define([ */ _onClickCriteriaSelector: function(e) { e.stopPropagation(); + e.preventDefault(); $('body').trigger('click'); if (!this.popupCriteriaShowed) { this._showCriteria(); 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 8b98faddf57..711c73832cb 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 @@ -83,6 +83,13 @@ define([ classes: 'select-filter-widget' }, + /** + * Selector, jQuery object or HTML element that will be target for append multiselect dropdown menu + * + * @property + */ + dropdownContainer: 'body', + /** * Select widget menu opened flag * @@ -114,7 +121,7 @@ define([ * @param {Object} options */ initialize: function(options) { - var opts = _.pick(options || {}, 'choices'); + var opts = _.pick(options || {}, ['choices', 'dropdownContainer']); _.extend(this, opts); // init filter content options if it was not initialized so far @@ -178,12 +185,23 @@ define([ return this; }, + /** + * Set dropdownContainer for dropdown element + * + * @param {(jQuery|Element|String)} container + * @protected + */ + setDropdownContainer: function(container) { + this.dropdownContainer = $(container); + }, + /** * Initialize multiselect widget * * @protected */ _initializeSelectWidget: function() { + var $dropdownContainer = this._findDropdownFitContainer(this.dropdownContainer) || this.dropdownContainer; this.selectWidget = new MultiselectDecorator({ element: this.$(this.inputSelector), parameters: _.extend({ @@ -194,7 +212,9 @@ define([ position: { my: 'left top+7', at: 'left bottom', - of: this.$(this.containerSelector) + of: this.$(this.containerSelector), + collision: 'fit none', + within: $dropdownContainer }, open: _.bind(function() { this.selectWidget.onOpenDropdown(); @@ -202,13 +222,17 @@ define([ this._setButtonPressed(this.$(this.containerSelector), true); this._clearChoicesStyle(); this.selectDropdownOpened = true; + this.selectWidget.updateDropdownPosition(); }, this), close: _.bind(function() { this._setButtonPressed(this.$(this.containerSelector), false); setTimeout(_.bind(function() { - this.selectDropdownOpened = false; + if (!this.disposed) { + this.selectDropdownOpened = false; + } }, this), 100); - }, this) + }, this), + appendTo: this.dropdownContainer }, this.widgetOptions), contextSearch: this.contextSearch }); 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 7cc81d71e5a..9cf14465fae 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 @@ -48,13 +48,6 @@ define([ */ criteriaSelector: '.filter-criteria', - /** - * Element enclosing a criteria dropdown - * - * @property {string|jQuery|HTMLElement} - */ - limitCriteriaTo: '#container:visible, body', - /** * Selectors for filter criteria elements * @@ -220,11 +213,21 @@ define([ * @private */ _alignCriteria: function() { - var $container = $(this.limitCriteriaTo); - var $criteria = this.$(this.criteriaSelector); - var shift = $container.prop('clientWidth') + $container.offset().left - - this.$el.offset().left - $criteria.outerWidth(); - $criteria.css('margin-left', shift < 0 ? shift : 0); + var $container = this._findDropdownFitContainer(); + if ($container === null) { + return; + } + var $dropdown = this.$(this.criteriaSelector); + var rect = $dropdown.get(0).getBoundingClientRect(); + var containerRect = $container.get(0).getBoundingClientRect(); + var shift = rect.right - (containerRect.left + $container.prop('clientWidth')); + if (shift > 0) { + /** + * reduce shift to avoid overlaping left edge of container + */ + shift -= Math.max(0, containerRect.left - (rect.left - shift)); + $dropdown.css('margin-left', -shift); + } }, /** 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 c597275e80d..1dd539fec92 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/public/js/filters-manager.js +++ b/src/Oro/Bundle/FilterBundle/Resources/public/js/filters-manager.js @@ -91,6 +91,13 @@ define(function(require) { */ buttonSelector: '.ui-multiselect.filter-list', + /** + * jQuery object that will be target for append multiselect dropdown menus + * + * @property + */ + dropdownContainer: 'body', + /** @property */ events: { 'change [data-action=add-filter-select]': '_onChangeFilterSelect', @@ -112,6 +119,8 @@ define(function(require) { this.filters = {}; + _.extend(this, _.pick(options, ['addButtonHint'])); + if (options.filters) { _.extend(this.filters, options.filters); } @@ -134,10 +143,6 @@ define(function(require) { this.listenTo(filter, filterListeners); }, this); - if (options.addButtonHint) { - this.addButtonHint = options.addButtonHint; - } - FiltersManager.__super__.initialize.apply(this, arguments); }, @@ -302,12 +307,17 @@ define(function(require) { * @return {*} */ render: function() { - var $container = $(this.template({filters: this.filters})); - this.setElement($container); + this.$el.html( + this.template({filters: this.filters}) + ); + this.dropdownContainer = this.$el.find('.filter-container'); var fragment = document.createDocumentFragment(); _.each(this.filters, function(filter) { + if (_.isFunction(filter.setDropdownContainer)) { + filter.setDropdownContainer(this.dropdownContainer); + } filter.render(); if (!filter.enabled) { filter.hide(); @@ -318,9 +328,9 @@ define(function(require) { this.trigger('rendered'); if (_.isEmpty(this.filters)) { - $container.hide(); + this.$el.hide(); } else { - $container.find('.filter-container').append(fragment); + this.dropdownContainer.append(fragment); this._initializeSelectWidget(); } @@ -333,23 +343,29 @@ define(function(require) { * @protected */ _initializeSelectWidget: function() { + var $button; this.selectWidget = new MultiselectDecorator({ element: this.$(this.filterSelector), parameters: { multiple: true, selectedList: 0, selectedText: this.addButtonHint, - classes: 'filter-list select-filter-widget', + classes: 'select-filter-widget', + position: { + my: 'left top+2', + at: 'left bottom' + }, open: $.proxy(function() { this.selectWidget.onOpenDropdown(); this._setDropdownWidth(); - this._updateDropdownPosition(); - }, this) + }, this), + appendTo: this.dropdownContainer } }); this.selectWidget.setViewDesign(this); - this.$('.filter-list span:first').replaceWith( + $button = this.selectWidget.multiselect('instance').button; + $button.find('span:first').replaceWith( '' + this.addButtonHint + '' ); @@ -382,29 +398,6 @@ define(function(require) { this.disableFilter(filter); } }, this); - - this._updateDropdownPosition(); - }, - - /** - * Set dropdown position according to current element - * - * @protected - */ - _updateDropdownPosition: function() { - var button = this.$(this.buttonSelector); - var buttonPosition = button.offset(); - var widgetWidth = this.selectWidget.getWidget().outerWidth(); - var windowWidth = $(window).width(); - var widgetLeftOffset = buttonPosition.left; - if (buttonPosition.left + widgetWidth > windowWidth) { - widgetLeftOffset = buttonPosition.left + button.outerWidth() - widgetWidth; - } - - this.selectWidget.getWidget().css({ - top: buttonPosition.top + button.outerHeight(), - left: widgetLeftOffset - }); }, /** diff --git a/src/Oro/Bundle/FilterBundle/Resources/views/Js/default_templates.js.twig b/src/Oro/Bundle/FilterBundle/Resources/views/Js/default_templates.js.twig index 39354e87ac0..5daea6038da 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/views/Js/default_templates.js.twig +++ b/src/Oro/Bundle/FilterBundle/Resources/views/Js/default_templates.js.twig @@ -189,22 +189,18 @@ {% else %} @@ -59,6 +84,7 @@ {% block style_widget %} {% if attr.href is defined and attr.href is not empty %} + {% set attr = attr|merge({href: asset(attr.href)}) %} {% else %} @@ -100,7 +126,11 @@ {% endblock %} {% block text_widget -%} - {{ text|block_text(translation_domain) }} + {% if escape %} + {{- text|block_text(translation_domain) -}} + {% else %} + {{- text|block_text(translation_domain)|raw -}} + {% endif %} {%- endblock %} {% block link_widget -%} diff --git a/src/Oro/Bundle/LayoutBundle/Resources/views/Layout/php/block_attributes.html.php b/src/Oro/Bundle/LayoutBundle/Resources/views/Layout/php/block_attributes.html.php index c7d9ca9af26..4057855695b 100644 --- a/src/Oro/Bundle/LayoutBundle/Resources/views/Layout/php/block_attributes.html.php +++ b/src/Oro/Bundle/LayoutBundle/Resources/views/Layout/php/block_attributes.html.php @@ -1,4 +1,5 @@ + $v): ?> -escape($k), $view->escape($k === 'title' ? $view['layout']->text($v, $translation_domain) : $v)) ?> + escape($k), $view->escape($k === 'title' ? $view['layout']->text($v, $translation_domain) : $v)) ?> diff --git a/src/Oro/Bundle/LayoutBundle/Resources/views/Layout/php/html_widget.html.php b/src/Oro/Bundle/LayoutBundle/Resources/views/Layout/php/html_widget.html.php new file mode 100644 index 00000000000..6099166cd73 --- /dev/null +++ b/src/Oro/Bundle/LayoutBundle/Resources/views/Layout/php/html_widget.html.php @@ -0,0 +1,2 @@ + +text($text, $translation_domain) ?> diff --git a/src/Oro/Bundle/LayoutBundle/Resources/views/Layout/php/input_widget.html.php b/src/Oro/Bundle/LayoutBundle/Resources/views/Layout/php/input_widget.html.php new file mode 100644 index 00000000000..fb8065fdc43 --- /dev/null +++ b/src/Oro/Bundle/LayoutBundle/Resources/views/Layout/php/input_widget.html.php @@ -0,0 +1,3 @@ + +text($attr['placeholder'], $translation_domain); ?> +block($block, 'block_attributes') ?>/> diff --git a/src/Oro/Bundle/LayoutBundle/Resources/views/Layout/php/script_widget.html.php b/src/Oro/Bundle/LayoutBundle/Resources/views/Layout/php/script_widget.html.php index daab0c869d0..be2f522bb64 100644 --- a/src/Oro/Bundle/LayoutBundle/Resources/views/Layout/php/script_widget.html.php +++ b/src/Oro/Bundle/LayoutBundle/Resources/views/Layout/php/script_widget.html.php @@ -1,5 +1,6 @@ + getUrl($attr['src']); ?> - + +
    • Hi World!
    • diff --git a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Assetic/LayoutFormulaLoaderTest.php b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Assetic/LayoutFormulaLoaderTest.php new file mode 100644 index 00000000000..c6841f4874a --- /dev/null +++ b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Assetic/LayoutFormulaLoaderTest.php @@ -0,0 +1,26 @@ +getMockBuilder('Oro\Bundle\LayoutBundle\Assetic\LayoutResource') + ->disableOriginalConstructor() + ->getMock(); + $layoutResource->expects($this->once()) + ->method('getContent') + ->willReturn($formulae); + + $this->assertEquals($formulae, $loader->load($layoutResource)); + } +} diff --git a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Assetic/LayoutResourceTest.php b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Assetic/LayoutResourceTest.php new file mode 100644 index 00000000000..a708405c068 --- /dev/null +++ b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Assetic/LayoutResourceTest.php @@ -0,0 +1,110 @@ +layoutResource = new LayoutResource($this->getThemeManager()); + } + + protected function tearDown() + { + unset($this->layoutResource, $this->themeManager); + } + + /** + * @return ThemeManager + */ + protected function getThemeManager() + { + if (!$this->themeManager) { + $this->themeManager = new ThemeManager(new ThemeFactory(), $this->getThemes()); + } + return $this->themeManager; + } + + /** + * @return array + */ + protected function getThemes() + { + $asset = [ + 'inputs' => ['styles.css'], + 'filters' => ['filters'], + 'output' => ['output.css'], + ]; + + return [ + 'without_assets' => [], + 'with_empty_assets' => [ + 'data' => ['assets' => []], + ], + 'with_one_asset' => [ + 'data' => [ + 'assets' => [ + 'first' => $asset, + ] + ], + ], + 'with_two_asset' => [ + 'data' => [ + 'assets' => [ + 'first' => $asset, + 'second' => $asset, + ] + ], + ], + ]; + } + + public function testIsFresh() + { + $this->assertTrue($this->layoutResource->isFresh(1)); + } + + public function testToString() + { + $this->assertEquals('layout', (string)$this->layoutResource); + } + + public function testGetContent() + { + $themes = $this->getThemes(); + $formulae = []; + foreach ($themes as $themeName => $theme) { + if (!isset($theme['data']) || !isset($theme['data']['assets']) || empty($theme['data']['assets'])) { + continue; + } + + $assets = $theme['data']['assets']; + foreach ($assets as $assetKey => $asset) { + $name = 'layout_' . $themeName . '_' . $assetKey; + $formulae[$name] = [ + $asset['inputs'], + $asset['filters'], + [ + 'output' => $asset['output'], + 'name' => $name, + ], + ]; + } + } + + $this->assertArrayHasKey('layout_with_one_asset_first', $formulae); + $this->assertArrayHasKey('layout_with_two_asset_first', $formulae); + $this->assertArrayHasKey('layout_with_two_asset_second', $formulae); + $this->assertEquals($formulae, $this->layoutResource->getContent()); + } +} diff --git a/src/Oro/Bundle/LayoutBundle/Tests/Unit/BlockTypeTestCase.php b/src/Oro/Bundle/LayoutBundle/Tests/Unit/BlockTypeTestCase.php index ea17024cf6a..c97ab53eeb8 100644 --- a/src/Oro/Bundle/LayoutBundle/Tests/Unit/BlockTypeTestCase.php +++ b/src/Oro/Bundle/LayoutBundle/Tests/Unit/BlockTypeTestCase.php @@ -45,6 +45,7 @@ protected function initializeLayoutFactoryBuilder(LayoutFactoryBuilderInterface ->addType(new Type\ButtonGroupType()) ->addType(new Type\ListType()) ->addType(new Type\OrderedListType()) - ->addType(new Type\ListItemType()); + ->addType(new Type\ListItemType()) + ->addType(new Type\InputType()); } } diff --git a/src/Oro/Bundle/LayoutBundle/Tests/Unit/DependencyInjection/Compiler/OverrideServiceCompilerPassTest.php b/src/Oro/Bundle/LayoutBundle/Tests/Unit/DependencyInjection/Compiler/OverrideServiceCompilerPassTest.php new file mode 100644 index 00000000000..0215cbb019a --- /dev/null +++ b/src/Oro/Bundle/LayoutBundle/Tests/Unit/DependencyInjection/Compiler/OverrideServiceCompilerPassTest.php @@ -0,0 +1,46 @@ +pass = new OverrideServiceCompilerPass(); + } + + protected function tearDown() + { + unset($this->pass); + } + + public function testProcess() + { + $container = new ContainerBuilder(); + $twigFormEngine = new Definition('\TwigFormEngineClass'); + $container->setDefinition('twig.form.engine', $twigFormEngine); + + $newTwigFormEngine = new Definition('\OroLayoutTwigFormEngineClass'); + $container->setDefinition('oro_layout.twig.form.engine', $newTwigFormEngine); + + $phpFormEngine = new Definition('\PhpFormEngineClass'); + $container->setDefinition('templating.form.engine', $phpFormEngine); + + $newPhpFormEngine = new Definition('\OroLayoutPhpFormEngineClass'); + $container->setDefinition('oro_layout.templating.form.engine', $newPhpFormEngine); + + $this->pass->process($container); + + $this->assertEquals($newTwigFormEngine, $container->getDefinition('twig.form.engine')); + $this->assertEquals($newPhpFormEngine, $container->getDefinition('templating.form.engine')); + } +} diff --git a/src/Oro/Bundle/LayoutBundle/Tests/Unit/DependencyInjection/OroLayoutExtensionTest.php b/src/Oro/Bundle/LayoutBundle/Tests/Unit/DependencyInjection/OroLayoutExtensionTest.php index 32b61b50ac4..aa4cdbac954 100644 --- a/src/Oro/Bundle/LayoutBundle/Tests/Unit/DependencyInjection/OroLayoutExtensionTest.php +++ b/src/Oro/Bundle/LayoutBundle/Tests/Unit/DependencyInjection/OroLayoutExtensionTest.php @@ -205,9 +205,14 @@ public function testLoadWithBundleThemesConfig() $extension = new OroLayoutExtension(); $extension->load([], $container); - $expectedResult = ['base', 'oro-black']; - $result = $container->get(OroLayoutExtension::THEME_MANAGER_SERVICE_ID)->getThemeNames(); - $this->assertSame(sort($expectedResult), sort($result)); + $expectedThemeNames = ['base', 'oro-black']; + + $themes = $container->get(OroLayoutExtension::THEME_MANAGER_SERVICE_ID)->getAllThemes(); + $themeNames = $container->get(OroLayoutExtension::THEME_MANAGER_SERVICE_ID)->getThemeNames(); + + $this->assertSame(sort($expectedThemeNames), sort($themeNames)); + $this->assertSame('Oro Black theme', $themes['oro-black']->getLabel()); + $this->assertSame('Oro Black theme description', $themes['oro-black']->getDescription()); } public function testLoadingLayoutUpdates() diff --git a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Form/RendererEngine/RendererEngineTest.php b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Form/RendererEngine/RendererEngineTest.php new file mode 100644 index 00000000000..346999bfa99 --- /dev/null +++ b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Form/RendererEngine/RendererEngineTest.php @@ -0,0 +1,34 @@ +createRendererEngine(); + + $reflectionClass = new \ReflectionClass(get_class($renderingEngine)); + $property = $reflectionClass->getProperty('defaultThemes'); + $property->setAccessible(true); + + $actual = $property->getValue($renderingEngine); + $this->assertNotContains('newThemePath', $actual); + + $renderingEngine->addDefaultThemes('newThemePath'); + $actual = $property->getValue($renderingEngine); + $this->assertContains('newThemePath', $actual); + + $renderingEngine->addDefaultThemes(['newThemePath2', 'newThemePath3']); + $actual = $property->getValue($renderingEngine); + $this->assertContains('newThemePath2', $actual); + $this->assertContains('newThemePath3', $actual); + } + + /** + * @return FormRendererEngineInterface + */ + abstract public function createRendererEngine(); +} diff --git a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Form/RendererEngine/TemplatingRendererEngineTest.php b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Form/RendererEngine/TemplatingRendererEngineTest.php new file mode 100644 index 00000000000..1797e9de97d --- /dev/null +++ b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Form/RendererEngine/TemplatingRendererEngineTest.php @@ -0,0 +1,19 @@ +getMock('Symfony\Component\Templating\EngineInterface'); + + return new TemplatingRendererEngine($templatingEngine); + } +} diff --git a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Form/RendererEngine/TwigRendererEngineTest.php b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Form/RendererEngine/TwigRendererEngineTest.php new file mode 100644 index 00000000000..5f47013f90b --- /dev/null +++ b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Form/RendererEngine/TwigRendererEngineTest.php @@ -0,0 +1,16 @@ +expressionAssembler = $this->getMock('Oro\Component\ConfigExpression\AssemblerInterface'); - - $encoderRegistry = $this - ->getMockBuilder('Oro\Bundle\LayoutBundle\Layout\Encoder\ConfigExpressionEncoderRegistry') + $this->processor = $this->getMockBuilder('Oro\Bundle\LayoutBundle\Layout\Processor\ConfigExpressionProcessor') ->disableOriginalConstructor() ->getMock(); - $encoderRegistry->expects($this->any()) - ->method('getEncoder') - ->with('json') - ->will($this->returnValue(new JsonConfigExpressionEncoder())); - $this->extension = new ConfigExpressionExtension( - $this->expressionAssembler, - $encoderRegistry - ); + $this->extension = new ConfigExpressionExtension($this->processor); } public function testGetExtendedType() @@ -48,13 +35,31 @@ public function testGetExtendedType() } /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + * @dataProvider deferredDataProvider + * @param bool $deferred */ - public function testFinishViewEvaluatesAllExpressions() + public function testNormalizeOptions($deferred) { - $context = new LayoutContext(); - $context->set('css_class', 'test_class'); - $data = $this->getMock('Oro\Component\Layout\DataAccessorInterface'); + $context = $this->getContextMock($deferred); + $data = $this->getDataAccessorMock(); + $options = []; + + $this->processor->expects($deferred ? $this->never() : $this->once()) + ->method('processExpressions') + ->with($options, $context, $data, true, 'json'); + + $this->extension->normalizeOptions($options, $context, $data); + } + + /** + * @dataProvider deferredDataProvider + * @param bool $deferred + */ + public function testFinishView($deferred) + { + $context = $this->getContextMock($deferred); + $data = $this->getDataAccessorMock(); + $block = $this->getMock('Oro\Component\Layout\BlockInterface'); $block->expects($this->once()) ->method('getContext') @@ -63,215 +68,45 @@ public function testFinishViewEvaluatesAllExpressions() ->method('getData') ->will($this->returnValue($data)); $view = new BlockView(); + $view->vars = ['test']; - $expr = $this->getMock('Oro\Component\ConfigExpression\ExpressionInterface'); - $expr->expects($this->once()) - ->method('evaluate') - ->with(['context' => $context, 'data' => $data]) - ->will($this->returnValue(true)); - - $classExpr = new Func\GetValue(); - $classExpr->initialize([new PropertyPath('context.css_class')]); - $classExpr->setContextAccessor(new ContextAccessor()); - $classAttr = new OptionValueBag(); - $classAttr->add(['@value' => ['$context.css_class']]); - $expectedClassAttr = new OptionValueBag(); - $expectedClassAttr->add('test_class'); + $this->processor->expects($deferred ? $this->once(): $this->never()) + ->method('processExpressions') + ->with($view->vars, $context, $data, true, 'json'); - $view->vars['expr_object'] = $expr; - $view->vars['expr_array'] = ['@true' => null]; - $view->vars['not_expr_array'] = ['\@true' => null]; - $view->vars['scalar'] = 123; - $view->vars['attr']['enabled'] = ['@true' => null]; - $view->vars['attr']['data-scalar'] = 'foo'; - $view->vars['attr']['data-expr'] = ['@true' => null]; - $view->vars['attr']['class'] = $classAttr; - $view->vars['label_attr']['enabled'] = ['@true' => null]; - $view->vars['array_with_expr'] = ['item1' => 'val1', 'item2' => ['@true' => null]]; - - $this->expressionAssembler->expects($this->exactly(6)) - ->method('assemble') - ->will( - $this->returnValueMap( - [ - [['@true' => null], new Condition\True()], - [['@value' => ['$context.css_class']], $classExpr] - ] - ) - ); - - $context['expressions_evaluate'] = true; $this->extension->finishView($view, $block, []); - - $this->assertSame( - true, - $view->vars['expr_object'], - 'Failed asserting that an expression is evaluated' - ); - $this->assertSame( - true, - $view->vars['expr_array'], - 'Failed asserting that an expression is assembled and evaluated' - ); - $this->assertSame( - ['@true' => null], - $view->vars['not_expr_array'], - 'Failed asserting that a backslash at the begin of the array key is removed' - ); - $this->assertSame( - 123, - $view->vars['scalar'], - 'Failed asserting that a scalar value is not changed' - ); - $this->assertSame( - true, - $view->vars['attr']['enabled'], - 'Failed asserting that an expression in "attr" is assembled and evaluated' - ); - $this->assertSame( - 'foo', - $view->vars['attr']['data-scalar'], - 'Failed asserting that "attr.data-scalar" exists' - ); - $this->assertSame( - true, - $view->vars['attr']['data-expr'], - 'Failed asserting that "attr.data-expr" is assembled and evaluated' - ); - $this->assertEquals( - $expectedClassAttr, - $view->vars['attr']['class'], - 'Failed asserting that "attr.class" is assembled and evaluated' - ); - $this->assertSame( - true, - $view->vars['label_attr']['enabled'], - 'Failed asserting that an expression in "label_attr" is assembled and evaluated' - ); - $this->assertSame( - ['item1' => 'val1', 'item2' => true], - $view->vars['array_with_expr'], - 'Failed asserting that an expression is assembled and evaluated in nested array' - ); } - public function testFinishViewDoNothingIfEvaluationOfExpressionsDisabledAndEncodingIsNotSet() + /** + * @return array + */ + public function deferredDataProvider() { - $context = new LayoutContext(); - $block = $this->getMock('Oro\Component\Layout\BlockInterface'); - $block->expects($this->once()) - ->method('getContext') - ->will($this->returnValue($context)); - $block->expects($this->never()) - ->method('getData'); - $view = new BlockView(); - - $expr = $this->getMock('Oro\Component\ConfigExpression\ExpressionInterface'); - $expr->expects($this->never()) - ->method('evaluate'); - $expr->expects($this->never()) - ->method('toArray'); - - $view->vars['expr_object'] = $expr; - $view->vars['expr_array'] = ['@true' => null]; - $view->vars['not_expr_array'] = ['\@true' => null]; - $view->vars['scalar'] = 123; - $view->vars['attr']['enabled'] = ['@true' => null]; - $view->vars['label_attr']['enabled'] = ['@true' => null]; - - $this->expressionAssembler->expects($this->never()) - ->method('assemble'); - - $initialVars = $view->vars; - - $context['expressions_evaluate'] = false; - $this->extension->finishView($view, $block, []); - - $this->assertSame($initialVars, $view->vars); + return [ + ['expressions_evaluate_deferred' => true], + ['expressions_evaluate_deferred' => false] + ]; } - public function testFinishViewEncodesAllExpressions() + /** + * @param bool $evaluateDeferred + * @return LayoutContext + */ + protected function getContextMock($evaluateDeferred) { $context = new LayoutContext(); - $data = $this->getMock('Oro\Component\Layout\DataAccessorInterface'); - $block = $this->getMock('Oro\Component\Layout\BlockInterface'); - $block->expects($this->once()) - ->method('getContext') - ->will($this->returnValue($context)); - $block->expects($this->once()) - ->method('getData') - ->will($this->returnValue($data)); - $view = new BlockView(); - - $expr = $this->getMock('Oro\Component\ConfigExpression\ExpressionInterface'); - $expr->expects($this->once()) - ->method('toArray') - ->will($this->returnValue(['@true' => null])); - - $classExpr = new Func\GetValue(); - $classExpr->initialize([new PropertyPath('context.css_class')]); - $classAttr = new OptionValueBag(); - $classAttr->add(['@value' => ['$context.css_class']]); - $expectedClassAttr = new OptionValueBag(); - $expectedClassAttr->add('{"@value":{"parameters":["$context.css_class"]}}'); - - $view->vars['expr_object'] = $expr; - $view->vars['expr_array'] = ['@true' => null]; - $view->vars['not_expr_array'] = ['\@true' => null]; - $view->vars['scalar'] = 123; - $view->vars['attr']['enabled'] = ['@true' => null]; - $view->vars['attr']['class'] = $classAttr; - $view->vars['label_attr']['enabled'] = ['@true' => null]; - - $this->expressionAssembler->expects($this->exactly(4)) - ->method('assemble') - ->will( - $this->returnValueMap( - [ - [['@true' => null], new Condition\True()], - [['@value' => ['$context.css_class']], $classExpr] - ] - ) - ); + $context->set('expressions_evaluate_deferred', $evaluateDeferred); + $context->set('expressions_evaluate', true); + $context->set('expressions_encoding', 'json'); - $context['expressions_evaluate'] = false; - $context['expressions_encoding'] = 'json'; - $this->extension->finishView($view, $block, []); + return $context; + } - $this->assertSame( - '{"@true":null}', - $view->vars['expr_object'], - 'Failed asserting that an expression is encoded' - ); - $this->assertSame( - '{"@true":null}', - $view->vars['expr_array'], - 'Failed asserting that an expression is assembled and encoded' - ); - $this->assertSame( - ['@true' => null], - $view->vars['not_expr_array'], - 'Failed asserting that a backslash at the begin of the array key is removed' - ); - $this->assertSame( - 123, - $view->vars['scalar'], - 'Failed asserting that a scalar value is not changed' - ); - $this->assertSame( - '{"@true":null}', - $view->vars['attr']['enabled'], - 'Failed asserting that an expression in "attr" is assembled and encoded' - ); - $this->assertEquals( - $expectedClassAttr, - $view->vars['attr']['class'], - 'Failed asserting that "attr.class" is assembled and encoded' - ); - $this->assertSame( - '{"@true":null}', - $view->vars['label_attr']['enabled'], - 'Failed asserting that an expression in "label_attr" is assembled and encoded' - ); + /** + * @return DataAccessorInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected function getDataAccessorMock() + { + return $this->getMock('Oro\Component\Layout\DataAccessorInterface'); } } diff --git a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Block/Type/FormTypeTest.php b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Block/Type/FormTypeTest.php index d8e2e4d4acf..ce96a10eab9 100644 --- a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Block/Type/FormTypeTest.php +++ b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Block/Type/FormTypeTest.php @@ -28,6 +28,7 @@ public function optionsDataProvider() 'no options' => [ 'options' => [], 'expected' => [ + 'form' => null, 'form_name' => 'form', 'preferred_fields' => [], 'groups' => [], @@ -41,6 +42,7 @@ public function optionsDataProvider() 'form_name' => 'test' ], 'expected' => [ + 'form' => null, 'form_name' => 'test', 'preferred_fields' => [], 'groups' => [], @@ -55,6 +57,7 @@ public function optionsDataProvider() 'form_prefix' => 'test_prefix' ], 'expected' => [ + 'form' => null, 'form_name' => 'test_form', 'preferred_fields' => [], 'groups' => [], @@ -73,6 +76,7 @@ public function optionsDataProvider() 'form_group_prefix' => 'form_group_prefix_' ], 'expected' => [ + 'form' => null, 'form_name' => 'test', 'preferred_fields' => ['field1'], 'groups' => ['group1' => ['title' => 'TestGroup']], @@ -168,7 +172,7 @@ public function testBuildView() $view = new BlockView(); $block = $this->getMock('Oro\Component\Layout\BlockInterface'); - $formAccessor = $this->getMock('Oro\Bundle\LayoutBundle\Layout\Form\FormAccessorInterface'); + $formAccessor = $this->getMock('Oro\Bundle\LayoutBundle\Layout\Form\ConfigurableFormAccessorInterface'); $context = new LayoutContext(); $formView = new FormView(); diff --git a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Block/Type/InputTypeTest.php b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Block/Type/InputTypeTest.php new file mode 100644 index 00000000000..8fb3adf2904 --- /dev/null +++ b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Block/Type/InputTypeTest.php @@ -0,0 +1,89 @@ +getBlockType(InputType::NAME); + + $this->assertSame(InputType::NAME, $type->getName()); + } + + public function testGetParent() + { + $type = $this->getBlockType(InputType::NAME); + + $this->assertSame(BaseType::NAME, $type->getParent()); + } + + public function testSetDefaultOptions() + { + $this->assertEquals( + [ + 'type' => 'text', + 'id' => null, + 'name' => null, + 'value' => null, + 'placeholder' => null, + ], + $this->resolveOptions(InputType::NAME, []) + ); + + $options = [ + 'type' => 'password', + 'id' => 'passwordId', + 'name' => 'passwordName', + 'value' => '***', + 'placeholder' => 'Enter password', + ]; + $this->assertEquals($options, $this->resolveOptions(InputType::NAME, $options)); + } + + public function testBuildViewPassword() + { + $view = $this->getBlockView(InputType::NAME, ['type' => 'password']); + + $this->assertEquals('password', $view->vars['type']); + $this->assertEquals( + [ + 'type' => 'password', + 'autocomplete' => 'off', + ], + $view->vars['attr'] + ); + } + + public function testBuildViewWithoutOptions() + { + $view = $this->getBlockView(InputType::NAME); + + $this->assertEquals( + [ + 'type' => 'text', + ], + $view->vars['attr'] + ); + } + + public function testBuildView() + { + $options = [ + 'id' => 'usernameId', + 'name' => 'usernameName', + 'value' => 'username', + 'placeholder' => 'Enter username', + ]; + + $view = $this->getBlockView(InputType::NAME, $options); + + $this->assertEquals('text', $view->vars['type']); + $this->assertEquals(['type' => 'text'] + $options, $view->vars['attr']); + } +} diff --git a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Block/Type/TextTypeTest.php b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Block/Type/TextTypeTest.php index 0bfe2c3711d..47ed79343ed 100644 --- a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Block/Type/TextTypeTest.php +++ b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Block/Type/TextTypeTest.php @@ -32,7 +32,7 @@ public function testBuildView() { $view = $this->getBlockView( TextType::NAME, - ['text' => 'test'] + ['text' => 'test', 'escape'=> false] ); $this->assertEquals('test', $view->vars['text']); diff --git a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Extension/ConfigExpressionContextConfiguratorTest.php b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Extension/ConfigExpressionContextConfiguratorTest.php index 32db1caeb6c..f11262aa34b 100644 --- a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Extension/ConfigExpressionContextConfiguratorTest.php +++ b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Extension/ConfigExpressionContextConfiguratorTest.php @@ -24,6 +24,7 @@ public function testDefaultValuesAfterConfigureContext() $context->resolve(); $this->assertTrue($context['expressions_evaluate']); + $this->assertFalse($context['expressions_evaluate_deferred']); $this->assertFalse(isset($context['expressions_encoding'])); } @@ -32,12 +33,14 @@ public function testConfigureContext() $context = new LayoutContext(); $context['expressions_evaluate'] = false; + $context['expressions_evaluate_deferred'] = true; $context['expressions_encoding'] = 'json'; $this->contextConfigurator->configureContext($context); $context->resolve(); $this->assertFalse($context['expressions_evaluate']); + $this->assertTrue($context['expressions_evaluate_deferred']); $this->assertEquals('json', $context['expressions_encoding']); } } diff --git a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Extension/DependencyInjectionFormContextConfiguratorTest.php b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Extension/DependencyInjectionFormContextConfiguratorTest.php new file mode 100644 index 00000000000..5fafa2065aa --- /dev/null +++ b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Extension/DependencyInjectionFormContextConfiguratorTest.php @@ -0,0 +1,51 @@ +container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + + $this->contextConfigurator = new DependencyInjectionFormContextConfigurator($this->container); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage formServiceId should be specified. + */ + public function testConfigureContextWithoutForm() + { + $context = new LayoutContext(); + $this->contextConfigurator->configureContext($context); + } + + public function testConfigureContext() + { + $serviceId = 'test_service_id'; + $contextOptionName = 'test_context_form'; + $this->contextConfigurator->setFormServiceId($serviceId); + $this->contextConfigurator->setContextOptionName($contextOptionName); + + $context = new LayoutContext(); + $this->contextConfigurator->configureContext($context); + $context->resolve(); + + $formAccessor = $context->get($contextOptionName); + $this->assertInstanceOf('Oro\Bundle\LayoutBundle\Layout\Form\DependencyInjectionFormAccessor', $formAccessor); + $this->assertAttributeEquals($serviceId, 'formServiceId', $formAccessor); + } +} diff --git a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Form/DependencyInjectionFormAccessorTest.php b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Form/DependencyInjectionFormAccessorTest.php index 9fc5fccee61..4cd777658ed 100644 --- a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Form/DependencyInjectionFormAccessorTest.php +++ b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Form/DependencyInjectionFormAccessorTest.php @@ -2,6 +2,7 @@ namespace Oro\Bundle\LayoutBundle\Tests\Unit\Layout\Form; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Form\FormView; use Oro\Bundle\LayoutBundle\Layout\Form\DependencyInjectionFormAccessor; @@ -11,7 +12,7 @@ class DependencyInjectionFormAccessorTest extends \PHPUnit_Framework_TestCase { const FORM_SERVICE_ID = 'test_service_id'; - /** @var \PHPUnit_Framework_MockObject_MockObject */ + /** @var ContainerInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $container; protected function setUp() @@ -22,6 +23,8 @@ protected function setUp() public function testGetForm() { $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); + $form->expects($this->once())->method('getName')->willReturn('form_name'); + $this->container->expects($this->once()) ->method('get') ->with(self::FORM_SERVICE_ID) @@ -29,6 +32,7 @@ public function testGetForm() $formAccessor = new DependencyInjectionFormAccessor($this->container, self::FORM_SERVICE_ID); $this->assertSame($form, $formAccessor->getForm()); + $this->assertEquals('form_name', $formAccessor->getName()); } public function testToString() @@ -130,6 +134,7 @@ public function testGetView() // field1 // field2 $formView = new FormView(); + $formView->vars['id'] = self::FORM_SERVICE_ID; $field1View = new FormView($formView); $formView->children['field1'] = $field1View; $field2View = new FormView($field1View); @@ -148,6 +153,7 @@ public function testGetView() $this->assertSame($formView, $formAccessor->getView()); $this->assertSame($field1View, $formAccessor->getView('field1')); $this->assertSame($field2View, $formAccessor->getView('field1.field2')); + $this->assertSame($formView->vars['id'], $formAccessor->getId()); } public function testProcessedFields() @@ -160,4 +166,58 @@ public function testProcessedFields() $formAccessor->setProcessedFields($processedFields); $this->assertSame($processedFields, $formAccessor->getProcessedFields()); } + + public function testSetFormData() + { + $data = ['test']; + + $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); + $form->expects($this->once())->method('setData')->with($data); + $form->expects($this->once())->method('getData')->willReturn($data); + + $this->container->expects($this->once()) + ->method('get') + ->with(self::FORM_SERVICE_ID) + ->will($this->returnValue($form)); + + $formAccessor = new DependencyInjectionFormAccessor($this->container, self::FORM_SERVICE_ID); + + $formAccessor->setFormData($data); + $this->assertEquals($data, $formAccessor->getForm()->getData()); + } + + public function testSetters() + { + $formConfig = $this->getMock('Symfony\Component\Form\FormConfigInterface'); + $formView = new FormView(); + $formView->vars['multipart'] = true; + + $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); + $form->expects($this->any()) + ->method('getConfig') + ->will($this->returnValue($formConfig)); + $form->expects($this->once()) + ->method('createView') + ->will($this->returnValue($formView)); + + $this->container->expects($this->once()) + ->method('get') + ->with(self::FORM_SERVICE_ID) + ->will($this->returnValue($form)); + + $formAccessor = new DependencyInjectionFormAccessor($this->container, self::FORM_SERVICE_ID); + + $action = FormAction::createByRoute('test_route', ['foo' => 'bar']); + $formAccessor->setAction($action); + $this->assertEquals($action, $formAccessor->getAction()); + + $formAccessor->setActionRoute('test_route', []); + $this->assertEquals(FormAction::createByRoute('test_route', []), $formAccessor->getAction()); + + $formAccessor->setMethod('post'); + $this->assertEquals('post', $formAccessor->getMethod()); + + $formAccessor->setEnctype('multipart/form-data'); + $this->assertEquals('multipart/form-data', $formAccessor->getEnctype()); + } } diff --git a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Form/FormLayoutBuilderTest.php b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Form/FormLayoutBuilderTest.php index b1af145d9ed..dd49df76bfd 100644 --- a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Form/FormLayoutBuilderTest.php +++ b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Form/FormLayoutBuilderTest.php @@ -69,7 +69,7 @@ public function testFlatForm() self::FIELD_PREFIX . 'field1', self::ROOT_ID, FormFieldType::NAME, - ['form_name' => self::FORM_NAME, 'field_path' => 'field1'] + ['form' => null, 'form_name' => self::FORM_NAME, 'field_path' => 'field1'] ) ->will($this->returnSelf()); $this->layoutManipulator->expects($this->at(1)) @@ -78,7 +78,7 @@ public function testFlatForm() self::FIELD_PREFIX . 'field2', self::ROOT_ID, FormFieldType::NAME, - ['form_name' => self::FORM_NAME, 'field_path' => 'field2'] + ['form' => null, 'form_name' => self::FORM_NAME, 'field_path' => 'field2'] ) ->will($this->returnSelf()); $this->layoutManipulator->expects($this->exactly(2)) @@ -110,7 +110,7 @@ public function testCompoundChildForm() self::FIELD_PREFIX . 'field1:field11', self::ROOT_ID, FormFieldType::NAME, - ['form_name' => self::FORM_NAME, 'field_path' => 'field1.field11'] + ['form' => null, 'form_name' => self::FORM_NAME, 'field_path' => 'field1.field11'] ) ->will($this->returnSelf()); $this->layoutManipulator->expects($this->at(1)) @@ -119,7 +119,7 @@ public function testCompoundChildForm() self::FIELD_PREFIX . 'field2', self::ROOT_ID, FormFieldType::NAME, - ['form_name' => self::FORM_NAME, 'field_path' => 'field2'] + ['form' => null, 'form_name' => self::FORM_NAME, 'field_path' => 'field2'] ) ->will($this->returnSelf()); $this->layoutManipulator->expects($this->exactly(2)) @@ -151,7 +151,7 @@ public function testCompoundChildFormWhichMarkedAsSimpleForm() self::FIELD_PREFIX . 'field1', self::ROOT_ID, FormFieldType::NAME, - ['form_name' => self::FORM_NAME, 'field_path' => 'field1'] + ['form' => null, 'form_name' => self::FORM_NAME, 'field_path' => 'field1'] ) ->will($this->returnSelf()); $this->layoutManipulator->expects($this->at(1)) @@ -160,7 +160,7 @@ public function testCompoundChildFormWhichMarkedAsSimpleForm() self::FIELD_PREFIX . 'field2', self::ROOT_ID, FormFieldType::NAME, - ['form_name' => self::FORM_NAME, 'field_path' => 'field2'] + ['form' => null, 'form_name' => self::FORM_NAME, 'field_path' => 'field2'] ) ->will($this->returnSelf()); $this->layoutManipulator->expects($this->exactly(2)) @@ -193,7 +193,7 @@ public function testPreferredFields() self::FIELD_PREFIX . 'field2', self::ROOT_ID, FormFieldType::NAME, - ['form_name' => self::FORM_NAME, 'field_path' => 'field2'] + ['form' => null, 'form_name' => self::FORM_NAME, 'field_path' => 'field2'] ) ->will($this->returnSelf()); $this->layoutManipulator->expects($this->at(1)) @@ -202,7 +202,7 @@ public function testPreferredFields() self::FIELD_PREFIX . 'field1', self::ROOT_ID, FormFieldType::NAME, - ['form_name' => self::FORM_NAME, 'field_path' => 'field1'] + ['form' => null, 'form_name' => self::FORM_NAME, 'field_path' => 'field1'] ) ->will($this->returnSelf()); $this->layoutManipulator->expects($this->exactly(2)) @@ -236,7 +236,7 @@ public function testPreferredCompoundFields() self::FIELD_PREFIX . 'field2:field21', self::ROOT_ID, FormFieldType::NAME, - ['form_name' => self::FORM_NAME, 'field_path' => 'field2.field21'] + ['form' => null, 'form_name' => self::FORM_NAME, 'field_path' => 'field2.field21'] ) ->will($this->returnSelf()); $this->layoutManipulator->expects($this->at(1)) @@ -245,7 +245,7 @@ public function testPreferredCompoundFields() self::FIELD_PREFIX . 'field1', self::ROOT_ID, FormFieldType::NAME, - ['form_name' => self::FORM_NAME, 'field_path' => 'field1'] + ['form' => null, 'form_name' => self::FORM_NAME, 'field_path' => 'field1'] ) ->will($this->returnSelf()); $this->layoutManipulator->expects($this->exactly(2)) @@ -296,6 +296,7 @@ protected function getForm($compound = true, $type = 'form', $name = 'some_form' protected function getOptions() { return [ + 'form' => null, 'form_name' => self::FORM_NAME, 'preferred_fields' => [], 'form_field_prefix' => self::FIELD_PREFIX, diff --git a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Form/GroupingFormLayoutBuilderTest.php b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Form/GroupingFormLayoutBuilderTest.php index e6a4bcffbc2..f3c680d969b 100644 --- a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Form/GroupingFormLayoutBuilderTest.php +++ b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Form/GroupingFormLayoutBuilderTest.php @@ -102,7 +102,7 @@ public function testGrouping() self::FIELD_PREFIX . 'field1', self::GROUP_PREFIX . 'group2', FormFieldType::NAME, - ['form_name' => self::FORM_NAME, 'field_path' => 'field1'] + ['form' => null, 'form_name' => self::FORM_NAME, 'field_path' => 'field1'] ) ->will($this->returnSelf()); $this->layoutManipulator->expects($this->at(1)) @@ -111,7 +111,7 @@ public function testGrouping() self::FIELD_PREFIX . 'field2:field21', self::GROUP_PREFIX . 'group1', FormFieldType::NAME, - ['form_name' => self::FORM_NAME, 'field_path' => 'field2.field21'] + ['form' => null, 'form_name' => self::FORM_NAME, 'field_path' => 'field2.field21'] ) ->will($this->returnSelf()); $this->layoutManipulator->expects($this->at(2)) @@ -177,7 +177,7 @@ public function testGroupingWithPreferredFields() self::FIELD_PREFIX . 'field2:field22', self::GROUP_PREFIX . 'group1', FormFieldType::NAME, - ['form_name' => self::FORM_NAME, 'field_path' => 'field2.field22'] + ['form' => null, 'form_name' => self::FORM_NAME, 'field_path' => 'field2.field22'] ) ->will($this->returnSelf()); $this->layoutManipulator->expects($this->at(1)) @@ -186,7 +186,7 @@ public function testGroupingWithPreferredFields() self::FIELD_PREFIX . 'field1', self::GROUP_PREFIX . 'group2', FormFieldType::NAME, - ['form_name' => self::FORM_NAME, 'field_path' => 'field1'] + ['form' => null, 'form_name' => self::FORM_NAME, 'field_path' => 'field1'] ) ->will($this->returnSelf()); $this->layoutManipulator->expects($this->at(2)) @@ -195,7 +195,7 @@ public function testGroupingWithPreferredFields() self::FIELD_PREFIX . 'field2:field21', self::GROUP_PREFIX . 'group1', FormFieldType::NAME, - ['form_name' => self::FORM_NAME, 'field_path' => 'field2.field21'] + ['form' => null, 'form_name' => self::FORM_NAME, 'field_path' => 'field2.field21'] ) ->will($this->returnSelf()); $this->layoutManipulator->expects($this->at(3)) @@ -261,7 +261,7 @@ public function testGroupingByParentFieldPath() self::FIELD_PREFIX . 'field1', self::GROUP_PREFIX . 'group2', FormFieldType::NAME, - ['form_name' => self::FORM_NAME, 'field_path' => 'field1'] + ['form' => null, 'form_name' => self::FORM_NAME, 'field_path' => 'field1'] ) ->will($this->returnSelf()); $this->layoutManipulator->expects($this->at(1)) @@ -270,7 +270,7 @@ public function testGroupingByParentFieldPath() self::FIELD_PREFIX . 'field2:field21', self::GROUP_PREFIX . 'group1', FormFieldType::NAME, - ['form_name' => self::FORM_NAME, 'field_path' => 'field2.field21'] + ['form' => null, 'form_name' => self::FORM_NAME, 'field_path' => 'field2.field21'] ) ->will($this->returnSelf()); $this->layoutManipulator->expects($this->at(2)) @@ -279,7 +279,7 @@ public function testGroupingByParentFieldPath() self::FIELD_PREFIX . 'field2:field22', self::GROUP_PREFIX . 'group1', FormFieldType::NAME, - ['form_name' => self::FORM_NAME, 'field_path' => 'field2.field22'] + ['form' => null, 'form_name' => self::FORM_NAME, 'field_path' => 'field2.field22'] ) ->will($this->returnSelf()); $this->layoutManipulator->expects($this->at(3)) @@ -320,6 +320,7 @@ public function testGroupingByParentFieldPath() protected function getOptions() { return [ + 'form' => null, 'form_name' => self::FORM_NAME, 'preferred_fields' => [], 'groups' => [], diff --git a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Processor/ConfigExpressionProcessorTest.php b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Processor/ConfigExpressionProcessorTest.php new file mode 100644 index 00000000000..2b05d740d37 --- /dev/null +++ b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/Processor/ConfigExpressionProcessorTest.php @@ -0,0 +1,245 @@ +expressionAssembler = $this->getMock('Oro\Component\ConfigExpression\AssemblerInterface'); + + $encoderRegistry = $this + ->getMockBuilder('Oro\Bundle\LayoutBundle\Layout\Encoder\ConfigExpressionEncoderRegistry') + ->disableOriginalConstructor() + ->getMock(); + $encoderRegistry->expects($this->any()) + ->method('getEncoder') + ->with('json') + ->will($this->returnValue(new JsonConfigExpressionEncoder())); + + $this->processor = new ConfigExpressionProcessor( + $this->expressionAssembler, + $encoderRegistry + ); + } + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testProcessExpressionsEvaluatesAllExpressions() + { + $context = new LayoutContext(); + $context->set('css_class', 'test_class'); + $data = $this->getMock('Oro\Component\Layout\DataAccessorInterface'); + + $expr = $this->getMock('Oro\Component\ConfigExpression\ExpressionInterface'); + $expr->expects($this->once()) + ->method('evaluate') + ->with(['context' => $context, 'data' => $data]) + ->will($this->returnValue(true)); + + $classExpr = new Func\GetValue(); + $classExpr->initialize([new PropertyPath('context.css_class')]); + $classExpr->setContextAccessor(new ContextAccessor()); + $classAttr = new OptionValueBag(); + $classAttr->add(['@value' => ['$context.css_class']]); + $expectedClassAttr = new OptionValueBag(); + $expectedClassAttr->add('test_class'); + + $values['expr_object'] = $expr; + $values['expr_array'] = ['@true' => null]; + $values['not_expr_array'] = ['\@true' => null]; + $values['scalar'] = 123; + $values['attr']['enabled'] = ['@true' => null]; + $values['attr']['data-scalar'] = 'foo'; + $values['attr']['data-expr'] = ['@true' => null]; + $values['attr']['class'] = $classAttr; + $values['label_attr']['enabled'] = ['@true' => null]; + $values['array_with_expr'] = ['item1' => 'val1', 'item2' => ['@true' => null]]; + + $this->expressionAssembler->expects($this->exactly(6)) + ->method('assemble') + ->will( + $this->returnValueMap( + [ + [['@true' => null], new Condition\True()], + [['@value' => ['$context.css_class']], $classExpr] + ] + ) + ); + + $this->processor->processExpressions($values, $context, $data, true, null); + + $this->assertSame( + true, + $values['expr_object'], + 'Failed asserting that an expression is evaluated' + ); + $this->assertSame( + true, + $values['expr_array'], + 'Failed asserting that an expression is assembled and evaluated' + ); + $this->assertSame( + ['@true' => null], + $values['not_expr_array'], + 'Failed asserting that a backslash at the begin of the array key is removed' + ); + $this->assertSame( + 123, + $values['scalar'], + 'Failed asserting that a scalar value is not changed' + ); + $this->assertSame( + true, + $values['attr']['enabled'], + 'Failed asserting that an expression in "attr" is assembled and evaluated' + ); + $this->assertSame( + 'foo', + $values['attr']['data-scalar'], + 'Failed asserting that "attr.data-scalar" exists' + ); + $this->assertSame( + true, + $values['attr']['data-expr'], + 'Failed asserting that "attr.data-expr" is assembled and evaluated' + ); + $this->assertEquals( + $expectedClassAttr, + $values['attr']['class'], + 'Failed asserting that "attr.class" is assembled and evaluated' + ); + $this->assertSame( + true, + $values['label_attr']['enabled'], + 'Failed asserting that an expression in "label_attr" is assembled and evaluated' + ); + $this->assertSame( + ['item1' => 'val1', 'item2' => true], + $values['array_with_expr'], + 'Failed asserting that an expression is assembled and evaluated in nested array' + ); + } + + public function testProcessExpressionsDoNothingIfEvaluationOfExpressionsDisabledAndEncodingIsNotSet() + { + $context = new LayoutContext(); + $data = $this->getMock('Oro\Component\Layout\DataAccessorInterface'); + + $expr = $this->getMock('Oro\Component\ConfigExpression\ExpressionInterface'); + $expr->expects($this->never()) + ->method('evaluate'); + $expr->expects($this->never()) + ->method('toArray'); + + $values['expr_object'] = $expr; + $values['expr_array'] = ['@true' => null]; + $values['not_expr_array'] = ['\@true' => null]; + $values['scalar'] = 123; + $values['attr']['enabled'] = ['@true' => null]; + $values['label_attr']['enabled'] = ['@true' => null]; + + $this->expressionAssembler->expects($this->never()) + ->method('assemble'); + + $initialVars = $values; + + $this->processor->processExpressions($values, $context, $data, false, null); + + $this->assertSame($initialVars, $values); + } + + public function testProcessExpressionsEncodesAllExpressions() + { + $context = new LayoutContext(); + $context->set('expressions_evaluate_deferred', true); + $data = $this->getMock('Oro\Component\Layout\DataAccessorInterface'); + + $expr = $this->getMock('Oro\Component\ConfigExpression\ExpressionInterface'); + $expr->expects($this->once()) + ->method('toArray') + ->will($this->returnValue(['@true' => null])); + + $classExpr = new Func\GetValue(); + $classExpr->initialize([new PropertyPath('context.css_class')]); + $classAttr = new OptionValueBag(); + $classAttr->add(['@value' => ['$context.css_class']]); + $expectedClassAttr = new OptionValueBag(); + $expectedClassAttr->add('{"@value":{"parameters":["$context.css_class"]}}'); + + $values['expr_object'] = $expr; + $values['expr_array'] = ['@true' => null]; + $values['not_expr_array'] = ['\@true' => null]; + $values['scalar'] = 123; + $values['attr']['enabled'] = ['@true' => null]; + $values['attr']['class'] = $classAttr; + $values['label_attr']['enabled'] = ['@true' => null]; + + $this->expressionAssembler->expects($this->exactly(4)) + ->method('assemble') + ->will( + $this->returnValueMap( + [ + [['@true' => null], new Condition\True()], + [['@value' => ['$context.css_class']], $classExpr] + ] + ) + ); + + $this->processor->processExpressions($values, $context, $data, false, 'json'); + + $this->assertSame( + '{"@true":null}', + $values['expr_object'], + 'Failed asserting that an expression is encoded' + ); + $this->assertSame( + '{"@true":null}', + $values['expr_array'], + 'Failed asserting that an expression is assembled and encoded' + ); + $this->assertSame( + ['@true' => null], + $values['not_expr_array'], + 'Failed asserting that a backslash at the begin of the array key is removed' + ); + $this->assertSame( + 123, + $values['scalar'], + 'Failed asserting that a scalar value is not changed' + ); + $this->assertSame( + '{"@true":null}', + $values['attr']['enabled'], + 'Failed asserting that an expression in "attr" is assembled and encoded' + ); + $this->assertEquals( + $expectedClassAttr, + $values['attr']['class'], + 'Failed asserting that "attr.class" is assembled and encoded' + ); + $this->assertSame( + '{"@true":null}', + $values['label_attr']['enabled'], + 'Failed asserting that an expression in "label_attr" is assembled and encoded' + ); + } +} diff --git a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/TwigLayoutRendererTest.php b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/TwigLayoutRendererTest.php index 146a2a297a2..703c6f9734a 100644 --- a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/TwigLayoutRendererTest.php +++ b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Layout/TwigLayoutRendererTest.php @@ -2,19 +2,26 @@ namespace Oro\Bundle\LayoutBundle\Tests\Unit\Layout; +use Symfony\Bridge\Twig\Form\TwigRendererInterface; + use Oro\Bundle\LayoutBundle\Layout\TwigLayoutRenderer; +use Oro\Component\Layout\Form\RendererEngine\FormRendererEngineInterface; class TwigLayoutRendererTest extends \PHPUnit_Framework_TestCase { public function testEnvironmentSet() { + /** @var TwigRendererInterface|\PHPUnit_Framework_MockObject_MockObject $innerRenderer */ $innerRenderer = $this->getMock('Symfony\Bridge\Twig\Form\TwigRendererInterface'); + /** @var \Twig_Environment $environment */ $environment = $this->getMockBuilder('\Twig_Environment')->getMock(); $innerRenderer->expects($this->once()) ->method('setEnvironment') ->with($this->identicalTo($environment)); + /** @var FormRendererEngineInterface $formRenderer */ + $formRenderer = $this->getMock('Oro\Component\Layout\Form\RendererEngine\FormRendererEngineInterface'); - new TwigLayoutRenderer($innerRenderer, $environment); + new TwigLayoutRenderer($innerRenderer, $formRenderer, $environment); } } diff --git a/src/Oro/Bundle/LayoutBundle/Tests/Unit/Stubs/Bundles/TestBundle/Resources/views/layouts/oro-black/theme.yml b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Stubs/Bundles/TestBundle/Resources/views/layouts/oro-black/theme.yml new file mode 100644 index 00000000000..8ea6f7f8f7c --- /dev/null +++ b/src/Oro/Bundle/LayoutBundle/Tests/Unit/Stubs/Bundles/TestBundle/Resources/views/layouts/oro-black/theme.yml @@ -0,0 +1 @@ +description: Oro Black theme description diff --git a/src/Oro/Bundle/LocaleBundle/Formatter/DateTimeFormatter.php b/src/Oro/Bundle/LocaleBundle/Formatter/DateTimeFormatter.php index 348becd9051..a5a35624f19 100644 --- a/src/Oro/Bundle/LocaleBundle/Formatter/DateTimeFormatter.php +++ b/src/Oro/Bundle/LocaleBundle/Formatter/DateTimeFormatter.php @@ -100,12 +100,14 @@ public function formatTime($date, $timeType = null, $locale = null, $timeZone = /** * Get the pattern used for the IntlDateFormatter * - * @param int|string $dateType Constant of IntlDateFormatter (NONE, FULL, LONG, MEDIUM, SHORT) or it's string name - * @param int|string $timeType Constant IntlDateFormatter (NONE, FULL, LONG, MEDIUM, SHORT) or it's string name + * @param int|string $dateType Constant of IntlDateFormatter (NONE, FULL, LONG, MEDIUM, SHORT) or it's string name + * @param int|string $timeType Constant IntlDateFormatter (NONE, FULL, LONG, MEDIUM, SHORT) or it's string name * @param string|null $locale + * @param string|null $value + * * @return string */ - public function getPattern($dateType, $timeType, $locale = null) + public function getPattern($dateType, $timeType, $locale = null, $value = null) { if (!$locale) { $locale = $this->localeSettings->getLocale(); @@ -131,15 +133,17 @@ public function getPattern($dateType, $timeType, $locale = null) * * @param string|int|null $dateType * @param string|int|null $timeType - * @param string|null $locale - * @param string|null $timeZone - * @param string|null $pattern + * @param string|null $locale + * @param string|null $timeZone + * @param string|null $pattern + * @param string|null $value + * * @return \IntlDateFormatter */ - protected function getFormatter($dateType, $timeType, $locale, $timeZone, $pattern) + protected function getFormatter($dateType, $timeType, $locale, $timeZone, $pattern, $value = null) { if (!$pattern) { - $pattern = $this->getPattern($dateType, $timeType, $locale); + $pattern = $this->getPattern($dateType, $timeType, $locale, $value); } return new \IntlDateFormatter( $this->localeSettings->getLanguage(), diff --git a/src/Oro/Bundle/LocaleBundle/Resources/public/js/formatter/address.js b/src/Oro/Bundle/LocaleBundle/Resources/public/js/formatter/address.js index 2466806e2e6..e4ec84de5fd 100644 --- a/src/Oro/Bundle/LocaleBundle/Resources/public/js/formatter/address.js +++ b/src/Oro/Bundle/LocaleBundle/Resources/public/js/formatter/address.js @@ -1,5 +1,5 @@ -define(['../locale-settings', './name' - ], function(localeSettings, nameFormatter) { +define(['jquery', '../locale-settings', './name' + ], function($, localeSettings, nameFormatter) { 'use strict'; /** @@ -50,8 +50,10 @@ define(['../locale-settings', './name' }); var addressLines = formatted - .replace(/ *(\\n)+/g, '\\n') .split('\\n'); + addressLines = addressLines.filter(function(element) { + return $.trim(element) !== ''; + }); if (typeof newLine === 'function') { for (var i = 0; i < addressLines.length; i++) { addressLines[i] = newLine(addressLines[i]); diff --git a/src/Oro/Bundle/LocaleBundle/Tests/Unit/Converter/AbstractFormatConverterTestCase.php b/src/Oro/Bundle/LocaleBundle/Tests/Unit/Converter/AbstractFormatConverterTestCase.php index 8c53f87618f..9190ba3c756 100644 --- a/src/Oro/Bundle/LocaleBundle/Tests/Unit/Converter/AbstractFormatConverterTestCase.php +++ b/src/Oro/Bundle/LocaleBundle/Tests/Unit/Converter/AbstractFormatConverterTestCase.php @@ -30,27 +30,27 @@ abstract class AbstractFormatConverterTestCase extends \PHPUnit_Framework_TestCa /** * @var array */ - protected $localFormatMap = array( - array(null, null, self::LOCALE_EN, "MMM d, y h:mm a"), - array(\IntlDateFormatter::LONG, \IntlDateFormatter::MEDIUM, self::LOCALE_EN, "MMMM d, y h:mm:ss a"), - array(\IntlDateFormatter::LONG, \IntlDateFormatter::NONE, self::LOCALE_EN, "MMMM d, y"), - array(\IntlDateFormatter::MEDIUM, \IntlDateFormatter::SHORT, self::LOCALE_EN, "MMM d, y h:mm a"), - array(\IntlDateFormatter::MEDIUM, \IntlDateFormatter::NONE, self::LOCALE_EN, "MMM d, y"), - array(null, \IntlDateFormatter::NONE, self::LOCALE_EN, "MMM d, y"), - array(\IntlDateFormatter::NONE, \IntlDateFormatter::MEDIUM, self::LOCALE_EN, "h:mm:ss a"), - array(\IntlDateFormatter::NONE, \IntlDateFormatter::SHORT, self::LOCALE_EN, "h:mm a"), - array(\IntlDateFormatter::NONE, null, self::LOCALE_EN, "h:mm a"), - - array(null, null, self::LOCALE_RU, "dd.MM.yyyy H:mm"), - array(\IntlDateFormatter::LONG, \IntlDateFormatter::MEDIUM, self::LOCALE_RU, "d MMMM y 'г.' H:mm:ss"), - array(\IntlDateFormatter::LONG, \IntlDateFormatter::NONE, self::LOCALE_RU, "d MMMM y 'г.'"), - array(\IntlDateFormatter::MEDIUM, \IntlDateFormatter::SHORT, self::LOCALE_RU, "dd.MM.yyyy H:mm"), - array(\IntlDateFormatter::MEDIUM, \IntlDateFormatter::NONE, self::LOCALE_RU, "dd.MM.yyyy"), - array(null, \IntlDateFormatter::NONE, self::LOCALE_RU, "dd.MM.yyyy"), - array(\IntlDateFormatter::NONE, \IntlDateFormatter::MEDIUM, self::LOCALE_RU, "H:mm:ss"), - array(\IntlDateFormatter::NONE, \IntlDateFormatter::SHORT, self::LOCALE_RU, "H:mm"), - array(\IntlDateFormatter::NONE, null, self::LOCALE_RU, "H:mm"), - ); + protected $localFormatMap = [ + [null, null, self::LOCALE_EN, null, "MMM d, y h:mm a"], + [\IntlDateFormatter::LONG, \IntlDateFormatter::MEDIUM, self::LOCALE_EN, null, "MMMM d, y h:mm:ss a"], + [\IntlDateFormatter::LONG, \IntlDateFormatter::NONE, self::LOCALE_EN, null, "MMMM d, y"], + [\IntlDateFormatter::MEDIUM, \IntlDateFormatter::SHORT, self::LOCALE_EN, null, "MMM d, y h:mm a"], + [\IntlDateFormatter::MEDIUM, \IntlDateFormatter::NONE, self::LOCALE_EN, null, "MMM d, y"], + [null, \IntlDateFormatter::NONE, self::LOCALE_EN, null, "MMM d, y"], + [\IntlDateFormatter::NONE, \IntlDateFormatter::MEDIUM, self::LOCALE_EN, null, "h:mm:ss a"], + [\IntlDateFormatter::NONE, \IntlDateFormatter::SHORT, self::LOCALE_EN, null, "h:mm a"], + [\IntlDateFormatter::NONE, null, self::LOCALE_EN, null, "h:mm a"], + + [null, null, self::LOCALE_RU, null, "dd.MM.yyyy H:mm"], + [\IntlDateFormatter::LONG, \IntlDateFormatter::MEDIUM, self::LOCALE_RU, null, "d MMMM y 'г.' H:mm:ss"], + [\IntlDateFormatter::LONG, \IntlDateFormatter::NONE, self::LOCALE_RU, null, "d MMMM y 'г.'"], + [\IntlDateFormatter::MEDIUM, \IntlDateFormatter::SHORT, self::LOCALE_RU, null, "dd.MM.yyyy H:mm"], + [\IntlDateFormatter::MEDIUM, \IntlDateFormatter::NONE, self::LOCALE_RU, null, "dd.MM.yyyy"], + [null, \IntlDateFormatter::NONE, self::LOCALE_RU, null, "dd.MM.yyyy"], + [\IntlDateFormatter::NONE, \IntlDateFormatter::MEDIUM, self::LOCALE_RU, null, "H:mm:ss"], + [\IntlDateFormatter::NONE, \IntlDateFormatter::SHORT, self::LOCALE_RU, null, "H:mm"], + [\IntlDateFormatter::NONE, null, self::LOCALE_RU, null, "H:mm"], + ]; /** * @SuppressWarnings(PHPMD.UnusedLocalVariable) @@ -59,7 +59,7 @@ protected function setUp() { $this->formatter = $this->getMockBuilder('Oro\Bundle\LocaleBundle\Formatter\DateTimeFormatter') ->disableOriginalConstructor() - ->setMethods(array('getPattern')) + ->setMethods(['getPattern']) ->getMock(); $this->formatter->expects($this->any()) @@ -103,8 +103,9 @@ abstract protected function createFormatConverter(); /** * @param string $expected - * @param int $dateFormat + * @param int $dateFormat * @param string $locale + * * @dataProvider getDateFormatDataProvider */ public function testGetDateFormat($expected, $dateFormat, $locale) @@ -119,8 +120,9 @@ abstract public function getDateFormatDataProvider(); /** * @param string $expected - * @param int $timeFormat + * @param int $timeFormat * @param string $locale + * * @dataProvider getTimeFormatDataProvider */ public function testGetTimeFormat($expected, $timeFormat, $locale) @@ -135,9 +137,10 @@ abstract public function getTimeFormatDataProvider(); /** * @param string $expected - * @param int $dateFormat - * @param int $timeFormat + * @param int $dateFormat + * @param int $timeFormat * @param string $locale + * * @dataProvider getDateTimeFormatDataProvider */ public function testGetDateTimeFormat($expected, $dateFormat, $timeFormat, $locale) @@ -153,6 +156,7 @@ abstract public function getDateTimeFormatDataProvider(); /** * @param string $expected * @param string $locale + * * @dataProvider getDateFormatDayProvider */ public function testGetDayFormat($expected, $locale) diff --git a/src/Oro/Bundle/MigrationBundle/Event/MigrationEvent.php b/src/Oro/Bundle/MigrationBundle/Event/MigrationEvent.php index 24f0c34a237..4933452936e 100644 --- a/src/Oro/Bundle/MigrationBundle/Event/MigrationEvent.php +++ b/src/Oro/Bundle/MigrationBundle/Event/MigrationEvent.php @@ -3,7 +3,10 @@ namespace Oro\Bundle\MigrationBundle\Event; use Doctrine\DBAL\Connection; + use Symfony\Component\EventDispatcher\Event; + +use Oro\Bundle\EntityBundle\Tools\SafeDatabaseChecker; use Oro\Bundle\MigrationBundle\Migration\Migration; class MigrationEvent extends Event @@ -61,7 +64,7 @@ public function getMigrations() */ public function getData($sql, array $params = array(), $types = array()) { - $this->ensureConnected(); + $this->connection->connect(); return $this->connection->fetchAll($sql, $params, $types); } @@ -74,28 +77,6 @@ public function getData($sql, array $params = array(), $types = array()) */ public function isTableExist($tableName) { - $result = false; - try { - $this->ensureConnected(); - - $result = $this->connection->isConnected() - && (bool)array_intersect( - [$tableName], - $this->connection->getSchemaManager()->listTableNames() - ); - } catch (\PDOException $e) { - } - - return $result; - } - - /** - * Makes sure that the connection is open - */ - protected function ensureConnected() - { - if (!$this->connection->isConnected()) { - $this->connection->connect(); - } + return SafeDatabaseChecker::tablesExist($this->connection, $tableName); } } diff --git a/src/Oro/Bundle/NavigationBundle/Event/JsRoutingDumpListener.php b/src/Oro/Bundle/NavigationBundle/Event/JsRoutingDumpListener.php new file mode 100644 index 00000000000..e0ce0c7ac24 --- /dev/null +++ b/src/Oro/Bundle/NavigationBundle/Event/JsRoutingDumpListener.php @@ -0,0 +1,31 @@ +assetVersionManager = $assetVersionManager; + } + + /** + * @param ConsoleCommandEvent $event + */ + public function onConsoleCommand(ConsoleCommandEvent $event) + { + if ('fos:js-routing:dump' === $event->getCommand()->getName()) { + $this->assetVersionManager->updateAssetVersion('routing'); + } + } +} diff --git a/src/Oro/Bundle/NavigationBundle/OroNavigationBundle.php b/src/Oro/Bundle/NavigationBundle/OroNavigationBundle.php index 650f573ae67..817c28a6192 100644 --- a/src/Oro/Bundle/NavigationBundle/OroNavigationBundle.php +++ b/src/Oro/Bundle/NavigationBundle/OroNavigationBundle.php @@ -8,6 +8,7 @@ use Oro\Bundle\NavigationBundle\DependencyInjection\Compiler\TagGeneratorPass; use Oro\Bundle\NavigationBundle\DependencyInjection\Compiler\MenuBuilderChainPass; use Oro\Bundle\NavigationBundle\DependencyInjection\Compiler\ChainBreadcrumbManagerPass; +use Oro\Bundle\UIBundle\DependencyInjection\Compiler\DynamicAssetVersionPass; class OroNavigationBundle extends Bundle { @@ -21,5 +22,6 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new MenuBuilderChainPass()); $container->addCompilerPass(new TagGeneratorPass()); $container->addCompilerPass(new ChainBreadcrumbManagerPass()); + $container->addCompilerPass(new DynamicAssetVersionPass('routing')); } } diff --git a/src/Oro/Bundle/NavigationBundle/Resources/config/oro/app.yml b/src/Oro/Bundle/NavigationBundle/Resources/config/oro/app.yml new file mode 100644 index 00000000000..4f665b6449e --- /dev/null +++ b/src/Oro/Bundle/NavigationBundle/Resources/config/oro/app.yml @@ -0,0 +1,10 @@ +framework: + templating: + packages: + routing: + version: %assets_version% + version_format: %%s?dynamic_version=%%s + # the default format cannot be used due the problem with urlArgs in require js + # in case if both static and dynamic versions exist we have duplicate of asset version parameter, + # something like this: routes.js?version=v123&version=v123-1 + # version_format: ~ # use the default format diff --git a/src/Oro/Bundle/NavigationBundle/Resources/config/services.yml b/src/Oro/Bundle/NavigationBundle/Resources/config/services.yml index 8a954840038..24bfc914cf9 100644 --- a/src/Oro/Bundle/NavigationBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/NavigationBundle/Resources/config/services.yml @@ -244,6 +244,13 @@ services: tags: - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest } + oro_navigation.event.js_routing_dump_listener: + class: Oro\Bundle\NavigationBundle\Event\JsRoutingDumpListener + arguments: + - @oro_ui.dynamic_asset_version_manager + tags: + - { name: kernel.event_listener, event: console.command, method: onConsoleCommand } + oro_navigation.content.topic_sender: class: %oro_navigation.content.topic_sender.class% arguments: diff --git a/src/Oro/Bundle/NavigationBundle/Resources/public/images/pinbar-location.jpg b/src/Oro/Bundle/NavigationBundle/Resources/public/images/pinbar-location.jpg new file mode 100644 index 00000000000..f7796c9b918 Binary files /dev/null and b/src/Oro/Bundle/NavigationBundle/Resources/public/images/pinbar-location.jpg differ diff --git a/src/Oro/Bundle/NavigationBundle/Resources/public/js/content-manager.js b/src/Oro/Bundle/NavigationBundle/Resources/public/js/content-manager.js index 0ed7e33c6a0..6ef9692e6e5 100644 --- a/src/Oro/Bundle/NavigationBundle/Resources/public/js/content-manager.js +++ b/src/Oro/Bundle/NavigationBundle/Resources/public/js/content-manager.js @@ -292,9 +292,6 @@ define([ */ add: function() { var path; - if (current.path[0] !== '/') { - current.path = '/' + current.path; - } path = current.path; pagesCache[path] = current; }, @@ -327,7 +324,7 @@ define([ * @param {string=} hash */ saveState: function(key, value, hash) { - if (value !== null) { + if (value !== null && value !== void 0) { current.state[key] = value; } else { delete current.state[key]; @@ -408,7 +405,7 @@ define([ checkState: function(key, hash) { var query; query = Chaplin.utils.queryParams.parse(current.query); - return query[key] === hash; + return query[key] === hash || query[key] === void 0 && hash === null; }, /** diff --git a/src/Oro/Bundle/NavigationBundle/Resources/translations/messages.en.yml b/src/Oro/Bundle/NavigationBundle/Resources/translations/messages.en.yml index b2b654e9b8f..d214e595db6 100644 --- a/src/Oro/Bundle/NavigationBundle/Resources/translations/messages.en.yml +++ b/src/Oro/Bundle/NavigationBundle/Resources/translations/messages.en.yml @@ -2,8 +2,7 @@ oro: navigation: help: title: How To Use Pinbar - save: Use the Pin Bar to “pin” documents and pages you want to continuously work on by clicking on the minimize button while on a specific page. - restore: To restore a page simply click on the title of the page you want to work on so that it is maximized back to your working area. + message: Use pin icon on the right top corner of page to create fast access link in the pinbar. menu: pinbar.label: Pinbar history.label: History diff --git a/src/Oro/Bundle/NavigationBundle/Resources/views/Js/requirejs.config.js.twig b/src/Oro/Bundle/NavigationBundle/Resources/views/Js/requirejs.config.js.twig index e6134340824..ec5202e0c99 100644 --- a/src/Oro/Bundle/NavigationBundle/Resources/views/Js/requirejs.config.js.twig +++ b/src/Oro/Bundle/NavigationBundle/Resources/views/Js/requirejs.config.js.twig @@ -26,7 +26,7 @@ require({ {% if app.debug %} 'oro/routes': '{{ path('fos_js_routing_js', {"callback": "fos.Router.setData"}) }}' {% else %} - 'oro/routes': '../js/routes' + 'oro/routes': {{ asset('../js/routes.js', 'routing')|json_encode|raw }} {% endif %} } }); diff --git a/src/Oro/Bundle/NavigationBundle/Resources/views/Pinbar/help.html.twig b/src/Oro/Bundle/NavigationBundle/Resources/views/Pinbar/help.html.twig index fcf952c682d..5a1fe633356 100644 --- a/src/Oro/Bundle/NavigationBundle/Resources/views/Pinbar/help.html.twig +++ b/src/Oro/Bundle/NavigationBundle/Resources/views/Pinbar/help.html.twig @@ -6,9 +6,11 @@

      {{ 'oro.navigation.help.title'|trans }}

      - {{ 'oro.navigation.help.save'|trans }} -
      - {{ 'oro.navigation.help.restore'|trans }} + {{ 'oro.navigation.help.message'|trans }} +

      +

      +

  • diff --git a/src/Oro/Bundle/NavigationBundle/Tests/Unit/OroNavigationBundleTest.php b/src/Oro/Bundle/NavigationBundle/Tests/Unit/OroNavigationBundleTest.php deleted file mode 100644 index 07aef7f203b..00000000000 --- a/src/Oro/Bundle/NavigationBundle/Tests/Unit/OroNavigationBundleTest.php +++ /dev/null @@ -1,29 +0,0 @@ -getMock('Symfony\Component\DependencyInjection\ContainerBuilder'); - $container->expects($this->at(0)) - ->method('addCompilerPass') - ->with( - $this->isInstanceOf( - 'Oro\Bundle\NavigationBundle\DependencyInjection\Compiler\MenuBuilderChainPass' - ) - ); - $container->expects($this->at(1)) - ->method('addCompilerPass') - ->with( - $this->isInstanceOf( - 'Oro\Bundle\NavigationBundle\DependencyInjection\Compiler\TagGeneratorPass' - ) - ); - - $bundle = new OroNavigationBundle(); - $bundle->build($container); - } -} diff --git a/src/Oro/Bundle/NoteBundle/Model/Strategy/ReplaceStrategy.php b/src/Oro/Bundle/NoteBundle/Model/Strategy/ReplaceStrategy.php index 42ae4ff3e52..bc3e769e42a 100644 --- a/src/Oro/Bundle/NoteBundle/Model/Strategy/ReplaceStrategy.php +++ b/src/Oro/Bundle/NoteBundle/Model/Strategy/ReplaceStrategy.php @@ -3,18 +3,16 @@ namespace Oro\Bundle\NoteBundle\Model\Strategy; use Symfony\Component\Security\Core\Util\ClassUtils; + +use Oro\Component\PhpUtils\ArrayUtil; + use Oro\Bundle\ActivityListBundle\Entity\Manager\ActivityListManager; use Oro\Bundle\ActivityListBundle\Entity\ActivityList; use Oro\Bundle\EntityBundle\ORM\DoctrineHelper; use Oro\Bundle\EntityMergeBundle\Data\FieldData; use Oro\Bundle\EntityMergeBundle\Model\Strategy\StrategyInterface; use Oro\Bundle\NoteBundle\Model\MergeModes; -use Oro\Bundle\UIBundle\Tools\ArrayUtils; -/** - * Class ReplaceStrategy - * @package Oro\Bundle\NoteBundle\Model\Strategy - */ class ReplaceStrategy implements StrategyInterface { /** @var DoctrineHelper */ @@ -71,7 +69,7 @@ public function merge(FieldData $fieldData) ->getActivityListQueryBuilderByActivityClass($entityClass, $sourceEntity->getId(), $activityClass); $activityListItems = $queryBuilder->getQuery()->getResult(); - $activityIds = ArrayUtils::arrayColumn($activityListItems, 'id'); + $activityIds = ArrayUtil::arrayColumn($activityListItems, 'id'); $this->activityListManager ->replaceActivityTargetWithPlainQuery( $activityIds, diff --git a/src/Oro/Bundle/NoteBundle/Model/Strategy/UniteStrategy.php b/src/Oro/Bundle/NoteBundle/Model/Strategy/UniteStrategy.php index 2c663fa7bd2..07912c7638f 100644 --- a/src/Oro/Bundle/NoteBundle/Model/Strategy/UniteStrategy.php +++ b/src/Oro/Bundle/NoteBundle/Model/Strategy/UniteStrategy.php @@ -2,14 +2,16 @@ namespace Oro\Bundle\NoteBundle\Model\Strategy; -use Oro\Bundle\NoteBundle\Model\MergeModes; use Symfony\Component\Security\Core\Util\ClassUtils; + +use Oro\Component\PhpUtils\ArrayUtil; + use Oro\Bundle\ActivityListBundle\Entity\Manager\ActivityListManager; use Oro\Bundle\ActivityListBundle\Entity\ActivityList; use Oro\Bundle\EntityBundle\ORM\DoctrineHelper; use Oro\Bundle\EntityMergeBundle\Model\Strategy\StrategyInterface; use Oro\Bundle\EntityMergeBundle\Data\FieldData; -use Oro\Bundle\UIBundle\Tools\ArrayUtils; +use Oro\Bundle\NoteBundle\Model\MergeModes; class UniteStrategy implements StrategyInterface { @@ -60,7 +62,7 @@ public function merge(FieldData $fieldData) $activityListItems = $queryBuilder->getQuery()->getResult(); - $activityIds = ArrayUtils::arrayColumn($activityListItems, 'id'); + $activityIds = ArrayUtil::arrayColumn($activityListItems, 'id'); $this->activityListManager ->replaceActivityTargetWithPlainQuery( $activityIds, diff --git a/src/Oro/Bundle/NoteBundle/Resources/public/js/app/views/notes-view.js b/src/Oro/Bundle/NoteBundle/Resources/public/js/app/views/notes-view.js index 7145ae60225..7e0e74ca299 100644 --- a/src/Oro/Bundle/NoteBundle/Resources/public/js/app/views/notes-view.js +++ b/src/Oro/Bundle/NoteBundle/Resources/public/js/app/views/notes-view.js @@ -70,13 +70,13 @@ define(function(require) { }, expandAll: function() { - _.each(this.subviews, function(itemView) { + _.each(this.getItemViews(), function(itemView) { itemView.toggle(false); }); }, collapseAll: function() { - _.each(this.subviews, function(itemView) { + _.each(this.getItemViews(), function(itemView) { itemView.toggle(true); }); }, @@ -104,13 +104,13 @@ define(function(require) { } this._showLoading(); try { - _.each(this.subviews, function(itemView) { + _.each(this.getItemViews(), function(itemView) { state[itemView.model.get('id')] = itemView.isCollapsed(); }); this.collection.fetch({ reset: true, success: _.bind(function() { - _.each(this.subviews, function(itemView) { + _.each(this.getItemViews(), function(itemView) { itemView.toggle(state[itemView.model.get('id')]); }); this._hideLoading(); diff --git a/src/Oro/Bundle/NotificationBundle/DependencyInjection/Compiler/EventsCompilerPass.php b/src/Oro/Bundle/NotificationBundle/DependencyInjection/Compiler/EventsCompilerPass.php index a374d94c1c2..67cd0def623 100644 --- a/src/Oro/Bundle/NotificationBundle/DependencyInjection/Compiler/EventsCompilerPass.php +++ b/src/Oro/Bundle/NotificationBundle/DependencyInjection/Compiler/EventsCompilerPass.php @@ -7,6 +7,8 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Oro\Bundle\EntityBundle\Tools\SafeDatabaseChecker; + class EventsCompilerPass implements CompilerPassInterface { /** a table name of {@see Oro\Bundle\NotificationBundle\Entity\Event} */ @@ -33,7 +35,7 @@ public function process(ContainerBuilder $container) // ORM usage leads unnecessary loading of Doctrine metadata and ORO entity configs which is not needed here /** @var Connection $connection */ $connection = $container->get('doctrine.dbal.default_connection'); - if ($this->checkDatabase($connection)) { + if (SafeDatabaseChecker::tablesExist($connection, self::EVENT_TABLE_NAME)) { $dispatcher = $container->findDefinition(self::DISPATCHER_KEY); $rows = $connection->fetchAll('SELECT name FROM ' . self::EVENT_TABLE_NAME); @@ -45,23 +47,4 @@ public function process(ContainerBuilder $container) } } } - - /** - * @param Connection $connection - * - * @return bool - */ - protected function checkDatabase(Connection $connection) - { - $result = false; - try { - $connection->connect(); - $result = - $connection->isConnected() - && $connection->getSchemaManager()->tablesExist([self::EVENT_TABLE_NAME]); - } catch (\PDOException $e) { - } - - return $result; - } } diff --git a/src/Oro/Bundle/NotificationBundle/Resources/config/datagrid.yml b/src/Oro/Bundle/NotificationBundle/Resources/config/datagrid.yml index c13647beea4..9cdd78ad18a 100644 --- a/src/Oro/Bundle/NotificationBundle/Resources/config/datagrid.yml +++ b/src/Oro/Bundle/NotificationBundle/Resources/config/datagrid.yml @@ -3,8 +3,8 @@ datagrid: options: entityHint: transactional email entity_pagination: true + acl_resource: oro_notification_emailnotification_view source: - acl_resource: oro_notification_emailnotification_view type: orm query: select: diff --git a/src/Oro/Bundle/NotificationBundle/Tests/Unit/DependencyInjection/Compiler/EventsCompilerPassTest.php b/src/Oro/Bundle/NotificationBundle/Tests/Unit/DependencyInjection/Compiler/EventsCompilerPassTest.php index 9c9874304e7..d8186158656 100644 --- a/src/Oro/Bundle/NotificationBundle/Tests/Unit/DependencyInjection/Compiler/EventsCompilerPassTest.php +++ b/src/Oro/Bundle/NotificationBundle/Tests/Unit/DependencyInjection/Compiler/EventsCompilerPassTest.php @@ -89,16 +89,13 @@ private function configureConnectionMock() $connection->expects($this->once()) ->method('connect'); - $connection->expects($this->once()) - ->method('isConnected') - ->will($this->returnValue(true)); $schemaManager = $this->getMockBuilder('Doctrine\DBAL\Schema\MySqlSchemaManager') ->disableOriginalConstructor() ->getMock(); $schemaManager->expects($this->once()) ->method('tablesExist') - ->with([EventsCompilerPass::EVENT_TABLE_NAME]) + ->with(EventsCompilerPass::EVENT_TABLE_NAME) ->will($this->returnValue(true)); $connection->expects($this->once()) diff --git a/src/Oro/Bundle/OrganizationBundle/Entity/BusinessUnit.php b/src/Oro/Bundle/OrganizationBundle/Entity/BusinessUnit.php index f9d850d7e76..5456adbe2e0 100644 --- a/src/Oro/Bundle/OrganizationBundle/Entity/BusinessUnit.php +++ b/src/Oro/Bundle/OrganizationBundle/Entity/BusinessUnit.php @@ -46,6 +46,9 @@ * "security"={ * "type"="ACL", * "group_name"="" + * }, + * "grid"={ + * "default"="business-unit-grid" * } * } * ) diff --git a/src/Oro/Bundle/OrganizationBundle/Event/BusinessUnitGridListener.php b/src/Oro/Bundle/OrganizationBundle/Event/BusinessUnitGridListener.php index d6e64cd0ba7..6f55005f4d5 100644 --- a/src/Oro/Bundle/OrganizationBundle/Event/BusinessUnitGridListener.php +++ b/src/Oro/Bundle/OrganizationBundle/Event/BusinessUnitGridListener.php @@ -72,10 +72,15 @@ public function onBuildBefore(BuildBefore $event) $organization->getId() ); } - $where = array_merge( - $where, - ['u.id in (' . implode(', ', $resultBuIds) . ')'] - ); + if (count($resultBuIds)) { + $where = array_merge( + $where, + ['u.id in (' . implode(', ', $resultBuIds) . ')'] + ); + } else { + // There are no records to show, make query to return empty result + $where = array_merge($where, ['1 = 0']); + } } if (count($where)) { $config->offsetSetByPath('[source][query][where][and]', $where); diff --git a/src/Oro/Bundle/OrganizationBundle/Filter/BusinessUnitChoiceFilter.php b/src/Oro/Bundle/OrganizationBundle/Filter/BusinessUnitChoiceFilter.php index 51a7127831d..8f0721375fe 100644 --- a/src/Oro/Bundle/OrganizationBundle/Filter/BusinessUnitChoiceFilter.php +++ b/src/Oro/Bundle/OrganizationBundle/Filter/BusinessUnitChoiceFilter.php @@ -18,7 +18,7 @@ public function apply(FilterDatasourceAdapterInterface $ds, $data) return false; } - $type = $data['type']; + $type = $data['type']; if (count($data['value']) > 1 || (isset($data['value'][0]) && $data['value'][0] != "")) { $parameterName = $ds->generateParameterName($this->getName()); @@ -26,30 +26,30 @@ public function apply(FilterDatasourceAdapterInterface $ds, $data) ->createQueryBuilder('u') ->select('u.id') ->leftJoin('u.businessUnits', 'bu') - ->where('bu.id in (:'.$parameterName.')') + ->where('bu.id in (:' . $parameterName . ')') ->getQuery() - ->getDQL(); + ->getDQL(); $this->applyFilterToClause( $ds, - $this->get(FilterUtility::DATA_NAME_KEY) . ' in ('.$qb2.')' + $this->get(FilterUtility::DATA_NAME_KEY) . ' in (' . $qb2 . ')' ); if (!in_array($type, [FilterUtility::TYPE_EMPTY, FilterUtility::TYPE_NOT_EMPTY], true)) { $ds->setParameter($parameterName, $data['value']); } } + return true; } /** - * @param $data - * - * @return mixed + * {@inheritDoc} */ public function parseData($data) { $data['value'] = explode(',', $data['value']); + return $data; } } diff --git a/src/Oro/Bundle/OrganizationBundle/Provider/Filter/ChoiceTreeBusinessUnitProvider.php b/src/Oro/Bundle/OrganizationBundle/Provider/Filter/ChoiceTreeBusinessUnitProvider.php index 650b51d2f2a..e68f05e24ca 100644 --- a/src/Oro/Bundle/OrganizationBundle/Provider/Filter/ChoiceTreeBusinessUnitProvider.php +++ b/src/Oro/Bundle/OrganizationBundle/Provider/Filter/ChoiceTreeBusinessUnitProvider.php @@ -5,10 +5,12 @@ use Doctrine\Bundle\DoctrineBundle\Registry; use Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper; +use Oro\Bundle\SecurityBundle\Owner\ChainOwnerTreeProvider; +use Oro\Bundle\SecurityBundle\Owner\OwnerTree; use Oro\Bundle\SecurityBundle\SecurityFacade; -use Oro\Bundle\OrganizationBundle\Entity\Organization; use Oro\Bundle\OrganizationBundle\Entity\Repository\BusinessUnitRepository; use Oro\Bundle\OrganizationBundle\Entity\BusinessUnit; +use Oro\Bundle\UserBundle\Entity\User; class ChoiceTreeBusinessUnitProvider { @@ -21,44 +23,57 @@ class ChoiceTreeBusinessUnitProvider /** @var SecurityFacade */ protected $securityFacade; + /** @var ChainOwnerTreeProvider */ + protected $treeProvider; + /** - * @param Registry $registry - * @param SecurityFacade $securityFacade - * @param AclHelper $aclHelper + * @param Registry $registry + * @param SecurityFacade $securityFacade + * @param AclHelper $aclHelper + * @param ChainOwnerTreeProvider $treeProvider */ public function __construct( Registry $registry, SecurityFacade $securityFacade, - AclHelper $aclHelper + AclHelper $aclHelper, + ChainOwnerTreeProvider $treeProvider ) { - $this->registry = $registry; + $this->registry = $registry; $this->securityFacade = $securityFacade; - $this->aclHelper = $aclHelper; + $this->aclHelper = $aclHelper; + $this->treeProvider = $treeProvider; } /** - * @param Organization $currentOrganization - * * @return array */ public function getList() { - $businessUnitRepository = $this->getBusinessUnitRepo(); + $businessUnitRepo = $this->getBusinessUnitRepo(); + $response = []; - $qb = $businessUnitRepository->getQueryBuilder(); + $qb = $businessUnitRepo->getQueryBuilder(); + $qb + ->andWhere( + $qb->expr()->in('businessUnit.id', ':ids') + ) + ->orderBy('businessUnit.id', 'ASC'); + + $qb->setParameter('ids', $this->getBusinessUnitIds()); + $businessUnits = $this->aclHelper->apply($qb)->getResult(); /** @var BusinessUnit $businessUnit */ foreach ($businessUnits as $businessUnit) { if ($businessUnit->getOwner()) { - $name =$businessUnit->getName(); + $name = $businessUnit->getName(); } else { $name = $this->getBusinessUnitName($businessUnit); } $response[] = [ - 'id' => $businessUnit->getId(), - 'name' => $name, + 'id' => $businessUnit->getId(), + 'name' => $name, 'owner_id' => $businessUnit->getOwner() ? $businessUnit->getOwner()->getId() : null ]; } @@ -81,6 +96,34 @@ protected function getBusinessUnitRepo() */ protected function getBusinessUnitName(BusinessUnit $businessUnit) { - return $businessUnit->getName(); + return $businessUnit->getName(); + } + + /** + * @return User + */ + protected function getUser() + { + return $this->securityFacade->getToken()->getUser(); + } + + /** + * @return array + */ + protected function getBusinessUnitIds() + { + /** @var OwnerTree $tree */ + $tree = $this->treeProvider->getTree(); + $user = $this->getUser(); + $result = []; + + $organizations = $user->getOrganizations(); + + foreach ($organizations as $organization) { + $subBUIds = $tree->getUserSubordinateBusinessUnitIds($user->getId(), $organization->getId()); + $result = array_merge($result, $subBUIds); + } + + return array_unique($result); } } diff --git a/src/Oro/Bundle/OrganizationBundle/Resources/config/datagrid.yml b/src/Oro/Bundle/OrganizationBundle/Resources/config/datagrid.yml index 59e48e6050f..13787f87a41 100644 --- a/src/Oro/Bundle/OrganizationBundle/Resources/config/datagrid.yml +++ b/src/Oro/Bundle/OrganizationBundle/Resources/config/datagrid.yml @@ -3,11 +3,10 @@ datagrid: extended_entity_name: %oro_organization.business_unit.entity.class% options: entityHint: business unit - skip_acl_check: true entity_pagination: true source: - acl_resource: oro_business_unit_view type: orm + skip_acl_apply: true query: select: - u.id @@ -117,8 +116,8 @@ datagrid: bu-update-users-grid: extends: user-relation-grid + acl_resource: oro_business_unit_update source: - acl_resource: oro_business_unit_update query: select: - > @@ -198,8 +197,8 @@ datagrid: extends: user-relation-grid options: entityHint: user + acl_resource: oro_business_unit_view source: - acl_resource: oro_business_unit_view query: where: and: diff --git a/src/Oro/Bundle/OrganizationBundle/Resources/config/services.yml b/src/Oro/Bundle/OrganizationBundle/Resources/config/services.yml index 6fc011d8f57..98d0ec35c6d 100644 --- a/src/Oro/Bundle/OrganizationBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/OrganizationBundle/Resources/config/services.yml @@ -200,6 +200,7 @@ services: - @doctrine - @oro_security.security_facade - @oro_security.acl_helper + - @oro_security.ownership_tree_provider.chain oro_organization.autocomplete.business_unit.search_handler: class: %oro_organization.autocomplete.business_unit.search_handler.class% diff --git a/src/Oro/Bundle/OrganizationBundle/Tests/Unit/Provider/Filter/ChoiceTreeBusinessUnitProviderTest.php b/src/Oro/Bundle/OrganizationBundle/Tests/Unit/Provider/Filter/ChoiceTreeBusinessUnitProviderTest.php index 0ea4d9a949f..a8f96c63806 100644 --- a/src/Oro/Bundle/OrganizationBundle/Tests/Unit/Provider/Filter/ChoiceTreeBusinessUnitProviderTest.php +++ b/src/Oro/Bundle/OrganizationBundle/Tests/Unit/Provider/Filter/ChoiceTreeBusinessUnitProviderTest.php @@ -7,7 +7,7 @@ class ChoiceTreeBusinessUnitProviderTest extends \PHPUnit_Framework_TestCase { /** @var ChoiceTreeBusinessUnitProvider */ - protected $choiceTreeBusinessUnitProvider; + protected $choiceTreeBUProvider; /** @var \PHPUnit_Framework_MockObject_MockObject */ protected $registry; @@ -18,133 +18,217 @@ class ChoiceTreeBusinessUnitProviderTest extends \PHPUnit_Framework_TestCase /** @var \PHPUnit_Framework_MockObject_MockObject */ protected $aclHelper; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $treeProvider; + public function setUp() { - $this->registry = $this->getMockBuilder('Doctrine\Bundle\DoctrineBundle\Registry') + $this->registry = $this->getMockBuilder('Doctrine\Bundle\DoctrineBundle\Registry') ->disableOriginalConstructor() ->getMock(); $this->securityFacade = $this->getMockBuilder('Oro\Bundle\SecurityBundle\SecurityFacade') ->disableOriginalConstructor() ->getMock(); - $this->aclHelper = $this->getMockBuilder('Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper') + $this->aclHelper = $this->getMockBuilder('Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper') + ->disableOriginalConstructor() + ->getMock(); + + $this->treeProvider = $this->getMockBuilder('Oro\Bundle\SecurityBundle\Owner\ChainOwnerTreeProvider') + ->setMethods(['getTree']) ->disableOriginalConstructor() ->getMock(); - $this->choiceTreeBusinessUnitProvider = new ChoiceTreeBusinessUnitProvider( + $this->choiceTreeBUProvider = new ChoiceTreeBusinessUnitProvider( $this->registry, $this->securityFacade, - $this->aclHelper + $this->aclHelper, + $this->treeProvider ); } - public function testGetList() + /** + * @dataProvider getListDataProvider + */ + public function testGetList($userBUIds, $queryResult, $result) { - $businessUnitRepository = $this->getMockBuilder('BusinessUnitRepository') + $businessUnitRepos = $this->getMockBuilder('BusinessUnitRepository') ->disableOriginalConstructor() ->setMethods(['getQueryBuilder']) ->getMock(); $qb = $this->getMockBuilder('Doctrine\ORM\QueryBuilder') - ->setMethods(['getResult']) + ->setMethods(['getResult', 'expr', 'setParameter']) ->disableOriginalConstructor() ->getMock(); - $this->aclHelper->expects($this->any())->method('apply')->willReturn($qb); - $businessUnitRepository->expects($this->any())->method('getQueryBuilder')->willReturn($qb); - $qb->expects($this->any())->method('getResult')->willReturn($this->getTestBusinessUnits()); + $treeOwner = $this->getMockBuilder('Oro\Bundle\SecurityBundle\Owner\OwnerTree') + ->setMethods(['getUserSubordinateBusinessUnitIds']) + ->disableOriginalConstructor() + ->getMock(); - $this->registry->expects($this->once())->method('getRepository')->with('OroOrganizationBundle:BusinessUnit') - ->willReturn($businessUnitRepository); + $this->treeProvider->expects(self::once())->method('getTree')->willReturn($treeOwner); - $result = $this->choiceTreeBusinessUnitProvider->getList(); + $treeOwner + ->expects(self::once()) + ->method('getUserSubordinateBusinessUnitIds') + ->willReturn($userBUIds); - $this->assertEquals($this->getExpectedData(), $result); - } + $this->aclHelper->expects(self::any())->method('apply')->willReturn($qb); + $businessUnitRepos->expects(self::any())->method('getQueryBuilder')->willReturn($qb); - public function testGetEmptyList() - { - $businessUnitRepository = $this->getMockBuilder('BusinessUnitRepository') + $expression = $this->getMockBuilder('Doctrine\ORM\Query\Expr') + ->setMethods(['in']) ->disableOriginalConstructor() - ->setMethods(['getQueryBuilder']) ->getMock(); - $qb = $this->getMockBuilder('Doctrine\ORM\QueryBuilder') - ->setMethods(['getResult']) + $qb->expects(self::once())->method('getResult')->willReturn($queryResult); + $qb->expects(self::once())->method('expr')->willReturn($expression); + $qb->expects(self::once())->method('setParameter'); + + $tokenStorage = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + + $user = $this->getMockBuilder('Oro\Bundle\UserBundle\Entity\User') + ->setMethods(['getId', 'getOrganizations']) ->disableOriginalConstructor() ->getMock(); - $this->aclHelper->expects($this->any())->method('apply')->willReturn($qb); - $businessUnitRepository->expects($this->any())->method('getQueryBuilder')->willReturn($qb); - $qb->expects($this->any())->method('getResult')->willReturn([]); + $organization = $this->getMock('Oro\Bundle\OrganizationBundle\Entity\OrganizationInterface'); + $organization->expects(self::once())->method('getId')->willReturn(1); + $user->expects(self::once())->method('getOrganizations')->willReturn([$organization]); + $this->securityFacade->expects(self::once())->method('getToken')->willReturn($tokenStorage); + $tokenStorage->expects(self::once())->method('getUser')->willReturn($user); - $this->registry->expects($this->once())->method('getRepository')->with('OroOrganizationBundle:BusinessUnit') - ->willReturn($businessUnitRepository); + $this->registry + ->expects(self::once()) + ->method('getRepository') + ->with('OroOrganizationBundle:BusinessUnit') + ->willReturn($businessUnitRepos); - $result = $this->choiceTreeBusinessUnitProvider->getList(); + $resultedUserBUids = $this->choiceTreeBUProvider->getList(); - $this->assertEquals([], $result); + self::assertEquals($result, $resultedUserBUids); } - protected function getTestBusinessUnits() + /** + * @return array + */ + public function getListDataProvider() { - $data = [ - [ - 'name' => 'Main Business Unit', - 'id' => 1, - 'owner_id' => null + return [ + 'Three elements in the list' => [ + 'userBUIds' => [1, 2, 3], + 'queryResult' => $this->getBusinessUnits('one'), + 'result' => [ + [ + 'name' => 'Main Business Unit 1', + 'id' => 1, + 'owner_id' => null + ], + [ + 'name' => 'Business Unit 1', + 'id' => 2, + 'owner_id' => 1 + ], + [ + 'name' => 'Business Unit 2', + 'id' => 3, + 'owner_id' => 1 + ], + ] + ], + 'Six elements in the list' => [ + 'userBUIds' => [1, 2, 3, 4, 5, 6], + 'queryResult' => $this->getBusinessUnits('two'), + 'result' => [ + [ + 'name' => 'Main Business Unit 1', + 'id' => 1, + 'owner_id' => null + ], + [ + 'name' => 'Main Business Unit 2', + 'id' => 2, + 'owner_id' => null + ], + [ + 'name' => 'Business Unit 1', + 'id' => 3, + 'owner_id' => 1 + ], + [ + 'name' => 'Business Unit 2', + 'id' => 4, + 'owner_id' => 1 + ], + [ + 'name' => 'Business Unit 3', + 'id' => 5, + 'owner_id' => 2 + ], + [ + 'name' => 'Business Unit 4', + 'id' => 6, + 'owner_id' => 2 + ], + [ + 'name' => 'Business Unit 5', + 'id' => 7, + 'owner_id' => 4 + ] + ] + ], + 'empty list' => [ + 'userBUIds' => [], + 'queryResult' => [], + 'result' => [] ], - [ - 'name' => 'Business Uit 1', - 'id' => 2, - 'owner_id' => $this->getTestDataRootBusinessUnit() - ] ]; - - return $this->convertTestDataToBusinessUnitEntity($data); } - protected function convertTestDataToBusinessUnitEntity($data) + /** + * @param string $name + * + * @return array + */ + protected function getBusinessUnits($name) { - $response = []; - foreach ($data as $item) { - $businessUnit = $this->getMockBuilder('Oro\Bundle\OrganizationBundle\Entity\BusinessUnit') + $scheme = [ + 'one' => [ + ['name' => 'Main Business Unit 1', 'owner' => null, 'id' => 1], + ['name' => 'Business Unit 1', 'owner' => 1, 'id' => 2], + ['name' => 'Business Unit 2', 'owner' => 1, 'id' => 3] + ], + 'two' => [ + ['name' => 'Main Business Unit 1', 'owner' => null, 'id' => 1], + ['name' => 'Main Business Unit 2', 'owner' => null, 'id' => 2], + ['name' => 'Business Unit 1', 'owner' => 1, 'id' => 3], + ['name' => 'Business Unit 2', 'owner' => 1, 'id' => 4], + ['name' => 'Business Unit 3', 'owner' => 2, 'id' => 5], + ['name' => 'Business Unit 4', 'owner' => 2, 'id' => 6], + ['name' => 'Business Unit 5', 'owner' => 4, 'id' => 7], + ], + ]; + + $result = []; + $schemeSet = $scheme[$name]; + $schemeSetCount = count($schemeSet); + + for ($i = 0; $i < $schemeSetCount; $i++) { + $element = $this->getMockBuilder('Oro\Bundle\OrganizationBundle\Entity\BusinessUnit') ->disableOriginalConstructor() ->getMock(); - $businessUnit->expects($this->any())->method('getId')->willReturn($item['id']); - $businessUnit->expects($this->any())->method('getOwner')->willReturn($item['owner_id']); - $businessUnit->expects($this->any())->method('getName')->willReturn($item['name']); - $response[] = $businessUnit; - } + $owner = (null === $schemeSet[$i]['owner']) + ? $schemeSet[$i]['owner'] + : $result[$schemeSet[$i]['owner'] - 1]; - return $response; - } + $element->expects(self::any())->method('getOwner')->willReturn($owner); + $element->expects(self::any())->method('getName')->willReturn($schemeSet[$i]['name']); + $element->expects(self::any())->method('getId')->willReturn($schemeSet[$i]['id']); - protected function getTestDataRootBusinessUnit() - { - $rootBusinessUnit = $this->getMockBuilder('Oro\Bundle\OrganizationBundle\Entity\BusinessUnit') - ->disableOriginalConstructor() - ->getMock(); - $rootBusinessUnit->expects($this->any())->method('getId')->willReturn('1'); - $rootBusinessUnit->expects($this->any())->method('getOwner')->willReturn(null); - $rootBusinessUnit->expects($this->any())->method('getName')->willReturn('Main Business Unit'); - - return $rootBusinessUnit; - } + $result[] = $element; + } - protected function getExpectedData() - { - return [ - [ - 'name' => 'Main Business Unit', - 'id' => 1, - 'owner_id' => null - ], - [ - 'name' => 'Business Uit 1', - 'id' => 2, - 'owner_id' => 1 - ] - ]; + return $result; } } diff --git a/src/Oro/Bundle/PlatformBundle/DependencyInjection/Compiler/UpdateDoctrineConfigurationPass.php b/src/Oro/Bundle/PlatformBundle/DependencyInjection/Compiler/UpdateDoctrineConfigurationPass.php new file mode 100644 index 00000000000..49b0cc78ebd --- /dev/null +++ b/src/Oro/Bundle/PlatformBundle/DependencyInjection/Compiler/UpdateDoctrineConfigurationPass.php @@ -0,0 +1,23 @@ +hasParameter('installed') && $container->getParameter('installed'); + if (!$isInstalled) { + $container->setParameter('doctrine.orm.auto_generate_proxy_classes', true); + } + } +} diff --git a/src/Oro/Bundle/PlatformBundle/DependencyInjection/OroPlatformExtension.php b/src/Oro/Bundle/PlatformBundle/DependencyInjection/OroPlatformExtension.php index bfd342a88ef..0cbf23da26c 100644 --- a/src/Oro/Bundle/PlatformBundle/DependencyInjection/OroPlatformExtension.php +++ b/src/Oro/Bundle/PlatformBundle/DependencyInjection/OroPlatformExtension.php @@ -11,8 +11,9 @@ use Oro\Component\Config\Loader\CumulativeConfigLoader; use Oro\Component\Config\Loader\YamlCumulativeFileLoader; use Oro\Component\DependencyInjection\ExtendedContainerBuilder; +use Oro\Component\PhpUtils\ArrayUtil; + use Oro\Bundle\EntityBundle\ORM\DatabaseDriverInterface; -use Oro\Bundle\UIBundle\Tools\ArrayUtils; class OroPlatformExtension extends Extension implements PrependExtensionInterface { @@ -111,7 +112,7 @@ private function mergeConfigIntoOne(ContainerBuilder $container, $name, array $c $originalConfig[] = array(); } - $mergedConfig = ArrayUtils::arrayMergeRecursiveDistinct($originalConfig[0], $config); + $mergedConfig = ArrayUtil::arrayMergeRecursiveDistinct($originalConfig[0], $config); $originalConfig[0] = $mergedConfig; $container->setExtensionConfig($name, $originalConfig); diff --git a/src/Oro/Bundle/PlatformBundle/OroPlatformBundle.php b/src/Oro/Bundle/PlatformBundle/OroPlatformBundle.php index 0746bd61772..790a78676e6 100644 --- a/src/Oro/Bundle/PlatformBundle/OroPlatformBundle.php +++ b/src/Oro/Bundle/PlatformBundle/OroPlatformBundle.php @@ -11,6 +11,7 @@ use Oro\Bundle\PlatformBundle\DependencyInjection\Compiler\LazyServicesCompilerPass; use Oro\Bundle\PlatformBundle\DependencyInjection\Compiler\OptionalListenersCompilerPass; +use Oro\Bundle\PlatformBundle\DependencyInjection\Compiler\UpdateDoctrineConfigurationPass; use Oro\Bundle\PlatformBundle\DependencyInjection\Compiler\UpdateDoctrineEventHandlersPass; class OroPlatformBundle extends Bundle @@ -32,6 +33,7 @@ public function build(ContainerBuilder $container) 'Oro\Bundle\EntityConfigBundle\DependencyInjection\Utils\ServiceLink' ) ); + $container->addCompilerPass(new UpdateDoctrineConfigurationPass()); if ($container instanceof ExtendedContainerBuilder) { $container->addCompilerPass(new UpdateDoctrineEventHandlersPass()); $container->moveCompilerPassBefore( diff --git a/src/Oro/Bundle/QueryDesignerBundle/Grid/Extension/GroupingOrmFilterDatasourceAdapter.php b/src/Oro/Bundle/QueryDesignerBundle/Grid/Extension/GroupingOrmFilterDatasourceAdapter.php index d80687f54f6..23bd47ebd27 100644 --- a/src/Oro/Bundle/QueryDesignerBundle/Grid/Extension/GroupingOrmFilterDatasourceAdapter.php +++ b/src/Oro/Bundle/QueryDesignerBundle/Grid/Extension/GroupingOrmFilterDatasourceAdapter.php @@ -3,9 +3,10 @@ namespace Oro\Bundle\QueryDesignerBundle\Grid\Extension; use Doctrine\ORM\QueryBuilder; -use Doctrine\ORM\Query\Expr; + use Oro\Bundle\FilterBundle\Datasource\Orm\OrmFilterDatasourceAdapter; -use Oro\Bundle\FilterBundle\Filter\FilterUtility; +use Oro\Bundle\QueryDesignerBundle\Model\ExpressionBuilder; +use Oro\Bundle\QueryDesignerBundle\Model\Restriction; /** * Represents ORM data source adapter which allows to combine restrictions in groups, @@ -13,25 +14,8 @@ */ class GroupingOrmFilterDatasourceAdapter extends OrmFilterDatasourceAdapter { - /** - * @var Expr\Composite[] - */ - protected $exprStack; - - /** - * @var string[] - */ - protected $conditionStack; - - /** - * @var Expr\Composite - */ - protected $currentExpr = null; - - /** - * @var string - */ - protected $currentCondition = null; + /** @var ExpressionBuilder */ + protected $expressionBuilder; /** * Constructor @@ -41,7 +25,7 @@ class GroupingOrmFilterDatasourceAdapter extends OrmFilterDatasourceAdapter public function __construct(QueryBuilder $qb) { parent::__construct($qb); - $this->resetState(); + $this->expressionBuilder = new ExpressionBuilder(); } /** @@ -49,48 +33,17 @@ public function __construct(QueryBuilder $qb) */ public function addRestriction($restriction, $condition, $isComputed = false) { - if ($isComputed) { - throw new \LogicException('The HAVING restrictions is not supported yet.'); - } - - if ($this->currentExpr === null) { - // this is the first item in a group - $this->currentExpr = $restriction; - } else { - // other items - if ($condition === FilterUtility::CONDITION_OR) { - if ($this->currentExpr instanceof Expr\Orx) { - $this->currentExpr->add($restriction); - } else { - $this->currentExpr = $this->qb->expr()->orX($this->currentExpr, $restriction); - } - } else { - if ($this->currentExpr instanceof Expr\Andx) { - $this->currentExpr->add($restriction); - } else { - $this->currentExpr = $this->qb->expr()->andX($this->currentExpr, $restriction); - } - } - } + $this->expressionBuilder->addRestriction(new Restriction($restriction, $condition, $isComputed)); } public function beginRestrictionGroup($condition) { - array_push($this->exprStack, $this->currentExpr); - array_push($this->conditionStack, $this->currentCondition); - - $this->currentExpr = null; - $this->currentCondition = $condition; + $this->expressionBuilder->beginGroup($condition); } public function endRestrictionGroup() { - $tmpExpr = $this->currentExpr; - $tmpCondition = $this->currentCondition; - $this->currentExpr = array_pop($this->exprStack); - $this->currentCondition = array_pop($this->conditionStack); - - $this->addRestriction($tmpExpr, $tmpCondition); + $this->expressionBuilder->endGroup(); } /** @@ -98,26 +51,6 @@ public function endRestrictionGroup() */ public function applyRestrictions() { - if ($this->currentExpr === null && empty($this->exprStack)) { - return; - } - - if ($this->currentExpr === null) { - $this->currentExpr = array_pop($this->exprStack); - } - - $this->qb->andWhere($this->currentExpr); - $this->resetState(); - } - - /** - * Resets all 'state' variables of this adapter - */ - protected function resetState() - { - $this->exprStack = []; - $this->conditionStack = []; - $this->currentExpr = null; - $this->currentCondition = null; + $this->expressionBuilder->applyRestrictions($this->qb); } } diff --git a/src/Oro/Bundle/QueryDesignerBundle/Grid/Extension/OrmDatasourceExtension.php b/src/Oro/Bundle/QueryDesignerBundle/Grid/Extension/OrmDatasourceExtension.php index a6b549f60f7..e325deceffb 100644 --- a/src/Oro/Bundle/QueryDesignerBundle/Grid/Extension/OrmDatasourceExtension.php +++ b/src/Oro/Bundle/QueryDesignerBundle/Grid/Extension/OrmDatasourceExtension.php @@ -4,7 +4,6 @@ use Doctrine\ORM\QueryBuilder; -use Oro\Bundle\DataGridBundle\Datagrid\Builder; use Oro\Bundle\DataGridBundle\Datagrid\ParameterBag; use Oro\Bundle\DataGridBundle\Extension\AbstractExtension; use Oro\Bundle\DataGridBundle\Datasource\Orm\OrmDatasource; @@ -38,7 +37,7 @@ public function __construct(RestrictionBuilderInterface $restrictionBuilder) */ public function isApplicable(DatagridConfiguration $config) { - return $config->offsetGetByPath(Builder::DATASOURCE_TYPE_PATH) == OrmDatasource::TYPE + return $config->getDatasourceType() == OrmDatasource::TYPE && $config->offsetGetByPath('[source][query_config][filters]'); } diff --git a/src/Oro/Bundle/QueryDesignerBundle/Model/ExpressionBuilder.php b/src/Oro/Bundle/QueryDesignerBundle/Model/ExpressionBuilder.php new file mode 100644 index 00000000000..a9a3f4c9f95 --- /dev/null +++ b/src/Oro/Bundle/QueryDesignerBundle/Model/ExpressionBuilder.php @@ -0,0 +1,155 @@ +validator = Validation::createValidator(); + } + + /** + * @param string $condition + */ + public function beginGroup($condition) + { + $groupNode = new GroupNode($condition); + if ($this->currentGroupNode) { + $this->currentGroupNode->addNode($groupNode); + $this->currentGroupNode = $groupNode; + } elseif ($this->groupNode) { + $this->currentGroupNode = $this->groupNode = $groupNode->addNode($this->groupNode); + } else { + $this->currentGroupNode = $this->groupNode = $groupNode; + } + } + + /** + * @param Restriction $restriction + */ + public function addRestriction(Restriction $restriction) + { + if (!$this->groupNode) { + $this->groupNode = new GroupNode(FilterUtility::CONDITION_AND); + $this->currentGroupNode = $this->groupNode; + } + + $this->currentGroupNode->addNode($restriction); + } + + public function endGroup() + { + $this->currentGroupNode = $this->currentGroupNode->getParent(); + } + + /** + * @param QueryBuilder $qb + */ + public function applyRestrictions(QueryBuilder $qb) + { + if (!$this->groupNode) { + return; + } + + $violationList = $this->validator->validate($this->groupNode, new GroupNodeConstraint()); + foreach ($violationList as $violation) { + throw new LogicException($violation->getMessage()); + } + + list($uncomputedExpr, $computedExpr) = $this->resolveGroupNode($this->groupNode); + if ($computedExpr) { + $qb->andHaving($computedExpr); + } + if ($uncomputedExpr) { + $qb->andWhere($uncomputedExpr); + } + } + + /** + * @param GroupNode $gNode + * + * @return mixed Expr[] Where first item is uncomputed expr and 2nd one is computed + */ + protected function resolveGroupNode(GroupNode $gNode) + { + $uncomputedRestrictions = []; + $computedRestrictions = []; + + foreach ($gNode->getChildren() as $node) { + if ($node instanceof Restriction) { + if ($node->isComputed()) { + $computedRestrictions[] = $node; + } else { + $uncomputedRestrictions[] = $node; + } + } else { + list($uncomputedExpr, $computedExpr) = $this->resolveGroupNode($node); + if ($uncomputedExpr) { + $uncomputedRestrictions[] = new Restriction($uncomputedExpr, $node->getCondition(), false); + } + if ($computedExpr) { + $computedRestrictions[] = new Restriction($computedExpr, $node->getCondition(), true); + } + } + } + + return [ + $this->createExprFromRestrictions($uncomputedRestrictions), + $this->createExprFromRestrictions($computedRestrictions), + ]; + } + + /** + * @param Restriction[] $restrictions + * + * @return mixed Expr + */ + protected function createExprFromRestrictions(array $restrictions) + { + return array_reduce( + $restrictions, + function ($expr = null, Restriction $restriction) { + if ($expr === null) { + return $restriction->getRestriction(); + } + + if ($restriction->getCondition() === FilterUtility::CONDITION_OR) { + if ($expr instanceof Expr\Orx) { + $expr->add($restriction->getRestriction()); + } else { + $expr = new Expr\Orx([$expr, $restriction->getRestriction()]); + } + } else { + if ($expr instanceof Expr\Andx) { + $expr->add($restriction->getRestriction()); + } else { + $expr = new Expr\Andx([$expr, $restriction->getRestriction()]); + } + } + + return $expr; + } + ); + } +} diff --git a/src/Oro/Bundle/QueryDesignerBundle/Model/GroupNode.php b/src/Oro/Bundle/QueryDesignerBundle/Model/GroupNode.php new file mode 100644 index 00000000000..42d6a220ce6 --- /dev/null +++ b/src/Oro/Bundle/QueryDesignerBundle/Model/GroupNode.php @@ -0,0 +1,130 @@ +condition = $condition; + } + + /** + * @param GroupNode|Restriction $node + * + * @return $this + */ + public function addNode($node) + { + $this->nodes[] = $node; + if ($node instanceof GroupNode) { + $node->setParent($this); + } + + return $this; + } + + /** + * @param GroupNode $node + * + * @return $this + */ + public function setParent(GroupNode $node) + { + $this->parentNode = $node; + + return $this; + } + + /** + * @return GroupNode|null + */ + public function getParent() + { + return $this->parentNode; + } + + /** + * @param GroupNode[]|Restriction[] $node + */ + public function getChildren() + { + return $this->nodes; + } + + /** + * @return string + */ + public function getCondition() + { + return $this->condition; + } + + /** + * @return string + */ + public function getType() + { + $mixed = $this->typedNodesExists(GroupNode::TYPE_MIXED); + + if ($mixed) { + return GroupNode::TYPE_MIXED; + } + + $computed = $this->typedNodesExists(GroupNode::TYPE_COMPUTED); + $unComputed = $this->typedNodesExists(GroupNode::TYPE_UNCOMPUTED); + + if ($computed && $unComputed) { + return static::TYPE_MIXED; + } + + return $computed ? static::TYPE_COMPUTED : static::TYPE_UNCOMPUTED; + } + + /** + * @param $type + * + * @return bool + */ + protected function typedNodesExists($type) + { + return ArrayUtil::some( + function ($node) use ($type) { + $exists = false; + + if ($node instanceof GroupNode) { + $exists = $node->getType() === $type; + } else { + switch ($type) { + case GroupNode::TYPE_COMPUTED: + $exists = $node->isComputed(); + break; + case GroupNode::TYPE_UNCOMPUTED: + $exists = !$node->isComputed(); + } + } + + return $exists; + }, + $this->nodes + ); + } +} diff --git a/src/Oro/Bundle/QueryDesignerBundle/Model/Restriction.php b/src/Oro/Bundle/QueryDesignerBundle/Model/Restriction.php new file mode 100644 index 00000000000..72161890ef0 --- /dev/null +++ b/src/Oro/Bundle/QueryDesignerBundle/Model/Restriction.php @@ -0,0 +1,51 @@ +restriction = $restriction; + $this->condition = $condition; + $this->computed = $computed; + } + + /** + * @return mixed Expr + */ + public function getRestriction() + { + return $this->restriction; + } + + /** + * @return string + */ + public function getCondition() + { + return $this->condition; + } + + /** + * @return bool + */ + public function isComputed() + { + return $this->computed; + } +} diff --git a/src/Oro/Bundle/QueryDesignerBundle/QueryDesigner/AbstractQueryConverter.php b/src/Oro/Bundle/QueryDesignerBundle/QueryDesigner/AbstractQueryConverter.php index 5b879885abf..6a9e075c421 100644 --- a/src/Oro/Bundle/QueryDesignerBundle/QueryDesigner/AbstractQueryConverter.php +++ b/src/Oro/Bundle/QueryDesignerBundle/QueryDesigner/AbstractQueryConverter.php @@ -440,6 +440,32 @@ protected function prepareColumnAliases() } } + /** + * @param array $column + * + * @return array Where array has elements: string|FunctionInterface|null, string|null + */ + protected function createColumnFunction(array $column) + { + if (!empty($column['func'])) { + $function = $this->functionProvider->getFunction( + $column['func']['name'], + $column['func']['group_name'], + $column['func']['group_type'] + ); + $functionExpr = $function['expr']; + if (isset($function['return_type'])) { + $functionReturnType = $function['return_type']; + } else { + $functionReturnType = null; + } + + return [$functionExpr, $functionReturnType]; + } + + return [null, null]; + } + /** * Performs conversion of SELECT statement */ @@ -448,21 +474,7 @@ protected function addSelectStatement() foreach ($this->definition['columns'] as $column) { $columnName = $column['name']; $fieldName = $this->getFieldName($columnName); - $functionExpr = null; - $functionReturnType = null; - if (!empty($column['func'])) { - $function = $this->functionProvider->getFunction( - $column['func']['name'], - $column['func']['group_name'], - $column['func']['group_type'] - ); - $functionExpr = $function['expr']; - if (isset($function['return_type'])) { - $functionReturnType = $function['return_type']; - } else { - $functionReturnType = null; - } - } + list($functionExpr, $functionReturnType) = $this->createColumnFunction($column); $isDistinct = !empty($column['distinct']); $tableAlias = $this->getTableAliasForColumn($columnName); if (isset($column['label'])) { @@ -651,6 +663,12 @@ protected function processFilter($filter) $fieldName = $this->getFieldName($columnName); $columnAliasKey = $this->buildColumnAliasKey($columnName); $tableAlias = $this->getTableAliasForColumn($columnName); + $column = ['name' => $fieldName]; + if (isset($filter['func'])) { + $column['func'] = $filter['func']; + } + list($functionExpr) = $this->createColumnFunction($column); + $this->addWhereCondition( $this->getEntityClassName($columnName), $tableAlias, @@ -658,7 +676,8 @@ protected function processFilter($filter) $this->buildColumnExpression($columnName, $tableAlias, $fieldName), $this->getColumnAlias($columnAliasKey), $filter['criterion']['filter'], - $filter['criterion']['data'] + $filter['criterion']['data'], + $functionExpr ); } diff --git a/src/Oro/Bundle/QueryDesignerBundle/QueryDesigner/GroupingOrmQueryConverter.php b/src/Oro/Bundle/QueryDesignerBundle/QueryDesigner/GroupingOrmQueryConverter.php index 9f7a948e2d7..344b8c20f20 100644 --- a/src/Oro/Bundle/QueryDesignerBundle/QueryDesigner/GroupingOrmQueryConverter.php +++ b/src/Oro/Bundle/QueryDesignerBundle/QueryDesigner/GroupingOrmQueryConverter.php @@ -91,10 +91,24 @@ protected function addWhereCondition( $columnExpr, $columnAlias, $filterName, - array $filterData + array $filterData, + $functionExpr = null ) { $filter = [ - 'column' => $this->getFilterByExpr($entityClassName, $tableAlias, $fieldName, $columnExpr), + 'column' => $this->getFilterByExpr( + $entityClassName, + $tableAlias, + $fieldName, + $functionExpr + ? $this->prepareFunctionExpression( + $functionExpr, + $tableAlias, + $fieldName, + $columnExpr, + $columnAlias + ) + : $columnExpr + ), 'filter' => $filterName, 'filterData' => $filterData ]; diff --git a/src/Oro/Bundle/QueryDesignerBundle/QueryDesigner/Manager.php b/src/Oro/Bundle/QueryDesignerBundle/QueryDesigner/Manager.php index 0d7a5938564..5f9dba7714d 100644 --- a/src/Oro/Bundle/QueryDesignerBundle/QueryDesigner/Manager.php +++ b/src/Oro/Bundle/QueryDesignerBundle/QueryDesigner/Manager.php @@ -217,11 +217,16 @@ protected function getMetadataForFunctions($groupType, $queryType) $hintText = empty($function['hint_label']) ? null // if a label is empty it means that this function should inherit a label : $this->translator->trans($function['hint_label']); - $functions[] = [ + $func = [ 'name' => $function['name'], 'label' => $nameText, 'title' => $hintText, ]; + if (isset($function['return_type'])) { + $func['return_type'] = $function['return_type']; + } + + $functions[] = $func; } $attr['functions'] = $functions; $result[$name] = $attr; diff --git a/src/Oro/Bundle/QueryDesignerBundle/Resources/config/requirejs.yml b/src/Oro/Bundle/QueryDesignerBundle/Resources/config/requirejs.yml index a9517905e07..9c997dc7b5c 100644 --- a/src/Oro/Bundle/QueryDesignerBundle/Resources/config/requirejs.yml +++ b/src/Oro/Bundle/QueryDesignerBundle/Resources/config/requirejs.yml @@ -2,6 +2,7 @@ config: paths: 'oroquerydesigner/js/condition-builder': 'bundles/oroquerydesigner/js/condition-builder.js' 'oroquerydesigner/js/field-condition': 'bundles/oroquerydesigner/js/field-condition.js' + 'oroquerydesigner/js/aggregated-field-condition': 'bundles/oroquerydesigner/js/aggregated-field-condition.js' 'oroquerydesigner/js/function-choice': 'bundles/oroquerydesigner/js/function-choice.js' 'oroquerydesigner/js/items-manager/grouping-model': 'bundles/oroquerydesigner/js/items-manager/grouping-model.js' 'oroquerydesigner/js/items-manager/column-model': 'bundles/oroquerydesigner/js/items-manager/column-model.js' diff --git a/src/Oro/Bundle/QueryDesignerBundle/Resources/public/css/less/condition-builder.less b/src/Oro/Bundle/QueryDesignerBundle/Resources/public/css/less/condition-builder.less index da8c1fc785b..09372a86b1d 100644 --- a/src/Oro/Bundle/QueryDesignerBundle/Resources/public/css/less/condition-builder.less +++ b/src/Oro/Bundle/QueryDesignerBundle/Resources/public/css/less/condition-builder.less @@ -26,10 +26,10 @@ display: none; } .conditions-group { + width: 100%; list-style: none; margin: 0; padding: 0; - min-height: 199px; box-sizing: border-box; &:before, &:after { content: ""; @@ -249,18 +249,23 @@ border: 1px solid #c9c9c9; min-width: 582px;/* @TODO temporary solution, will be fixed in CRM-2025 */ background-color: #eff2f5; + + & > div { + display: flex; + } .left-area { width: 150px; - float: left; } .right-area { + display: flex; width: calc(~"100% - 150px"); - float: left; padding: 0 10px; border-left: 1px solid #c9c9c9; background-color: white; > div { position: relative; + display: flex; + width: 100%; } } } diff --git a/src/Oro/Bundle/QueryDesignerBundle/Resources/public/js/aggregated-field-condition.js b/src/Oro/Bundle/QueryDesignerBundle/Resources/public/js/aggregated-field-condition.js new file mode 100644 index 00000000000..207956267cd --- /dev/null +++ b/src/Oro/Bundle/QueryDesignerBundle/Resources/public/js/aggregated-field-condition.js @@ -0,0 +1,169 @@ +define([ + 'jquery', + 'underscore', + 'orotranslation/js/translator', + 'oroquerydesigner/js/field-condition' +], function($, _, __) { + 'use strict'; + + $.widget('oroauditquerydesigner.aggregatedFieldCondition', $.oroquerydesigner.fieldCondition, { + options: { + columnsCollection: null + }, + + _create: function() { + var data = this.element.data('value'); + + this.$fieldChoice = $('').addClass(this.options.fieldChoiceClass); + this.$filterContainer = $('').addClass(this.options.filterContainerClass); + this.element.append(this.$fieldChoice, this.$filterContainer); + + this.$fieldChoice.fieldChoice(this.options.fieldChoice); + + this._updateFieldChoice(); + this.options.columnsCollection.on('remove', function(model) { + if (model.get('label') === this._getColumnLabel()) { + this.element.closest('.condition').find('.close').click(); + } + }, this); + this.options.columnsCollection.on('change:func change:label', function(model) { + if (model._previousAttributes.label === this._getColumnLabel()) { + this.element.closest('.condition').find('.close').click(); + } + }, this); + if (data && data.columnName && data.func) { + var column = this._getColumnByNameAndFunc(data.columnName, data.func); + if (column) { + this.$fieldChoice.fieldChoice('setData', {id: column.get('name'), text: column.get('label')}); + this._renderFilter(column.get('name')); + } + } + + this._on(this.$fieldChoice, { + changed: function(e, fieldId) { + $(':focus').blur(); + // reset current value on field change + this.element.data('value', {}); + this._renderFilter(fieldId); + e.stopPropagation(); + } + }); + + this._on(this.$filterContainer, { + change: function() { + if (this.filter) { + this.filter.applyValue(); + } + } + }); + }, + + _updateFieldChoice: function() { + var fieldChoice = this.$fieldChoice.fieldChoice().data('oroentity-fieldChoice'); + fieldChoice._select2Data = _.bind(this._getAggregatedSelectData, this); + + fieldChoice.setData = function(data) { + this.element.select2('data', data, true); + }; + + var self = this; + + fieldChoice.formatChoice = _.wrap(fieldChoice.formatChoice, function(original) { + var formatted = original.apply(this, _.rest(arguments)); + var func = self._getCurrentFunc(); + if (func && func.name) { + formatted += ' (' + func.name + ')'; + } + + return formatted; + }); + + fieldChoice.getApplicableConditions = _.wrap( + fieldChoice.getApplicableConditions, + function(original) { + var conditions = original.apply(this, _.rest(arguments)); + var func = self._getCurrentFunc(); + if (func && func.return_type) { + conditions.type = func.return_type; + } + + return conditions; + } + ); + }, + + _getAggregatedSelectData: function() { + return _.map( + this._getAggregatedColumns(), + function(model) { + return { + id: model.get('name'), + text: model.get('label') + }; + } + ); + }, + + _getAggregatedColumns: function() { + return _.filter( + this.options.columnsCollection.models, + _.compose(_.negate(_.isEmpty), _.property('func'), _.property('attributes')) + ); + }, + + _onUpdate: function() { + var value; + var columnName = this._getColumnName(); + var columnFunc = this._getCurrentFunc(); + + if (this.filter && !this.filter.isEmptyValue() && !_.isEmpty(columnFunc)) { + value = { + columnName: columnName, + criterion: this._getFilterCriterion(), + func: columnFunc + }; + } else { + value = {}; + } + + this.element.data('value', value); + this.element.trigger('changed'); + }, + + _getColumnName: function() { + return this.element.find('input.select').select2('val'); + }, + + _getColumnLabel: function() { + var obj = this.element.find('input.select').select2('data'); + + return obj ? obj.text : undefined; + }, + + _getCurrentFunc: function() { + var column = this.options.columnsCollection.findWhere({label: this._getColumnLabel()}); + if (_.isEmpty(column)) { + return; + } + + return column.get('func'); + }, + + _getColumnByNameAndFunc: function(name, func) { + if (!func) { + return; + } + + return _.find(this.options.columnsCollection.where({name: name}), function(column) { + return column.get('func') && column.get('func').name === func.name; + }); + }, + + _getFilterCriterion: function() { + var criterion = this._superApply(arguments); + $.extend(true, criterion, {'data': {'params': {'filter_by_having': true}}}); + + return criterion; + } + }); +}); diff --git a/src/Oro/Bundle/QueryDesignerBundle/Resources/public/js/condition-builder.js b/src/Oro/Bundle/QueryDesignerBundle/Resources/public/js/condition-builder.js index 64e4426f2d9..d41793c134d 100644 --- a/src/Oro/Bundle/QueryDesignerBundle/Resources/public/js/condition-builder.js +++ b/src/Oro/Bundle/QueryDesignerBundle/Resources/public/js/condition-builder.js @@ -82,6 +82,7 @@ define(['jquery', 'underscore', 'orotranslation/js/translator', 'jquery-ui', opts.criteriaList = $.extend({}, opts.sortable, opts.criteriaList); opts.criteriaList.start = $.proxy(this._onCriteriaGrab, this); opts.criteriaList.stop = $.proxy(this._onCriteriaDrop, this); + opts.criteriaList.change = $.proxy(this._onCriteriaChange, this); opts.conditionsGroup.start = $.proxy(this._onConditionsGroupGrab, this); opts.conditionsGroup.stop = $.proxy(this._onConditionsGroupDrop, this); @@ -229,12 +230,46 @@ define(['jquery', 'underscore', 'orotranslation/js/translator', 'jquery-ui', this.$rootCondition.find('.hide-operator').removeClass('hide-operator'); }, - syncDropAreaOver: function() { + _onCriteriaChange: function(e, ui) { + if (this._isPlaceholderInValidPosition(e, ui)) { + this.element.find('.sortable-placeholder').removeClass('hide'); + } else { + this.element.find('.sortable-placeholder').addClass('hide'); + } + }, + + syncDropAreaOver: function(e, ui) { this.$rootCondition .parent() .toggleClass('drop-area-over', this.$rootCondition.find('.sortable-placeholder').length !== 0); }, + _isPlaceholderInValidPosition: function(e, ui) { + if (ui.item.data('criteria') === 'aggregated-condition-item') { + if ( + ui.placeholder.closest('[condition-type=aggregated-condition-item]').length || + (ui.placeholder.is(':last-child') && + !this.element.find('[condition-type=aggregated-condition-item]').length) + ) { + return true; + } + + return false; + } else if (ui.item.data('criteria') !== 'conditions-group' && + ui.placeholder.closest('[condition-type=aggregated-condition-item]').length + ) { + return false; + } + + return !ui.placeholder.prev('[condition-type=aggregated-condition-item]').length; + }, + + _updateRootAggregatedCondition: function($condition) { + $condition + .attr('condition-type', 'aggregated-condition-item') + .find('>.operator [data-value=OR]').parent('li').remove(); + }, + _getCriteriaOrigin: function(criteria) { var $criteria = this.$criteriaList.find('[data-criteria="' + criteria + '"]'); return $criteria.data('origin') || $criteria; @@ -329,8 +364,22 @@ define(['jquery', 'underscore', 'orotranslation/js/translator', 'jquery-ui', var $condition; // new condition if (ui.sender && ui.sender.is(this.$criteriaList)) { - $condition = this._createCondition(ui.item.data('criteria')); - $condition.insertBefore(ui.item); + if (ui.placeholder && ui.placeholder.hasClass('hide')) { + return; + } + + var criteria = ui.item.data('criteria'); + if (criteria === 'aggregated-condition-item' && + !this.element.find('[condition-type=aggregated-condition-item]').length + ) { + var $conditionsGroup = this._createCondition('conditions-group'); + $conditionsGroup.insertBefore(ui.item); + $condition = this._createCondition(ui.item.data('criteria')); + $conditionsGroup.find('.conditions-group').append($condition); + } else { + $condition = this._createCondition(criteria); + $condition.insertBefore(ui.item); + } } else { $condition = ui.item; } @@ -353,7 +402,7 @@ define(['jquery', 'underscore', 'orotranslation/js/translator', 'jquery-ui', // add operators to proper conditions $conditions.filter(':not(:first-child)').not(':has(>.operator)') .each(function() { - self._initConditionOperation(this); + self._initConditionOperation($(this)); }).trigger('changed'); }, @@ -365,7 +414,8 @@ define(['jquery', 'underscore', 'orotranslation/js/translator', 'jquery-ui', .prependTo($condition) .dropdownSelect({ buttonClass: 'btn btn-mini', - options: this.options.operations, + options: $condition.is('[condition-type=aggregated-condition-item]') ? + [operation] : this.options.operations, selected: operation }); }, @@ -387,6 +437,9 @@ define(['jquery', 'underscore', 'orotranslation/js/translator', 'jquery-ui', _onChanged: function() { this._setSourceValue(this.$rootCondition.data('value')); + this._updateRootAggregatedCondition( + this.$rootCondition.find('>[data-criteria]').has('[data-criteria=aggregated-condition-item]') + ); }, _setSourceValue: function(value) { diff --git a/src/Oro/Bundle/QueryDesignerBundle/Resources/public/js/function-choice.js b/src/Oro/Bundle/QueryDesignerBundle/Resources/public/js/function-choice.js index 60f30e496b9..477c60c8137 100644 --- a/src/Oro/Bundle/QueryDesignerBundle/Resources/public/js/function-choice.js +++ b/src/Oro/Bundle/QueryDesignerBundle/Resources/public/js/function-choice.js @@ -7,9 +7,10 @@ define(['jquery', 'underscore', 'jquery-ui'], function($, _) { $.widget('oroquerydesigner.functionChoice', { options: { fieldChoiceSelector: '', - optionTemplate: _.template(''), converters: [], aggregates: [] @@ -90,7 +91,7 @@ define(['jquery', 'underscore', 'jquery-ui'], function($, _) { }); _.each(functions, function(func) { - content += options.optionTemplate(func); + content += options.optionTemplate({data: func}); }); if (content !== '') { diff --git a/src/Oro/Bundle/QueryDesignerBundle/Resources/translations/messages.en.yml b/src/Oro/Bundle/QueryDesignerBundle/Resources/translations/messages.en.yml index 1197fd0a151..37c3fc4ced7 100644 --- a/src/Oro/Bundle/QueryDesignerBundle/Resources/translations/messages.en.yml +++ b/src/Oro/Bundle/QueryDesignerBundle/Resources/translations/messages.en.yml @@ -34,6 +34,7 @@ oro: criteria: drag_hint: drag to select field_condition: Field Condition + aggregated_field_condition: Aggregation column conditions_group: Conditions Group choose_entity_field: Choose a field... validation: diff --git a/src/Oro/Bundle/QueryDesignerBundle/Tests/Unit/Grid/Extension/GroupingOrmFilterDatasourceAdapterTest.php b/src/Oro/Bundle/QueryDesignerBundle/Tests/Unit/Grid/Extension/GroupingOrmFilterDatasourceAdapterTest.php index dd510acb10d..26c58c75af7 100644 --- a/src/Oro/Bundle/QueryDesignerBundle/Tests/Unit/Grid/Extension/GroupingOrmFilterDatasourceAdapterTest.php +++ b/src/Oro/Bundle/QueryDesignerBundle/Tests/Unit/Grid/Extension/GroupingOrmFilterDatasourceAdapterTest.php @@ -5,7 +5,6 @@ use Doctrine\ORM\QueryBuilder; use Oro\Bundle\TestFrameworkBundle\Test\Doctrine\ORM\OrmTestCase; - use Oro\Bundle\FilterBundle\Filter\FilterUtility; use Oro\Bundle\QueryDesignerBundle\Grid\Extension\GroupingOrmFilterDatasourceAdapter; @@ -45,6 +44,25 @@ public function testOneRestriction() ); } + public function testOneComputedRestriction() + { + $qb = new QueryBuilder($this->getTestEntityManager()); + $qb->select(['u.status, COUNT(u.id)']) + ->from('Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser', 'u') + ->groupBy('u.status'); + $ds = new GroupingOrmFilterDatasourceAdapter($qb); + + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '1'), FilterUtility::CONDITION_AND, true); + $ds->applyRestrictions(); + + $this->assertEquals( + 'SELECT u.status, COUNT(u.id) FROM Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser u ' + . 'GROUP BY u.status ' + . 'HAVING COUNT(u.id) = 1', + $qb->getDQL() + ); + } + public function testSeveralRestrictions() { $qb = new QueryBuilder($this->getTestEntityManager()); @@ -66,6 +84,27 @@ public function testSeveralRestrictions() ); } + public function testSeveralComputedRestrictions() + { + $qb = new QueryBuilder($this->getTestEntityManager()); + $qb->select(['u.status, COUNT(u.id)']) + ->from('Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser', 'u') + ->groupBy('u.status'); + $ds = new GroupingOrmFilterDatasourceAdapter($qb); + + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '1'), FilterUtility::CONDITION_AND, true); + $ds->addRestriction($qb->expr()->eq('MIN(u.id)', '2'), FilterUtility::CONDITION_OR, true); + $ds->addRestriction($qb->expr()->eq('MAX(u.id)', '3'), FilterUtility::CONDITION_AND, true); + $ds->applyRestrictions(); + + $this->assertEquals( + 'SELECT u.status, COUNT(u.id) FROM Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser u ' + . 'GROUP BY u.status ' + . 'HAVING (COUNT(u.id) = 1 OR MIN(u.id) = 2) AND MAX(u.id) = 3', + $qb->getDQL() + ); + } + public function testEmptyGroup() { $qb = new QueryBuilder($this->getTestEntityManager()); @@ -105,6 +144,27 @@ public function testOneRestrictionInGroup() ); } + public function testOneComputedRestrictionInGroup() + { + $qb = new QueryBuilder($this->getTestEntityManager()); + $qb->select(['u.status, COUNT(u.id)']) + ->from('Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser', 'u') + ->groupBy('u.status'); + $ds = new GroupingOrmFilterDatasourceAdapter($qb); + + $ds->beginRestrictionGroup(FilterUtility::CONDITION_AND); + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '1'), FilterUtility::CONDITION_AND, true); + $ds->endRestrictionGroup(); + $ds->applyRestrictions(); + + $this->assertEquals( + 'SELECT u.status, COUNT(u.id) FROM Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser u ' + . 'GROUP BY u.status ' + . 'HAVING COUNT(u.id) = 1', + $qb->getDQL() + ); + } + public function testSeveralRestrictionsInGroup() { $qb = new QueryBuilder($this->getTestEntityManager()); @@ -128,6 +188,29 @@ public function testSeveralRestrictionsInGroup() ); } + public function testSeveralComputedRestrictionsInGroup() + { + $qb = new QueryBuilder($this->getTestEntityManager()); + $qb->select(['u.status, COUNT(u.id)']) + ->from('Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser', 'u') + ->groupBy('u.status'); + $ds = new GroupingOrmFilterDatasourceAdapter($qb); + + $ds->beginRestrictionGroup(FilterUtility::CONDITION_AND); + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '1'), FilterUtility::CONDITION_AND, true); + $ds->addRestriction($qb->expr()->eq('MIN(u.id)', '2'), FilterUtility::CONDITION_OR, true); + $ds->addRestriction($qb->expr()->eq('MAX(u.id)', '3'), FilterUtility::CONDITION_AND, true); + $ds->endRestrictionGroup(); + $ds->applyRestrictions(); + + $this->assertEquals( + 'SELECT u.status, COUNT(u.id) FROM Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser u ' + . 'GROUP BY u.status ' + . 'HAVING (COUNT(u.id) = 1 OR MIN(u.id) = 2) AND MAX(u.id) = 3', + $qb->getDQL() + ); + } + public function testNestedGroupsWithOneRestrictionInNestedGroup() { $qb = new QueryBuilder($this->getTestEntityManager()); @@ -154,6 +237,33 @@ public function testNestedGroupsWithOneRestrictionInNestedGroup() ); } + public function testNestedGroupsWithOneComputedRestrictionInNestedGroup() + { + $qb = new QueryBuilder($this->getTestEntityManager()); + $qb->select(['u.status, COUNT(u.id)']) + ->from('Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser', 'u') + ->groupBy('u.status') + ->andHaving('MAX(u.id) > 0'); + $ds = new GroupingOrmFilterDatasourceAdapter($qb); + + // src: (1 OR (2)) + // dest: (1 OR 2) + $ds->beginRestrictionGroup(FilterUtility::CONDITION_AND); + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '1'), FilterUtility::CONDITION_AND, true); + $ds->beginRestrictionGroup(FilterUtility::CONDITION_OR); + $ds->addRestriction($qb->expr()->eq('MIN(u.id)', '2'), FilterUtility::CONDITION_AND, true); + $ds->endRestrictionGroup(); + $ds->endRestrictionGroup(); + $ds->applyRestrictions(); + + $this->assertEquals( + 'SELECT u.status, COUNT(u.id) FROM Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser u ' + . 'GROUP BY u.status ' + . 'HAVING MAX(u.id) > 0 AND (COUNT(u.id) = 1 OR MIN(u.id) = 2)', + $qb->getDQL() + ); + } + public function testNestedGroupsWithSameCondition() { $qb = new QueryBuilder($this->getTestEntityManager()); @@ -181,6 +291,33 @@ public function testNestedGroupsWithSameCondition() ); } + public function testNestedComputedGroupsWithSameCondition() + { + $qb = new QueryBuilder($this->getTestEntityManager()); + $qb->select(['u.status, COUNT(u.id)']) + ->from('Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser', 'u') + ->groupBy('u.status'); + $ds = new GroupingOrmFilterDatasourceAdapter($qb); + + // src: (1 OR (2 OR 3)) + // dest: (1 OR (2 OR 3)) + $ds->beginRestrictionGroup(FilterUtility::CONDITION_AND); + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '1'), FilterUtility::CONDITION_AND, true); + $ds->beginRestrictionGroup(FilterUtility::CONDITION_OR); + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '2'), FilterUtility::CONDITION_AND, true); + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '3'), FilterUtility::CONDITION_OR, true); + $ds->endRestrictionGroup(); + $ds->endRestrictionGroup(); + $ds->applyRestrictions(); + + $this->assertEquals( + 'SELECT u.status, COUNT(u.id) FROM Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser u ' + . 'GROUP BY u.status ' + . 'HAVING COUNT(u.id) = 1 OR (COUNT(u.id) = 2 OR COUNT(u.id) = 3)', + $qb->getDQL() + ); + } + public function testNestedGroupsWithDifferentConditions() { $qb = new QueryBuilder($this->getTestEntityManager()); @@ -208,6 +345,33 @@ public function testNestedGroupsWithDifferentConditions() ); } + public function testNestedComputedGroupsWithDifferentConditions() + { + $qb = new QueryBuilder($this->getTestEntityManager()); + $qb->select(['u.status, COUNT(u.id)']) + ->from('Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser', 'u') + ->groupBy('u.status'); + $ds = new GroupingOrmFilterDatasourceAdapter($qb); + + // src: (1 OR (2 AND 3)) + // dest: (1 OR (2 AND 3)) + $ds->beginRestrictionGroup(FilterUtility::CONDITION_AND); + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '1'), FilterUtility::CONDITION_AND, true); + $ds->beginRestrictionGroup(FilterUtility::CONDITION_OR); + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '2'), FilterUtility::CONDITION_AND, true); + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '3'), FilterUtility::CONDITION_AND, true); + $ds->endRestrictionGroup(); + $ds->endRestrictionGroup(); + $ds->applyRestrictions(); + + $this->assertEquals( + 'SELECT u.status, COUNT(u.id) FROM Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser u ' + . 'GROUP BY u.status ' + . 'HAVING COUNT(u.id) = 1 OR (COUNT(u.id) = 2 AND COUNT(u.id) = 3)', + $qb->getDQL() + ); + } + public function testComplexExpr() { $qb = new QueryBuilder($this->getTestEntityManager()); @@ -247,4 +411,96 @@ public function testComplexExpr() $qb->getDQL() ); } + + public function testComplexComputedExpr() + { + $qb = new QueryBuilder($this->getTestEntityManager()); + $qb->select(['u.status, COUNT(u.id)']) + ->from('Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser', 'u') + ->groupBy('u.status') + ->having('COUNT(u.id) = 0'); + $ds = new GroupingOrmFilterDatasourceAdapter($qb); + + // src: (1 AND ((2 AND (3 OR 4)) OR (5) OR (6 AND 7)) AND 8) + // dest: (1 AND ((2 AND (3 OR 4)) OR 5 OR (6 AND 7)) AND 8) + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '1'), FilterUtility::CONDITION_AND, true); + $ds->beginRestrictionGroup(FilterUtility::CONDITION_AND); + $ds->beginRestrictionGroup(FilterUtility::CONDITION_AND); + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '2'), FilterUtility::CONDITION_AND, true); + $ds->beginRestrictionGroup(FilterUtility::CONDITION_AND); + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '3'), FilterUtility::CONDITION_AND, true); + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '4'), FilterUtility::CONDITION_OR, true); + $ds->endRestrictionGroup(); + $ds->endRestrictionGroup(); + $ds->beginRestrictionGroup(FilterUtility::CONDITION_OR); + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '5'), FilterUtility::CONDITION_AND, true); + $ds->endRestrictionGroup(); + $ds->beginRestrictionGroup(FilterUtility::CONDITION_OR); + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '6'), FilterUtility::CONDITION_AND, true); + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '7'), FilterUtility::CONDITION_AND, true); + $ds->endRestrictionGroup(); + $ds->endRestrictionGroup(); + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '8'), FilterUtility::CONDITION_AND, true); + $ds->applyRestrictions(); + + $this->assertEquals( + 'SELECT u.status, COUNT(u.id) FROM Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser u ' + . 'GROUP BY u.status ' + . 'HAVING COUNT(u.id) = 0 AND ' + . '(COUNT(u.id) = 1 AND ' + . '((COUNT(u.id) = 2 AND (COUNT(u.id) = 3 OR COUNT(u.id) = 4)) ' + . 'OR COUNT(u.id) = 5 OR (COUNT(u.id) = 6 AND COUNT(u.id) = 7)) AND ' + . 'COUNT(u.id) = 8)', + $qb->getDQL() + ); + } + + public function testComputedWithUnComputedRestrictionsTogether() + { + $qb = new QueryBuilder($this->getTestEntityManager()); + $qb->select(['u.status, COUNT(u.id)']) + ->from('Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser', 'u') + ->groupBy('u.status'); + $ds = new GroupingOrmFilterDatasourceAdapter($qb); + + $ds->addRestriction($qb->expr()->eq('u.id', '1'), FilterUtility::CONDITION_AND); + $ds->addRestriction($qb->expr()->eq('u.id', '2'), FilterUtility::CONDITION_AND); + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '3'), FilterUtility::CONDITION_AND, true); + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '4'), FilterUtility::CONDITION_OR, true); + $ds->applyRestrictions(); + + $this->assertEquals( + 'SELECT u.status, COUNT(u.id) FROM Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser u ' + . 'WHERE u.id = 1 AND u.id = 2 ' + . 'GROUP BY u.status ' + . 'HAVING COUNT(u.id) = 3 OR COUNT(u.id) = 4', + $qb->getDQL() + ); + } + + /** + * @expectedException LogicException + * @expectedExceptionMessage Computed conditions cannot be mixed with uncomputed. + */ + public function testComputedWithUnComputedRestrictionsTogetherShouldReturnExceptionWhenRestrictionsAreMixed() + { + $qb = new QueryBuilder($this->getTestEntityManager()); + $qb->select(['u.status, COUNT(u.id)']) + ->from('Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser', 'u') + ->groupBy('u.status'); + $ds = new GroupingOrmFilterDatasourceAdapter($qb); + + $ds->addRestriction($qb->expr()->eq('u.id', '1'), FilterUtility::CONDITION_AND); + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '2'), FilterUtility::CONDITION_AND, true); + $ds->addRestriction($qb->expr()->eq('COUNT(u.id)', '3'), FilterUtility::CONDITION_OR); + $ds->applyRestrictions(); + + $this->assertEquals( + 'SELECT u.status, COUNT(u.id) FROM Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser u ' + . 'WHERE u.id = 1 AND u.id = 2 ' + . 'GROUP BY u.status ' + . 'HAVING COUNT(u.id) = 3 OR COUNT(u.id) = 4', + $qb->getDQL() + ); + } } diff --git a/src/Oro/Bundle/QueryDesignerBundle/Tests/Unit/Grid/Extension/OrmDatasourceExtensionTest.php b/src/Oro/Bundle/QueryDesignerBundle/Tests/Unit/Grid/Extension/OrmDatasourceExtensionTest.php index e9c48f4c291..9cf29548b33 100644 --- a/src/Oro/Bundle/QueryDesignerBundle/Tests/Unit/Grid/Extension/OrmDatasourceExtensionTest.php +++ b/src/Oro/Bundle/QueryDesignerBundle/Tests/Unit/Grid/Extension/OrmDatasourceExtensionTest.php @@ -188,10 +188,9 @@ function ($matches) use (&$counter) { . 'FROM Oro\Bundle\QueryDesignerBundle\Tests\Unit\Fixtures\Models\CMS\CmsUser user ' . 'INNER JOIN user.address address ' //. 'WHERE (user_name IS NULL OR NOT(user_name LIKE :string1)) AND (' - . 'WHERE user_name NOT LIKE :string1 AND (' + . 'WHERE user_name NOT LIKE :string1 AND ' . '(user_status < :datetime2 OR user_status > :datetime3) AND ' - . '(address.country LIKE :string4 OR address.city LIKE :string5 OR address.zip LIKE :string6)' - . ')', + . '(address.country LIKE :string4 OR address.city LIKE :string5 OR address.zip LIKE :string6)', $result ); } diff --git a/src/Oro/Bundle/QueryDesignerBundle/Tests/Unit/Validator/GroupNodeValidatorTest.php b/src/Oro/Bundle/QueryDesignerBundle/Tests/Unit/Validator/GroupNodeValidatorTest.php new file mode 100644 index 00000000000..4ee1aab9b5f --- /dev/null +++ b/src/Oro/Bundle/QueryDesignerBundle/Tests/Unit/Validator/GroupNodeValidatorTest.php @@ -0,0 +1,117 @@ +executionContext = $this->getMock('\Symfony\Component\Validator\ExecutionContextInterface'); + + $this->validator = new GroupNodeValidator(); + $this->validator->initialize($this->executionContext); + + $this->constraint = new GroupNodeConstraint(); + } + + /** + * @dataProvider validGroupNodesProvider + */ + public function testValidGroupNodes(GroupNode $node) + { + $this->executionContext->expects($this->never()) + ->method('addViolation'); + + $this->validator->validate($node, $this->constraint); + } + + public function validGroupNodesProvider() + { + return [ + [new GroupNode(FilterUtility::CONDITION_AND)], + [ + (new GroupNode(FilterUtility::CONDITION_AND)) + ->addNode(new Restriction('a = 5', FilterUtility::CONDITION_AND, false)) + ], + [ + (new GroupNode(FilterUtility::CONDITION_AND)) + ->addNode(new Restriction('a = 5', FilterUtility::CONDITION_AND, true)) + ], + [ + (new GroupNode(FilterUtility::CONDITION_AND)) + ->addNode(new Restriction('a = 5', FilterUtility::CONDITION_AND, false)) + ->addNode(new Restriction('a = 5', FilterUtility::CONDITION_AND, true)) + ], + [ + (new GroupNode(FilterUtility::CONDITION_AND)) + ->addNode( + (new GroupNode(FilterUtility::CONDITION_AND)) + ->addNode(new Restriction('a = 5', FilterUtility::CONDITION_AND, false)) + ) + ->addNode(new Restriction('a = 5', FilterUtility::CONDITION_AND, false)) + ->addNode(new Restriction('a = 5', FilterUtility::CONDITION_AND, true)) + ], + [ + (new GroupNode(FilterUtility::CONDITION_AND)) + ->addNode(new Restriction('a = 5', FilterUtility::CONDITION_AND, false)) + ->addNode( + (new GroupNode(FilterUtility::CONDITION_AND)) + ->addNode(new Restriction('a = 5', FilterUtility::CONDITION_AND, true)) + ) + ], + ]; + } + + /** + * @dataProvider invalidGroupNodesProvider + */ + public function testInvalidGroupNodes(GroupNode $node) + { + $this->executionContext->expects($this->once()) + ->method('addViolation') + ->with($this->constraint->mixedConditionsMessage); + + $this->validator->validate($node, $this->constraint); + } + + public function invalidGroupNodesProvider() + { + return [ + [ + (new GroupNode(FilterUtility::CONDITION_AND)) + ->addNode(new Restriction('a = 5', FilterUtility::CONDITION_AND, true)) + ->addNode(new Restriction('a = 5', FilterUtility::CONDITION_AND, false)) + ], + [ + (new GroupNode(FilterUtility::CONDITION_AND)) + ->addNode( + (new GroupNode(FilterUtility::CONDITION_AND)) + ->addNode(new Restriction('a = 5', FilterUtility::CONDITION_AND, true)) + ) + ->addNode(new Restriction('a = 5', FilterUtility::CONDITION_AND, false)) + ->addNode(new Restriction('a = 5', FilterUtility::CONDITION_AND, true)) + ], + [ + (new GroupNode(FilterUtility::CONDITION_AND)) + ->addNode( + (new GroupNode(FilterUtility::CONDITION_AND)) + ->addNode(new Restriction('a = 5', FilterUtility::CONDITION_AND, false)) + ->addNode(new Restriction('a = 5', FilterUtility::CONDITION_AND, true)) + ) + ->addNode(new Restriction('a = 5', FilterUtility::CONDITION_AND, false)) + ->addNode(new Restriction('a = 5', FilterUtility::CONDITION_AND, false)) + ], + ]; + } +} diff --git a/src/Oro/Bundle/QueryDesignerBundle/Validator/Constraints/GroupNodeConstraint.php b/src/Oro/Bundle/QueryDesignerBundle/Validator/Constraints/GroupNodeConstraint.php new file mode 100644 index 00000000000..fc41c512826 --- /dev/null +++ b/src/Oro/Bundle/QueryDesignerBundle/Validator/Constraints/GroupNodeConstraint.php @@ -0,0 +1,19 @@ +isValid($value)) { + $this->context->addViolation($constraint->mixedConditionsMessage); + } + } + + /** + * @param GroupNode $rootNode + * + * @return boolean + */ + protected function isValid(GroupNode $rootNode) + { + if ($rootNode->getType() !== GroupNode::TYPE_MIXED) { + return true; + } + + $types = array_map( + function ($node) { + if ($node instanceof GroupNode) { + return $node->getType(); + } + + return $node->isComputed() ? GroupNode::TYPE_COMPUTED : GroupNode::TYPE_UNCOMPUTED; + }, + $rootNode->getChildren() + ); + + if (in_array(GroupNode::TYPE_MIXED, $types)) { + return false; + } + + $computedTypes = ArrayUtil::dropWhile( + function ($type) { + return $type === GroupNode::TYPE_UNCOMPUTED; + }, + $types + ); + + return !in_array(GroupNode::TYPE_UNCOMPUTED, $computedTypes); + } +} diff --git a/src/Oro/Bundle/QueryDesignerBundle/Validator/GroupingValidator.php b/src/Oro/Bundle/QueryDesignerBundle/Validator/GroupingValidator.php index 64e2b4c13f2..05a99061910 100644 --- a/src/Oro/Bundle/QueryDesignerBundle/Validator/GroupingValidator.php +++ b/src/Oro/Bundle/QueryDesignerBundle/Validator/GroupingValidator.php @@ -6,9 +6,10 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; +use Oro\Component\PhpUtils\ArrayUtil; + use Oro\Bundle\QueryDesignerBundle\Model\AbstractQueryDesigner; use Oro\Bundle\QueryDesignerBundle\Validator\Constraints\GroupingConstraint; -use Oro\Bundle\UIBundle\Tools\ArrayUtils; class GroupingValidator extends ConstraintValidator { @@ -55,11 +56,11 @@ function (array $column) { $groupingColumns = $definition['grouping_columns']; } - $groupingColumnNames = ArrayUtils::arrayColumn($groupingColumns, 'name'); - $columnNames = ArrayUtils::arrayColumn($columns, 'name'); + $groupingColumnNames = ArrayUtil::arrayColumn($groupingColumns, 'name'); + $columnNames = ArrayUtil::arrayColumn($columns, 'name'); $columnNamesToCheck = array_diff( $columnNames, - ArrayUtils::arrayColumn($aggregateColumns, 'name') + ArrayUtil::arrayColumn($aggregateColumns, 'name') ); $columnsToGroup = array_diff($columnNamesToCheck, $groupingColumnNames); diff --git a/src/Oro/Bundle/ReminderBundle/Resources/public/less/mobile/main.less b/src/Oro/Bundle/ReminderBundle/Resources/public/less/mobile/main.less new file mode 100644 index 00000000000..190650f2ca4 --- /dev/null +++ b/src/Oro/Bundle/ReminderBundle/Resources/public/less/mobile/main.less @@ -0,0 +1,8 @@ +.mobile-version { + .reminders-collection, .ui-dialog .form-horizontal .control-group .controls .reminders-collection { + input.number { + height: 30px; + width: 40px !important; + } + } +} diff --git a/src/Oro/Bundle/ReminderBundle/Resources/public/less/style.less b/src/Oro/Bundle/ReminderBundle/Resources/public/less/style.less index 0a941810610..3dbeb53377f 100644 --- a/src/Oro/Bundle/ReminderBundle/Resources/public/less/style.less +++ b/src/Oro/Bundle/ReminderBundle/Resources/public/less/style.less @@ -55,3 +55,5 @@ padding: 0 9px 0 8px; } } + +@import "./mobile/main"; diff --git a/src/Oro/Bundle/ReportBundle/EventListener/SegmentSubscriber.php b/src/Oro/Bundle/ReportBundle/EventListener/SegmentSubscriber.php new file mode 100644 index 00000000000..1021b8b5d45 --- /dev/null +++ b/src/Oro/Bundle/ReportBundle/EventListener/SegmentSubscriber.php @@ -0,0 +1,58 @@ + 'loadAggregatedFieldsWidgetOptions', + ConditionBuilderOptionsLoadEvent::EVENT_NAME => 'loadAggregatedFieldsBuilderOptions', + ]; + } + + /** + * @param WidgetOptionsLoadEvent $event + */ + public function loadAggregatedFieldsWidgetOptions(WidgetOptionsLoadEvent $event) + { + if ($event->getWidgetType() !== 'oro_report') { + return; + } + + $event->setWidgetOptions(array_merge_recursive( + $event->getWidgetOptions(), + [ + 'extensions' => [ + 'orosegment/js/app/components/aggregated-field-condition-extension', + ], + ] + )); + } + + /** + * @param ConditionBuilderOptionsLoadEvent $event + */ + public function loadAggregatedFieldsBuilderOptions(ConditionBuilderOptionsLoadEvent $event) + { + $event->setOptions(array_merge_recursive( + $event->getOptions(), + [ + 'onFieldsUpdate' => [ + 'toggleCriteria' => [ + 'aggregated-condition-item', + ], + ], + ] + )); + } +} diff --git a/src/Oro/Bundle/ReportBundle/Resources/config/datagrid.yml b/src/Oro/Bundle/ReportBundle/Resources/config/datagrid.yml index 48db774d122..ecd26b22792 100644 --- a/src/Oro/Bundle/ReportBundle/Resources/config/datagrid.yml +++ b/src/Oro/Bundle/ReportBundle/Resources/config/datagrid.yml @@ -1,8 +1,8 @@ datagrid: reports-grid: + acl_resource: oro_report_view source: type: orm - acl_resource: oro_report_view query: select: - r.id diff --git a/src/Oro/Bundle/ReportBundle/Resources/config/placeholders.yml b/src/Oro/Bundle/ReportBundle/Resources/config/placeholders.yml new file mode 100644 index 00000000000..f07a4d54a81 --- /dev/null +++ b/src/Oro/Bundle/ReportBundle/Resources/config/placeholders.yml @@ -0,0 +1,10 @@ +placeholders: + segment_criteria_list: + items: + aggregated_field_condition: + order: 10 + +items: + aggregated_field_condition: + template: OroReportBundle:Segment:aggregated_field_condition.html.twig + applicable: @oro_ui.placeholder.filter->isSame($params.id$, oro_report-condition-builder) diff --git a/src/Oro/Bundle/ReportBundle/Resources/config/query_designer.yml b/src/Oro/Bundle/ReportBundle/Resources/config/query_designer.yml index fab369f3daa..ebbde9b0357 100644 --- a/src/Oro/Bundle/ReportBundle/Resources/config/query_designer.yml +++ b/src/Oro/Bundle/ReportBundle/Resources/config/query_designer.yml @@ -19,3 +19,10 @@ query_designer: - { name: Min, expr: MIN($column) } - { name: Max, expr: MAX($column) } query_type: [report] + date: + applicable: [{type: date}, {type: datetime}] + functions: + - { name: Count, expr: COUNT($column), return_type: integer } + - { name: Min, expr: MIN($column) } + - { name: Max, expr: MAX($column) } + query_type: [report] diff --git a/src/Oro/Bundle/ReportBundle/Resources/config/services.yml b/src/Oro/Bundle/ReportBundle/Resources/config/services.yml index e034476d3f4..0ea72443c82 100644 --- a/src/Oro/Bundle/ReportBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/ReportBundle/Resources/config/services.yml @@ -4,6 +4,7 @@ parameters: oro_report.listener.navigation_listener.class: Oro\Bundle\ReportBundle\EventListener\NavigationListener oro_report.report_manager.class: Oro\Bundle\ReportBundle\Entity\Manager\ReportManager oro_report.grid.base_configuration_builder.class: Oro\Bundle\ReportBundle\Grid\BaseReportConfigurationBuilder + oro_report.listener.segment_subscriber.class: Oro\Bundle\ReportBundle\EventListener\SegmentSubscriber oro_report.grid.datagrid_configuration_builder.class: Oro\Bundle\ReportBundle\Grid\ReportDatagridConfigurationBuilder services: @@ -42,3 +43,8 @@ services: oro_report.grid.datagrid_configuration_builder: class: %oro_report.grid.datagrid_configuration_builder.class% parent: oro_report.grid.base_configuration_builder + + oro_report.listener.segment_subscriber: + class: %oro_report.listener.segment_subscriber.class% + tags: + - { name: kernel.event_subscriber } diff --git a/src/Oro/Bundle/ReportBundle/Resources/translations/messages.en.yml b/src/Oro/Bundle/ReportBundle/Resources/translations/messages.en.yml index 3dbc7513c2a..077c6487251 100644 --- a/src/Oro/Bundle/ReportBundle/Resources/translations/messages.en.yml +++ b/src/Oro/Bundle/ReportBundle/Resources/translations/messages.en.yml @@ -25,6 +25,16 @@ oro: Max: name: Max hint: Maximum value + date: + Count: + name: Count + hint: Number of items + Min: + name: Min + hint: Minimum value + Max: + name: Max + hint: Maximum value report: menu: diff --git a/src/Oro/Bundle/ReportBundle/Resources/views/Report/table/view.html.twig b/src/Oro/Bundle/ReportBundle/Resources/views/Report/table/view.html.twig index 32880ffa91d..611f73a193c 100644 --- a/src/Oro/Bundle/ReportBundle/Resources/views/Report/table/view.html.twig +++ b/src/Oro/Bundle/ReportBundle/Resources/views/Report/table/view.html.twig @@ -40,8 +40,18 @@ {{ chartView.render()|raw }}
    {% endif %} - {% set renderParams = renderParams|default({})|merge({enableFullScreenLayout: true}) %} - {{ dataGrid.renderGrid(gridName, params|default({}), renderParams) }} + {% set renderParams = renderParams|default({})|merge({enableFullScreenLayout: true, enableViews: false}) %} + {% set params = params|default({})|merge({ + '_grid_view': { + '_disabled': true + }, + '_tags': { + '_disabled': true + } + }) + %} + + {{ dataGrid.renderGrid(gridName, params, renderParams) }} {% else %}
    diff --git a/src/Oro/Bundle/ReportBundle/Resources/views/Segment/aggregated_field_condition.html.twig b/src/Oro/Bundle/ReportBundle/Resources/views/Segment/aggregated_field_condition.html.twig new file mode 100644 index 00000000000..1b16c9e4fa3 --- /dev/null +++ b/src/Oro/Bundle/ReportBundle/Resources/views/Segment/aggregated_field_condition.html.twig @@ -0,0 +1,15 @@ +{% set aggregatedFieldConditionOptions = { + fieldChoice: { + select2: { + placeholder: 'oro.query_designer.condition_builder.choose_entity_field'|trans + }, + fieldsLoaderSelector: '[data-ftid=' ~ params.entity_choice_id ~ 'oro_api_querydesigner_fields_entity]' + } +} %} + +
  • + {{ 'oro.query_designer.condition_builder.criteria.aggregated_field_condition'|trans }} +
  • diff --git a/src/Oro/Bundle/RequireJSBundle/Provider/Config.php b/src/Oro/Bundle/RequireJSBundle/Provider/Config.php index 15c4f467169..800adc06dab 100644 --- a/src/Oro/Bundle/RequireJSBundle/Provider/Config.php +++ b/src/Oro/Bundle/RequireJSBundle/Provider/Config.php @@ -9,7 +9,7 @@ use Doctrine\Common\Cache\CacheProvider; -use Oro\Bundle\UIBundle\Tools\ArrayUtils; +use Oro\Component\PhpUtils\ArrayUtil; class Config { @@ -149,7 +149,7 @@ public function collectConfigs() $reflection = new \ReflectionClass($bundle); if (is_file($file = dirname($reflection->getFilename()) . '/Resources/config/requirejs.yml')) { $requirejs = Yaml::parse(realpath($file)); - $config = ArrayUtils::arrayMergeRecursiveDistinct($config, $requirejs); + $config = ArrayUtil::arrayMergeRecursiveDistinct($config, $requirejs); } } diff --git a/src/Oro/Bundle/RequireJSBundle/Resources/views/scripts.html.twig b/src/Oro/Bundle/RequireJSBundle/Resources/views/scripts.html.twig index dbe84f3c940..8fb691f5b16 100644 --- a/src/Oro/Bundle/RequireJSBundle/Resources/views/scripts.html.twig +++ b/src/Oro/Bundle/RequireJSBundle/Resources/views/scripts.html.twig @@ -1,7 +1,9 @@ {% set compressed = compressed is defined ? compressed : true %} +{% set baseAssetParts = asset('bundles')|split('?', 2) %} {% set config_extend %} require({ - baseUrl: {{ asset('bundles')|split('?', 2)[0]|json_encode|raw }} + baseUrl: {{ baseAssetParts[0]|json_encode|raw }}, + urlArgs: '{{ baseAssetParts[1] is defined ? baseAssetParts[1] : '' }}' }); {{ config_extend|default('') }} {% endset %} diff --git a/src/Oro/Bundle/SSOBundle/DependencyInjection/Compiler/HwiConfigurationPass.php b/src/Oro/Bundle/SSOBundle/DependencyInjection/Compiler/HwiConfigurationPass.php index 226ab63cc43..c51a6ed8643 100644 --- a/src/Oro/Bundle/SSOBundle/DependencyInjection/Compiler/HwiConfigurationPass.php +++ b/src/Oro/Bundle/SSOBundle/DependencyInjection/Compiler/HwiConfigurationPass.php @@ -28,5 +28,10 @@ public function process(ContainerBuilder $container) new Reference('oro_config.global'), ]); } + + if ($container->hasDefinition('hwi_oauth.authentication.provider.oauth')) { + $definition = $container->getDefinition('hwi_oauth.authentication.provider.oauth'); + $definition->addMethodCall('setTokenFactory', [new Reference('oro_sso.token.factory.oauth')]); + } } } diff --git a/src/Oro/Bundle/SSOBundle/Resources/config/services.yml b/src/Oro/Bundle/SSOBundle/Resources/config/services.yml index 7e9de4322ca..bf35d4d014a 100644 --- a/src/Oro/Bundle/SSOBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/SSOBundle/Resources/config/services.yml @@ -3,6 +3,7 @@ parameters: oro_sso.event_listener.user_email_change_listener.class: Oro\Bundle\SSOBundle\EventListener\UserEmailChangeListener hwi_oauth.authentication.provider.oauth.class: Oro\Bundle\SSOBundle\Security\OAuthProvider hwi_oauth.resource_owner.google.class: Oro\Bundle\SSOBundle\OAuth\ResourceOwner\GoogleResourceOwner + oro_sso.token.factory.oauth.class: Oro\Bundle\SSOBundle\Security\OAuthTokenFactory services: oro_sso.oauth_provider: @@ -13,3 +14,6 @@ services: class: %oro_sso.event_listener.user_email_change_listener.class% tags: - { name: doctrine.event_listener, event: preUpdate } + + oro_sso.token.factory.oauth: + class: %oro_sso.token.factory.oauth.class% diff --git a/src/Oro/Bundle/SSOBundle/Security/OAuthProvider.php b/src/Oro/Bundle/SSOBundle/Security/OAuthProvider.php index 32017da89e4..c96c948e620 100644 --- a/src/Oro/Bundle/SSOBundle/Security/OAuthProvider.php +++ b/src/Oro/Bundle/SSOBundle/Security/OAuthProvider.php @@ -12,6 +12,7 @@ use Oro\Bundle\UserBundle\Entity\User; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\User\UserCheckerInterface; @@ -32,6 +33,11 @@ class OAuthProvider extends HWIOAuthProvider */ protected $userChecker; + /** + * @var OAuthTokenFactoryInterface + */ + protected $tokenFactory; + /** * @param OAuthAwareUserProviderInterface $userProvider User provider * @param ResourceOwnerMap $resourceOwnerMap Resource owner map @@ -47,6 +53,14 @@ public function __construct( $this->userChecker = $userChecker; } + /** + * @param OAuthTokenFactoryInterface $tokenFactory + */ + public function setTokenFactory(OAuthTokenFactoryInterface $tokenFactory) + { + $this->tokenFactory = $tokenFactory; + } + /** * {@inheritDoc} * @@ -54,6 +68,10 @@ public function __construct( */ public function authenticate(TokenInterface $token) { + if (null === $this->tokenFactory) { + throw new AuthenticationException('Token Factory is not set in OAuthProvider.'); + } + /* @var OAuthToken $token */ $resourceOwner = $this->resourceOwnerMap->getResourceOwnerByName($token->getResourceOwnerName()); @@ -69,7 +87,7 @@ public function authenticate(TokenInterface $token) $organization = $this->guessOrganization($user, $token); - $token = new OAuthToken($token->getRawToken(), $user->getRoles()); + $token = $this->tokenFactory->create($token->getRawToken(), $user->getRoles()); $token->setResourceOwnerName($resourceOwner->getName()); $token->setOrganizationContext($organization); $token->setUser($user); diff --git a/src/Oro/Bundle/SSOBundle/Security/OAuthTokenFactory.php b/src/Oro/Bundle/SSOBundle/Security/OAuthTokenFactory.php new file mode 100644 index 00000000000..9eadbedc8ed --- /dev/null +++ b/src/Oro/Bundle/SSOBundle/Security/OAuthTokenFactory.php @@ -0,0 +1,16 @@ +userProvider = $this @@ -22,11 +47,13 @@ public function setUp() ->disableOriginalConstructor() ->getMock(); $this->userChecker = $this->getMock('Symfony\Component\Security\Core\User\UserCheckerInterface'); - + + $this->tokenFactory = new OAuthTokenFactory(); + $this->oauthProvider = new OAuthProvider($this->userProvider, $this->resourceOwnerMap, $this->userChecker); } - public function testSupportsShuldReturnTrueForOAuthToken() + public function testSupportsShouldReturnTrueForOAuthToken() { $this->resourceOwnerMap->expects($this->once()) ->method('hasResourceOwnerByName') @@ -37,9 +64,21 @@ public function testSupportsShuldReturnTrueForOAuthToken() $token->setResourceOwnerName('google'); $this->assertTrue($this->oauthProvider->supports($token)); } - + + /** + * @expectedException \Symfony\Component\Security\Core\Exception\AuthenticationException + * @expectedExceptionMessage Token Factory is not set in OAuthProvider. + */ + public function testAuthenticateIfTokenFactoryIsNotSet() + { + $token = new OAuthToken('token'); + $this->oauthProvider->authenticate($token); + } + public function testTokenShouldBeAuthenticated() { + $this->oauthProvider->setTokenFactory($this->tokenFactory); + $token = new OAuthToken('token'); $token->setResourceOwnerName('google'); $organization = new Organization(); @@ -47,7 +86,7 @@ public function testTokenShouldBeAuthenticated() $token->setOrganizationContext($organization); $userResponse = $this->getMock('HWI\Bundle\OAuthBundle\OAuth\Response\UserResponseInterface'); - + $resourceOwner = $this->getMock('HWI\Bundle\OAuthBundle\OAuth\ResourceOwnerInterface'); $resourceOwner ->expects($this->any()) diff --git a/src/Oro/Bundle/SSOBundle/Tests/Unit/Security/OAuthTokenFactoryTest.php b/src/Oro/Bundle/SSOBundle/Tests/Unit/Security/OAuthTokenFactoryTest.php new file mode 100644 index 00000000000..0f3d55ad4ca --- /dev/null +++ b/src/Oro/Bundle/SSOBundle/Tests/Unit/Security/OAuthTokenFactoryTest.php @@ -0,0 +1,17 @@ +create('accessToken'); + + $this->assertInstanceOf('Oro\Bundle\SSOBundle\Security\OAuthToken', $token); + $this->assertEquals('accessToken', $token->getAccessToken()); + } +} diff --git a/src/Oro/Bundle/SearchBundle/Extension/Pager/SearchPagerExtension.php b/src/Oro/Bundle/SearchBundle/Extension/Pager/SearchPagerExtension.php index e49a3baa935..b649aa7b44e 100644 --- a/src/Oro/Bundle/SearchBundle/Extension/Pager/SearchPagerExtension.php +++ b/src/Oro/Bundle/SearchBundle/Extension/Pager/SearchPagerExtension.php @@ -2,7 +2,6 @@ namespace Oro\Bundle\SearchBundle\Extension\Pager; -use Oro\Bundle\DataGridBundle\Datagrid\Builder; use Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration; use Oro\Bundle\DataGridBundle\Datasource\DatasourceInterface; use Oro\Bundle\DataGridBundle\Extension\Pager\PagerInterface; @@ -29,7 +28,7 @@ public function __construct(IndexerPager $pager) public function isApplicable(DatagridConfiguration $config) { // enabled by default for search datasource - return $config->offsetGetByPath(Builder::DATASOURCE_TYPE_PATH) == SearchDatasource::TYPE; + return $config->getDatasourceType() == SearchDatasource::TYPE; } /** diff --git a/src/Oro/Bundle/SecurityBundle/Acl/Persistence/AclPrivilegeRepository.php b/src/Oro/Bundle/SecurityBundle/Acl/Persistence/AclPrivilegeRepository.php index ec8b772c23c..2a7dc117db3 100644 --- a/src/Oro/Bundle/SecurityBundle/Acl/Persistence/AclPrivilegeRepository.php +++ b/src/Oro/Bundle/SecurityBundle/Acl/Persistence/AclPrivilegeRepository.php @@ -449,34 +449,38 @@ protected function findAcls(SID $sid, array $oids) * Sorts the given privileges by name in alphabetical order. * The root privilege is moved at the top of the list. * - * @param ArrayCollection|AclPrivilege[] $privileges [input/output] + * @param ArrayCollection $privileges */ - protected function sortPrivileges(ArrayCollection &$privileges) + protected function sortPrivileges(ArrayCollection $privileges) { - /** @var \ArrayIterator $iterator */ - $iterator = $privileges->getIterator(); - $iterator->uasort( - function (AclPrivilege $a, AclPrivilege $b) { - if (strpos($a->getIdentity()->getId(), ObjectIdentityFactory::ROOT_IDENTITY_TYPE)) { + $data = []; + /** @var AclPrivilege $privilege */ + foreach ($privileges->getIterator() as $privilege) { + $isRoot = false !== strpos($privilege->getIdentity()->getId(), ObjectIdentityFactory::ROOT_IDENTITY_TYPE); + $label = !$isRoot + ? $this->translator->trans($privilege->getIdentity()->getName()) + : null; + + $data[] = [$privilege, $isRoot, $label]; + } + uasort( + $data, + function ($a, $b) { + if ($a[1]) { return -1; } - if (strpos($b->getIdentity()->getId(), ObjectIdentityFactory::ROOT_IDENTITY_TYPE)) { + if ($b[1]) { return 1; } - return strcmp( - $this->translator->trans($a->getIdentity()->getName()), - $this->translator->trans($b->getIdentity()->getName()) - ); + return strcmp($a[2], $b[2]); } ); - $result = new ArrayCollection(); - foreach ($iterator as $item) { - $result->add($item); + $privileges->clear(); + foreach ($data as $item) { + $privileges->add($item[0]); } - - $privileges = $result; } /** diff --git a/src/Oro/Bundle/SecurityBundle/Authentication/Provider/OrganizationRememberMeAuthenticationProvider.php b/src/Oro/Bundle/SecurityBundle/Authentication/Provider/OrganizationRememberMeAuthenticationProvider.php index eec23366d77..0f7341b80c9 100644 --- a/src/Oro/Bundle/SecurityBundle/Authentication/Provider/OrganizationRememberMeAuthenticationProvider.php +++ b/src/Oro/Bundle/SecurityBundle/Authentication/Provider/OrganizationRememberMeAuthenticationProvider.php @@ -2,21 +2,41 @@ namespace Oro\Bundle\SecurityBundle\Authentication\Provider; +use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Provider\RememberMeAuthenticationProvider; use Oro\Bundle\UserBundle\Entity\User; use Oro\Bundle\SecurityBundle\Authentication\Guesser\UserOrganizationGuesser; -use Oro\Bundle\SecurityBundle\Authentication\Token\OrganizationRememberMeToken; +use Oro\Bundle\SecurityBundle\Authentication\Token\OrganizationRememberMeTokenFactoryInterface; class OrganizationRememberMeAuthenticationProvider extends RememberMeAuthenticationProvider { + /** + * @var OrganizationRememberMeTokenFactoryInterface + */ + protected $tokenFactory; + + /** + * @param OrganizationRememberMeTokenFactoryInterface $tokenFactory + */ + public function setTokenFactory(OrganizationRememberMeTokenFactoryInterface $tokenFactory) + { + $this->tokenFactory = $tokenFactory; + } + /** * {@inheritdoc} */ public function authenticate(TokenInterface $token) { + if (null === $this->tokenFactory) { + throw new AuthenticationException( + 'Token Factory is not set in OrganizationRememberMeAuthenticationProvider.' + ); + } + $guesser = new UserOrganizationGuesser(); /** @var TokenInterface $token */ $authenticatedToken = parent::authenticate($token); @@ -33,12 +53,13 @@ public function authenticate(TokenInterface $token) ); } - $authenticatedToken = new OrganizationRememberMeToken( - $user, - $authenticatedToken->getProviderKey(), - $authenticatedToken->getKey(), - $organization - ); + $authenticatedToken = $this->tokenFactory + ->create( + $user, + $authenticatedToken->getProviderKey(), + $authenticatedToken->getKey(), + $organization + ); return $authenticatedToken; } diff --git a/src/Oro/Bundle/SecurityBundle/Authentication/Provider/UsernamePasswordOrganizationAuthenticationProvider.php b/src/Oro/Bundle/SecurityBundle/Authentication/Provider/UsernamePasswordOrganizationAuthenticationProvider.php index 8971dc68639..99051f757f7 100644 --- a/src/Oro/Bundle/SecurityBundle/Authentication/Provider/UsernamePasswordOrganizationAuthenticationProvider.php +++ b/src/Oro/Bundle/SecurityBundle/Authentication/Provider/UsernamePasswordOrganizationAuthenticationProvider.php @@ -2,21 +2,41 @@ namespace Oro\Bundle\SecurityBundle\Authentication\Provider; +use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider; use Oro\Bundle\UserBundle\Entity\User; use Oro\Bundle\SecurityBundle\Authentication\Guesser\UserOrganizationGuesser; -use Oro\Bundle\SecurityBundle\Authentication\Token\UsernamePasswordOrganizationToken; +use Oro\Bundle\SecurityBundle\Authentication\Token\UsernamePasswordOrganizationTokenFactoryInterface; class UsernamePasswordOrganizationAuthenticationProvider extends DaoAuthenticationProvider { + /** + * @var UsernamePasswordOrganizationTokenFactoryInterface + */ + protected $tokenFactory; + + /** + * @param UsernamePasswordOrganizationTokenFactoryInterface $tokenFactory + */ + public function setTokenFactory(UsernamePasswordOrganizationTokenFactoryInterface $tokenFactory) + { + $this->tokenFactory = $tokenFactory; + } + /** * {@inheritdoc} */ public function authenticate(TokenInterface $token) { + if (null === $this->tokenFactory) { + throw new AuthenticationException( + 'Token Factory is not set in UsernamePasswordOrganizationAuthenticationProvider.' + ); + } + $guesser = new UserOrganizationGuesser(); /** @var TokenInterface $token */ $authenticatedToken = parent::authenticate($token); @@ -33,7 +53,7 @@ public function authenticate(TokenInterface $token) ); } - $authenticatedToken = new UsernamePasswordOrganizationToken( + $authenticatedToken = $this->tokenFactory->create( $authenticatedToken->getUser(), $authenticatedToken->getCredentials(), $authenticatedToken->getProviderKey(), diff --git a/src/Oro/Bundle/SecurityBundle/Authentication/Token/OrganizationRememberMeTokenFactory.php b/src/Oro/Bundle/SecurityBundle/Authentication/Token/OrganizationRememberMeTokenFactory.php new file mode 100644 index 00000000000..8289c2e77e9 --- /dev/null +++ b/src/Oro/Bundle/SecurityBundle/Authentication/Token/OrganizationRememberMeTokenFactory.php @@ -0,0 +1,25 @@ +ignoreFailure = false; } + /** + * @param UsernamePasswordOrganizationTokenFactoryInterface $tokenFactory + */ + public function setTokenFactory(UsernamePasswordOrganizationTokenFactoryInterface $tokenFactory) + { + $this->tokenFactory = $tokenFactory; + } + /** * Handles basic authentication. * @@ -86,12 +107,13 @@ public function handle(GetResponseEvent $event) try { $organizationId = $request->headers->get('PHP_AUTH_ORGANIZATION'); if ($organizationId) { - $authToken = new UsernamePasswordOrganizationToken( - $username, - $request->headers->get('PHP_AUTH_PW'), - $this->providerKey, - $this->manager->getOrganizationById($organizationId) - ); + $authToken = $this->tokenFactory + ->create( + $username, + $request->headers->get('PHP_AUTH_PW'), + $this->providerKey, + $this->manager->getOrganizationById($organizationId) + ); } else { $authToken = new UsernamePasswordToken( $username, diff --git a/src/Oro/Bundle/SecurityBundle/ORM/Walker/OwnershipConditionDataBuilder.php b/src/Oro/Bundle/SecurityBundle/ORM/Walker/OwnershipConditionDataBuilder.php index ab7090ce683..c1825a78ec8 100644 --- a/src/Oro/Bundle/SecurityBundle/ORM/Walker/OwnershipConditionDataBuilder.php +++ b/src/Oro/Bundle/SecurityBundle/ORM/Walker/OwnershipConditionDataBuilder.php @@ -83,13 +83,21 @@ public function setAclGroupProvider($aclGroupProvider) /** * Get data for query acl access level check - * Return empty array if entity has full access, null if user does't have access to the entity - * and array with entity field and field values which user have access. * * @param $entityClassName * @param $permissions * - * @return null|array + * @return array Returns empty array if entity has full access, + * array with null values if user does't have access to the entity + * and array with entity field and field values which user has access to. + * Array structure: + * 0 - owner field name + * 1 - owner values + * 2 - owner association type + * 3 - organization field name + * 4 - organization values + * 5 - should owners be checked + * (for example, in case of Organization ownership type, owners should not be checked) */ public function getAclConditionData($entityClassName, $permissions = 'VIEW') { diff --git a/src/Oro/Bundle/SecurityBundle/Owner/AbstractOwnerTreeProvider.php b/src/Oro/Bundle/SecurityBundle/Owner/AbstractOwnerTreeProvider.php index 1ee2a7442f9..38f1474bae4 100644 --- a/src/Oro/Bundle/SecurityBundle/Owner/AbstractOwnerTreeProvider.php +++ b/src/Oro/Bundle/SecurityBundle/Owner/AbstractOwnerTreeProvider.php @@ -9,6 +9,7 @@ use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Oro\Bundle\EntityBundle\Tools\SafeDatabaseChecker; use Oro\Bundle\SecurityBundle\Owner\Metadata\MetadataProviderInterface; abstract class AbstractOwnerTreeProvider implements ContainerAwareInterface, OwnerTreeProviderInterface @@ -142,22 +143,11 @@ protected function loadTree() protected function checkDatabase() { $className = $this->getOwnershipMetadataProvider()->getBasicLevelClass(); - $em = $this->getManagerForClass($className); - $tableName = $em->getClassMetadata($className)->getTableName(); - $result = false; - try { - $conn = $em->getConnection(); - - if (!$conn->isConnected()) { - $em->getConnection()->connect(); - } - - $result = $conn->isConnected() - && (bool)array_intersect([$tableName], $em->getConnection()->getSchemaManager()->listTableNames()); - } catch (\PDOException $e) { - } - return $result; + return SafeDatabaseChecker::tablesExist( + $this->getManagerForClass($className)->getConnection(), + SafeDatabaseChecker::getTableName($this->getContainer()->get('doctrine'), $className) + ); } /** diff --git a/src/Oro/Bundle/SecurityBundle/Owner/OwnerTreeProvider.php b/src/Oro/Bundle/SecurityBundle/Owner/OwnerTreeProvider.php index 914b656f5ee..e2398a942c3 100644 --- a/src/Oro/Bundle/SecurityBundle/Owner/OwnerTreeProvider.php +++ b/src/Oro/Bundle/SecurityBundle/Owner/OwnerTreeProvider.php @@ -106,7 +106,7 @@ protected function fillTree(OwnerTreeInterface $tree) foreach ($businessUnits as $businessUnit) { if (!empty($businessUnit['organization'])) { - $tree->addLocalEntity($businessUnit['id'], $businessUnit['organization']); + $tree->addLocalEntity($businessUnit['id'], (int)$businessUnit['organization']); if ($businessUnit['owner']) { $tree->addDeepEntity($businessUnit['id'], $businessUnit['owner']); } diff --git a/src/Oro/Bundle/SecurityBundle/Resources/config/services.yml b/src/Oro/Bundle/SecurityBundle/Resources/config/services.yml index 180e701133b..b69dc01e873 100644 --- a/src/Oro/Bundle/SecurityBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/SecurityBundle/Resources/config/services.yml @@ -51,6 +51,9 @@ parameters: oro_security.encoder.mcrypt.class: Oro\Bundle\SecurityBundle\Encoder\Mcrypt + oro_security.token.factory.username_password_organization.class: Oro\Bundle\SecurityBundle\Authentication\Token\UsernamePasswordOrganizationTokenFactory + oro_security.token.factory.organization_rememberme.class: Oro\Bundle\SecurityBundle\Authentication\Token\OrganizationRememberMeTokenFactory + oro_security.acl_helper.class: Oro\Bundle\SecurityBundle\ORM\Walker\AclHelper oro_security.search.acl_helper.class: Oro\Bundle\SecurityBundle\Search\AclHelper oro_security.orm.ownership_sql_walker_builder.class: Oro\Bundle\SecurityBundle\ORM\Walker\OwnershipConditionDataBuilder @@ -472,6 +475,12 @@ services: tags: - { name: doctrine.event_listener, event: onFlush } + oro_security.token.factory.username_password_organization: + class: %oro_security.token.factory.username_password_organization.class% + + oro_security.token.factory.organization_rememberme: + class: %oro_security.token.factory.organization_rememberme.class% + oro_security.authentication.listener.basic: class: %oro-security.authentication.listener.basic.class% arguments: @@ -481,6 +490,8 @@ services: - null - @oro_organization.organization_manager - @?logger + calls: + - [setTokenFactory, [@oro_security.token.factory.username_password_organization]] public: false abstract: true @@ -492,6 +503,8 @@ services: - null - @security.encoder_factory - %security.authentication.hide_user_not_found% + calls: + - [setTokenFactory, [@oro_security.token.factory.username_password_organization]] abstract: true public: false @@ -499,6 +512,8 @@ services: class: %oro_security.authentication.provider.organization_rememberme.class% arguments: - @security.user_checker + calls: + - [setTokenFactory, [@oro_security.token.factory.organization_rememberme]] abstract: true public: false diff --git a/src/Oro/Bundle/SecurityBundle/Search/AclHelper.php b/src/Oro/Bundle/SecurityBundle/Search/AclHelper.php index 9f9f6b06861..706b96c332e 100644 --- a/src/Oro/Bundle/SecurityBundle/Search/AclHelper.php +++ b/src/Oro/Bundle/SecurityBundle/Search/AclHelper.php @@ -46,23 +46,18 @@ public function __construct( */ public function apply(Query $query, $permission = 'VIEW') { - $queryFromEntities = $query->getFrom(); - - // in query, from record !== '*' - if ($queryFromEntities[0] === '*') { - $queryFromEntities = $this->mappingProvider->getEntitiesListAliases(); - } + $querySearchAliases = $this->getSearchAliases($query); $allowedAliases = []; $ownerExpressions = []; $expr = $query->getCriteria()->expr(); - if (!empty($queryFromEntities)) { - foreach ($queryFromEntities as $entityAlias) { + if (count($querySearchAliases) !== 0) { + foreach ($querySearchAliases as $entityAlias) { $className = $this->mappingProvider->getEntityClass($entityAlias); if ($className) { $ownerField = sprintf('%s_owner', $entityAlias); $condition = $this->ownershipDataBuilder->getAclConditionData($className, $permission); - if ($condition !== null) { + if (count($condition) === 0 || !($condition[0] === null && $condition[3] === null)) { $allowedAliases[] = $entityAlias; // in case if we should not limit data for entity @@ -72,25 +67,19 @@ public function apply(Query $query, $permission = 'VIEW') continue; } - $owners = [SearchListener::EMPTY_OWNER_ID]; - if (!empty($condition[1])) { - $owners = $condition[1]; - if (is_array($owners) && count($owners) === 1) { - $owners = $owners[0]; - } - } + $owners = !empty($condition[1]) + ? $condition[1] + : SearchListener::EMPTY_OWNER_ID; - if (is_array($owners)) { - $ownerExpressions[] = $expr->in('integer.' . $ownerField, $owners); - } else { - $ownerExpressions[] = $expr->eq('integer.' . $ownerField, $owners); - } + $ownerExpressions[] = (!is_array($owners) || count($owners) === 1) + ? $expr->eq('integer.' . $ownerField, $owners) + : $expr->in('integer.' . $ownerField, $owners); } } } } - if (!empty($ownerExpressions)) { + if (count($ownerExpressions) !== 0) { $query->getCriteria()->andWhere(new CompositeExpression(CompositeExpression::TYPE_OR, $ownerExpressions)); } $query->from($allowedAliases); @@ -121,4 +110,22 @@ protected function addOrganizationLimits(Query $query, $expr) ); } } + + /** + * Get search query 'from' aliases + * + * @param Query $query + * + * @return array Return search aliases from Query. In case if from part = *, return all search aliases + */ + protected function getSearchAliases(Query $query) + { + $queryAliases = $query->getFrom(); + + if ($queryAliases[0] === '*') { + $queryAliases = $this->mappingProvider->getEntitiesListAliases(); + } + + return $queryAliases; + } } diff --git a/src/Oro/Bundle/SecurityBundle/Tests/Unit/Acl/Persistence/AclPrivilegeRepositoryTest.php b/src/Oro/Bundle/SecurityBundle/Tests/Unit/Acl/Persistence/AclPrivilegeRepositoryTest.php index 3d173d66454..4f3a7e1d88d 100644 --- a/src/Oro/Bundle/SecurityBundle/Tests/Unit/Acl/Persistence/AclPrivilegeRepositoryTest.php +++ b/src/Oro/Bundle/SecurityBundle/Tests/Unit/Acl/Persistence/AclPrivilegeRepositoryTest.php @@ -3,18 +3,18 @@ namespace Oro\Bundle\SecurityBundle\Tests\Unit\Acl\Persistence; use Doctrine\Common\Collections\ArrayCollection; + +use Symfony\Component\Security\Acl\Domain\ObjectIdentity; +use Symfony\Component\Security\Acl\Exception\NotAllAclsFoundException; + use Oro\Bundle\SecurityBundle\Acl\AccessLevel; use Oro\Bundle\SecurityBundle\Acl\Domain\ObjectIdentityFactory; use Oro\Bundle\SecurityBundle\Acl\Extension\EntityMaskBuilder; use Oro\Bundle\SecurityBundle\Acl\Permission\MaskBuilder; -use Oro\Bundle\SecurityBundle\Acl\Persistence\AclManager; use Oro\Bundle\SecurityBundle\Acl\Persistence\AclPrivilegeRepository; use Oro\Bundle\SecurityBundle\Model\AclPermission; use Oro\Bundle\SecurityBundle\Model\AclPrivilege; use Oro\Bundle\SecurityBundle\Model\AclPrivilegeIdentity; -use Symfony\Component\Security\Acl\Domain\ObjectIdentity; -use Symfony\Component\Security\Acl\Exception\NotAllAclsFoundException; -use Symfony\Component\Security\Acl\Model\EntryInterface; /** * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) @@ -307,13 +307,8 @@ function ($acl, $type, $field) use (&$rootAcl, &$oid1Acl) { ) ); - $this->translator->expects($this->at(0)) - ->method('trans') - ->with('Class 1') - ->will($this->returnArgument(0)); - $this->translator->expects($this->at(1)) + $this->translator->expects($this->exactly(2)) ->method('trans') - ->with('Class 2') ->will($this->returnArgument(0)); $result = $this->repository->getPrivileges($sid); diff --git a/src/Oro/Bundle/SecurityBundle/Tests/Unit/Authentication/Provider/OrganizationRememberMeAuthenticationProviderTest.php b/src/Oro/Bundle/SecurityBundle/Tests/Unit/Authentication/Provider/OrganizationRememberMeAuthenticationProviderTest.php index b87ae7477de..54a0f9ba6e6 100644 --- a/src/Oro/Bundle/SecurityBundle/Tests/Unit/Authentication/Provider/OrganizationRememberMeAuthenticationProviderTest.php +++ b/src/Oro/Bundle/SecurityBundle/Tests/Unit/Authentication/Provider/OrganizationRememberMeAuthenticationProviderTest.php @@ -6,6 +6,7 @@ use Oro\Bundle\SecurityBundle\Tests\Unit\Acl\Domain\Fixtures\Entity\User; use Oro\Bundle\SecurityBundle\Authentication\Token\OrganizationRememberMeToken; +use Oro\Bundle\SecurityBundle\Authentication\Token\OrganizationRememberMeTokenFactory; use Oro\Bundle\SecurityBundle\Tests\Unit\Acl\Domain\Fixtures\Entity\Organization; use Oro\Bundle\SecurityBundle\Authentication\Provider\OrganizationRememberMeAuthenticationProvider; @@ -25,6 +26,7 @@ public function setUp() { $this->userChecker = $this->getMock('Symfony\Component\Security\Core\User\UserCheckerInterface'); $this->provider = new OrganizationRememberMeAuthenticationProvider($this->userChecker, 'testKey', 'provider'); + $this->provider->setTokenFactory(new OrganizationRememberMeTokenFactory()); } public function testSupports() @@ -43,6 +45,19 @@ public function testSupports() $this->assertFalse($this->provider->supports($token)); } + /** + * @expectedException \Symfony\Component\Security\Core\Exception\AuthenticationException + * @expectedExceptionMessage Token Factory is not set in OrganizationRememberMeAuthenticationProvider. + */ + public function testAuthenticateIfTokenFactoryIsNotSet() + { + $user = new User(1); + $organization = new Organization(2); + $token = new OrganizationRememberMeToken($user, 'provider', 'testKey', $organization); + $provider = new OrganizationRememberMeAuthenticationProvider($this->userChecker, 'testKey', 'provider'); + $provider->authenticate($token); + } + public function testAuthenticate() { $organization = new Organization(2); diff --git a/src/Oro/Bundle/SecurityBundle/Tests/Unit/Authentication/Token/OrganizationRememberMeTokenFactoryTest.php b/src/Oro/Bundle/SecurityBundle/Tests/Unit/Authentication/Token/OrganizationRememberMeTokenFactoryTest.php new file mode 100644 index 00000000000..f366fd687e6 --- /dev/null +++ b/src/Oro/Bundle/SecurityBundle/Tests/Unit/Authentication/Token/OrganizationRememberMeTokenFactoryTest.php @@ -0,0 +1,24 @@ +create($user, 'testProvider', 'testKey', $organization); + + $this->assertInstanceOf('Oro\Bundle\SecurityBundle\Authentication\Token\OrganizationRememberMeToken', $token); + $this->assertEquals($user, $token->getUser()); + $this->assertEquals($organization, $token->getOrganizationContext()); + $this->assertEquals('testProvider', $token->getProviderKey()); + $this->assertEquals('testKey', $token->getKey()); + } +} diff --git a/src/Oro/Bundle/SecurityBundle/Tests/Unit/Authentication/Token/UsernamePasswordOrganizationTokenFactoryTest.php b/src/Oro/Bundle/SecurityBundle/Tests/Unit/Authentication/Token/UsernamePasswordOrganizationTokenFactoryTest.php new file mode 100644 index 00000000000..a20ae07afb1 --- /dev/null +++ b/src/Oro/Bundle/SecurityBundle/Tests/Unit/Authentication/Token/UsernamePasswordOrganizationTokenFactoryTest.php @@ -0,0 +1,25 @@ +create('username', 'credentials', 'testProvider', $organization); + + $this->assertInstanceOf( + 'Oro\Bundle\SecurityBundle\Authentication\Token\UsernamePasswordOrganizationToken', + $token + ); + $this->assertEquals($organization, $token->getOrganizationContext()); + $this->assertEquals('username', $token->getUser()); + $this->assertEquals('credentials', $token->getCredentials()); + $this->assertEquals('testProvider', $token->getProviderKey()); + } +} diff --git a/src/Oro/Bundle/SecurityBundle/Tests/Unit/Owner/OwnerTreeProviderTest.php b/src/Oro/Bundle/SecurityBundle/Tests/Unit/Owner/OwnerTreeProviderTest.php index 6e53c7840f3..6a0abb9f7be 100644 --- a/src/Oro/Bundle/SecurityBundle/Tests/Unit/Owner/OwnerTreeProviderTest.php +++ b/src/Oro/Bundle/SecurityBundle/Tests/Unit/Owner/OwnerTreeProviderTest.php @@ -161,9 +161,6 @@ public function testGetTree() $this->em->expects($this->any()) ->method('getConnection') ->will($this->returnValue($connection)); - $connection->expects($this->any()) - ->method('isConnected') - ->will($this->returnValue(true)); $schemaManager = $this->getMockBuilder('Doctrine\DBAL\Schema\MySqlSchemaManager') ->disableOriginalConstructor() ->getMock(); @@ -171,8 +168,9 @@ public function testGetTree() ->method('getSchemaManager') ->will($this->returnValue($schemaManager)); $schemaManager->expects($this->any()) - ->method('listTableNames') - ->will($this->returnValue(['test'])); + ->method('tablesExist') + ->with('test') + ->will($this->returnValue(true)); $this->treeProvider->warmUpCache(); diff --git a/src/Oro/Bundle/SegmentBundle/Entity/Repository/SegmentSnapshotRepository.php b/src/Oro/Bundle/SegmentBundle/Entity/Repository/SegmentSnapshotRepository.php index 2a4214dc79c..75b6485d3b5 100644 --- a/src/Oro/Bundle/SegmentBundle/Entity/Repository/SegmentSnapshotRepository.php +++ b/src/Oro/Bundle/SegmentBundle/Entity/Repository/SegmentSnapshotRepository.php @@ -134,7 +134,7 @@ protected function getDeleteQueryBuilderByParameters($deleteParams) $returnQueryBuilder = false; foreach ($deleteParams as $params) { - if (empty($params['segmentIds'])) { + if (empty($params['segmentIds']) || empty($params['entityIds'])) { continue; } diff --git a/src/Oro/Bundle/SegmentBundle/Event/WidgetOptionsLoadEvent.php b/src/Oro/Bundle/SegmentBundle/Event/WidgetOptionsLoadEvent.php index 511b33e6e5f..ea9cbfda306 100644 --- a/src/Oro/Bundle/SegmentBundle/Event/WidgetOptionsLoadEvent.php +++ b/src/Oro/Bundle/SegmentBundle/Event/WidgetOptionsLoadEvent.php @@ -11,12 +11,17 @@ class WidgetOptionsLoadEvent extends Event /** @var array */ protected $widgetOptions; + /** @var string|null */ + protected $type; + /** * @param array $widgetOptions + * @param string|null $type */ - public function __construct(array $widgetOptions) + public function __construct(array $widgetOptions, $type = null) { $this->widgetOptions = $widgetOptions; + $this->type = $type; } /** @@ -34,4 +39,12 @@ public function setWidgetOptions(array $widgetOptions) { $this->widgetOptions = $widgetOptions; } + + /** + * @return string|null + */ + public function getWidgetType() + { + return $this->type; + } } diff --git a/src/Oro/Bundle/SegmentBundle/Resources/config/datagrid.yml b/src/Oro/Bundle/SegmentBundle/Resources/config/datagrid.yml index c65c31635b2..9b46ee6cdb6 100644 --- a/src/Oro/Bundle/SegmentBundle/Resources/config/datagrid.yml +++ b/src/Oro/Bundle/SegmentBundle/Resources/config/datagrid.yml @@ -1,8 +1,8 @@ datagrid: oro_segments-grid: + acl_resource: oro_segment_view source: - type: orm - acl_resource: oro_segment_view + type: orm query: select: - s.id diff --git a/src/Oro/Bundle/SegmentBundle/Resources/config/placeholders.yml b/src/Oro/Bundle/SegmentBundle/Resources/config/placeholders.yml new file mode 100644 index 00000000000..6ee0b46990a --- /dev/null +++ b/src/Oro/Bundle/SegmentBundle/Resources/config/placeholders.yml @@ -0,0 +1,8 @@ +placeholders: + segment_criteria_list: + items: + field_condition: ~ + +items: + field_condition: + template: OroSegmentBundle:Segment:field_condition.html.twig diff --git a/src/Oro/Bundle/SegmentBundle/Resources/config/requirejs.yml b/src/Oro/Bundle/SegmentBundle/Resources/config/requirejs.yml index 25b864e348f..d6047e72de4 100644 --- a/src/Oro/Bundle/SegmentBundle/Resources/config/requirejs.yml +++ b/src/Oro/Bundle/SegmentBundle/Resources/config/requirejs.yml @@ -2,4 +2,5 @@ config: paths: 'orosegment/js/app/components/refresh-button': 'bundles/orosegment/js/app/components/refresh-button.js' 'orosegment/js/app/components/segment-component': 'bundles/orosegment/js/app/components/segment-component.js' + 'orosegment/js/app/components/aggregated-field-condition-extension': 'bundles/orosegment/js/app/components/aggregated-field-condition-extension.js' 'orosegment/js/segment-condition': 'bundles/orosegment/js/segment-condition.js' diff --git a/src/Oro/Bundle/SegmentBundle/Resources/public/js/app/components/aggregated-field-condition-extension.js b/src/Oro/Bundle/SegmentBundle/Resources/public/js/app/components/aggregated-field-condition-extension.js new file mode 100644 index 00000000000..18d58297d42 --- /dev/null +++ b/src/Oro/Bundle/SegmentBundle/Resources/public/js/app/components/aggregated-field-condition-extension.js @@ -0,0 +1,29 @@ +define([ + 'jquery', + 'underscore' +], function($, _) { + 'use strict'; + + return { + load: function(segment) { + segment.configureFilters = _.wrap(segment.configureFilters, function(original) { + var $criteria = $(this.options.filters.criteriaList); + + var aggregatedFieldCondition = $criteria.find('[data-criteria=aggregated-condition-item]'); + if (!_.isEmpty(aggregatedFieldCondition)) { + var $itemContainer = $(this.options.column.itemContainer); + var columnsCollection = $itemContainer.data('oroui-itemsManagerTable').options.collection; + + $.extend(true, aggregatedFieldCondition.data('options'), { + fieldChoice: this.options.fieldChoiceOptions, + filters: this.options.metadata.filters, + hierarchy: this.options.metadata.hierarchy, + columnsCollection: columnsCollection + }); + } + + original.apply(this, _.rest(arguments)); + }); + } + }; +}); diff --git a/src/Oro/Bundle/SegmentBundle/Resources/public/js/app/components/segment-component.js b/src/Oro/Bundle/SegmentBundle/Resources/public/js/app/components/segment-component.js index f06bccc07f8..d014cdc385d 100644 --- a/src/Oro/Bundle/SegmentBundle/Resources/public/js/app/components/segment-component.js +++ b/src/Oro/Bundle/SegmentBundle/Resources/public/js/app/components/segment-component.js @@ -492,6 +492,11 @@ define(function(require) { group_type: $el.find(':selected').data('group_type'), group_name: $el.find(':selected').data('group_name') }; + + var returnType = $el.find(':selected').data('return_type'); + if (value && returnType) { + value.return_type = returnType; + } } return value; } diff --git a/src/Oro/Bundle/SegmentBundle/Resources/views/Segment/field_condition.html.twig b/src/Oro/Bundle/SegmentBundle/Resources/views/Segment/field_condition.html.twig new file mode 100644 index 00000000000..26aebb9bc61 --- /dev/null +++ b/src/Oro/Bundle/SegmentBundle/Resources/views/Segment/field_condition.html.twig @@ -0,0 +1,15 @@ +{% set fieldConditionOptions = { + fieldChoice: { + select2: { + placeholder: 'oro.query_designer.condition_builder.choose_entity_field'|trans + }, + fieldsLoaderSelector: '[data-ftid=' ~ params.entity_choice_id ~ 'oro_api_querydesigner_fields_entity]' + } +} %} + +
  • + {{ 'oro.query_designer.condition_builder.criteria.field_condition'|trans }} +
  • diff --git a/src/Oro/Bundle/SegmentBundle/Resources/views/Segment/view.html.twig b/src/Oro/Bundle/SegmentBundle/Resources/views/Segment/view.html.twig index 3cd25950712..42172436033 100644 --- a/src/Oro/Bundle/SegmentBundle/Resources/views/Segment/view.html.twig +++ b/src/Oro/Bundle/SegmentBundle/Resources/views/Segment/view.html.twig @@ -37,8 +37,18 @@ {% block content_data %} {% if gridName is defined and gridName %} - {% set renderParams = renderParams|default({})|merge({enableFullScreenLayout: true}) %} - {{ dataGrid.renderGrid(gridName, params|default({}), renderParams) }} + {% set renderParams = renderParams|default({})|merge({enableFullScreenLayout: true, enableViews: false}) %} + {% set params = params|default({})|merge({ + '_grid_view': { + '_disabled': true + }, + '_tags': { + '_disabled': true + } + }) + %} + + {{ dataGrid.renderGrid(gridName, params, renderParams) }} {% else %}
    diff --git a/src/Oro/Bundle/SegmentBundle/Resources/views/macros.html.twig b/src/Oro/Bundle/SegmentBundle/Resources/views/macros.html.twig index 6eff3740499..c3096497062 100644 --- a/src/Oro/Bundle/SegmentBundle/Resources/views/macros.html.twig +++ b/src/Oro/Bundle/SegmentBundle/Resources/views/macros.html.twig @@ -1,12 +1,4 @@ {% macro query_designer_condition_builder(params) %} - {% set fieldConditionOptions = { - fieldChoice: { - select2: { - placeholder: 'oro.query_designer.condition_builder.choose_entity_field'|trans - }, - fieldsLoaderSelector: '[data-ftid=' ~ params.entity_choice_id ~ 'oro_api_querydesigner_fields_entity]' - } - } %} {% set segmentConditionOptions = { segmentChoice: { select2: { @@ -35,12 +27,6 @@
    {{ 'oro.query_designer.condition_builder.criteria.drag_hint'|trans }}
      {% placeholder segment_criteria_list with {params: params} %} -
    • - {{ 'oro.query_designer.condition_builder.criteria.field_condition'|trans }} -
    diff --git a/src/Oro/Bundle/SegmentBundle/Twig/SegmentExtension.php b/src/Oro/Bundle/SegmentBundle/Twig/SegmentExtension.php index ed15ca4c5e5..37ed62dfa28 100644 --- a/src/Oro/Bundle/SegmentBundle/Twig/SegmentExtension.php +++ b/src/Oro/Bundle/SegmentBundle/Twig/SegmentExtension.php @@ -41,16 +41,17 @@ public function getFunctions() /** * @param array $widgetOptions + * @param string|null $type * * @return array */ - public function updateSegmentWidgetOptions(array $widgetOptions) + public function updateSegmentWidgetOptions(array $widgetOptions, $type = null) { if (!$this->dispatcher->hasListeners(WidgetOptionsLoadEvent::EVENT_NAME)) { return $widgetOptions; } - $event = new WidgetOptionsLoadEvent($widgetOptions); + $event = new WidgetOptionsLoadEvent($widgetOptions, $type); $this->dispatcher->dispatch(WidgetOptionsLoadEvent::EVENT_NAME, $event); return $event->getWidgetOptions(); diff --git a/src/Oro/Bundle/TagBundle/Autocomplete/SearchHandler.php b/src/Oro/Bundle/TagBundle/Autocomplete/SearchHandler.php new file mode 100644 index 00000000000..d1a1042d59b --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Autocomplete/SearchHandler.php @@ -0,0 +1,54 @@ +securityFacade = $securityFacade; + } + + /** + * @param string $search + * @param int $firstResult + * @param int $maxResults + * + * @return array + */ + protected function searchIds($search, $firstResult, $maxResults) + { + $ids = parent::searchIds($search, $firstResult, $maxResults); + // Need to make additional query cause of Mysql Full-Text Search limitation and databases stop words. + // http://dev.mysql.com/doc/refman/5.0/en/server-system-variables.html#sysvar_ft_min_word_len + // http://dev.mysql.com/doc/refman/5.0/en/fulltext-stopwords.html + // http://www.postgresql.org/docs/9.1/static/textsearch-dictionaries.html + $object = $this->entityRepository->findOneBy( + [ + 'name' => $search, + 'organization' => $this->securityFacade->getOrganization() + ] + ); + if ($object !== null) { + $id = $object->getId(); + if (!in_array($id, $ids, true)) { + $ids[] = $id; + } + } + + return $ids; + } +} diff --git a/src/Oro/Bundle/TagBundle/Controller/Api/Rest/TaggableController.php b/src/Oro/Bundle/TagBundle/Controller/Api/Rest/TaggableController.php index 86bf165b8e9..794e82d027a 100644 --- a/src/Oro/Bundle/TagBundle/Controller/Api/Rest/TaggableController.php +++ b/src/Oro/Bundle/TagBundle/Controller/Api/Rest/TaggableController.php @@ -9,6 +9,7 @@ use FOS\RestBundle\Controller\Annotations\NamePrefix; use FOS\RestBundle\Controller\Annotations\RouteResource; use FOS\RestBundle\Controller\Annotations\Post; +use FOS\RestBundle\Util\Codes; use Oro\Bundle\SoapBundle\Controller\Api\Rest\RestController; @@ -19,7 +20,7 @@ class TaggableController extends RestController { /** - * Sets tags to the target entity. + * Sets tags to the target entity and return them. * * @param string $entity The type of the target entity. * @param int $entityId The id of the target entity. @@ -27,7 +28,7 @@ class TaggableController extends RestController * @Post("/tags/{entity}/{entityId}") * * @ApiDoc( - * description="Sets tags to the target entity", + * description="Sets tags to the target entity and return them", * resource=true * ) * @@ -38,7 +39,28 @@ public function postAction($entity, $entityId) $manager = $this->getManager(); $manager->setClass($manager->resolveEntityClass($entity)); - return $this->handleUpdateRequest($entityId); + $entity = $this->getManager()->find($entityId); + + if ($entity) { + $entity = $this->processForm($entity); + if ($entity) { + $result = $this->get('oro_tag.tag.manager')->getPreparedArray($entity); + + // Returns tags for the updated entity. + return $this->buildResponse( + ['tags' => $result], + self::ACTION_READ, + ['result' => $result], + Codes::HTTP_OK + ); + } else { + $view = $this->view($this->getForm(), Codes::HTTP_BAD_REQUEST); + } + } else { + $view = $this->view(null, Codes::HTTP_NOT_FOUND); + } + + return $this->buildResponse($view, self::ACTION_UPDATE, ['id' => $entityId, 'entity' => $entity]); } /** diff --git a/src/Oro/Bundle/TagBundle/Entity/Tag.php b/src/Oro/Bundle/TagBundle/Entity/Tag.php index 51d2cfac3f7..ce8879da21b 100644 --- a/src/Oro/Bundle/TagBundle/Entity/Tag.php +++ b/src/Oro/Bundle/TagBundle/Entity/Tag.php @@ -15,7 +15,12 @@ /** * Tag * - * @ORM\Table(name="oro_tag_tag") + * @ORM\Table( + * name="oro_tag_tag", + * indexes={ + * @ORM\Index(name="name_organization_idx", columns={"name", "organization_id"}) + * } + * ) * @ORM\HasLifecycleCallbacks * @ORM\Entity(repositoryClass="Oro\Bundle\TagBundle\Entity\Repository\TagRepository") * @Config( @@ -227,6 +232,16 @@ public function getTagging() return $this->tagging; } + /** + * @param Tagging $tagging + */ + public function addTagging(Tagging $tagging) + { + if (!$this->tagging->contains($tagging)) { + $this->tagging->add($tagging); + } + } + /** * To string * diff --git a/src/Oro/Bundle/TagBundle/Entity/Tagging.php b/src/Oro/Bundle/TagBundle/Entity/Tagging.php index b41d3ddbf7c..62cc4fd5fce 100644 --- a/src/Oro/Bundle/TagBundle/Entity/Tagging.php +++ b/src/Oro/Bundle/TagBundle/Entity/Tagging.php @@ -136,6 +136,7 @@ public function getId() public function setTag(Tag $tag) { $this->tag = $tag; + $this->tag->addTagging($this); } /** diff --git a/src/Oro/Bundle/TagBundle/Grid/AbstractTagsExtension.php b/src/Oro/Bundle/TagBundle/Grid/AbstractTagsExtension.php index 8839cbae99a..65ff7ba82f8 100644 --- a/src/Oro/Bundle/TagBundle/Grid/AbstractTagsExtension.php +++ b/src/Oro/Bundle/TagBundle/Grid/AbstractTagsExtension.php @@ -5,12 +5,11 @@ use Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration; use Oro\Bundle\DataGridBundle\Datasource\ResultRecord; use Oro\Bundle\DataGridBundle\Extension\AbstractExtension; -use Oro\Bundle\EntityBundle\ORM\EntityClassResolver; +use Oro\Bundle\DataGridBundle\Tools\GridConfigurationHelper; use Oro\Bundle\TagBundle\Entity\TagManager; abstract class AbstractTagsExtension extends AbstractExtension { - const GRID_EXTEND_ENTITY_PATH = '[extended_entity_name]'; const GRID_FROM_PATH = '[source][query][from]'; const GRID_COLUMN_ALIAS_PATH = '[source][query_config][column_aliases]'; const GRID_FILTERS_PATH = '[filters][columns]'; @@ -22,17 +21,22 @@ abstract class AbstractTagsExtension extends AbstractExtension /** @var TagManager */ protected $tagManager; - /** @var EntityClassResolver */ - protected $entityClassResolver; + /** @var GridConfigurationHelper */ + protected $gridConfigurationHelper; + + /** @var string|null */ + protected $entityClassName; /** - * @param TagManager $tagManager - * @param EntityClassResolver $entityClassResolver + * @param TagManager $tagManager + * @param GridConfigurationHelper $gridConfigurationHelper */ - public function __construct(TagManager $tagManager, EntityClassResolver $entityClassResolver) - { - $this->tagManager = $tagManager; - $this->entityClassResolver = $entityClassResolver; + public function __construct( + TagManager $tagManager, + GridConfigurationHelper $gridConfigurationHelper + ) { + $this->tagManager = $tagManager; + $this->gridConfigurationHelper = $gridConfigurationHelper; } /** @@ -56,19 +60,13 @@ protected function isReportOrSegmentGrid(DatagridConfiguration $config) * * @return string|null */ - protected function getEntityClassName(DatagridConfiguration $config) + protected function getEntity(DatagridConfiguration $config) { - $entityClassName = $config->offsetGetByPath(self::GRID_EXTEND_ENTITY_PATH); - if (!$entityClassName) { - $from = $config->offsetGetByPath(self::GRID_FROM_PATH); - if (!$from) { - return null; - } - - $entityClassName = $this->entityClassResolver->getEntityClass($from[0]['table']); + if ($this->entityClassName === null) { + $this->entityClassName = $this->gridConfigurationHelper->getEntity($config); } - return $entityClassName; + return $this->entityClassName; } protected function getTagsForEntityClass($entityClass, array $ids) diff --git a/src/Oro/Bundle/TagBundle/Grid/TagsExtension.php b/src/Oro/Bundle/TagBundle/Grid/TagsExtension.php index aa5534724bd..affa1eb31ce 100644 --- a/src/Oro/Bundle/TagBundle/Grid/TagsExtension.php +++ b/src/Oro/Bundle/TagBundle/Grid/TagsExtension.php @@ -5,7 +5,7 @@ use Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration; use Oro\Bundle\DataGridBundle\Datagrid\Common\ResultsObject; use Oro\Bundle\DataGridBundle\Datasource\ResultRecordInterface; -use Oro\Bundle\EntityBundle\ORM\EntityClassResolver; +use Oro\Bundle\DataGridBundle\Tools\GridConfigurationHelper; use Oro\Bundle\EntityBundle\Tools\EntityRoutingHelper; use Oro\Bundle\TagBundle\Entity\TagManager; use Oro\Bundle\TagBundle\Helper\TaggableHelper; @@ -13,6 +13,9 @@ class TagsExtension extends AbstractTagsExtension { + const TAGS_ROOT_PARAM = '_tags'; + const DISABLED_PARAM = '_disabled'; + const COLUMN_NAME = 'tags'; /** @var TaggableHelper */ @@ -25,20 +28,20 @@ class TagsExtension extends AbstractTagsExtension protected $securityFacade; /** - * @param TagManager $tagManager - * @param EntityClassResolver $resolver - * @param TaggableHelper $helper - * @param EntityRoutingHelper $entityRoutingHelper - * @param SecurityFacade $securityFacade + * @param TagManager $tagManager + * @param GridConfigurationHelper $gridConfigurationHelper + * @param TaggableHelper $helper + * @param EntityRoutingHelper $entityRoutingHelper + * @param SecurityFacade $securityFacade */ public function __construct( TagManager $tagManager, - EntityClassResolver $resolver, + GridConfigurationHelper $gridConfigurationHelper, TaggableHelper $helper, EntityRoutingHelper $entityRoutingHelper, SecurityFacade $securityFacade ) { - parent::__construct($tagManager, $resolver); + parent::__construct($tagManager, $gridConfigurationHelper); $this->taggableHelper = $helper; $this->entityRoutingHelper = $entityRoutingHelper; @@ -58,13 +61,44 @@ public function getPriority() */ public function isApplicable(DatagridConfiguration $config) { - $className = $this->getEntityClassName($config); - return + !$this->isDisabled() && !$this->isReportOrSegmentGrid($config) && - $className && - $this->taggableHelper->isTaggable($className) && + $this->isGridRootEntityTaggable($config) && null !== $config->offsetGetByPath(self::PROPERTY_ID_PATH) && + $this->isAccessGranted(); + } + + /** + * @return bool + */ + protected function isDisabled() + { + $tagParameters = $this->getParameters()->get(self::TAGS_ROOT_PARAM); + + return + $tagParameters && + !empty($tagParameters[self::DISABLED_PARAM]); + } + + /** + * @param DatagridConfiguration $configuration + * + * @return bool + */ + protected function isGridRootEntityTaggable(DatagridConfiguration $configuration) + { + $className = $this->getEntity($configuration); + + return $className && $this->taggableHelper->isTaggable($className); + } + + /** + * @return bool + */ + protected function isAccessGranted() + { + return null !== $this->securityFacade->getToken() && $this->securityFacade->isGranted('oro_tag_view'); } @@ -95,7 +129,7 @@ public function processConfigs(DatagridConfiguration $config) */ protected function getColumnDefinition(DatagridConfiguration $config) { - $className = $this->getEntityClassName($config); + $className = $this->getEntity($config); $urlSafeClassName = $this->entityRoutingHelper->getUrlSafeClassName($className); $permissions = [ @@ -151,12 +185,12 @@ protected function getColumnDefinition(DatagridConfiguration $config) */ protected function getColumnFilterDefinition(DatagridConfiguration $config) { - $className = $this->getEntityClassName($config); - $from = $config->offsetGetByPath(self::GRID_FROM_PATH); + $className = $this->getEntity($config); + $alias = $this->gridConfigurationHelper->getEntityRootAlias($config); return [ 'type' => 'tag', - 'data_name' => $from[0]['alias'] .'.id', + 'data_name' => sprintf('%s.%s', $alias, 'id'), 'label' => 'oro.tag.entity_plural_label', 'enabled' => $this->taggableHelper->isEnableGridFilter($className), 'options' => [ @@ -175,7 +209,7 @@ public function visitResult(DatagridConfiguration $config, ResultsObject $result $rows = (array)$result->offsetGetOr('data', []); $idField = 'id'; $tags = $this->getTagsForEntityClass( - $this->getEntityClassName($config), + $this->getEntity($config), $this->extractEntityIds($rows, $idField) ); diff --git a/src/Oro/Bundle/TagBundle/Grid/TagsReportExtension.php b/src/Oro/Bundle/TagBundle/Grid/TagsReportExtension.php index 218c5eab7b3..7c92e0fc15a 100644 --- a/src/Oro/Bundle/TagBundle/Grid/TagsReportExtension.php +++ b/src/Oro/Bundle/TagBundle/Grid/TagsReportExtension.php @@ -5,7 +5,7 @@ use Oro\Bundle\DataGridBundle\Datagrid\Common\DatagridConfiguration; use Oro\Bundle\DataGridBundle\Datagrid\Common\ResultsObject; use Oro\Bundle\DataGridBundle\Datasource\ResultRecordInterface; -use Oro\Bundle\EntityBundle\ORM\EntityClassResolver; +use Oro\Bundle\DataGridBundle\Tools\GridConfigurationHelper; use Oro\Bundle\EntityBundle\Tools\EntityRoutingHelper; use Oro\Bundle\QueryDesignerBundle\QueryDesigner\JoinIdentifierHelper; use Oro\Bundle\TagBundle\Entity\TagManager; @@ -24,18 +24,18 @@ class TagsReportExtension extends AbstractTagsExtension protected $joinIdentifierHelper; /** - * @param TagManager $tagManager - * @param EntityClassResolver $resolver - * @param TaggableHelper $helper - * @param EntityRoutingHelper $entityRoutingHelper + * @param TagManager $tagManager + * @param GridConfigurationHelper $gridConfigurationHelper + * @param TaggableHelper $helper + * @param EntityRoutingHelper $entityRoutingHelper */ public function __construct( TagManager $tagManager, - EntityClassResolver $resolver, + GridConfigurationHelper $gridConfigurationHelper, TaggableHelper $helper, EntityRoutingHelper $entityRoutingHelper ) { - parent::__construct($tagManager, $resolver); + parent::__construct($tagManager, $gridConfigurationHelper); $this->taggableHelper = $helper; $this->entityRoutingHelper = $entityRoutingHelper; @@ -172,7 +172,7 @@ protected function getTagColumnDefinitions(DatagridConfiguration $config) $field = $joinIdentifierHelper->getFieldName($key); if ($field === TagVirtualFieldProvider::TAG_FIELD) { // get entity class from relations aliases if tag_field configured for relations - $entityClassName = $joinIdentifierHelper->getEntityClassName($key)?:parent::getEntityClassName($config); + $entityClassName = $joinIdentifierHelper->getEntityClassName($key) ?: parent::getEntity($config); $tagColumns[] = [ 'idAlias' => $alias, 'entityClass' => $entityClassName diff --git a/src/Oro/Bundle/TagBundle/Migrations/Schema/OroTagBundleInstaller.php b/src/Oro/Bundle/TagBundle/Migrations/Schema/OroTagBundleInstaller.php index c6001bbc986..34bae964d8d 100644 --- a/src/Oro/Bundle/TagBundle/Migrations/Schema/OroTagBundleInstaller.php +++ b/src/Oro/Bundle/TagBundle/Migrations/Schema/OroTagBundleInstaller.php @@ -17,7 +17,7 @@ class OroTagBundleInstaller implements Installation */ public function getMigrationVersion() { - return 'v1_6'; + return 'v1_7'; } /** @@ -69,6 +69,7 @@ protected function createOroTagTagTable(Schema $schema) $table->addColumn('name', 'string', ['length' => 50]); $table->addColumn('created', 'datetime', ['comment' => '(DC2Type:datetime)']); $table->addColumn('updated', 'datetime', ['comment' => '(DC2Type:datetime)']); + $table->addIndex(['name', 'organization_id'], 'name_organization_idx', []); $table->addIndex(['organization_id'], 'idx_caf0db5732c8a3de', []); $table->setPrimaryKey(['id']); $table->addIndex(['user_owner_id'], 'idx_caf0db579eb185f9', []); diff --git a/src/Oro/Bundle/TagBundle/Migrations/Schema/v1_7/AddTagNameOrganizationIndex.php b/src/Oro/Bundle/TagBundle/Migrations/Schema/v1_7/AddTagNameOrganizationIndex.php new file mode 100644 index 00000000000..1d2715ca024 --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Migrations/Schema/v1_7/AddTagNameOrganizationIndex.php @@ -0,0 +1,20 @@ +getTable('oro_tag_tag'); + $table->addIndex(['name', 'organization_id'], 'name_organization_idx', []); + } +} diff --git a/src/Oro/Bundle/TagBundle/Resources/config/assets.yml b/src/Oro/Bundle/TagBundle/Resources/config/assets.yml index 04869b0550e..ee834f8473c 100644 --- a/src/Oro/Bundle/TagBundle/Resources/config/assets.yml +++ b/src/Oro/Bundle/TagBundle/Resources/config/assets.yml @@ -1,5 +1,3 @@ css: 'orotag': - - 'bundles/orotag/css/tag-grid.less' - - 'bundles/orotag/css/tags-container.less' - - 'bundles/orotag/css/form.less' + - 'bundles/orotag/css/main.less' diff --git a/src/Oro/Bundle/TagBundle/Resources/config/datagrid.yml b/src/Oro/Bundle/TagBundle/Resources/config/datagrid.yml index cdbc22ab1be..63a7a8944f2 100644 --- a/src/Oro/Bundle/TagBundle/Resources/config/datagrid.yml +++ b/src/Oro/Bundle/TagBundle/Resources/config/datagrid.yml @@ -3,8 +3,8 @@ datagrid: options: entityHint: tag entity_pagination: true + acl_resource: oro_tag_view source: - acl_resource: oro_tag_view type: orm query: select: @@ -90,8 +90,8 @@ datagrid: tag-results-grid: options: entityHint: result + acl_resource: oro_tag_view source: - acl_resource: oro_tag_view type: orm query: select: diff --git a/src/Oro/Bundle/TagBundle/Resources/config/entity_config.yml b/src/Oro/Bundle/TagBundle/Resources/config/entity_config.yml index a3b8c2a5dfa..468c8880fcd 100644 --- a/src/Oro/Bundle/TagBundle/Resources/config/entity_config.yml +++ b/src/Oro/Bundle/TagBundle/Resources/config/entity_config.yml @@ -13,7 +13,7 @@ oro_entity_config: options: block: other required: true - label: oro.tag.enabled + label: oro.tag.config.enabled # this attribute can be used to prohibit changing the tag state (no matter whether it is enabled or not) # for the entity if TRUE than the current state cannot be changed diff --git a/src/Oro/Bundle/TagBundle/Resources/config/form.yml b/src/Oro/Bundle/TagBundle/Resources/config/form.yml index 58792bc2e4b..a3c33492f83 100644 --- a/src/Oro/Bundle/TagBundle/Resources/config/form.yml +++ b/src/Oro/Bundle/TagBundle/Resources/config/form.yml @@ -76,11 +76,12 @@ services: # Autocomplete oro_tag.autocomplete.tag.search_handler: - class: %oro_tag.autocomplete.tag.search_handler.class% + class: Oro\Bundle\TagBundle\Autocomplete\SearchHandler parent: oro_form.autocomplete.search_handler arguments: - %oro_tag.tag.entity.class% - ["name"] + - @oro_security.security_facade tags: - { name: oro_form.autocomplete.search_handler, alias: tags, acl_resource: oro_tag_assign_unassign } diff --git a/src/Oro/Bundle/TagBundle/Resources/config/oro/twig.yml b/src/Oro/Bundle/TagBundle/Resources/config/oro/twig.yml new file mode 100644 index 00000000000..d6ed08cb502 --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Resources/config/oro/twig.yml @@ -0,0 +1,2 @@ +bundles: + - OroTagBundle:Form/Include:fields.html.twig diff --git a/src/Oro/Bundle/TagBundle/Resources/config/services.yml b/src/Oro/Bundle/TagBundle/Resources/config/services.yml index ce1f78d0a27..f2052d28a6f 100644 --- a/src/Oro/Bundle/TagBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/TagBundle/Resources/config/services.yml @@ -120,7 +120,7 @@ services: class: Oro\Bundle\TagBundle\Grid\TagsExtension arguments: - @oro_tag.tag.manager - - @oro_entity.orm.entity_class_resolver + - @oro_datagrid.grid_configuration.helper - @oro_tag.helper.taggable_helper - @oro_entity.routing_helper - @oro_security.security_facade @@ -131,7 +131,7 @@ services: class: Oro\Bundle\TagBundle\Grid\TagsReportExtension arguments: - @oro_tag.tag.manager - - @oro_entity.orm.entity_class_resolver + - @oro_datagrid.grid_configuration.helper - @oro_tag.helper.taggable_helper - @oro_entity.routing_helper tags: diff --git a/src/Oro/Bundle/TagBundle/Resources/public/css/config.less b/src/Oro/Bundle/TagBundle/Resources/public/css/config.less new file mode 100644 index 00000000000..ffb60da6c33 --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Resources/public/css/config.less @@ -0,0 +1,5 @@ +.alert-danger.tags-config { + width: 294px; + margin: 10px 0; + float: none; +} diff --git a/src/Oro/Bundle/TagBundle/Resources/public/css/form.less b/src/Oro/Bundle/TagBundle/Resources/public/css/form.less index 024625a7cb6..82ee6d2c4a3 100644 --- a/src/Oro/Bundle/TagBundle/Resources/public/css/form.less +++ b/src/Oro/Bundle/TagBundle/Resources/public/css/form.less @@ -1,11 +1,3 @@ -.tags-view-form-container { - margin-top: -5px; - margin-left: -2px; - min-height: 27px; - .inline-editable-wrapper { - padding-left: 2px; - } -} .tags-select-editor { .select2-container { width: calc(~"100% - 62px") !important; diff --git a/src/Oro/Bundle/TagBundle/Resources/public/css/main.less b/src/Oro/Bundle/TagBundle/Resources/public/css/main.less new file mode 100644 index 00000000000..74b39e2d149 --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Resources/public/css/main.less @@ -0,0 +1,8 @@ +@import "oroui/css/less/mixins"; + +@import "./tag-grid"; +@import "./tags-container"; +@import "./form"; +@import "./config"; + +@import "./mobile/main"; diff --git a/src/Oro/Bundle/TagBundle/Resources/public/css/mobile/main.less b/src/Oro/Bundle/TagBundle/Resources/public/css/mobile/main.less new file mode 100644 index 00000000000..d23dba9b2a4 --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Resources/public/css/mobile/main.less @@ -0,0 +1,3 @@ +.mobile-version { + @import "./tags-container"; +} diff --git a/src/Oro/Bundle/TagBundle/Resources/public/css/mobile/tags-container.less b/src/Oro/Bundle/TagBundle/Resources/public/css/mobile/tags-container.less new file mode 100644 index 00000000000..7fd6f6f14d1 --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Resources/public/css/mobile/tags-container.less @@ -0,0 +1,6 @@ +.inline-actions-element { + .tags-container { + top: -3px; + position: relative; + } +} diff --git a/src/Oro/Bundle/TagBundle/Resources/public/css/tags-container.less b/src/Oro/Bundle/TagBundle/Resources/public/css/tags-container.less index f42a2aee375..b232e76e29a 100644 --- a/src/Oro/Bundle/TagBundle/Resources/public/css/tags-container.less +++ b/src/Oro/Bundle/TagBundle/Resources/public/css/tags-container.less @@ -1,24 +1,27 @@ .tags-container { - line-height: 27px; + line-height: 23px; } .table td.tags-container { - line-height: 27px; - padding-top: 1px; - padding-bottom: 1px; + line-height: 24px; + padding-top: 2px; + padding-bottom: 2px; } .tags-container__tag-entry { - border: 1px solid #ddd; - background: white; - border-radius: 3px; + color: @tagItemColor; + background-color: @tagItemBackground; + border-radius: 10px; margin-right: 0px; - font-size: 13px; - line-height: 13px; - padding: 4px 6px; + font-size: 12px; + line-height: 20px; + padding: 1px 8px; display: inline-block; - vertical-align: baseline; + vertical-align: bottom; + &:hover, &:focus { + color: @tagItemColor; + outline-style: none; + } } .tags-container__tag-entry_is-my { - border-color: #bbbbee; - background: #fafaff; + background-color: @tagItemBackground; } diff --git a/src/Oro/Bundle/TagBundle/Resources/public/js/app/views/editor/tags-editor-view.js b/src/Oro/Bundle/TagBundle/Resources/public/js/app/views/editor/tags-editor-view.js index 4525883f115..d1544859a6e 100644 --- a/src/Oro/Bundle/TagBundle/Resources/public/js/app/views/editor/tags-editor-view.js +++ b/src/Oro/Bundle/TagBundle/Resources/public/js/app/views/editor/tags-editor-view.js @@ -84,6 +84,7 @@ define(function(require) { initialize: function(options) { TagsEditorView.__super__.initialize.apply(this, arguments); + this.listenTo(this.autocompleteApiAccessor, 'cache:clear', this.onCacheClear); this.permissions = options.permissions || {}; }, @@ -139,8 +140,8 @@ define(function(require) { return _this.isLoading ? __('oro.tag.inline_editing.loading') : (_this.isCurrentTagSelected() ? - __('oro.tag.inline_editing.existing_tag') : - __('oro.tag.inline_editing.no_matches') + __('oro.tag.inline_editing.existing_tag') : + __('oro.tag.inline_editing.no_matches') ); }, initSelection: function(element, callback) { @@ -155,23 +156,12 @@ define(function(require) { // immediately show first item _this.showResults(); } - options.callback = function(data) { - _this.currentData = data; - if (data.page === 1) { - _this.firstPageData = data; - } - _this.isLoading = false; - _this.showResults(); - }; + options.callback = _.bind(_this.commonDataCallback, _this); if (_this.currentRequest && _this.currentRequest.term !== '' && _this.currentRequest.state() !== 'resolved') { _this.currentRequest.abort(); } - var autoCompleteUrlParameters = _.extend(_this.model.toJSON(), { - term: options.term, - page: options.page, - per_page: _this.perPage - }); + var autoCompleteUrlParameters = _this.buildAutoCompleteUrlParameters(); if (options.term !== '' && !_this.autocompleteApiAccessor.isCacheExistsFor(autoCompleteUrlParameters)) { _this.debouncedMakeRequest(options, autoCompleteUrlParameters); @@ -182,6 +172,32 @@ define(function(require) { }; }, + buildAutoCompleteUrlParameters: function() { + return _.extend(this.model.toJSON(), { + term: this.currentTerm, + page: this.currentPage, + per_page: this.perPage + }); + }, + + commonDataCallback: function(data) { + this.currentData = data; + if (data.page === 1) { + this.firstPageData = data; + } + this.isLoading = false; + this.showResults(); + }, + + onCacheClear: function() { + this.makeRequest({ + callback: _.bind(this.commonDataCallback, this), + term: this.currentTerm, + page: this.currentPage, + per_page: this.perPage + }, this.buildAutoCompleteUrlParameters()); + }, + isCurrentTagSelected: function() { var select2Data = this.$('.select2-container').select2('data'); for (var i = 0; i < select2Data.length; i++) { @@ -197,22 +213,20 @@ define(function(require) { var data; if (this.currentPage === 1) { data = $.extend({}, this.firstPageData); - data.results = this.filterTermFromResults(this.currentTerm, data.results); - if (this.currentPage === 1) { - if (this.permissions.oro_tag_create && this.isValidTerm(this.currentTerm)) { - if (this.firstPageData.term === this.currentTerm) { - data.results.unshift({ - id: this.currentTerm, - label: this.currentTerm, - isNew: true, - owner: true - }); - } - } else { - if (this.firstPageData.isDummy) { - // do not update list until choices will be loaded - return; - } + if (this.permissions.oro_tag_create && this.isValidTerm(this.currentTerm)) { + if (this.firstPageData.term === this.currentTerm && + -1 === this.indexOfTermInResults(this.currentTerm, data.results)) { + data.results.unshift({ + id: this.currentTerm, + label: this.currentTerm, + isNew: true, + owner: true + }); + } + } else { + if (this.firstPageData.isDummy) { + // do not update list until choices will be loaded + return; } } data.results.sort(_.bind(this.tagSortCallback, this)); @@ -223,6 +237,16 @@ define(function(require) { this.currentCallback(data); }, + indexOfTermInResults: function(term, results) { + for (var i = 0; i < results.length; i++) { + var result = results[i]; + if (result.label === term) { + return i; + } + } + return -1; + }, + filterTermFromResults: function(term, results) { results = _.clone(results); for (var i = 0; i < results.length; i++) { @@ -236,7 +260,8 @@ define(function(require) { }, tagSortCallback: function(a, b) { - return this.getTermSimilarity(a.label) - this.getTermSimilarity(b.label); + var firstCondition = this.getTermSimilarity(a.label) - this.getTermSimilarity(b.label); + return firstCondition !== 0 ? firstCondition : a.label.length - b.label.length; }, getTermSimilarity: function(term) { @@ -301,7 +326,13 @@ define(function(require) { return data; } }, { - processMetadata: AbstractRelationEditorView.processMetadata + processMetadata: AbstractRelationEditorView.processMetadata, + processSavePromise: function(promise, metadata) { + promise.done(function() { + metadata.inline_editing.autocomplete_api_accessor.instance.clearCache(); + }); + return promise; + } }); return TagsEditorView; diff --git a/src/Oro/Bundle/TagBundle/Resources/public/js/app/views/viewer/tags-view.js b/src/Oro/Bundle/TagBundle/Resources/public/js/app/views/viewer/tags-view.js index 006ce0d5307..8f32a48cf80 100644 --- a/src/Oro/Bundle/TagBundle/Resources/public/js/app/views/viewer/tags-view.js +++ b/src/Oro/Bundle/TagBundle/Resources/public/js/app/views/viewer/tags-view.js @@ -3,7 +3,6 @@ define(function(require) { 'use strict'; var BaseView = require('oroui/js/app/views/base/view'); var _ = require('underscore'); - var routing = require('routing'); /** * Tags view, able to handle tags array in model. @@ -28,6 +27,7 @@ define(function(require) { * @exports TagsView */ var TagsView = BaseView.extend(/** @exports TagsView.prototype */{ + showDefault: true, template: require('tpl!orotag/templates/viewer/tags-view.html'), listen: { 'change model': 'render' @@ -38,16 +38,10 @@ define(function(require) { }, getTemplateData: function() { var tags = this.model.get(this.fieldName); - tags = _.map(tags, function(tag) { - tag.url = routing.generate('oro_tag_search', { - id: tag.id - }); - return tag; - }); tags = _.sortBy(tags, this.tagSortCallback); return { tags: tags, - showDefault: true + showDefault: this.showDefault }; }, tagSortCallback: function(a, b) { diff --git a/src/Oro/Bundle/TagBundle/Resources/public/js/datagrid/cell/tags-cell.js b/src/Oro/Bundle/TagBundle/Resources/public/js/datagrid/cell/tags-cell.js index e02c79dc40c..3c2360c0e69 100644 --- a/src/Oro/Bundle/TagBundle/Resources/public/js/datagrid/cell/tags-cell.js +++ b/src/Oro/Bundle/TagBundle/Resources/public/js/datagrid/cell/tags-cell.js @@ -4,6 +4,7 @@ define(function(require) { var TagsCell; var Backgrid = require('backgrid'); var _ = require('underscore'); + var routing = require('routing'); var TagsView = require('orotag/js/app/views/viewer/tags-view'); /** @@ -27,6 +28,8 @@ define(function(require) { 'getTemplateData', 'render' ]), { + + showDefault: false, /** * @property {string} */ @@ -40,6 +43,17 @@ define(function(require) { initialize: function() { Backgrid.StringCell.__super__.initialize.apply(this, arguments); this.fieldName = this.column.get('name'); + //TODO move url generation to server side + var tags = this.model.get(this.fieldName); + tags = _.map(tags, function(tag) { + if (!tag.hasOwnProperty('url')) { + tag.url = routing.generate('oro_tag_search', { + id: tag.id + }); + } + return tag; + }); + this.model.set(this.fieldName, tags); } }) ); diff --git a/src/Oro/Bundle/TagBundle/Resources/public/templates/viewer/tags-view.html b/src/Oro/Bundle/TagBundle/Resources/public/templates/viewer/tags-view.html index 82d8322f7ba..f2b5ce593ae 100644 --- a/src/Oro/Bundle/TagBundle/Resources/public/templates/viewer/tags-view.html +++ b/src/Oro/Bundle/TagBundle/Resources/public/templates/viewer/tags-view.html @@ -1,4 +1,4 @@ -
    + <% if (tags.length > 0) { %> <% _.each(tags , function(tag) { %> <%= _.__('N/A') %> <% } %> -
    + diff --git a/src/Oro/Bundle/TagBundle/Resources/translations/messages.en.yml b/src/Oro/Bundle/TagBundle/Resources/translations/messages.en.yml index 0f4ba231b4f..e472e5ea422 100644 --- a/src/Oro/Bundle/TagBundle/Resources/translations/messages.en.yml +++ b/src/Oro/Bundle/TagBundle/Resources/translations/messages.en.yml @@ -11,7 +11,7 @@ oro: tag.saved.message: "Tag saved" form: choose_tag: "Choose a tag" - choose_or_create_tag: "Select an existing tag or create new" + choose_or_create_tag: "Select an existing tag or create a new one" datagrid: usage_count: Usage Count search: Search by tag @@ -45,4 +45,7 @@ oro: tags_label: Tags - enabled: Enable tags + config: + enabled: Enable tags + disable.alert: Disabling Tags will irreversibly erase all existing tags + diff --git a/src/Oro/Bundle/TagBundle/Resources/views/Form/Include/fields.html.twig b/src/Oro/Bundle/TagBundle/Resources/views/Form/Include/fields.html.twig new file mode 100644 index 00000000000..cfd058af7bf --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Resources/views/Form/Include/fields.html.twig @@ -0,0 +1,9 @@ +{% block oro_tag_config_choice_widget %} + {{ block('choice_widget') }} + + {% if(value) %} +
    + {{ 'oro.tag.config.disable.alert'|trans }} +
    + {% endif %} +{% endblock %} diff --git a/src/Oro/Bundle/TagBundle/Resources/views/macros.html.twig b/src/Oro/Bundle/TagBundle/Resources/views/macros.html.twig index 9a7899a3973..e07637dfaf5 100644 --- a/src/Oro/Bundle/TagBundle/Resources/views/macros.html.twig +++ b/src/Oro/Bundle/TagBundle/Resources/views/macros.html.twig @@ -1,7 +1,7 @@ {% macro renderView(entity) %} {% set tagCloudElId = 'tags-' ~ random() %} {% import 'OroUIBundle::macros.html.twig' as UI %} -
    assertEmptyResponseStatusCodeEquals($this->client->getResponse(), 204); + + $result = $this->getJsonResponseContent($this->client->getResponse(), 200); + $this->assertNotEmpty($result); + $this->assertCount(2, $result['tags'], 'Result should contains information about 2 tags.'); } } diff --git a/src/Oro/Bundle/TestFrameworkBundle/Pages/AbstractPageGrid.php b/src/Oro/Bundle/TestFrameworkBundle/Pages/AbstractPageGrid.php index 1d2eb22ead2..6d8a74918f2 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Pages/AbstractPageGrid.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Pages/AbstractPageGrid.php @@ -501,4 +501,25 @@ public function assertNoActionMenu($actionName) return $this; } + + /** + * Method checks if "No data message" is present and displayed + * + * @param string $message Grid message to verify + * + * @return $this + * @throws \PHPUnit_Framework_AssertionFailedError + */ + public function assertNoDataMessageAndDisplayed($message) + { + $this->assertNoDataMessage($message); + $noDataMessage = $this->test->byXPath("//div[@class='no-data']/span[contains(., '{$message}')]"); + if (!$noDataMessage->displayed()) { + PHPUnit_Framework_Assert::fail( + "//div[@class='no-data']/span[contains(., '{$message}')] is not displayed" + ); + } + + return $this; + } } diff --git a/src/Oro/Bundle/TrackingBundle/Entity/TrackingEvent.php b/src/Oro/Bundle/TrackingBundle/Entity/TrackingEvent.php index e954e13a2af..3786299c6bb 100644 --- a/src/Oro/Bundle/TrackingBundle/Entity/TrackingEvent.php +++ b/src/Oro/Bundle/TrackingBundle/Entity/TrackingEvent.php @@ -22,7 +22,10 @@ * defaultValues={ * "entity"={ * "icon"="icon-external-link" - * } + * }, + * "grid"={ + * "default"="tracking-events-grid" + * } * } * ) */ diff --git a/src/Oro/Bundle/TrackingBundle/Entity/TrackingWebsite.php b/src/Oro/Bundle/TrackingBundle/Entity/TrackingWebsite.php index 8aa0906edc6..067c2c6c2b6 100644 --- a/src/Oro/Bundle/TrackingBundle/Entity/TrackingWebsite.php +++ b/src/Oro/Bundle/TrackingBundle/Entity/TrackingWebsite.php @@ -31,6 +31,9 @@ * "organization_field_name"="organization", * "organization_column_name"="organization_id" * }, + * "grid"={ + * "default"="website-grid" + * } * } * ) */ diff --git a/src/Oro/Bundle/TrackingBundle/Resources/config/datagrid.yml b/src/Oro/Bundle/TrackingBundle/Resources/config/datagrid.yml index 098806bc058..ae4e173cceb 100644 --- a/src/Oro/Bundle/TrackingBundle/Resources/config/datagrid.yml +++ b/src/Oro/Bundle/TrackingBundle/Resources/config/datagrid.yml @@ -1,9 +1,9 @@ datagrid: website-grid: extended_entity_name: %oro_tracking.tracking_website.class% + acl_resource: oro_tracking_website_view source: type: orm - acl_resource: oro_tracking_website_view query: select: - website.id @@ -124,9 +124,9 @@ datagrid: tracking-events-grid: extended_entity_name: %oro_tracking.tracking_event.class% + acl_resource: oro_tracking_website_view source: type: orm - acl_resource: oro_tracking_website_view query: select: - e.id diff --git a/src/Oro/Bundle/TrackingBundle/Resources/public/css/less/code.less b/src/Oro/Bundle/TrackingBundle/Resources/public/css/less/code.less index 949a9dd632a..6aa997af6ed 100644 --- a/src/Oro/Bundle/TrackingBundle/Resources/public/css/less/code.less +++ b/src/Oro/Bundle/TrackingBundle/Resources/public/css/less/code.less @@ -1,15 +1,10 @@ -#websiteView + div .responsive-block { - padding: 10px; - - .code { - width: 100%; - - &.code-event { - height: 60px; - } - - &.code-script { - height: 265px; - } +.code { + width: 100%; + box-sizing: border-box; + &.code-event { + height: 60px; + } + &.code-script { + height: 265px; } } diff --git a/src/Oro/Bundle/TrackingBundle/Resources/views/TrackingWebsite/view.html.twig b/src/Oro/Bundle/TrackingBundle/Resources/views/TrackingWebsite/view.html.twig index ca7918ee19c..423d5501be0 100644 --- a/src/Oro/Bundle/TrackingBundle/Resources/views/TrackingWebsite/view.html.twig +++ b/src/Oro/Bundle/TrackingBundle/Resources/views/TrackingWebsite/view.html.twig @@ -36,7 +36,7 @@ {% block content_data %} {%- set generalInformation -%} -
    +
    {{ UI.renderProperty('oro.tracking.trackingwebsite.name.label'|trans, entity.name) }} {{ UI.renderProperty('oro.tracking.trackingwebsite.identifier.label'|trans, entity.identifier) }} @@ -67,7 +67,7 @@
    {%- endset -%} {%- set trackingCode -%} -
    +

    {{ 'oro.tracking.help.event_tooltip'|trans }}

    {% if not app.request.secure %} diff --git a/src/Oro/Bundle/TranslationBundle/Tests/Unit/Translation/OrmTranslationLoaderTest.php b/src/Oro/Bundle/TranslationBundle/Tests/Unit/Translation/OrmTranslationLoaderTest.php index 544f27fa7cf..67e0b07932a 100644 --- a/src/Oro/Bundle/TranslationBundle/Tests/Unit/Translation/OrmTranslationLoaderTest.php +++ b/src/Oro/Bundle/TranslationBundle/Tests/Unit/Translation/OrmTranslationLoaderTest.php @@ -53,7 +53,7 @@ public function testLoadWhenDatabaseDoesNotContainTranslationTable() ->getMock(); $resource = new OrmTranslationResource($locale, $metadataCache); - $translationTable = 'translation_able'; + $translationTable = 'translation_table'; $metadata = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadata') ->disableOriginalConstructor() @@ -77,7 +77,7 @@ public function testLoadWhenDatabaseDoesNotContainTranslationTable() ->willReturn($schemaManager); $schemaManager->expects($this->once()) ->method('tablesExist') - ->with([$translationTable]) + ->with($translationTable) ->willReturn(false); /** @var MessageCatalogue $result */ @@ -127,7 +127,7 @@ public function testLoad() $values = [$value1, $value2, $value3]; - $translationTable = 'translation_able'; + $translationTable = 'translation_table'; $repo = $this ->getMockBuilder('Oro\Bundle\TranslationBundle\Entity\Repository\TranslationRepository') @@ -164,7 +164,7 @@ public function testLoad() ->willReturn($schemaManager); $schemaManager->expects($this->once()) ->method('tablesExist') - ->with([$translationTable]) + ->with($translationTable) ->willReturn(true); /** @var MessageCatalogue $result */ diff --git a/src/Oro/Bundle/TranslationBundle/Tests/Unit/Translation/TranslatorTest.php b/src/Oro/Bundle/TranslationBundle/Tests/Unit/Translation/TranslatorTest.php index eff370568f9..1fb17ecf50b 100644 --- a/src/Oro/Bundle/TranslationBundle/Tests/Unit/Translation/TranslatorTest.php +++ b/src/Oro/Bundle/TranslationBundle/Tests/Unit/Translation/TranslatorTest.php @@ -298,7 +298,18 @@ public function testLoadingOfDynamicResources() ]; $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + $doctrine = $this->getMock('Doctrine\Common\Persistence\ManagerRegistry'); $em = $this->getMock('Doctrine\ORM\EntityManagerInterface'); + $connection = $this->getMockBuilder('Doctrine\DBAL\Connection') + ->disableOriginalConstructor() + ->getMock(); + $schemaManager = $this->getMockBuilder('Doctrine\DBAL\Schema\AbstractSchemaManager') + ->disableOriginalConstructor() + ->setMethods(['tablesExist']) + ->getMockForAbstractClass(); + $classMetadata = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadata') + ->disableOriginalConstructor() + ->getMock(); $repository = $this ->getMockBuilder('Oro\Bundle\TranslationBundle\Entity\Repository\TranslationRepository') ->disableOriginalConstructor() @@ -315,6 +326,8 @@ public function testLoadingOfDynamicResources() $translator->setLocale($locale); $translator->setDatabaseMetadataCache($databaseCache); + $translationTable = 'translation_table'; + $container ->expects($this->any()) ->method('hasParameter') @@ -329,14 +342,44 @@ public function testLoadingOfDynamicResources() ->expects($this->any()) ->method('get') ->with('doctrine') - ->willReturn($em); - $em + ->willReturn($doctrine); + $doctrine ->expects($this->any()) + ->method('getManagerForClass') + ->with(Translation::ENTITY_NAME) + ->willReturn($em); + $doctrine + ->expects($this->once()) ->method('getRepository') ->with(Translation::ENTITY_NAME) ->willReturn($repository); + $em + ->expects($this->once()) + ->method('getConnection') + ->willReturn($connection); + $em + ->expects($this->once()) + ->method('getClassMetadata') + ->with(Translation::ENTITY_NAME) + ->willReturn($classMetadata); + $connection + ->expects($this->once()) + ->method('connect'); + $connection + ->expects($this->once()) + ->method('getSchemaManager') + ->willReturn($schemaManager); + $schemaManager + ->expects($this->once()) + ->method('tablesExist') + ->with($translationTable) + ->willReturn(true); + $classMetadata + ->expects($this->once()) + ->method('getTableName') + ->willReturn($translationTable); $repository - ->expects($this->any()) + ->expects($this->once()) ->method('findAvailableDomainsForLocales') ->willReturn($translate); diff --git a/src/Oro/Bundle/TranslationBundle/Translation/OrmTranslationLoader.php b/src/Oro/Bundle/TranslationBundle/Translation/OrmTranslationLoader.php index 92ecf49639c..2be6a25458a 100644 --- a/src/Oro/Bundle/TranslationBundle/Translation/OrmTranslationLoader.php +++ b/src/Oro/Bundle/TranslationBundle/Translation/OrmTranslationLoader.php @@ -8,6 +8,7 @@ use Symfony\Component\Translation\Loader\LoaderInterface; use Symfony\Component\Translation\MessageCatalogue; +use Oro\Bundle\EntityBundle\Tools\SafeDatabaseChecker; use Oro\Bundle\TranslationBundle\Entity\Translation; use Oro\Bundle\TranslationBundle\Entity\Repository\TranslationRepository; @@ -55,24 +56,17 @@ public function load($resource, $locale, $domain = 'messages') } /** - * Check if translations table exists in db + * Checks whether the translations table exists in the database * * @return bool */ protected function checkDatabase() { if (null === $this->dbCheck) { - $this->dbCheck = false; - - $em = $this->getEntityManager(); - try { - $conn = $em->getConnection(); - $conn->connect(); - $this->dbCheck = $conn->getSchemaManager()->tablesExist( - [$em->getClassMetadata(Translation::ENTITY_NAME)->getTableName()] - ); - } catch (\PDOException $e) { - } + $this->dbCheck = SafeDatabaseChecker::tablesExist( + $this->getEntityManager()->getConnection(), + SafeDatabaseChecker::getTableName($this->doctrine, Translation::ENTITY_NAME) + ); } return $this->dbCheck; diff --git a/src/Oro/Bundle/TranslationBundle/Translation/Translator.php b/src/Oro/Bundle/TranslationBundle/Translation/Translator.php index 834ff1a5c22..562a5a42593 100644 --- a/src/Oro/Bundle/TranslationBundle/Translation/Translator.php +++ b/src/Oro/Bundle/TranslationBundle/Translation/Translator.php @@ -8,6 +8,7 @@ use Symfony\Component\Translation\Loader\LoaderInterface; use Symfony\Bundle\FrameworkBundle\Translation\Translator as BaseTranslator; +use Oro\Bundle\EntityBundle\Tools\SafeDatabaseChecker; use Oro\Bundle\TranslationBundle\Entity\Translation; class Translator extends BaseTranslator @@ -35,6 +36,9 @@ class Translator extends BaseTranslator */ protected $dynamicResources = []; + /** @var bool|null */ + protected $dbCheck; + /** * Collector of translations * @@ -231,7 +235,7 @@ protected function ensureDynamicResourcesLoaded($locale) } } } - if (!$hasDatabaseResources) { + if (!$hasDatabaseResources && $this->checkDatabase()) { $locales = $this->getFallbackLocales(); array_unshift($locales, $locale); $locales = array_unique($locales); @@ -261,4 +265,22 @@ protected function isInstalled() { return $this->container->hasParameter('installed') && $this->container->getParameter('installed'); } + + /** + * Checks whether the translations table exists in the database + * + * @return bool + */ + protected function checkDatabase() + { + if (null === $this->dbCheck) { + $doctrine = $this->container->get('doctrine'); + $this->dbCheck = SafeDatabaseChecker::tablesExist( + $doctrine->getManagerForClass(Translation::ENTITY_NAME)->getConnection(), + SafeDatabaseChecker::getTableName($doctrine, Translation::ENTITY_NAME) + ); + } + + return $this->dbCheck; + } } diff --git a/src/Oro/Bundle/UIBundle/Asset/DynamicAssetVersionManager.php b/src/Oro/Bundle/UIBundle/Asset/DynamicAssetVersionManager.php new file mode 100644 index 00000000000..b9383fb5753 --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Asset/DynamicAssetVersionManager.php @@ -0,0 +1,73 @@ +cache = $cache; + $this->localCache = []; + } + + /** + * Gets the current version of a given asset package. + * + * @param string $packageName + * + * @return string the current version of the given asset package + */ + public function getAssetVersion($packageName) + { + $versionNumber = $this->getCachedAssetVersion($packageName); + + return 0 !== $versionNumber + ? (string)$versionNumber + : ''; + } + + /** + * Increase the number of the current version of a given asset package. + * + * @param string $packageName + */ + public function updateAssetVersion($packageName) + { + $versionNumber = $this->getCachedAssetVersion($packageName) + 1; + + $this->localCache[$packageName] = $versionNumber; + $this->cache->save($packageName, $versionNumber); + } + + /** + * @param string $packageName + * + * @return int + */ + protected function getCachedAssetVersion($packageName) + { + if (!array_key_exists($packageName, $this->localCache)) { + $version = $this->cache->fetch($packageName); + + $this->localCache[$packageName] = false !== $version + ? $version + : 0; + } + + return $this->localCache[$packageName]; + } +} diff --git a/src/Oro/Bundle/UIBundle/Asset/DynamicAssetVersionStrategy.php b/src/Oro/Bundle/UIBundle/Asset/DynamicAssetVersionStrategy.php new file mode 100644 index 00000000000..e9c69c5c9e2 --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Asset/DynamicAssetVersionStrategy.php @@ -0,0 +1,73 @@ +staticVersion = $staticVersion; + $this->format = $format ?: '%s?%s'; + } + + /** + * @param DynamicAssetVersionManager $assetVersionManager + */ + public function setAssetVersionManager(DynamicAssetVersionManager $assetVersionManager) + { + $this->assetVersionManager = $assetVersionManager; + } + + /** + * @param string $packageName + */ + public function setAssetPackageName($packageName) + { + $this->packageName = $packageName; + } + + /** + * {@inheritdoc} + */ + public function getVersion($path) + { + $dynamicVersion = $this->assetVersionManager->getAssetVersion($this->packageName); + + return !empty($dynamicVersion) + ? $this->staticVersion . '-' . $dynamicVersion + : $this->staticVersion; + } + + /** + * {@inheritdoc} + */ + public function applyVersion($path) + { + $versionized = sprintf($this->format, ltrim($path, '/'), $this->getVersion($path)); + + return $path && '/' === $path[0] + ? '/' . $versionized + : $versionized; + } +} diff --git a/src/Oro/Bundle/UIBundle/DependencyInjection/Compiler/DynamicAssetVersionPass.php b/src/Oro/Bundle/UIBundle/DependencyInjection/Compiler/DynamicAssetVersionPass.php new file mode 100644 index 00000000000..f7083222da6 --- /dev/null +++ b/src/Oro/Bundle/UIBundle/DependencyInjection/Compiler/DynamicAssetVersionPass.php @@ -0,0 +1,53 @@ +packageName = $packageName; + } + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + $assetVersionServiceId = sprintf(self::ASSET_VERSION_SERVICE_TEMPLATE, $this->packageName); + if (!$container->hasDefinition($assetVersionServiceId)) { + return; + } + if (!$container->hasDefinition(self::ASSET_VERSION_MANAGER_SERVICE)) { + return; + } + + $assetVersionDef = $container->getDefinition($assetVersionServiceId); + $assetVersionDef->setClass(self::ASSET_VERSION_SERVICE_CLASS); + $assetVersionDef->addMethodCall( + 'setAssetVersionManager', + [new Reference(self::ASSET_VERSION_MANAGER_SERVICE)] + ); + $assetVersionDef->addMethodCall( + 'setAssetPackageName', + [$this->packageName] + ); + } +} diff --git a/src/Oro/Bundle/UIBundle/DependencyInjection/Configuration.php b/src/Oro/Bundle/UIBundle/DependencyInjection/Configuration.php index afe7d2fb3c8..2dbd4847bb1 100644 --- a/src/Oro/Bundle/UIBundle/DependencyInjection/Configuration.php +++ b/src/Oro/Bundle/UIBundle/DependencyInjection/Configuration.php @@ -6,8 +6,9 @@ use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; +use Oro\Component\PhpUtils\ArrayUtil; + use Oro\Bundle\ConfigBundle\DependencyInjection\SettingsBuilder; -use Oro\Bundle\UIBundle\Tools\ArrayUtils; /** * This is the class that validates and merges configuration from your app/config files @@ -218,7 +219,7 @@ function ($v) { */ protected function sortItems($items) { - ArrayUtils::sortBy($items, false, 'order'); + ArrayUtil::sortBy($items, false, 'order'); return array_keys($items); } diff --git a/src/Oro/Bundle/UIBundle/Placeholder/PlaceholderFilter.php b/src/Oro/Bundle/UIBundle/Placeholder/PlaceholderFilter.php index 203d1ca6c14..b897b6814ca 100644 --- a/src/Oro/Bundle/UIBundle/Placeholder/PlaceholderFilter.php +++ b/src/Oro/Bundle/UIBundle/Placeholder/PlaceholderFilter.php @@ -4,6 +4,17 @@ class PlaceholderFilter { + /** + * @param mixed $param1 + * @param mixed $param2 + * + * @return bool + */ + public function isSame($param1, $param2) + { + return $param1 === $param2; + } + /** * Checks the object is an instance of a given class. * diff --git a/src/Oro/Bundle/UIBundle/Provider/UserAgent.php b/src/Oro/Bundle/UIBundle/Provider/UserAgent.php index 39de47ee1d6..52c991a87c9 100644 --- a/src/Oro/Bundle/UIBundle/Provider/UserAgent.php +++ b/src/Oro/Bundle/UIBundle/Provider/UserAgent.php @@ -127,7 +127,7 @@ protected function detectMobile($useragent) // @codingStandardsIgnoreStart $result = // all Android devices considered as mobile - preg_match('/android|ipad|kindle|silk/i',$useragent) + preg_match('/android|webos|ipad|ipod|blackberry|windows phone|iemobile|kindle|silk/i', $useragent) // from http://detectmobilebrowsers.com (Regex updated: 1 August 2014) || preg_match('/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i',$useragent) || preg_match('/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i',substr($useragent,0,4)); diff --git a/src/Oro/Bundle/UIBundle/README.md b/src/Oro/Bundle/UIBundle/README.md index e92af524cdc..dcac0ba1e1f 100644 --- a/src/Oro/Bundle/UIBundle/README.md +++ b/src/Oro/Bundle/UIBundle/README.md @@ -16,6 +16,7 @@ User interface layouts and controls. - [Loading Mask View](./Resources/doc/reference/client-side/loading-mask-view.md) - [Scroll Data Customization](./Resources/doc/reference/scroll-data-customization.md) - [formatters](./Resources/doc/reference/formatters.md) +- [Dynamic Assets](./Resources/doc/dynamic-assets.md) ## Configuration Settings diff --git a/src/Oro/Bundle/UIBundle/Resources/config/assets.yml b/src/Oro/Bundle/UIBundle/Resources/config/assets.yml index a1c27b8e2c1..2133f7cc99f 100644 --- a/src/Oro/Bundle/UIBundle/Resources/config/assets.yml +++ b/src/Oro/Bundle/UIBundle/Resources/config/assets.yml @@ -2,9 +2,9 @@ css: 'timepicker': - 'bundles/oroui/lib/jquery.timepicker-1.4.13/jquery.timepicker.css' 'oroui': + - 'bundles/jstree/themes/default/style.min.css' # libraries' styles - 'bundles/oroui/lib/bootstrap/bootstrap.less' - - 'bundles/oroui/lib/jstree/themes/default/style.css' - 'bundles/oroui/lib/font-awesome/css/font-awesome.css' - 'bundles/oroui/lib/simplecolorpicker/jquery.simplecolorpicker.css' - 'bundles/oroui/lib/simplecolorpicker/jquery.simplecolorpicker-fontawesome.css' diff --git a/src/Oro/Bundle/UIBundle/Resources/config/oro/app.yml b/src/Oro/Bundle/UIBundle/Resources/config/oro/app.yml index 423b0f8d25b..c5a2693e6fe 100644 --- a/src/Oro/Bundle/UIBundle/Resources/config/oro/app.yml +++ b/src/Oro/Bundle/UIBundle/Resources/config/oro/app.yml @@ -11,6 +11,25 @@ liip_imagine: thumbnail: { size: [16, 16], mode: outbound } strip: ~ -twig: - globals: - assets_version: "%assets_version%" +assetic: + assets: + jstree: + inputs: + - %kernel.root_dir%/../vendor/vakata/jstree/dist/jstree.min.js + output: bundles/jstree/jquery.jstree.min.js + jstree_default_theme_style: + inputs: + - %kernel.root_dir%/../vendor/vakata/jstree/dist/themes/default/style.min.css + output: bundles/jstree/themes/default/style.min.css + jstree_default_theme_32px_png: + inputs: + - %kernel.root_dir%/../vendor/vakata/jstree/dist/themes/default/32px.png + output: bundles/jstree/themes/default/32px.png + jstree_default_theme_40px_png: + inputs: + - %kernel.root_dir%/../vendor/vakata/jstree/dist/themes/default/40px.png + output: bundles/jstree/themes/default/40px.png + jstree_default_theme_throbber_gif: + inputs: + - %kernel.root_dir%/../vendor/vakata/jstree/dist/themes/default/throbber.gif + output: bundles/jstree/themes/default/throbber.gif diff --git a/src/Oro/Bundle/UIBundle/Resources/config/requirejs.yml b/src/Oro/Bundle/UIBundle/Resources/config/requirejs.yml index 567bebe0d03..b30578695fa 100644 --- a/src/Oro/Bundle/UIBundle/Resources/config/requirejs.yml +++ b/src/Oro/Bundle/UIBundle/Resources/config/requirejs.yml @@ -19,16 +19,12 @@ config: 'jquery.minicolors': deps: - 'jquery' - 'jquery.jstree': + 'jstree/jquery.jstree.min': deps: - 'jquery' - - 'jquery.cookie' 'jquery.cookie': deps: - 'jquery' - 'jstree/jquery.hotkeys': - deps: - - 'jquery.jstree' 'bootstrap': deps: - 'jquery' @@ -132,10 +128,7 @@ config: 'jquery.simplecolorpicker': 'bundles/oroui/lib/simplecolorpicker/jquery.simplecolorpicker.js' 'jquery.minicolors': 'bundles/oroui/lib/minicolors/jquery.minicolors.js' 'jquery.cookie': 'bundles/oroui/lib/jquery/jquery.cookie.js' - 'jquery.jstree': 'bundles/oroui/lib/jstree/jquery.jstree.js' 'jquery.ajax.queue': 'bundles/oroui/lib/jquery/jquery.ajaxQueue.min.js' - 'jstree/jquery.cookie': 'bundles/oroui/lib/jstree/jquery.cookie.js' - 'jstree/jquery.hotkeys': 'bundles/oroui/lib/jstree/jquery.hotkeys.js' 'bootstrap': 'bundles/oroui/lib/bootstrap.min.js' # 'bootstrap': 'bundles/oroui/js/bootstrap-dev.js' 'underscore': 'bundles/components/underscore/underscore.js' @@ -188,6 +181,8 @@ config: 'oroui/js/app/components/tabs-component': 'bundles/oroui/js/app/components/tabs-component.js' 'oroui/js/app/components/view-component': 'bundles/oroui/js/app/components/view-component.js' 'oroui/js/app/components/widget-component': 'bundles/oroui/js/app/components/widget-component.js' + 'oroui/js/app/components/basic-tree-component': 'bundles/oroui/js/app/components/basic-tree-component.js' + 'oroui/js/app/components/basic-tree-manage-component': 'bundles/oroui/js/app/components/basic-tree-manage-component.js' 'oroui/js/widget-manager': 'bundles/oroui/js/widget/widget-manager.js' 'oroui/js/widget/abstract-widget': 'bundles/oroui/js/widget/abstract-widget.js' 'oro/block-widget': 'bundles/oroui/js/widget/block-widget.js' @@ -213,6 +208,7 @@ config: build: paths: + 'jstree/jquery.jstree.min': 'empty:' # Backbone after v.1.1.1 started to support AMD. And that broke a js-build with # files which does not support AMD. backbone-bootstrap-modal use AMD without dependency, # therefore it's excluded diff --git a/src/Oro/Bundle/UIBundle/Resources/config/services.yml b/src/Oro/Bundle/UIBundle/Resources/config/services.yml index e5dc4a76a14..7b43dd995df 100644 --- a/src/Oro/Bundle/UIBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/UIBundle/Resources/config/services.yml @@ -48,6 +48,17 @@ services: scope: request arguments: [@request, @router, @oro_security.security_facade] + oro_ui.dynamic_asset_version_manager: + class: Oro\Bundle\UIBundle\Asset\DynamicAssetVersionManager + arguments: + - @oro_ui.dynamic_asset_version.cache + + oro_ui.dynamic_asset_version.cache: + public: false + parent: oro.cache.abstract + calls: + - [ setNamespace, [ 'oro_dynamic_asset_version' ] ] + oro_ui.view.listener: class: %oro_ui.view.listener.class% arguments: diff --git a/src/Oro/Bundle/UIBundle/Resources/doc/dynamic-assets.md b/src/Oro/Bundle/UIBundle/Resources/doc/dynamic-assets.md new file mode 100644 index 00000000000..8bdeed4f6b7 --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Resources/doc/dynamic-assets.md @@ -0,0 +1,83 @@ +Dynamic Assets +============== + +Sometime assets can be changed during an application life cycle, for instance when an administrator does some configuration of a site. When such situation happens the assets cache should be busted properly. Unfortunately Symfony does not manage this case out of the box. But, fortunately, the [Asset Component](http://symfony.com/doc/current/components/asset/introduction.html) can be easily enhanced to support this feature. + +The following samples of code show how to add dynamic versioning for any asset package. + +Lets suppose that `acme` asset package should use the dynamic versioning. + +At first the package should be registered. You can use `Resources/config/oro/app.yml` in your bundle or `app/config/config.yml`: + +```yaml +framework: + templating: + packages: + acme: + version: %assets_version% + version_format: ~ # use the default format +``` + +The next step is to set [DynamicAssetVersionStrategy](../../Asset/DynamicAssetVersionStrategy.php) for this package. It can be done be DI compiler pass: + +```php +addCompilerPass(new DynamicAssetVersionPass('acme')); + } +} +``` + +That's all, the configuration is finished. + +Now you need to take care about updating of the package version when your assets are changed. The following code shows how to update the version: + +```php +get('oro_ui.dynamic_asset_version_manager'); + $assetVersionManager->updateAssetVersion('acme'); + + ... + } +} +``` + +The usage of your assets is the same as other assets, for example by the well-known `asset()` Twig function: + +```twig +{{ asset('test.js', 'acme') }} +{# the result may be something like this: test.js?version=123-2 #} +{# where #} +{# '123' is the static asset version specified in %assets_version% parameter #} +{# '2' is the dynamic asset version; this number is increased each time you call $assetVersionManager->updateAssetVersion('acme') #} +``` + +Please pay attention that the package name should be passed to the `asset()` function. This tells Symfony that the asset belongs your package and the dynamic versioning strategy should be applied. diff --git a/src/Oro/Bundle/UIBundle/Resources/doc/reference/client-side/api-accessor.md b/src/Oro/Bundle/UIBundle/Resources/doc/reference/client-side/api-accessor.md index fba4db84455..6a6acf91966 100644 --- a/src/Oro/Bundle/UIBundle/Resources/doc/reference/client-side/api-accessor.md +++ b/src/Oro/Bundle/UIBundle/Resources/doc/reference/client-side/api-accessor.md @@ -44,6 +44,7 @@ and will put response to console after completion * [ApiAccessor](#module_ApiAccessor) ⇐ [BaseClass](./base-class.md) * [.initialize(options)](#module_ApiAccessor#initialize) * [.isCacheAllowed()](#module_ApiAccessor#isCacheAllowed) ⇒ boolean + * [.clearCache()](#module_ApiAccessor#clearCache) * [.validateUrlParameters(urlParameters)](#module_ApiAccessor#validateUrlParameters) ⇒ boolean * [.send(urlParameters, body, headers, options)](#module_ApiAccessor#send) ⇒ $.Promise * [._makeAjaxRequest(options)](#module_ApiAccessor#_makeAjaxRequest) @@ -67,6 +68,11 @@ and will put response to console after completion ### apiAccessor.isCacheAllowed() ⇒ boolean Returns true if selected HTTP_METHOD allows caching +**Kind**: instance method of [ApiAccessor](#module_ApiAccessor) +
    +### apiAccessor.clearCache() +Clears response cache + **Kind**: instance method of [ApiAccessor](#module_ApiAccessor) ### apiAccessor.validateUrlParameters(urlParameters) ⇒ boolean diff --git a/src/Oro/Bundle/UIBundle/Resources/doc/reference/client-side/hidden-initialization-view.md b/src/Oro/Bundle/UIBundle/Resources/doc/reference/client-side/hidden-initialization-view.md index 272254786c6..80af34b06b0 100644 --- a/src/Oro/Bundle/UIBundle/Resources/doc/reference/client-side/hidden-initialization-view.md +++ b/src/Oro/Bundle/UIBundle/Resources/doc/reference/client-side/hidden-initialization-view.md @@ -1,7 +1,7 @@ ## HiddenInitializationView ⇐ BaseView -**Kind**: global class **Extends:** BaseView +**Kind**: global class ### new HiddenInitializationView() View allows hide part of DOM tree till all page components will be initialized diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/desktop/filter.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/desktop/filter.less new file mode 100644 index 00000000000..1674b96aa3e --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/desktop/filter.less @@ -0,0 +1,3 @@ +.filter-box > .filter-container > .select-filter-widget { + margin: -8px 0 0 -118px; +} diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/desktop/layout.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/desktop/layout.less index 3eb7470c718..f89fd9c30f1 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/desktop/layout.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/desktop/layout.less @@ -1,15 +1,8 @@ -@import "../mixins"; -html, body, #page, #top-page { - height: 100%; - overflow: hidden; // prevents scroll bar appearance -} -html{ - overflow-x: auto; -} .responsive-block{ min-width: 340px; padding-right: 5px; } + #left-panel, #right-panel { overflow: visible; @@ -17,7 +10,8 @@ html{ #left-panel, #right-panel, #main { float: left; } -.desktop-version .oro-page { + +.oro-page { &.collapsible-sidebar { .oro-page-sidebar { > .dropdown-menu { @@ -28,19 +22,3 @@ html{ } } } -body.backdrop:before { - position: absolute; - content: ""; - display: block; - width: 100%; - height: 100%; - top: 0; - left: 0; - background: rgba(0,0,0,0.1); - z-index: 9999; -} -body.backdrop { - .select2-drop, #ui-datepicker-div{ - z-index: 10001 !important; - } -} diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/desktop/main.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/desktop/main.less index 1d4074f5c18..0eb8469b95c 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/desktop/main.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/desktop/main.less @@ -5,5 +5,6 @@ @import "scrollspy"; @import "pin-bar"; @import "form"; + @import "filter"; + @import "layout"; } -@import "layout"; diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/layout.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/layout.less index c0c212154c9..3452fbb50ab 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/layout.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/layout.less @@ -1,3 +1,11 @@ +html, body, #page, #top-page { + height: 100%; + overflow: hidden; // prevents scroll bar appearance +} +html{ + overflow-x: auto; +} + .responsive-inline-position () { float: left; width: 50%; @@ -67,13 +75,6 @@ } } -.ui-dialog { - max-width: 100%; - .row-fluid.row-fluid-divider { - background: none; - } -} - .container-fluid > .responsive-section { margin: 0 -@horizontalPadding; padding-right: @horizontalPadding; @@ -93,6 +94,31 @@ padding-right: 0; } } + +body.backdrop:before { + position: absolute; + content: ""; + display: block; + width: 100%; + height: 100%; + top: 0; + left: 0; + background: rgba(0,0,0,0.1); + z-index: 9999; +} +body.backdrop { + .select2-drop, #ui-datepicker-div{ + z-index: 10001 !important; + } +} + +.ui-dialog { + max-width: 100%; + .row-fluid.row-fluid-divider { + background: none; + } +} + .oro-page-menu-items { list-style: none; margin: 0; diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/mixins.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/mixins.less index e5dba6fe26f..f91a8b6b71e 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/mixins.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/mixins.less @@ -164,3 +164,7 @@ margin-top: -6px; } } + +.inline-actions-element-outline(@color, @width: 2px) { + box-shadow: 0 0 0 @width @color; +} diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/dashboard.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/dashboard.less index 4e0c26d59e8..3a8f98e44f3 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/dashboard.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/dashboard.less @@ -46,8 +46,7 @@ /*border: 1px solid #b6b6b6;*/ .border-radius(3px); .title{ - /*padding: @contentPadding @contentPadding @contentPadding 0;*/ - padding: @contentPadding; + padding: @contentPadding @contentPadding @contentPadding 0; } .calendar-container { padding: 0; @@ -62,8 +61,11 @@ margin-right: 0; } } +.contact-box-wrapper{ + padding: 0; +} .contact-box{ - width: 100%; + width: calc(~"100% - 20px"); } .oro-tabs{ .pagination-container{ diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/dialog.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/dialog.less index accf1887067..f2cbee2bfe2 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/dialog.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/dialog.less @@ -24,6 +24,11 @@ box-shadow: none !important; max-width: 100vw; min-width: initial !important; + &.ui-resizable:not(.ui-dialog-buttons) { + &:after { + display: none; + } + } .ui-widget-header { .ui-dialog-titlebar-buttonpane [class*='ui-dialog-titlebar-']:first-child:before { font-size: 18px; diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/filter.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/filter.less index 4ecf01619bd..1f131395708 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/filter.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/filter.less @@ -20,10 +20,10 @@ display: inline-block; } .dropdown > .dropdown-menu { + padding: 10px; margin: 10px auto 0; max-width: 506px; width: 100%; - z-index: @zindexPopover; // to show over grid's loading mask @media only screen and (min-width: 527px) { left: 50%; margin-left: -253px; @@ -52,7 +52,7 @@ top: -7px; } .filter-container { - padding: 10px; + padding: 0; .reset-filter-button { padding: 6px 0 4px 6px; position: absolute; @@ -62,6 +62,9 @@ margin: 0 0 10px 0; width: 100%; white-space: normal; + &:last-child { + margin-bottom: 0; + } .btn-group { margin-bottom: 0; } @@ -124,7 +127,7 @@ } } } - .filter-list { + .filter-list, .select-filter-widget { margin: 0 0 5px -5px; } .filter-criteria { diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/form.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/form.less index f26e4b96b8f..fe9a1272f12 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/form.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/form.less @@ -68,6 +68,7 @@ } .select2-container-multi .select2-choices li { float: none; + margin-right: 5px; } .oro-item-collection { input[type="email"], diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/contact-element.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/inline-actions.less similarity index 74% rename from src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/contact-element.less rename to src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/inline-actions.less index 62205c8a006..8a64c4deaf3 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/contact-element.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/inline-actions.less @@ -1,13 +1,13 @@ -.contact-element { +.inline-actions-element { top: 0; - .contact-element-actions { + .inline-actions-element_actions { visibility: visible; } - .contact-element-wrapper { + .inline-actions-element_wrapper { line-height: 28px; } } -.contact-element-actions { +.inline-actions-element_actions { .btn { width: 28px; height: 28px; diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/layout.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/layout.less index e2a9c2eaad7..432b6c2a642 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/layout.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/layout.less @@ -50,14 +50,19 @@ padding-right: @contentPadding; } } +.scrollspy.container-fluid, .page-title + [data-bound-component="orodatagrid/js/app/components/datagrid-component"] { + margin-top: -10px; +} .layout-content .responsive-section{ padding: 0; .form-horizontal { padding: @contentPadding 0; } } -.container-fluid > .responsive-section .responsive-cell { - padding: 0 @contentPadding; +.container-fluid > .responsive-section { + .responsive-cell, .responsive-cell:only-child { + padding: 0 @contentPadding; + } } .responsive-cell .box-type1{ .filter-box { @@ -122,8 +127,8 @@ } .container-fluid, .grid-toolbar{ - padding-left: 10px; - padding-right: 10px; + padding-left: 0; + padding-right: 0; } } } diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/main.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/main.less index 1a893e6cb24..f37da202523 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/main.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/main.less @@ -13,5 +13,5 @@ @import "page-header"; @import "scrollspy"; @import "flash-messages"; - @import "contact-element"; + @import "inline-actions"; } diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/page-header.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/page-header.less index 0c669fe55e9..f8dba2f1ef7 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/page-header.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/mobile/page-header.less @@ -5,13 +5,15 @@ float: left; } .breadcrumb-pin, -.container-fluid.page-title { +.container-fluid.page-title, .navbar-extra { background-color: #f1f1f1; padding-right: 0; padding-left: 0; } +.container-fluid.page-title { + margin-bottom: 10px; +} .navbar-extra { - .container-fluid.page-title; & > .row > [class^=span] { margin: 0; width: auto; @@ -25,25 +27,38 @@ .user-info-state + .title-buttons-container:not(:empty) { margin-top: 10px; } - .pull-left{ - .oro-subtitle{ - margin: 8px 0 0; - } - } .pull-left-extra{ margin-left: 0; } .oro-subtitle { - white-space: nowrap; + text-align: left; + white-space: normal; + padding-right: 13px; } .grid-views { margin-left: 0; - .views-group>.dropdown-toggle { - height: auto; + .views-group { + margin-right: 12px; + vertical-align: baseline; + & > .dropdown-toggle { + height: auto; + vertical-align: baseline; + } + &.open .btn.dropdown-toggle .caret, .caret { + margin: 0 -13px 0 5px; + vertical-align: baseline; + display: inline; + line-height: initial; + font-size: 4px; + } + } + .actions-group { + margin-left: 0; + vertical-align: baseline; } - .actions-group+.edited-label, + .edited-label, .actions-group .dropdown-toggle { - height: 28px; + vertical-align: baseline; &:after { content: ''; font-size: 20px; @@ -52,11 +67,11 @@ } } -.customer-info { +.page-title { h1 { font-size: @baseFontSize * 1.75; - margin: 5px 0 10px; - line-height: @baseFontSize * 2; + margin: 0; + line-height: @baseFontSize * 2.5; display: block; } .sub-title { @@ -83,7 +98,6 @@ } .title-buttons-container, .ui-dialog-buttonpane .widget-actions { - margin-top: 5px; .btn [class*=icon-].hide-text, .btn-group > .dropdown-menu li [class^=icon-] { width: 20px; @@ -99,22 +113,22 @@ } .btn { font-size: @fontSizeLarge; - line-height: @fontSizeLarge * 1.8; + line-height: @fontSizeLarge * 2; height: 34px; padding-left: 14px; padding-right: 14px; .caret { border-top-color: #666; - margin-top: 15px; - } - &:active .cater{ - border-top-color: #444; + margin-top: 16px; } [class^=icon-].hide-text:before { - margin: 7px 0 0 -9px; + margin: 0 0 0 -9px; + line-height: 35px; } - &:active [class^=icon-]:before { - color: #444; + &:active { + [class^=icon-]:before, .caret { + color: #444; + } } } .btn-group > .dropdown-menu { diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less index 09ef6c83988..f7cb7a59592 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less @@ -120,11 +120,12 @@ label, input, button, select, textarea { font-size:13px;} } .control-label.wrap { float:left; - padding-top:7px; + padding-top: 6px; color: #777; font-weight: normal; font-size: 13px; - text-align:right; + line-height: 20px; + text-align: right; .tooltip-icon { margin-right: 5px; cursor: pointer; @@ -292,6 +293,9 @@ i.color { max-width: 100%; .controls { margin-left: 240px; + div.control-label { + text-align: left; + } } div.control-label, label.control-label { @@ -869,14 +873,17 @@ footer { cursor: move; } } -.ui-dialog:not(.ui-dialog-buttons) .ui-dialog-content{ - height: calc(~"100% - 32px") !important; - border-radius: 0 0 3px 3px; +.ui-dialog:not(.ui-dialog-buttons){ + .ui-dialog-content{ + border-radius: 0 0 3px 3px; + } } -.ui-dialog.ui-resizable:not(.ui-dialog-buttons) .ui-dialog-content{ - height: calc(~"100% - 50px") !important; - border-radius: 0 0 3px 3px; - border-bottom: 1px solid #e5e5e5; +.ui-dialog.ui-resizable:not(.ui-dialog-buttons) { + .ui-dialog-content{ + height: calc(~"100% - 50px") !important; + border-radius: 0 0 3px 3px; + border-bottom: 1px solid #e5e5e5; + } } .ui-dialog .widget-actions-section { .btn, .action { @@ -2116,14 +2123,14 @@ textarea { } .horizontal .oro-clearfix input[type="checkbox"], .horizontal .oro-clearfix input[type="radio"] { - margin: -4px 10px 0 0; + margin: -4px 6px 0 0; } .horizontal .oro-clearfix .label { float: left; } .controls { .horizontal { - padding-top: 5px; + padding-top: 7px; } > input[type=checkbox]:only-child { margin-top: 10px; @@ -2237,11 +2244,10 @@ select { line-height: 26px; } div.selector { - line-height: 28px; + line-height: 30px; height: 30px; position: relative; width: 235px; - font-size: 12px; border: 1px solid #ddd; #gradient > .vertical(#ffffff, #f3f3f3); -webkit-border-radius: 3px; @@ -2251,19 +2257,19 @@ div.selector { } div.selector span { - height: 28px; + height: 30px; display: block; - line-height: 28px; + line-height: 30px; .box-sizing(border-box); - padding: 0 5px; + padding: 0 5px 0 8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; position: relative; &:after { - margin:2px 2px 0 0; + margin: 3px 2px 0 0; display:inline-block; - font:15px 'FontAwesome'; + font:16px 'FontAwesome'; content: "\f0dd"; position: absolute; right: 5px; @@ -2619,7 +2625,7 @@ label.radio.error { float: left; background: none; margin: 0; - padding: 6px 15px 0 20px; + padding: 6px 15px 6px 20px; } .breadcrumb > li.active, .breadcrumb > li { diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/datagrid.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/datagrid.less index 601c198ca64..0bb7cf517a3 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/datagrid.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/datagrid.less @@ -69,14 +69,16 @@ padding-top: 7px; } } + .action.btn { + border-color:#ccc; + line-height:26px; + font-size:12px; + } .btn-group { .btn { - border-color:#ccc; height:27px; padding-left:7px; padding-right:9px; - line-height:26px; - font-size:12px; [class^='icon-'] { &:before { display:inline-block; diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/filter.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/filter.less index bacf48bfd7b..1e8db2dd613 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/filter.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/filter.less @@ -103,7 +103,7 @@ left: 10px; position: absolute; bottom: -10px; - z-index: 10001; + z-index: @oroZindexDropdown + 1; } } .divider { diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/fs-toolbar.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/fs-toolbar.less index 18233886940..ae5f021e8c2 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/fs-toolbar.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/fs-toolbar.less @@ -11,9 +11,11 @@ & > .sf-toolbar-block{ border-right: 1px solid #13161a; border-left: 1px solid #424951; + border-bottom-color: #424951; &:hover{ border-right: 1px solid #bbb; border-left: 1px solid #bbb; + border-bottom-color: #bbb; } } & > .hide-button{ diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/contact-element.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/inline-actions.less similarity index 51% rename from src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/contact-element.less rename to src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/inline-actions.less index c1ea5cc1cc1..c9599d07afb 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/contact-element.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/inline-actions.less @@ -1,48 +1,52 @@ -.contact-element { +.inline-actions-element { position: relative; top: -4px; left: -2px; - padding: 0 0 0 2px; line-height: 19px; display: inline-block; - .contact-element-actions { + margin-bottom: -3px; + .inline-actions-element_actions { visibility: hidden; } &:hover { - background: @rowSelectedBackground; - outline: solid 2px @rowSelectedBackground; - .contact-element-actions { + background: @inlineActionBackground; + .inline-actions-element-outline(@inlineActionBackground); + .inline-actions-element_actions { visibility: visible; } } - &.contact-element-no-actions:hover { + &.inline-actions-element_no-actions:hover { background: transparent; outline: none; } - .contact-element-wrapper { + .inline-actions-element_wrapper { word-break: break-all; padding: 5px 0 3px; line-height: 23px; - margin-right: 5px; + margin-right: 2px; } } -.contact-element-actions { +.inline-actions-element_actions { display: inline-block; vertical-align: top; .btn { font: 0/0 a; - color: transparent; - text-shadow: none; - background-color: transparent; width: 23px; height: 23px; padding: 0; + &, &:focus, &:active, &:hover { + color: transparent; + text-shadow: none; + border-color: transparent; + background: transparent none; + box-shadow: none; + } [class^="icon-"], [class*=" icon-"] { width: 14px; height: 20px; - font: @baseFontSize/26px 'FontAwesome'; - color: @btnTextColor; &:before { + font: @baseFontSize/26px 'FontAwesome'; + color: @inlineActionColor; font-size: 15px; line-height: 22px; height: 22px; @@ -51,8 +55,5 @@ .hide-text(); } } - &:hover{ - box-shadow: 0 1px 4px rgba(0,0,0,0.2); - } } } diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/main.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/main.less index a6a9ff3224a..00220f49666 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/main.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/main.less @@ -13,4 +13,4 @@ @import "footer"; @import "fs-toolbar"; @import "flash-messages"; -@import "contact-element"; +@import "inline-actions"; diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/select2.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/select2.less index 1402c016812..e772bcbb2a6 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/select2.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro/select2.less @@ -19,11 +19,11 @@ Version: 3.4.1 Timestamp: Thu Jun 27 18:02:10 PDT 2013 .select2-choice { display: block; height: 28px; - padding: 0 0 0 8px; + padding: 0; overflow: hidden; position: relative; white-space: nowrap; - line-height: 28px; + line-height: 1em; color: #444; text-decoration: none; -webkit-background-clip: padding-box; @@ -40,6 +40,10 @@ Version: 3.4.1 Timestamp: Thu Jun 27 18:02:10 PDT 2013 } img{ max-width: 16px; + margin-top: -2px; + display: block; + float: left; + margin-right: 2px; } &:focus { border: none !important; @@ -426,7 +430,7 @@ disabled look for disabled choices in the results dropdown height: 1%; margin: 0; padding: 0; - min-height: 26px; + min-height: 28px; position: relative; cursor: text; overflow: hidden; @@ -447,7 +451,7 @@ disabled look for disabled choices in the results dropdown } .select2-container-multi .select2-choices .select2-search-field input { - padding:3px 5px; + padding: 5px 5px 1px; margin: 1px 0; font-family:@baseFontFamily; font-size: 100%; @@ -508,7 +512,13 @@ disabled look for disabled choices in the results dropdown } .select2-container a.select2-choice .select2-chosen { - padding-right: 30px; + padding: 8px 30px 7px 8px; + & > i[class^=icon-] { + height: 16px; + margin-top: -3px; + vertical-align: middle; + float: left; + } } .select2-search-choice-close { diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/variables.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/variables.less index ddcb2e674a7..d1698d0367b 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/variables.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/variables.less @@ -28,8 +28,11 @@ @headerIconButtonColorEnd: #454f58; @headerIconButtonTextColor: #2a313a; @headerTextColor: #cad2da; +@inlineActionColor: #94b4c9; +@inlineActionBackground: #edf5fb; @rowHighlightBackground: #fffeea; -@rowSelectedBackground: #f4efd4; +@rowSelectedBackground: @inlineActionBackground; +@cellSelectedBorder: @inlineActionColor; @sideBarClosedWidth: 33px; @sideBarOpenedWidth: 200px; @@ -73,6 +76,10 @@ @infoBorder: #bce8f1; @infoClose: #b6d0dc; +@tagItemColor: #fff; +@tagItemBackground: #aaa; +@tagMyItemBackground: #ccc; + // Links @linkColor: #006acc; @linkColorHover: darken(@linkColor, 10%); diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/app/components/basic-tree-component.js b/src/Oro/Bundle/UIBundle/Resources/public/js/app/components/basic-tree-component.js new file mode 100644 index 00000000000..db01a07af53 --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/app/components/basic-tree-component.js @@ -0,0 +1,142 @@ +define(function(require) { + 'use strict'; + + var BasicTreeComponent; + var $ = require('jquery'); + var _ = require('underscore'); + var mediator = require('oroui/js/mediator'); + var layout = require('oroui/js/layout'); + var BaseComponent = require('oroui/js/app/components/base/component'); + + require('jstree/jquery.jstree.min'); + + /** + * Options: + * - data - tree structure in jstree json format + * - nodeId - identifier of selected node + * + * @export oroui/js/app/components/basic-tree-component + * @extends oroui.app.components.base.Component + * @class oroui.app.components.BasicTreeComponent + */ + BasicTreeComponent = BaseComponent.extend({ + /** + * @property {Object} + */ + $tree: null, + + /** + * @property {Number} + */ + nodeId: null, + + /** + * @property {Boolean} + */ + initialization: false, + + /** + * @param {Object} options + */ + initialize: function(options) { + var nodeList = options.data; + if (!nodeList) { + return; + } + + this.$tree = $(options._sourceElement); + + var config = { + 'core': { + 'multiple': false, + 'data': nodeList, + 'check_callback': true + }, + 'state': { + 'key': options.key, + 'filter': _.bind(this.onFilter, this) + }, + 'plugins': ['state'] + }; + config = this.customizeTreeConfig(options, config); + + this.nodeId = options.nodeId; + + this._deferredInit(); + this.initialization = true; + + this.$tree.jstree(config); + + var self = this; + this.$tree.on('ready.jstree', function() { + self._resolveDeferredInit(); + self.initialization = false; + }); + + self._fixContainerHeight(); + }, + + /** + * Customize jstree config to add plugins, callbacks etc. + * + * @param {Object} options + * @param {Object} config + * @returns {Object} + */ + customizeTreeConfig: function(options, config) { + return config; + }, + + /** + * Filters tree state + * + * @param {Object} state + * @returns {Object} + */ + onFilter: function(state) { + if (this.nodeId) { + state.core.selected = [this.nodeId]; + } else { + state.core.selected = []; + } + return state; + }, + + /** + * Fix scrollable container height + * TODO: This method should be removed during fixing of https://magecore.atlassian.net/browse/BB-336 + * + */ + _fixContainerHeight: function() { + var tree = this.$tree.parent(); + if (!tree.hasClass('tree-component')) { + return; + } + + var container = tree.parent(); + if (!container.hasClass('tree-component-container')) { + return; + } + + var fixHeight = function() { + var anchor = $('#bottom-anchor').position().top; + var containerTop = container.position().top; + var debugBarHeight = $('.sf-toolbar:visible').height() || 0; + var footerHeight = $('#footer:visible').height() || 0; + var fixContent = 1; + + tree.height(anchor - containerTop - debugBarHeight - footerHeight + fixContent); + }; + + layout.onPageRendered(fixHeight); + $(window).on('resize', _.debounce(fixHeight, 50)); + mediator.on('page:afterChange', fixHeight); + mediator.on('layout:adjustReloaded', fixHeight); + mediator.on('layout:adjustHeight', fixHeight); + + fixHeight(); + } + }); + + return BasicTreeComponent; +}); diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/app/components/basic-tree-manage-component.js b/src/Oro/Bundle/UIBundle/Resources/public/js/app/components/basic-tree-manage-component.js new file mode 100644 index 00000000000..657ca136e26 --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/app/components/basic-tree-manage-component.js @@ -0,0 +1,158 @@ +define(function(require) { + 'use strict'; + + var BasicTreeManageComponent; + var $ = require('jquery'); + var _ = require('underscore'); + var __ = require('orotranslation/js/translator'); + var widgetManager = require('oroui/js/widget-manager'); + var mediator = require('oroui/js/mediator'); + var messenger = require('oroui/js/messenger'); + var routing = require('routing'); + var BasicTreeComponent = require('oroui/js/app/components/basic-tree-component'); + + /** + * @export oroui/js/app/components/basic-tree-manage-component + * @extends oroui.app.components.BasicTreeComponent + * @class oroui.app.components.BasicTreeManageComponent + */ + BasicTreeManageComponent = BasicTreeComponent.extend({ + /** + * @property {Boolean} + */ + updateAllowed: false, + + /** + * @property {Boolean} + */ + moveTriggered: false, + + /** + * @property {String} + */ + reloadWidget: '', + + /** + * @property {String} + */ + onSelectRoute: '', + + /** + * @property {String} + */ + onMoveRoute: '', + + /** + * @param {Object} options + */ + initialize: function(options) { + BasicTreeManageComponent.__super__.initialize.call(this, options); + if (!this.$tree) { + return; + } + + this.updateAllowed = options.updateAllowed; + this.reloadWidget = options.reloadWidget; + this.onSelectRoute = options.onSelectRoute; + this.onMoveRoute = options.onMoveRoute; + + this.$tree.on('select_node.jstree', _.bind(this.onSelect, this)); + this.$tree.on('move_node.jstree', _.bind(this.onMove, this)); + }, + + /** + * @param {Object} options + * @param {Object} config + * @returns {Object} + */ + customizeTreeConfig: function(options, config) { + if (options.updateAllowed) { + config.plugins.push('dnd'); + config.dnd = { + 'copy': false + }; + } + + return config; + }, + + /** + * Triggers after node selection in tree + * + * @param {Object} node + * @param {Object} selected + */ + onSelect: function(node, selected) { + if (this.initialization || !this.updateAllowed) { + return; + } + + var url = routing.generate(this.onSelectRoute, {id: selected.node.id}); + mediator.execute('redirectTo', {url: url}); + }, + + /** + * Triggers after node move + * + * @param {Object} e + * @param {Object} data + */ + onMove: function(e, data) { + if (this.moveTriggered) { + return; + } + + var self = this; + $.ajax({ + async: false, + type: 'PUT', + url: routing.generate(self.onMoveRoute), + data: { + id: data.node.id, + parent: data.parent, + position: data.position + }, + success: function(result) { + if (!result.status) { + self.rollback(data); + messenger.notificationFlashMessage( + 'error', + __('oro.ui.jstree.move_node_error', {nodeText: data.node.text}) + ); + } else if (self.reloadWidget) { + widgetManager.getWidgetInstanceByAlias(self.reloadWidget, function(widget) { + widget.render(); + }); + } + } + }); + }, + + /** + * Rollback node move + * + * @param {Object} data + */ + rollback: function(data) { + this.moveTriggered = true; + this.$tree.jstree('move_node', data.node, data.old_parent, data.old_position); + this.moveTriggered = false; + }, + + /** + * Off events + */ + dispose: function() { + if (this.disposed) { + return; + } + this.$tree + .off('select_node.jstree') + .off('move_node.jstree'); + + BasicTreeManageComponent.__super__.dispose.call(this); + } + }); + + return BasicTreeManageComponent; +}); diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/app/views/page/debug-toolbar-view.js b/src/Oro/Bundle/UIBundle/Resources/public/js/app/views/page/debug-toolbar-view.js index 439b2ea5868..42e42d5a78a 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/app/views/page/debug-toolbar-view.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/app/views/page/debug-toolbar-view.js @@ -14,6 +14,11 @@ define([ 'page:error mediator': 'onPageUpdate' }, + events: { + 'click .hide-button': 'sendUpdates', + 'click .sf-minitoolbar': 'sendUpdates' + }, + /** * Handles page load event * - loads debug data @@ -25,7 +30,11 @@ define([ * @override */ onPageUpdate: function(data, actionArgs, xhr) { - if (!xhr) { + if (!actionArgs.route.previous) { + this.sendUpdates(); + // nothing to do, the page just loaded + return; + } else if (!xhr) { this.$el.empty(); mediator.trigger('layout:adjustHeight'); return; @@ -65,6 +74,14 @@ define([ .attr('data-sfurl', url); this.$el.html(data); + this.sendUpdates(); + }, + + /** + * Notifies application about updates + */ + sendUpdates: function() { + mediator.trigger('debugToolbar:afterUpdateView'); mediator.trigger('layout:adjustHeight'); } }); diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/delete-confirmation.js b/src/Oro/Bundle/UIBundle/Resources/public/js/delete-confirmation.js index 22cdfd58a8d..5b592d8f4da 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/delete-confirmation.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/delete-confirmation.js @@ -1,7 +1,10 @@ -define(['underscore', 'orotranslation/js/translator', 'oroui/js/modal' - ], function(_, __, Modal) { +define(function(require) { 'use strict'; + var _ = require('underscore'); + var __ = require('orotranslation/js/translator'); + var Modal = require('oroui/js/modal'); + /** * Delete confirmation dialog * @@ -10,20 +13,35 @@ define(['underscore', 'orotranslation/js/translator', 'oroui/js/modal' * @extends oroui.Modal */ return Modal.extend({ + + /** @property {String} */ + template: require('text!oroui/templates/delete-confirmation.html'), + /** @property {String} */ className: 'modal oro-modal-danger', /** @property {String} */ okButtonClass: 'btn-danger', + /** @property {Boolean} */ + allowOk: true, + /** * @param {Object} options */ initialize: function(options) { + //Set custom template settings + var interpolate = { + interpolate: /\{\{(.+?)\}\}/g, + evaluate: /<%([\s\S]+?)%>/g + }; + options = _.extend({ title: __('Delete Confirmation'), okText: __('Yes, Delete'), - cancelText: __('Cancel') + cancelText: __('Cancel'), + template: _.template(this.template, interpolate), + allowOk: this.allowOk }, options); arguments[0] = options; diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/extend/bootstrap.js b/src/Oro/Bundle/UIBundle/Resources/public/js/extend/bootstrap.js index 69694bfd521..5c2f07b5ad8 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/extend/bootstrap.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/extend/bootstrap.js @@ -1,8 +1,9 @@ define([ 'jquery', + 'underscore', 'oroui/js/tools/scroll-helper', 'bootstrap' -], function($, scrollHelper) { +], function($, _, scrollHelper) { 'use strict'; /** @@ -24,6 +25,25 @@ define([ } Dropdown.prototype = $.fn.dropdown.Constructor.prototype; + + $(document).off('click.dropdown.data-api', '[data-toggle=dropdown]', Dropdown.prototype.toggle); + Dropdown.prototype.toggle = _.wrap(Dropdown.prototype.toggle, function(func, event) { + var result = func.apply(this, _.rest(arguments)); + var href = $(this).attr('href'); + var selector = $(this).attr('data-target') || /#/.test(href) && href; + var $parent = selector ? $(selector) : null; + + if (!$parent || $parent.length === 0) { + $parent = $(this).parent(); + } + if ($parent.hasClass('open')) { + $parent.find('.dropdown-menu').trigger('shown.bs.dropdown'); + } + + return result; + }); + $(document).on('click.dropdown.data-api', '[data-toggle=dropdown]', Dropdown.prototype.toggle); + Dropdown.prototype.destroy = function() { var globalHandlers = this.data('globalHandlers'); $('html').off(globalHandlers); diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/extend/jquery.js b/src/Oro/Bundle/UIBundle/Resources/public/js/extend/jquery.js index 9001e46d220..3329a42dce6 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/extend/jquery.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/extend/jquery.js @@ -137,6 +137,20 @@ define(['jquery'], function($) { var newEvent = typeEvents.pop(); typeEvents.unshift(newEvent); }); + }, + + /** + * Temporarily adds class to element + */ + addClassTemporarily: function(className, delay) { + delay = delay || 0; + return this.each(function() { + var $el = $(this); + $el.addClass(className); + setTimeout(function() { + $el.removeClass(className); + }, delay); + }); } }); diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/init-layout.js b/src/Oro/Bundle/UIBundle/Resources/public/js/init-layout.js index 343a003532d..d381c83225a 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/init-layout.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/init-layout.js @@ -264,7 +264,7 @@ require(['jquery', 'underscore', 'orotranslation/js/translator', 'oroui/js/tools $main.width(realWidth($topPage) - realWidth($leftPanel) - realWidth($rightPanel)); layout.updateResponsiveLayout(); - var debugBarHeight = $('.sf-toolbar:visible').height() || 0; + var debugBarHeight = $('.sf-toolbarreset').height(); var anchorTop = anchor.position().top; var footerHeight = $('#footer:visible').height() || 0; var fixContent = 1; diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/items-manager/table.js b/src/Oro/Bundle/UIBundle/Resources/public/js/items-manager/table.js index e24814fc6d6..c588823ffd0 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/items-manager/table.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/items-manager/table.js @@ -44,6 +44,7 @@ define(['jquery', 'underscore', 'oroui/js/mediator', 'jquery-ui'], function($, _ options.collection.on('remove', this._onModelDeleted, this); options.collection.on('change', this._onModelChanged, this); options.collection.on('reset', this._onResetCollection, this); + options.collection.on('sort', this._renderCollection, this); this._initSorting(); this._onResetCollection(); @@ -85,32 +86,16 @@ define(['jquery', 'underscore', 'oroui/js/mediator', 'jquery-ui'], function($, _ }, _sortCollection: function() { - var collectionChanged = false; var collection = this.options.collection; - - _.each(this.element.find('tr'), function(el, index) { - var cid = $(el).data('cid'); - var model = collection.at(index); - if (cid === model.cid) { - return; - } - var anotherModel = collection.get(cid); - var anotherIndex = collection.indexOf(anotherModel); - collection.remove(model, {silent: true}); - collection.remove(anotherModel, {silent: true}); - if (index < anotherIndex) { - collection.add(anotherModel, {silent: true, at: index}); - collection.add(model, {silent: true, at: anotherIndex}); - } else { - collection.add(model, {silent: true, at: anotherIndex}); - collection.add(anotherModel, {silent: true, at: index}); - } - collectionChanged = true; + var positions = {}; + this.element.find('tr').each(function(index) { + positions[$(this).data('cid')] = index; }); - - if (collectionChanged) { - collection.trigger('sort'); - } + collection.models.sort(function(left, right) { + var diff = positions[left.cid] - positions[right.cid]; + return diff > 0 ? 1 : (diff < 0 ? -1 : 0); + }); + collection.trigger('sort'); }, _onModelAdded: function(model) { @@ -150,8 +135,7 @@ define(['jquery', 'underscore', 'oroui/js/mediator', 'jquery-ui'], function($, _ }, _onResetCollection: function() { - this.element.empty(); - this.options.collection.each(this._onModelAdded, this); + this._renderCollection(); mediator.trigger( 'items-manager:table:reset:' + this._getIdentifier(), @@ -160,8 +144,19 @@ define(['jquery', 'underscore', 'oroui/js/mediator', 'jquery-ui'], function($, _ ); }, + _renderCollection: function() { + this.element.empty(); + this.options.collection.each(this._onModelAdded, this); + }, + _renderModel: function(model) { - var data = _.extend({cid: model.cid}, model.toJSON()); + var collection = this.options.collection; + var index = collection.indexOf(model); + var data = _.extend({ + cid: model.cid, + isFirst: index === 0, + isLast: index === collection.length - 1 + }, model.toJSON()); return this._itemRender(this.itemTemplate, data); }, diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/jquery-ui-datepicker-l10n.js b/src/Oro/Bundle/UIBundle/Resources/public/js/jquery-ui-datepicker-l10n.js index 80971a5437b..3c1b354cd67 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/jquery-ui-datepicker-l10n.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/jquery-ui-datepicker-l10n.js @@ -76,10 +76,13 @@ define(function(require) { .find('a').removeClass('ui-state-highlight'); if (inst.drawYear === today.year() && inst.drawMonth === today.month()) { - // highlighted today date in system tumezone + // highlighted today date in system timezone inst.dpDiv - .find('td > a.ui-state-default:contains("' + today.date() + '")').addClass('ui-state-highlight') - .parent().addClass('ui-datepicker-today'); + .find('td > a.ui-state-default').each(function() { + if (today.date().toString() === this.innerHTML) { + $(this).addClass('ui-state-highlight').parent().addClass('ui-datepicker-today'); + } + }); } }; })(); diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/layout.js b/src/Oro/Bundle/UIBundle/Resources/public/js/layout.js index 7691dd48e92..c7b834dcc9f 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/layout.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/layout.js @@ -53,10 +53,24 @@ define(function(require) { * @returns {number} development toolbar height in dev mode, 0 in production mode */ getDevToolbarHeight: function() { - if (!this.devToolbarHeight) { + if (!mediator.execute('retrieveOption', 'debug')) { + return 0; + } + if (!this.devToolbarHeightListenersAttached) { + this.devToolbarHeightListenersAttached = true; + $(window).on('resize', function() { + delete layout.devToolbarHeight; + }); + mediator.on('debugToolbar:afterUpdateView', function() { + delete layout.devToolbarHeight; + }); + } + if (this.devToolbarHeight === void 0) { var devToolbarComposition = mediator.execute('composer:retrieve', 'debugToolbar', true); - if (devToolbarComposition && devToolbarComposition.view) { - this.devToolbarHeight = devToolbarComposition.view.$el.height(); + if (devToolbarComposition && + devToolbarComposition.view && + devToolbarComposition.view.$('.sf-toolbarreset').is(':visible')) { + this.devToolbarHeight = devToolbarComposition.view.$('.sf-toolbarreset').height(); } else { this.devToolbarHeight = 0; } diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/messenger.js b/src/Oro/Bundle/UIBundle/Resources/public/js/messenger.js index 701033917c8..83d111d519a 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/messenger.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/messenger.js @@ -53,19 +53,26 @@ define([ * or false - means to not close automatically * @param {Function} options.template template function * @param {boolean} options.flash flag to turn on default delay close call, it's 5s + * @param {boolean} options.afterReload whether the message should be shown after a page is reloaded * * @return {Object} collection of methods - actions over message element, * at the moment there's only one method 'close', allows to close the message */ notificationMessage: function(type, message, options) { - var container = (options || {}).container || defaults.container; + var container = (options || {}).container || defaults.container; + var afterReload = (options || {}).afterReload || false; + var afterReloadQueue = []; var args = Array.prototype.slice.call(arguments); var actions = {close: $.noop}; - if (container && $(container).length) { + if (afterReload && window.localStorage) { + afterReloadQueue = JSON.parse(localStorage.getItem('oroAfterReloadMessages') || '[]'); + afterReloadQueue.push(args); + localStorage.setItem('oroAfterReloadMessages', JSON.stringify(afterReloadQueue)); + } else if (container && $(container).length) { actions = showMessage.apply(null, args); } else { // if container is not ready then save message for later - queue.push([args, actions]); + queue.push(args); } return actions; }, @@ -82,6 +89,7 @@ define([ * or false - means to not close automatically * @param {Function} options.template template function * @param {boolean} options.flash flag to turn on default delay close call, it's 5s + * @param {boolean} options.afterReload whether the message should be shown after a page is reloaded * * @return {Object} collection of methods - actions over message element, * at the moment there's only one method 'close', allows to close the message @@ -137,17 +145,18 @@ define([ setup: function(options) { _.extend(defaults, options); + if (window.localStorage) { + queue = queue.concat(JSON.parse(localStorage.getItem('oroAfterReloadMessages') || '[]')); + localStorage.removeItem('oroAfterReloadMessages'); + } + while (queue.length) { - var args = queue.shift(); - _.extend(args[1], showMessage.apply(null, args[0])); + showMessage.apply(null, queue.shift()); } }, addMessage: function(type, message, options) { - var args = [type, message, _.extend({flash: true}, options)]; - var actions = {close: $.noop}; - - queue.push([args, actions]); + queue.push([type, message, _.extend({flash: true}, options)]); }, showProcessingMessage: function(message, promise, type) { diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/tools/api-accessor.js b/src/Oro/Bundle/UIBundle/Resources/public/js/tools/api-accessor.js index 98637525204..30970ea36f3 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/tools/api-accessor.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/tools/api-accessor.js @@ -114,6 +114,14 @@ define(function(require) { return this.httpMethod === 'GET' || this.httpMethod === 'OPTIONS' || this.httpMethod === 'HEAD'; }, + /** + * Clears response cache + */ + clearCache: function() { + this.cache = {}; + this.trigger('cache:clear'); + }, + /** * Validates url parameters * diff --git a/src/Oro/Bundle/UIBundle/Resources/public/lib/jquery/select2.js b/src/Oro/Bundle/UIBundle/Resources/public/lib/jquery/select2.js index 1d559412421..3824dda15f0 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/lib/jquery/select2.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/lib/jquery/select2.js @@ -2944,7 +2944,7 @@ the specific language governing permissions and limitations under the Apache Lic self.updateSelection(data); self.clearSearch(); if (triggerChange) { - self.triggerChange(this.buildChangeDetails(oldData, this.data())); + self.triggerChange(self.buildChangeDetails(oldData, self.data())); } }); } diff --git a/src/Oro/Bundle/UIBundle/Resources/public/lib/jstree/jquery.cookie.js b/src/Oro/Bundle/UIBundle/Resources/public/lib/jstree/jquery.cookie.js deleted file mode 100644 index 6df1faca25f..00000000000 --- a/src/Oro/Bundle/UIBundle/Resources/public/lib/jstree/jquery.cookie.js +++ /dev/null @@ -1,96 +0,0 @@ -/** - * Cookie plugin - * - * Copyright (c) 2006 Klaus Hartl (stilbuero.de) - * Dual licensed under the MIT and GPL licenses: - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - * - */ - -/** - * Create a cookie with the given name and value and other optional parameters. - * - * @example $.cookie('the_cookie', 'the_value'); - * @desc Set the value of a cookie. - * @example $.cookie('the_cookie', 'the_value', { expires: 7, path: '/', domain: 'jquery.com', secure: true }); - * @desc Create a cookie with all available options. - * @example $.cookie('the_cookie', 'the_value'); - * @desc Create a session cookie. - * @example $.cookie('the_cookie', null); - * @desc Delete a cookie by passing null as value. Keep in mind that you have to use the same path and domain - * used when the cookie was set. - * - * @param String name The name of the cookie. - * @param String value The value of the cookie. - * @param Object options An object literal containing key/value pairs to provide optional cookie attributes. - * @option Number|Date expires Either an integer specifying the expiration date from now on in days or a Date object. - * If a negative value is specified (e.g. a date in the past), the cookie will be deleted. - * If set to null or omitted, the cookie will be a session cookie and will not be retained - * when the the browser exits. - * @option String path The value of the path atribute of the cookie (default: path of page that created the cookie). - * @option String domain The value of the domain attribute of the cookie (default: domain of page that created the cookie). - * @option Boolean secure If true, the secure attribute of the cookie will be set and the cookie transmission will - * require a secure protocol (like HTTPS). - * @type undefined - * - * @name $.cookie - * @cat Plugins/Cookie - * @author Klaus Hartl/klaus.hartl@stilbuero.de - */ - -/** - * Get the value of a cookie with the given name. - * - * @example $.cookie('the_cookie'); - * @desc Get the value of a cookie. - * - * @param String name The name of the cookie. - * @return The value of the cookie. - * @type String - * - * @name $.cookie - * @cat Plugins/Cookie - * @author Klaus Hartl/klaus.hartl@stilbuero.de - */ -jQuery.cookie = function(name, value, options) { - if (typeof value != 'undefined') { // name and value given, set cookie - options = options || {}; - if (value === null) { - value = ''; - options.expires = -1; - } - var expires = ''; - if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) { - var date; - if (typeof options.expires == 'number') { - date = new Date(); - date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000)); - } else { - date = options.expires; - } - expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE - } - // CAUTION: Needed to parenthesize options.path and options.domain - // in the following expressions, otherwise they evaluate to undefined - // in the packed version for some reason... - var path = options.path ? '; path=' + (options.path) : ''; - var domain = options.domain ? '; domain=' + (options.domain) : ''; - var secure = options.secure ? '; secure' : ''; - document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join(''); - } else { // only name given, get cookie - var cookieValue = null; - if (document.cookie && document.cookie != '') { - var cookies = document.cookie.split(';'); - for (var i = 0; i < cookies.length; i++) { - var cookie = jQuery.trim(cookies[i]); - // Does this cookie string begin with the name we want? - if (cookie.substring(0, name.length + 1) == (name + '=')) { - cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); - break; - } - } - } - return cookieValue; - } -}; \ No newline at end of file diff --git a/src/Oro/Bundle/UIBundle/Resources/public/lib/jstree/jquery.hotkeys.js b/src/Oro/Bundle/UIBundle/Resources/public/lib/jstree/jquery.hotkeys.js deleted file mode 100644 index c240a7fd7f2..00000000000 --- a/src/Oro/Bundle/UIBundle/Resources/public/lib/jstree/jquery.hotkeys.js +++ /dev/null @@ -1,111 +0,0 @@ -/* - * jQuery Hotkeys Plugin - * Copyright 2010, John Resig - * Dual licensed under the MIT or GPL Version 2 licenses. - * - * Based upon the plugin by Tzury Bar Yochay: - * http://github.com/tzuryby/hotkeys - * - * Original idea by: - * Binny V A, http://www.openjs.com/scripts/events/keyboard_shortcuts/ -*/ - -/* - * One small change is: now keys are passed by object { keys: '...' } - * Might be useful, when you want to pass some other data to your handler - */ - -(function(jQuery){ - - jQuery.hotkeys = { - version: "0.8", - - specialKeys: { - 8: "backspace", 9: "tab", 10: "return", 13: "return", 16: "shift", 17: "ctrl", 18: "alt", 19: "pause", - 20: "capslock", 27: "esc", 32: "space", 33: "pageup", 34: "pagedown", 35: "end", 36: "home", - 37: "left", 38: "up", 39: "right", 40: "down", 45: "insert", 46: "del", - 96: "0", 97: "1", 98: "2", 99: "3", 100: "4", 101: "5", 102: "6", 103: "7", - 104: "8", 105: "9", 106: "*", 107: "+", 109: "-", 110: ".", 111 : "/", - 112: "f1", 113: "f2", 114: "f3", 115: "f4", 116: "f5", 117: "f6", 118: "f7", 119: "f8", - 120: "f9", 121: "f10", 122: "f11", 123: "f12", 144: "numlock", 145: "scroll", 186: ";", 191: "/", - 220: "\\", 222: "'", 224: "meta" - }, - - shiftNums: { - "`": "~", "1": "!", "2": "@", "3": "#", "4": "$", "5": "%", "6": "^", "7": "&", - "8": "*", "9": "(", "0": ")", "-": "_", "=": "+", ";": ": ", "'": "\"", ",": "<", - ".": ">", "/": "?", "\\": "|" - } - }; - - function keyHandler( handleObj ) { - if ( typeof handleObj.data === "string" ) { - handleObj.data = { keys: handleObj.data }; - } - - // Only care when a possible input has been specified - if ( !handleObj.data || !handleObj.data.keys || typeof handleObj.data.keys !== "string" ) { - return; - } - - var origHandler = handleObj.handler, - keys = handleObj.data.keys.toLowerCase().split(" "), - textAcceptingInputTypes = ["text", "password", "number", "email", "url", "range", "date", "month", "week", "time", "datetime", "datetime-local", "search", "color", "tel"]; - - handleObj.handler = function( event ) { - // Don't fire in text-accepting inputs that we didn't directly bind to - if ( this !== event.target && (/textarea|select/i.test( event.target.nodeName ) || - jQuery.inArray(event.target.type, textAcceptingInputTypes) > -1 ) ) { - return; - } - - var special = jQuery.hotkeys.specialKeys[ event.keyCode ], - // character codes are available only in keypress - character = event.type === "keypress" && String.fromCharCode( event.which ).toLowerCase(), - modif = "", possible = {}; - - // check combinations (alt|ctrl|shift+anything) - if ( event.altKey && special !== "alt" ) { - modif += "alt+"; - } - - if ( event.ctrlKey && special !== "ctrl" ) { - modif += "ctrl+"; - } - - // TODO: Need to make sure this works consistently across platforms - if ( event.metaKey && !event.ctrlKey && special !== "meta" ) { - modif += "meta+"; - } - - if ( event.shiftKey && special !== "shift" ) { - modif += "shift+"; - } - - if ( special ) { - possible[ modif + special ] = true; - } - - if ( character ) { - possible[ modif + character ] = true; - possible[ modif + jQuery.hotkeys.shiftNums[ character ] ] = true; - - // "$" can be triggered as "Shift+4" or "Shift+$" or just "$" - if ( modif === "shift+" ) { - possible[ jQuery.hotkeys.shiftNums[ character ] ] = true; - } - } - - for ( var i = 0, l = keys.length; i < l; i++ ) { - if ( possible[ keys[i] ] ) { - return origHandler.apply( this, arguments ); - } - } - }; - } - - jQuery.each([ "keydown", "keyup", "keypress" ], function() { - jQuery.event.special[ this ] = { add: keyHandler }; - }); - -})( this.jQuery ); diff --git a/src/Oro/Bundle/UIBundle/Resources/public/lib/jstree/jquery.jstree.js b/src/Oro/Bundle/UIBundle/Resources/public/lib/jstree/jquery.jstree.js deleted file mode 100644 index 6b23c05f3e1..00000000000 --- a/src/Oro/Bundle/UIBundle/Resources/public/lib/jstree/jquery.jstree.js +++ /dev/null @@ -1,4562 +0,0 @@ -/* - * jsTree 1.0-rc3 - * http://jstree.com/ - * - * Copyright (c) 2010 Ivan Bozhanov (vakata.com) - * - * Licensed same as jquery - under the terms of either the MIT License or the GPL Version 2 License - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - * - * $Date: 2011-02-09 01:17:14 +0200 (ср, 09 февр 2011) $ - * $Revision: 236 $ - */ - - -"use strict"; - -// top wrapper to prevent multiple inclusion (is this OK?) -(function () { if(jQuery && jQuery.jstree) { return; } - var is_ie6 = false, is_ie7 = false, is_ff2 = false; - -/* - * jsTree core - */ -(function ($) { - // Common functions not related to jsTree - // decided to move them to a `vakata` "namespace" - $.vakata = {}; - // CSS related functions - $.vakata.css = { - get_css : function(rule_name, delete_flag, sheet) { - rule_name = rule_name.toLowerCase(); - var css_rules = sheet.cssRules || sheet.rules, - j = 0; - do { - if(css_rules.length && j > css_rules.length + 5) { return false; } - if(css_rules[j].selectorText && css_rules[j].selectorText.toLowerCase() == rule_name) { - if(delete_flag === true) { - if(sheet.removeRule) { sheet.removeRule(j); } - if(sheet.deleteRule) { sheet.deleteRule(j); } - return true; - } - else { return css_rules[j]; } - } - } - while (css_rules[++j]); - return false; - }, - add_css : function(rule_name, sheet) { - if($.jstree.css.get_css(rule_name, false, sheet)) { return false; } - if(sheet.insertRule) { sheet.insertRule(rule_name + ' { }', 0); } else { sheet.addRule(rule_name, null, 0); } - return $.vakata.css.get_css(rule_name); - }, - remove_css : function(rule_name, sheet) { - return $.vakata.css.get_css(rule_name, true, sheet); - }, - add_sheet : function(opts) { - var tmp = false, is_new = true; - if(opts.str) { - if(opts.title) { tmp = $("style[id='" + opts.title + "-stylesheet']")[0]; } - if(tmp) { is_new = false; } - else { - tmp = document.createElement("style"); - tmp.setAttribute('type',"text/css"); - if(opts.title) { tmp.setAttribute("id", opts.title + "-stylesheet"); } - } - if(tmp.styleSheet) { - if(is_new) { - document.getElementsByTagName("head")[0].appendChild(tmp); - tmp.styleSheet.cssText = opts.str; - } - else { - tmp.styleSheet.cssText = tmp.styleSheet.cssText + " " + opts.str; - } - } - else { - tmp.appendChild(document.createTextNode(opts.str)); - document.getElementsByTagName("head")[0].appendChild(tmp); - } - return tmp.sheet || tmp.styleSheet; - } - if(opts.url) { - if(document.createStyleSheet) { - try { tmp = document.createStyleSheet(opts.url); } catch (e) { } - } - else { - tmp = document.createElement('link'); - tmp.rel = 'stylesheet'; - tmp.type = 'text/css'; - tmp.media = "all"; - tmp.href = opts.url; - document.getElementsByTagName("head")[0].appendChild(tmp); - return tmp.styleSheet; - } - } - } - }; - - // private variables - var instances = [], // instance array (used by $.jstree.reference/create/focused) - focused_instance = -1, // the index in the instance array of the currently focused instance - plugins = {}, // list of included plugins - prepared_move = {}; // for the move_node function - - // jQuery plugin wrapper (thanks to jquery UI widget function) - $.fn.jstree = function (settings) { - var isMethodCall = (typeof settings == 'string'), // is this a method call like $().jstree("open_node") - args = Array.prototype.slice.call(arguments, 1), - returnValue = this; - - // if a method call execute the method on all selected instances - if(isMethodCall) { - if(settings.substring(0, 1) == '_') { return returnValue; } - this.each(function() { - var instance = instances[$.data(this, "jstree_instance_id")], - methodValue = (instance && $.isFunction(instance[settings])) ? instance[settings].apply(instance, args) : instance; - if(typeof methodValue !== "undefined" && (settings.indexOf("is_") === 0 || (methodValue !== true && methodValue !== false))) { returnValue = methodValue; return false; } - }); - } - else { - this.each(function() { - // extend settings and allow for multiple hashes and $.data - var instance_id = $.data(this, "jstree_instance_id"), - a = [], - b = settings ? $.extend({}, true, settings) : {}, - c = $(this), - s = false, - t = []; - a = a.concat(args); - if(c.data("jstree")) { a.push(c.data("jstree")); } - b = a.length ? $.extend.apply(null, [true, b].concat(a)) : b; - - // if an instance already exists, destroy it first - if(typeof instance_id !== "undefined" && instances[instance_id]) { instances[instance_id].destroy(); } - // push a new empty object to the instances array - instance_id = parseInt(instances.push({}),10) - 1; - // store the jstree instance id to the container element - $.data(this, "jstree_instance_id", instance_id); - // clean up all plugins - b.plugins = $.isArray(b.plugins) ? b.plugins : $.jstree.defaults.plugins.slice(); - b.plugins.unshift("core"); - // only unique plugins - b.plugins = b.plugins.sort().join(",,").replace(/(,|^)([^,]+)(,,\2)+(,|$)/g,"$1$2$4").replace(/,,+/g,",").replace(/,$/,"").split(","); - - // extend defaults with passed data - s = $.extend(true, {}, $.jstree.defaults, b); - s.plugins = b.plugins; - $.each(plugins, function (i, val) { - if($.inArray(i, s.plugins) === -1) { s[i] = null; delete s[i]; } - else { t.push(i); } - }); - s.plugins = t; - - // push the new object to the instances array (at the same time set the default classes to the container) and init - instances[instance_id] = new $.jstree._instance(instance_id, $(this).addClass("jstree jstree-" + instance_id), s); - // init all activated plugins for this instance - $.each(instances[instance_id]._get_settings().plugins, function (i, val) { instances[instance_id].data[val] = {}; }); - $.each(instances[instance_id]._get_settings().plugins, function (i, val) { if(plugins[val]) { plugins[val].__init.apply(instances[instance_id]); } }); - // initialize the instance - setTimeout(function() { if(instances[instance_id]) { instances[instance_id].init(); } }, 0); - }); - } - // return the jquery selection (or if it was a method call that returned a value - the returned value) - return returnValue; - }; - // object to store exposed functions and objects - $.jstree = { - defaults : { - plugins : [] - }, - _focused : function () { return instances[focused_instance] || null; }, - _reference : function (needle) { - // get by instance id - if(instances[needle]) { return instances[needle]; } - // get by DOM (if still no luck - return null - var o = $(needle); - if(!o.length && typeof needle === "string") { o = $("#" + needle); } - if(!o.length) { return null; } - return instances[o.closest(".jstree").data("jstree_instance_id")] || null; - }, - _instance : function (index, container, settings) { - // for plugins to store data in - this.data = { core : {} }; - this.get_settings = function () { return $.extend(true, {}, settings); }; - this._get_settings = function () { return settings; }; - this.get_index = function () { return index; }; - this.get_container = function () { return container; }; - this.get_container_ul = function () { return container.children("ul:eq(0)"); }; - this._set_settings = function (s) { - settings = $.extend(true, {}, settings, s); - }; - }, - _fn : { }, - plugin : function (pname, pdata) { - pdata = $.extend({}, { - __init : $.noop, - __destroy : $.noop, - _fn : {}, - defaults : false - }, pdata); - plugins[pname] = pdata; - - $.jstree.defaults[pname] = pdata.defaults; - $.each(pdata._fn, function (i, val) { - val.plugin = pname; - val.old = $.jstree._fn[i]; - $.jstree._fn[i] = function () { - var rslt, - func = val, - args = Array.prototype.slice.call(arguments), - evnt = new $.Event("before.jstree"), - rlbk = false; - - if(this.data.core.locked === true && i !== "unlock" && i !== "is_locked") { return; } - - // Check if function belongs to the included plugins of this instance - do { - if(func && func.plugin && $.inArray(func.plugin, this._get_settings().plugins) !== -1) { break; } - func = func.old; - } while(func); - if(!func) { return; } - - // context and function to trigger events, then finally call the function - if(i.indexOf("_") === 0) { - rslt = func.apply(this, args); - } - else { - rslt = this.get_container().triggerHandler(evnt, { "func" : i, "inst" : this, "args" : args, "plugin" : func.plugin }); - if(rslt === false) { return; } - if(typeof rslt !== "undefined") { args = rslt; } - - rslt = func.apply( - $.extend({}, this, { - __callback : function (data) { - this.get_container().triggerHandler( i + '.jstree', { "inst" : this, "args" : args, "rslt" : data, "rlbk" : rlbk }); - }, - __rollback : function () { - rlbk = this.get_rollback(); - return rlbk; - }, - __call_old : function (replace_arguments) { - return func.old.apply(this, (replace_arguments ? Array.prototype.slice.call(arguments, 1) : args ) ); - } - }), args); - } - - // return the result - return rslt; - }; - $.jstree._fn[i].old = val.old; - $.jstree._fn[i].plugin = pname; - }); - }, - rollback : function (rb) { - if(rb) { - if(!$.isArray(rb)) { rb = [ rb ]; } - $.each(rb, function (i, val) { - instances[val.i].set_rollback(val.h, val.d); - }); - } - } - }; - // set the prototype for all instances - $.jstree._fn = $.jstree._instance.prototype = {}; - - // load the css when DOM is ready - $(function() { - // code is copied from jQuery ($.browser is deprecated + there is a bug in IE) - var u = navigator.userAgent.toLowerCase(), - v = (u.match( /.+?(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [0,'0'])[1], - css_string = '' + - '.jstree ul, .jstree li { display:block; margin:0 0 0 0; padding:0 0 0 0; list-style-type:none; } ' + - '.jstree li { display:block; min-height:18px; line-height:18px; white-space:nowrap; margin-left:18px; min-width:18px; } ' + - '.jstree-rtl li { margin-left:0; margin-right:18px; } ' + - '.jstree > ul > li { margin-left:0px; } ' + - '.jstree-rtl > ul > li { margin-right:0px; } ' + - '.jstree ins { display:inline-block; text-decoration:none; width:18px; height:18px; margin:0 0 0 0; padding:0; } ' + - '.jstree a { display:inline-block; line-height:16px; height:16px; color:black; white-space:nowrap; text-decoration:none; padding:1px 2px; margin:0; } ' + - '.jstree a:focus { outline: none; } ' + - '.jstree a > ins { height:16px; width:16px; } ' + - '.jstree a > .jstree-icon { margin-right:3px; } ' + - '.jstree-rtl a > .jstree-icon { margin-left:3px; margin-right:0; } ' + - 'li.jstree-open > ul { display:block; } ' + - 'li.jstree-closed > ul { display:none; } '; - // Correct IE 6 (does not support the > CSS selector) - if(/msie/.test(u) && parseInt(v, 10) == 6) { - is_ie6 = true; - - // fix image flicker and lack of caching - try { - document.execCommand("BackgroundImageCache", false, true); - } catch (err) { } - - css_string += '' + - '.jstree li { height:18px; margin-left:0; margin-right:0; } ' + - '.jstree li li { margin-left:18px; } ' + - '.jstree-rtl li li { margin-left:0px; margin-right:18px; } ' + - 'li.jstree-open ul { display:block; } ' + - 'li.jstree-closed ul { display:none !important; } ' + - '.jstree li a { display:inline; border-width:0 !important; padding:0px 2px !important; } ' + - '.jstree li a ins { height:16px; width:16px; margin-right:3px; } ' + - '.jstree-rtl li a ins { margin-right:0px; margin-left:3px; } '; - } - // Correct IE 7 (shifts anchor nodes onhover) - if(/msie/.test(u) && parseInt(v, 10) == 7) { - is_ie7 = true; - css_string += '.jstree li a { border-width:0 !important; padding:0px 2px !important; } '; - } - // correct ff2 lack of display:inline-block - if(!/compatible/.test(u) && /mozilla/.test(u) && parseFloat(v, 10) < 1.9) { - is_ff2 = true; - css_string += '' + - '.jstree ins { display:-moz-inline-box; } ' + - '.jstree li { line-height:12px; } ' + // WHY?? - '.jstree a { display:-moz-inline-box; } ' + - '.jstree .jstree-no-icons .jstree-checkbox { display:-moz-inline-stack !important; } '; - /* this shouldn't be here as it is theme specific */ - } - // the default stylesheet - $.vakata.css.add_sheet({ str : css_string, title : "jstree" }); - }); - - // core functions (open, close, create, update, delete) - $.jstree.plugin("core", { - __init : function () { - this.data.core.locked = false; - this.data.core.to_open = this.get_settings().core.initially_open; - this.data.core.to_load = this.get_settings().core.initially_load; - }, - defaults : { - html_titles : false, - animation : 500, - initially_open : [], - initially_load : [], - open_parents : true, - notify_plugins : true, - rtl : false, - load_open : false, - strings : { - loading : "Loading ...", - new_node : "New node", - multiple_selection : "Multiple selection" - } - }, - _fn : { - init : function () { - this.set_focus(); - if(this._get_settings().core.rtl) { - this.get_container().addClass("jstree-rtl").css("direction", "rtl"); - } - this.get_container().html(""); - this.data.core.li_height = this.get_container_ul().find("li.jstree-closed, li.jstree-leaf").eq(0).height() || 18; - - this.get_container() - .delegate("li > ins", "click.jstree", $.proxy(function (event) { - var trgt = $(event.target); - // if(trgt.is("ins") && event.pageY - trgt.offset().top < this.data.core.li_height) { this.toggle_node(trgt); } - this.toggle_node(trgt); - }, this)) - .bind("mousedown.jstree", $.proxy(function () { - this.set_focus(); // This used to be setTimeout(set_focus,0) - why? - }, this)) - .bind("dblclick.jstree", function (event) { - var sel; - if(document.selection && document.selection.empty) { document.selection.empty(); } - else { - if(window.getSelection) { - sel = window.getSelection(); - try { - sel.removeAllRanges(); - sel.collapse(); - } catch (err) { } - } - } - }); - if(this._get_settings().core.notify_plugins) { - this.get_container() - .bind("load_node.jstree", $.proxy(function (e, data) { - var o = this._get_node(data.rslt.obj), - t = this; - if(o === -1) { o = this.get_container_ul(); } - if(!o.length) { return; } - o.find("li").each(function () { - var th = $(this); - if(th.data("jstree")) { - $.each(th.data("jstree"), function (plugin, values) { - if(t.data[plugin] && $.isFunction(t["_" + plugin + "_notify"])) { - t["_" + plugin + "_notify"].call(t, th, values); - } - }); - } - }); - }, this)); - } - if(this._get_settings().core.load_open) { - this.get_container() - .bind("load_node.jstree", $.proxy(function (e, data) { - var o = this._get_node(data.rslt.obj), - t = this; - if(o === -1) { o = this.get_container_ul(); } - if(!o.length) { return; } - o.find("li.jstree-open:not(:has(ul))").each(function () { - t.load_node(this, $.noop, $.noop); - }); - }, this)); - } - this.__callback(); - this.load_node(-1, function () { this.loaded(); this.reload_nodes(); }); - }, - destroy : function () { - var i, - n = this.get_index(), - s = this._get_settings(), - _this = this; - - $.each(s.plugins, function (i, val) { - try { plugins[val].__destroy.apply(_this); } catch(err) { } - }); - this.__callback(); - // set focus to another instance if this one is focused - if(this.is_focused()) { - for(i in instances) { - if(instances.hasOwnProperty(i) && i != n) { - instances[i].set_focus(); - break; - } - } - } - // if no other instance found - if(n === focused_instance) { focused_instance = -1; } - // remove all traces of jstree in the DOM (only the ones set using jstree*) and cleans all events - this.get_container() - .unbind(".jstree") - .undelegate(".jstree") - .removeData("jstree_instance_id") - .find("[class^='jstree']") - .addBack() - .attr("class", function () { return this.className.replace(/jstree[^ ]*|$/ig,''); }); - $(document) - .unbind(".jstree-" + n) - .undelegate(".jstree-" + n); - // remove the actual data - instances[n] = null; - delete instances[n]; - }, - - _core_notify : function (n, data) { - if(data.opened) { - this.open_node(n, false, true); - } - }, - - lock : function () { - this.data.core.locked = true; - this.get_container().children("ul").addClass("jstree-locked").css("opacity","0.7"); - this.__callback({}); - }, - unlock : function () { - this.data.core.locked = false; - this.get_container().children("ul").removeClass("jstree-locked").css("opacity","1"); - this.__callback({}); - }, - is_locked : function () { return this.data.core.locked; }, - save_opened : function () { - var _this = this; - this.data.core.to_open = []; - this.get_container_ul().find("li.jstree-open").each(function () { - if(this.id) { _this.data.core.to_open.push("#" + this.id.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:")); } - }); - this.__callback(_this.data.core.to_open); - }, - save_loaded : function () { }, - reload_nodes : function (is_callback) { - var _this = this, - done = true, - current = [], - remaining = []; - if(!is_callback) { - this.data.core.reopen = false; - this.data.core.refreshing = true; - this.data.core.to_open = $.map($.makeArray(this.data.core.to_open), function (n) { return "#" + n.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:"); }); - this.data.core.to_load = $.map($.makeArray(this.data.core.to_load), function (n) { return "#" + n.toString().replace(/^#/,"").replace(/\\\//g,"/").replace(/\//g,"\\\/").replace(/\\\./g,".").replace(/\./g,"\\.").replace(/\:/g,"\\:"); }); - if(this.data.core.to_open.length) { - this.data.core.to_load = this.data.core.to_load.concat(this.data.core.to_open); - } - } - if(this.data.core.to_load.length) { - $.each(this.data.core.to_load, function (i, val) { - if(val == "#") { return true; } - if($(val).length) { current.push(val); } - else { remaining.push(val); } - }); - if(current.length) { - this.data.core.to_load = remaining; - $.each(current, function (i, val) { - if(!_this._is_loaded(val)) { - _this.load_node(val, function () { _this.reload_nodes(true); }, function () { _this.reload_nodes(true); }); - done = false; - } - }); - } - } - if(this.data.core.to_open.length) { - $.each(this.data.core.to_open, function (i, val) { - _this.open_node(val, false, true); - }); - } - if(done) { - // TODO: find a more elegant approach to syncronizing returning requests - if(this.data.core.reopen) { clearTimeout(this.data.core.reopen); } - this.data.core.reopen = setTimeout(function () { _this.__callback({}, _this); }, 50); - this.data.core.refreshing = false; - this.reopen(); - } - }, - reopen : function () { - var _this = this; - if(this.data.core.to_open.length) { - $.each(this.data.core.to_open, function (i, val) { - _this.open_node(val, false, true); - }); - } - this.__callback({}); - }, - refresh : function (obj) { - var _this = this; - this.save_opened(); - if(!obj) { obj = -1; } - obj = this._get_node(obj); - if(!obj) { obj = -1; } - if(obj !== -1) { obj.children("UL").remove(); } - else { this.get_container_ul().empty(); } - this.load_node(obj, function () { _this.__callback({ "obj" : obj}); _this.reload_nodes(); }); - }, - // Dummy function to fire after the first load (so that there is a jstree.loaded event) - loaded : function () { - this.__callback(); - }, - // deal with focus - set_focus : function () { - if(this.is_focused()) { return; } - var f = $.jstree._focused(); - if(f) { f.unset_focus(); } - - this.get_container().addClass("jstree-focused"); - focused_instance = this.get_index(); - this.__callback(); - }, - is_focused : function () { - return focused_instance == this.get_index(); - }, - unset_focus : function () { - if(this.is_focused()) { - this.get_container().removeClass("jstree-focused"); - focused_instance = -1; - } - this.__callback(); - }, - - // traverse - _get_node : function (obj) { - var $obj = $(obj, this.get_container()); - if($obj.is(".jstree") || obj == -1) { return -1; } - $obj = $obj.closest("li", this.get_container()); - return $obj.length ? $obj : false; - }, - _get_next : function (obj, strict) { - obj = this._get_node(obj); - if(obj === -1) { return this.get_container().find("> ul > li:first-child"); } - if(!obj.length) { return false; } - if(strict) { return (obj.nextAll("li").size() > 0) ? obj.nextAll("li:eq(0)") : false; } - - if(obj.hasClass("jstree-open")) { return obj.find("li:eq(0)"); } - else if(obj.nextAll("li").size() > 0) { return obj.nextAll("li:eq(0)"); } - else { return obj.parentsUntil(".jstree","li").next("li").eq(0); } - }, - _get_prev : function (obj, strict) { - obj = this._get_node(obj); - if(obj === -1) { return this.get_container().find("> ul > li:last-child"); } - if(!obj.length) { return false; } - if(strict) { return (obj.prevAll("li").length > 0) ? obj.prevAll("li:eq(0)") : false; } - - if(obj.prev("li").length) { - obj = obj.prev("li").eq(0); - while(obj.hasClass("jstree-open")) { obj = obj.children("ul:eq(0)").children("li:last"); } - return obj; - } - else { var o = obj.parentsUntil(".jstree","li:eq(0)"); return o.length ? o : false; } - }, - _get_parent : function (obj) { - obj = this._get_node(obj); - if(obj == -1 || !obj.length) { return false; } - var o = obj.parentsUntil(".jstree", "li:eq(0)"); - return o.length ? o : -1; - }, - _get_children : function (obj) { - obj = this._get_node(obj); - if(obj === -1) { return this.get_container().children("ul:eq(0)").children("li"); } - if(!obj.length) { return false; } - return obj.children("ul:eq(0)").children("li"); - }, - get_path : function (obj, id_mode) { - var p = [], - _this = this; - obj = this._get_node(obj); - if(obj === -1 || !obj || !obj.length) { return false; } - obj.parentsUntil(".jstree", "li").each(function () { - p.push( id_mode ? this.id : _this.get_text(this) ); - }); - p.reverse(); - p.push( id_mode ? obj.attr("id") : this.get_text(obj) ); - return p; - }, - - // string functions - _get_string : function (key) { - return this._get_settings().core.strings[key] || key; - }, - - is_open : function (obj) { obj = this._get_node(obj); return obj && obj !== -1 && obj.hasClass("jstree-open"); }, - is_closed : function (obj) { obj = this._get_node(obj); return obj && obj !== -1 && obj.hasClass("jstree-closed"); }, - is_leaf : function (obj) { obj = this._get_node(obj); return obj && obj !== -1 && obj.hasClass("jstree-leaf"); }, - correct_state : function (obj) { - obj = this._get_node(obj); - if(!obj || obj === -1) { return false; } - obj.removeClass("jstree-closed jstree-open").addClass("jstree-leaf").children("ul").remove(); - this.__callback({ "obj" : obj }); - }, - // open/close - open_node : function (obj, callback, skip_animation) { - obj = this._get_node(obj); - if(!obj.length) { return false; } - if(!obj.hasClass("jstree-closed")) { if(callback) { callback.call(); } return false; } - var s = skip_animation || is_ie6 ? 0 : this._get_settings().core.animation, - t = this; - if(!this._is_loaded(obj)) { - obj.children("a").addClass("jstree-loading"); - this.load_node(obj, function () { t.open_node(obj, callback, skip_animation); }, callback); - } - else { - if(this._get_settings().core.open_parents) { - obj.parentsUntil(".jstree",".jstree-closed").each(function () { - t.open_node(this, false, true); - }); - } - if(s) { obj.children("ul").css("display","none"); } - obj.removeClass("jstree-closed").addClass("jstree-open").children("a").removeClass("jstree-loading"); - if(s) { obj.children("ul").stop(true, true).slideDown(s, function () { this.style.display = ""; t.after_open(obj); }); } - else { t.after_open(obj); } - this.__callback({ "obj" : obj }); - if(callback) { callback.call(); } - } - }, - after_open : function (obj) { this.__callback({ "obj" : obj }); }, - close_node : function (obj, skip_animation) { - obj = this._get_node(obj); - var s = skip_animation || is_ie6 ? 0 : this._get_settings().core.animation, - t = this; - if(!obj.length || !obj.hasClass("jstree-open")) { return false; } - if(s) { obj.children("ul").attr("style","display:block !important"); } - obj.removeClass("jstree-open").addClass("jstree-closed"); - if(s) { obj.children("ul").stop(true, true).slideUp(s, function () { this.style.display = ""; t.after_close(obj); }); } - else { t.after_close(obj); } - this.__callback({ "obj" : obj }); - }, - after_close : function (obj) { this.__callback({ "obj" : obj }); }, - toggle_node : function (obj) { - obj = this._get_node(obj); - if(obj.hasClass("jstree-closed")) { return this.open_node(obj); } - if(obj.hasClass("jstree-open")) { return this.close_node(obj); } - }, - open_all : function (obj, do_animation, original_obj) { - obj = obj ? this._get_node(obj) : -1; - if(!obj || obj === -1) { obj = this.get_container_ul(); } - if(original_obj) { - obj = obj.find("li.jstree-closed"); - } - else { - original_obj = obj; - if(obj.is(".jstree-closed")) { obj = obj.find("li.jstree-closed").addBack(); } - else { obj = obj.find("li.jstree-closed"); } - } - var _this = this; - obj.each(function () { - var __this = this; - if(!_this._is_loaded(this)) { _this.open_node(this, function() { _this.open_all(__this, do_animation, original_obj); }, !do_animation); } - else { _this.open_node(this, false, !do_animation); } - }); - // so that callback is fired AFTER all nodes are open - if(original_obj.find('li.jstree-closed').length === 0) { this.__callback({ "obj" : original_obj }); } - }, - close_all : function (obj, do_animation) { - var _this = this; - obj = obj ? this._get_node(obj) : this.get_container(); - if(!obj || obj === -1) { obj = this.get_container_ul(); } - obj.find("li.jstree-open").addBack().each(function () { _this.close_node(this, !do_animation); }); - this.__callback({ "obj" : obj }); - }, - clean_node : function (obj) { - obj = obj && obj != -1 ? $(obj) : this.get_container_ul(); - obj = obj.is("li") ? obj.find("li").addBack() : obj.find("li"); - obj.removeClass("jstree-last") - .filter("li:last-child").addClass("jstree-last").end() - .filter(":has(li)") - .not(".jstree-open").removeClass("jstree-leaf").addClass("jstree-closed"); - obj.not(".jstree-open, .jstree-closed").addClass("jstree-leaf").children("ul").remove(); - this.__callback({ "obj" : obj }); - }, - // rollback - get_rollback : function () { - this.__callback(); - return { i : this.get_index(), h : this.get_container().children("ul").clone(true), d : this.data }; - }, - set_rollback : function (html, data) { - this.get_container().empty().append(html); - this.data = data; - this.__callback(); - }, - // Dummy functions to be overwritten by any datastore plugin included - load_node : function (obj, s_call, e_call) { this.__callback({ "obj" : obj }); }, - _is_loaded : function (obj) { return true; }, - - // Basic operations: create - create_node : function (obj, position, js, callback, is_loaded) { - obj = this._get_node(obj); - position = typeof position === "undefined" ? "last" : position; - var d = $("
  • "), - s = this._get_settings().core, - tmp; - - if(obj !== -1 && !obj.length) { return false; } - if(!is_loaded && !this._is_loaded(obj)) { this.load_node(obj, function () { this.create_node(obj, position, js, callback, true); }); return false; } - - this.__rollback(); - - if(typeof js === "string") { js = { "data" : js }; } - if(!js) { js = {}; } - if(js.attr) { d.attr(js.attr); } - if(js.metadata) { d.data(js.metadata); } - if(js.state) { d.addClass("jstree-" + js.state); } - if(!js.data) { js.data = this._get_string("new_node"); } - if(!$.isArray(js.data)) { tmp = js.data; js.data = []; js.data.push(tmp); } - $.each(js.data, function (i, m) { - tmp = $(""); - if($.isFunction(m)) { m = m.call(this, js); } - if(typeof m == "string") { tmp.attr('href','#')[ s.html_titles ? "html" : "text" ](m); } - else { - if(!m.attr) { m.attr = {}; } - if(!m.attr.href) { m.attr.href = '#'; } - tmp.attr(m.attr)[ s.html_titles ? "html" : "text" ](m.title); - if(m.language) { tmp.addClass(m.language); } - } - tmp.prepend(" "); - if(!m.icon && js.icon) { m.icon = js.icon; } - if(m.icon) { - if(m.icon.indexOf("/") === -1) { tmp.children("ins").addClass(m.icon); } - else { tmp.children("ins").css("background","url('" + m.icon + "') center center no-repeat"); } - } - d.append(tmp); - }); - d.prepend(" "); - if(obj === -1) { - obj = this.get_container(); - if(position === "before") { position = "first"; } - if(position === "after") { position = "last"; } - } - switch(position) { - case "before": obj.before(d); tmp = this._get_parent(obj); break; - case "after" : obj.after(d); tmp = this._get_parent(obj); break; - case "inside": - case "first" : - if(!obj.children("ul").length) { obj.append("