diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f2bcd34 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +; top-most EditorConfig file +root = true + +[*] +end_of_line = LF +insert_final_newline = true +indent_style = space +indent_size = 4 + +[*.js] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ab4548a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +/.editorconfig export-ignore +/.github export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.php-cs-fixer.dist.php export-ignore +/eslint.config.mjs export-ignore +/karma.conf.js export-ignore +/phpstan-baseline.neon export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore +/tests export-ignore +/webpack.config.js export-ignore +/webpack-encore-config.js export-ignore diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..6ac4368 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,178 @@ +name: Tests + +on: + push: + pull_request: + schedule: + - cron: '30 7 * * 1' + +jobs: + tests: + if: (github.event_name == 'schedule' && github.repository == 'e-commit/EcommitCrudBundle') || (github.event_name != 'schedule') + strategy: + fail-fast: false + matrix: + include: + #Mini (for each Symfony version) + - php-version: '8.1' + symfony-version: '6.4.*' + composer-flags: '--prefer-stable --prefer-lowest' + description: 'with SF 6.4.* lowest' + - php-version: '8.2' + symfony-version: '7.4.*' + composer-flags: '--prefer-stable --prefer-lowest' + description: 'with SF 7.4.* lowest' + - php-version: '8.4' + symfony-version: '8.0.*' + composer-flags: '--prefer-stable --prefer-lowest' + description: 'with SF 8.0.* lowest' + - php-version: '8.4' + symfony-version: '8.1.*' + composer-flags: '--prefer-stable --prefer-lowest' + description: 'with SF 8.1.* lowest' + + #Symfony versions + - php-version: '8.5' + symfony-version: '6.4.*' + description: 'with SF 6.4.*' + - php-version: '8.5' + symfony-version: '6.4.*@dev' + description: 'with SF 6.4.* dev' + - php-version: '8.5' + symfony-version: '7.4.*' + description: 'with SF 7.4.*' + - php-version: '8.5' + symfony-version: '7.4.*@dev' + description: 'with SF 7.4.* dev' + - php-version: '8.5' + symfony-version: '8.0.*' + description: 'with SF 8.0.*' + - php-version: '8.5' + symfony-version: '8.0.*@dev' + description: 'with SF 8.0.* dev' + - php-version: '8.5' + symfony-version: '8.1.*' + description: 'with SF 8.1.*' + - php-version: '8.5' + symfony-version: '8.1.*@dev' + description: 'with SF 8.1.* dev' + - php-version: '8.5' + symfony-version: '8.2.*@dev' + description: 'with SF 8.2.* dev' + + #PHP versions + - php-version: '8.1' + - php-version: '8.2' + - php-version: '8.3' + - php-version: '8.4' + - php-version: '8.5' + + #CS + - php-version: '8.5' + description: 'with Coding Standards' + coding-standards: true + + # Dependencies + - php-version: '8.5' + description: 'with Analyze dependencies' + analyze-dependencies: true + + #Static Analysis (min PHP version) + - php-version: '8.1' + description: 'with Static Analysis' + static-analysis: true + + name: PHP ${{ matrix.php-version }} ${{ matrix.description }} + + runs-on: 'ubuntu-latest' + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install system dependencies + run: | + sudo rm -rf ~/.nvm && curl -sL "https://deb.nodesource.com/setup_16.x" | sudo -E bash - + sudo apt-get install -y nodejs + sudo apt-get install xvfb + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + extensions: "pdo_sqlite, zip" + coverage: none + tools: flex + env: + update: true + + - name: Display versions + run: | + php -r 'foreach (get_loaded_extensions() as $extension) echo $extension . " " . phpversion($extension) . PHP_EOL;' + php -i + + - name: Get Composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-dir)" + + - name: Get npm cache directory + id: npm-cache + run: echo "::set-output name=dir::$(npm config get cache)" + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: | + ${{ steps.composer-cache.outputs.dir }} + ${{ steps.npm-cache.outputs.dir }} + key: composer-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('**/composer.*') }}-${{ matrix.composer-flags }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: composer-${{ runner.os }}-${{ matrix.php-version }}- + + - name: Configure minimum stability + if: matrix.symfony-version && contains(matrix.symfony-version, '@dev') + run: composer config minimum-stability dev + + - name: Remove friendsofphp/php-cs-fixer + if: matrix.coding-standards != true + run: composer remove friendsofphp/php-cs-fixer --dev --no-update + + - name: Remove vimeo/psalm + if: matrix.static-analysis != true + run: composer remove vimeo/psalm --dev --no-update + + - name: Configure Symfony Flex + if: matrix.symfony-version + run: composer config extra.symfony.require ${{ matrix.symfony-version }} + + - name: Install dependencies + run: | + composer update --no-interaction --no-progress ${{ matrix.composer-flags }} + vendor/bin/bdi detect drivers + npm install + npm run dev + + - name: Run PHPUnit + if: matrix.coding-standards != true && matrix.analyze-dependencies != true && matrix.static-analysis != true + run: php vendor/bin/phpunit + + - name: Run Karma + if: matrix.coding-standards != true && matrix.analyze-dependencies != true && matrix.static-analysis != true + run: xvfb-run ./node_modules/.bin/karma start --single-run + + - name: Run PHP CS Fixer + if: matrix.coding-standards + run: php vendor/bin/php-cs-fixer fix --diff --dry-run -v + + - name: Run ESLint + if: matrix.coding-standards + run: ./node_modules/.bin/eslint assets/js/* tests/assets/js/* + + - name: Analyze composer dependencies + if: matrix.analyze-dependencies + run: | + curl -LSs https://github.com/maglnet/ComposerRequireChecker/releases/latest/download/composer-require-checker.phar > composer-require-checker.phar + php composer-require-checker.phar check composer.json + + - name: Run Psalm + if: matrix.static-analysis + run: php vendor/bin/psalm --stats --output-format=github diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eaf34b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.php-cs-fixer.cache +.phpunit.cache/ +composer.lock +/composer-require-checker.phar +/drivers/ +/node_modules/ +/package-lock.json +/tests/Functional/App/config/reference.php +/tests/Functional/App/public/build/ +/tests/Functional/App/var/ +/vendor/ diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..0909099 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,43 @@ + + +For the full copyright and license information, please view the LICENSE +file that was distributed with this source code. +COMMENT; + +$finder = PhpCsFixer\Finder::create() + ->in(__DIR__) + ->exclude('node_modules') + ->exclude('tests/Functional/App/var') +; +$config = new PhpCsFixer\Config(); + +return $config->setRiskyAllowed(true) + ->setRules([ + '@Symfony' => true, + '@Symfony:risky' => true, + '@DoctrineAnnotation' => true, + '@PHP8x1Migration' => true, + '@PHP8x0Migration:risky' => true, + '@PHPUnit10x0Migration:risky' => true, + 'array_syntax' => ['syntax' => 'short'], + 'fopen_flags' => true, + 'header_comment' => ['header' => $fileHeaderComment, 'separate' => 'both'], + 'linebreak_after_opening_tag' => true, + 'mb_str_functions' => true, + 'no_php4_constructor' => true, + 'no_unreachable_default_argument_value' => true, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'ordered_imports' => true, + 'phpdoc_order' => true, + 'protected_to_private' => false, + 'fully_qualified_strict_types' => false, + 'phpdoc_to_comment' => ['ignored_tags' => ['psalm-suppress']], + ]) + ->setFinder($finder) +; diff --git a/Controller/AbstractCrudController.php b/Controller/AbstractCrudController.php deleted file mode 100644 index 71eb8b9..0000000 --- a/Controller/AbstractCrudController.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Controller; - -use Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait; -use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\DependencyInjection\ContainerAwareTrait; - -abstract class AbstractCrudController implements ContainerAwareInterface -{ - use ContainerAwareTrait; - use ControllerTrait; - use CrudControllerTrait; - - /** - * Gets a container configuration parameter by its name. - * - * @return mixed - * - * @final - */ - protected function getParameter(string $name) - { - return $this->container->getParameter($name); - } -} diff --git a/Controller/CrudControllerTrait.php b/Controller/CrudControllerTrait.php deleted file mode 100644 index 9e1caab..0000000 --- a/Controller/CrudControllerTrait.php +++ /dev/null @@ -1,188 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Controller; - -use Ecommit\CrudBundle\Crud\Crud; -use Ecommit\CrudBundle\Crud\CrudFactory; -use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -use Twig\Environment; - -trait CrudControllerTrait -{ - protected $cm; - - /** - * @param $sessionName - * @return Crud - */ - protected function createCrud($sessionName) - { - return $this->container->get('ecommit_crud.factory')->create($sessionName); - } - - public function autoListAction() - { - $data = $this->prepareList(); - - return $this->renderCrud($this->getTemplateName('index'), \array_merge($data, array('crud' => $this->cm))); - } - - protected function prepareList() - { - $this->cm = $this->configCrud(); - $this->beforeBuildQuery(); - $this->cm->buildQuery(); - $this->afterBuildQuery(); - $data = $this->addDataAfterBuildQuery(); - $this->cm->clearTemplate(); - - return $data; - } - - /** - * Configures and returns the CRUD - * - * @return Crud - */ - abstract protected function configCrud(); - - /** - * - * @return array - */ - protected function addDataAfterBuildQuery() - { - return array(); - } - - protected function beforeBuildQuery() - { - } - - protected function afterBuildQuery() - { - } - - /** - * Returns template for action - * - * @param string $action Action - * @return string - */ - protected function getTemplateName($action) - { - trigger_error('The "getTemplateName" must be overrided. This method will soon be abstract.', E_USER_DEPRECATED); - - return $this->getPathView($action); - } - - /** - * Returns the path of the template given - * - * @param string $name Template name - * @return string - * @deprecated Deprecated since version 2.4. Override getTemplateName method instead. - */ - protected function getPathView($name) - { - trigger_error('The "getPathView" method is deprecated since version 2.4. Override "getTemplateName" method instead.', E_USER_DEPRECATED); - - if (preg_match('/^(?P\w+)\\\(?P\w+)\\\Controller\\\(?P\w+)Controller$/', get_class($this), $matches)) { - return sprintf('%s%s:%s:%s.html.twig', $matches['vendor'], $matches['bundle'], $matches['controller'], $name); - } elseif (preg_match('/^(?P\w+)\\\(?P\w+)\\\Controller\\\(?P\w+)\\\(?P\w+)Controller$/', get_class($this), $matches)) { - return sprintf('%s%s:%s/%s:%s.html.twig', $matches['vendor'], $matches['bundle'], $matches['dir'], $matches['controller'], $name); - } elseif (preg_match('/^AppBundle\\\Controller\\\(?P\w+)Controller$/', get_class($this), $matches)) { - return sprintf('AppBundle:%s:%s.html.twig', $matches['controller'], $name); - } elseif (preg_match('/^AppBundle\\\Controller\\\(?P\w+)\\\(?P\w+)Controller$/', get_class($this), $matches)) { - return sprintf('AppBundle:%s/%s:%s.html.twig', $matches['dir'], $matches['controller'], $name); - } - - new \Exception('getPathView: Bad structure'); - } - - public function autoAjaxListAction() - { - $masterRequest = $this->get('request_stack')->getMasterRequest(); - if (!$masterRequest->isXmlHttpRequest()) { - throw new NotFoundHttpException('Ajax is required'); - } - $data = $this->prepareList(); - - return $this->renderCrud($this->getTemplateName('list'), \array_merge($data, array('crud' => $this->cm))); - } - - public function autoAjaxSearchAction() - { - $masterRequest = $this->get('request_stack')->getMasterRequest(); - if (!$masterRequest->isXmlHttpRequest()) { - throw new NotFoundHttpException('Ajax is required'); - } - $data = $this->processSearch(); - $renderSearch = $this->renderCrudView( - $this->getTemplateName('search'), - \array_merge($data, array('crud' => $this->cm)) - ); - $renderList = $this->renderCrudView($this->getTemplateName('list'), \array_merge($data, array('crud' => $this->cm))); - - return $this->renderCrud( - '@EcommitCrud/Crud/double_search.html.twig', - array( - 'id_search' => $this->cm->getDivIdSearch(), - 'id_list' => $this->cm->getDivIdList(), - 'render_search' => $renderSearch, - 'render_list' => $renderList - ) - ); - } - - protected function processSearch() - { - $this->cm = $this->configCrud(); - $this->cm->processForm(); - $this->beforeBuildQuery(); - $this->cm->buildQuery(); - $this->afterBuildQuery(); - $data = $this->addDataAfterBuildQuery(); - $this->cm->clearTemplate(); - - return $data; - } - - protected function renderCrudView(string $view, array $parameters = []): string - { - return $this->container->get('twig')->render($view, $parameters); - } - - protected function renderCrud(string $view, array $parameters = [], Response $response = null): Response - { - $content = $this->container->get('twig')->render($view, $parameters); - - if (null === $response) { - $response = new Response(); - } - - $response->setContent($content); - - return $response; - } - - protected static function getCrudRequiredServices() - { - return [ - 'ecommit_crud.factory' => CrudFactory::class, - 'twig' => Environment::class, - 'request_stack' => RequestStack::class, - ]; - } -} diff --git a/Crud/Crud.php b/Crud/Crud.php deleted file mode 100644 index d6c56b8..0000000 --- a/Crud/Crud.php +++ /dev/null @@ -1,1228 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Crud; - -use Doctrine\Bundle\DoctrineBundle\Registry; -use Ecommit\CrudBundle\DoctrineExtension\Paginate; -use Ecommit\CrudBundle\Entity\UserCrudSettings; -use Ecommit\CrudBundle\Form\Searcher\AbstractFormSearcher; -use Ecommit\CrudBundle\Form\Type\FormSearchType; -use Symfony\Component\Form\Form; -use Symfony\Component\Form\FormFactoryInterface; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Routing\RouterInterface; -use Symfony\Component\Security\Core\User\UserInterface; - -class Crud -{ - const DESC = 'DESC'; - const ASC = 'ASC'; - - protected $sessionName; - - /** - * @var CrudSession - */ - protected $sessionValues; - - /** - * @var Form - */ - protected $formSearcher = null; - - protected $availableColumns = array(); - protected $availableVirtualColumns = array(); - protected $availableResultsPerPage = array(); - protected $defaultSort = null; - protected $defaultPersonalizedSort = array(); - protected $defaultSense = null; - protected $defaultResultsPerPage = null; - protected $defaultFormSearcherData = null; - - protected $queryBuilder = null; - protected $useDbal = false; //Deprecated. Not used - protected $persistentSettings = false; - protected $updateDatabase = false; - protected $paginator = null; - protected $buildPaginator = true; - protected $displayResultsOnlyIfSearch = false; - protected $displayResults = true; - - /* - * Router - */ - protected $routeName = null; - protected $routeParams = array(); - protected $searchRouteName = null; - protected $searchRouteParams = array(); - - protected $divIdSearch = 'crud_search'; - protected $divIdList = 'crud_list'; - - /** - * @var RouterInterface - */ - protected $router; - - /** - * @var FormFactoryInterface - */ - protected $formFactory; - - /** - * @var Request - */ - protected $request; - - /** - * @var Registry - */ - protected $registry; - - /** - * @var User - */ - protected $user; - - /** - * @var array - */ - protected $templateConfiguration; - - /** - * Constructor - * - * @param string $sessionName Session name - * @return Crud - */ - public function __construct( - $sessionName, - RouterInterface $router, - FormFactoryInterface $formFactory, - Request $request, - Registry $registry, - $user, - array $templateConfiguration = array() - ) { - if (!\preg_match('/^[a-zA-Z0-9_]{1,50}$/', $sessionName)) { - throw new \Exception('Variable sessionName is not given or is invalid'); - } - $this->sessionName = $sessionName; - $this->router = $router; - $this->formFactory = $formFactory; - $this->request = $request; - $this->registry = $registry; - $this->user = $user; - $this->sessionValues = new CrudSession(); - foreach ($templateConfiguration as $functionName => $value) { - $this->configureTemplate($functionName, $value); - } - - return $this; - } - - /** - * Add a column inside the crud - * - * @param string $id Column id (used everywhere inside the crud) - * @param string $alias Column SQL alias - * @param string $label Column label (used in the header table) - * @param array $options Options: - * * sortable: If the column is sortable (Default: true) - * * default_displayed: If the column is displayed, by default (Default: true) - * * alias_search: Column SQL alias, used during searchs. If null, $alias is used. - * * alias_sort: Column(s) SQL alias (string or array of strings), used during sorting. If null, $alias is used. - * @return Crud - */ - public function addColumn($id, $alias, $label, $options = array()) - { - if (\strlen($id) > 30) { - throw new \Exception('Column id is too long'); - } - - $resolver = new OptionsResolver(); - $resolver->setDefaults( - array( - 'sortable' => true, - 'default_displayed' => true, - 'alias_search' => null, - 'alias_sort' => null, - ) - ); - $resolver->setAllowedTypes('sortable', 'bool'); - $resolver->setAllowedTypes('default_displayed', 'bool'); - $options = $resolver->resolve($options); - - $column = new CrudColumn( - $id, - $alias, - $label, - $options['sortable'], - $options['default_displayed'], - $options['alias_search'], - $options['alias_sort'] - ); - $this->availableColumns[$id] = $column; - - return $this; - } - - /** - * Add a virtual column inside the crud - * - * @param string $id Column id (used everywhere inside the crud) - * @param string $aliasSearch Column SQL alias, used during searchs. - * @return Crud - */ - public function addVirtualColumn($id, $aliasSearch) - { - $column = new CrudColumn($id, $aliasSearch, null, false, false, null, null); - $this->availableVirtualColumns[$id] = $column; - - return $this; - } - - /** - * Gets the query builder - * - * @return QueryBuilder - */ - public function getQueryBuilder() - { - return $this->queryBuilder; - } - - /** - * Sets the query builder - * - * @param QueryBuilder $queryBuilder - * @return Crud - */ - public function setQueryBuilder($queryBuilder) - { - if (!($queryBuilder instanceof \Doctrine\ORM\QueryBuilder) && - !($queryBuilder instanceof \Doctrine\DBAL\Query\QueryBuilder) && - !($queryBuilder instanceof QueryBuilderInterface)) { - throw new \Exception('Bad query builder'); - } - $this->queryBuilder = $queryBuilder; - - return $this; - } - - /** - * Returns available results per page - * - * @return array - */ - public function getAvailableResultsPerPage() - { - return $this->availableResultsPerPage; - } - - /** - * Sets available results per page - * - * @param array $availableResultsPerPage - * @param int $defaultValue - * @return Crud - */ - public function setAvailableResultsPerPage(Array $availableResultsPerPage, $defaultValue) - { - $this->availableResultsPerPage = $availableResultsPerPage; - $this->defaultResultsPerPage = $defaultValue; - - return $this; - } - - /** - * Set the default sort - * - * @param string $sort Column id - * @param const $sense Sense (Crud::ASC / Crud::DESC) - * @return Crud - */ - public function setDefaultSort($sort, $sense) - { - $this->defaultSort = $sort; - $this->defaultSense = $sense; - - return $this; - } - - /** - * Set the default personalized sort - * - * @param array $criterias Criterias : - * If key is defined: Key = Sort Value = Sense - * If key is not defined: Value = Sort - * @return Crud - */ - public function setDefaultPersonalizedSort(array $criterias) - { - $this->defaultPersonalizedSort = $criterias; - - $this->defaultSort = 'defaultPersonalizedSort'; - $this->defaultSense = self::ASC; //Used if not defined in criterias - - return $this; - } - - /** - * Sets the list route - * - * @param string $routeName - * @param array $parameters - * @return Crud - */ - public function setRoute($routeName, $parameters = array()) - { - $this->routeName = $routeName; - $this->routeParams = $parameters; - - return $this; - } - - /** - * Returns the route name - * - * @return string - */ - public function getRouteName() - { - return $this->routeName; - } - - /** - * Returns the route params - * - * @return array - */ - public function getRouteParams() - { - return $this->routeParams; - } - - /** - * Returns the list url - * - * @param array $parameters Additional parameters - * @return string - */ - public function getUrl($parameters = array()) - { - $parameters = \array_merge($this->routeParams, $parameters); - - return $this->router->generate($this->routeName, $parameters); - } - - /** - * Sets the search route - * - * @param string $routeName - * @param array $parameters - * @return Crud - */ - public function setSearchRoute($routeName, $parameters = array()) - { - $this->searchRouteName = $routeName; - $this->searchRouteParams = $parameters; - - return $this; - } - - /** - * Returns the search url - * - * @param array $parameters Additional parameters - * @return string - */ - public function getSearchUrl($parameters = array()) - { - $parameters = \array_merge($this->searchRouteParams, $parameters); - - return $this->router->generate($this->searchRouteName, $parameters); - } - - /** - * Enables (or not) the auto build paginator - * - * @param bool|closure|array $value - */ - public function setBuildPaginator($value) - { - $this->buildPaginator = $value; - - return $this; - } - - /* - * Use (or not) DBAL - * - * @param bool $value - * @deprecated Deprecated since version 2.3. - */ - public function setUseDbal($value) - { - trigger_error('setUseDbal is deprecated since 2.3 version.', E_USER_DEPRECATED); - - $this->useDbal = $value; - - return $this; - } - - /* - * Use (or not) persistent settings - * - * @param bool $value - */ - public function setPersistentSettings($value) - { - if ($value && !($this->user instanceof UserInterface)) { - $value = false; - } - $this->persistentSettings = $value; - - return $this; - } - - /** - * Adds search form - * - * @param AbstractFormSearcher $defaultFormSearcherData - * @param null|string $type The type of the form. If null, FormSearchType is used - * @param array $options The options - * @return Crud - */ - public function createSearcherForm(AbstractFormSearcher $defaultFormSearcherData, $type = null, $options = array()) - { - $this->defaultFormSearcherData = $defaultFormSearcherData; - $this->initializeFieldsFilter($defaultFormSearcherData); - - if ($type) { - $formBuilder = $this->formFactory->createBuilder($type, null, $options); - } else { - $formName = sprintf('crud_search_%s', $this->sessionName); - $formBuilder = $this->formFactory->createNamedBuilder($formName, FormSearchType::class, null, $options); - } - foreach ($defaultFormSearcherData->getFieldsFilter($this->registry) as $field) { - if (!($field instanceof \Ecommit\CrudBundle\Form\Filter\AbstractFieldFilter)) { - throw new \Exception( - 'Crud: AbstractFormSearcher: getFieldsFilter() must only returns AbstractFieldFilter implementations' - ); - } - - $formBuilder = $field->addField($formBuilder); - } - //Global - $formBuilder = $defaultFormSearcherData->globalBuildForm($formBuilder); - $this->formSearcher = $formBuilder; - - return $this; - } - - /** - * Process search form - * - */ - public function processForm() - { - if (empty($this->defaultFormSearcherData)) { - throw new NotFoundHttpException('Crud: Form searcher does not exist'); - } - - if ($this->request->query->has('raz')) { - return; - } - if ($this->request->getMethod() == 'POST') { - if ($this->displayResultsOnlyIfSearch) { - $this->displayResults = false; - } - $this->formSearcher->handleRequest($this->request); - if ($this->formSearcher->isSubmitted() && $this->formSearcher->isValid()) { - $this->displayResults = true; - $this->formSearcher->getData()->isSubmitted = true; - $this->changeFilterValues($this->formSearcher->getData()); - $this->changePage(1); - $this->save(); - } - } - } - - /** - * User action: Changes search form values - * - * @param Object $value - */ - protected function changeFilterValues($value) - { - if (empty($this->defaultFormSearcherData)) { - return; - } - if (\get_class($value) == \get_class($this->defaultFormSearcherData)) { - $this->sessionValues->formSearcherData = $value; - } else { - $this->sessionValues->formSearcherData = clone $this->defaultFormSearcherData; - } - } - - /** - * User action: Changes page number - * - * @param string $value Page number - */ - protected function changePage($value) - { - if (!is_scalar($value)) { - $value = 1; - } - $value = \intval($value); - if ($value > 1000000000000) { - $value = 1; - } - $this->sessionValues->page = $value; - } - - /** - * Saves user value - * - */ - protected function save() - { - //Save in session - $session = $this->request->getSession(); - $sessionValuesClean = clone $this->sessionValues; - if (is_object($this->sessionValues->formSearcherData)) { - $sessionValuesClean->formSearcherData = clone $this->sessionValues->formSearcherData; - $sessionValuesClean->formSearcherData->clear(); - } - $session->set($this->sessionName, $sessionValuesClean); - - //Save in database - if ($this->persistentSettings && $this->updateDatabase) { - $objectDatabase = $this->registry->getRepository('EcommitCrudBundle:UserCrudSettings')->findOneBy( - array( - 'user' => $this->user, - 'crudName' => $this->sessionName - ) - ); - $em = $this->registry->getManager(); - - if ($objectDatabase) { - //Update object in database - $objectDatabase->updateFromSessionManager($this->sessionValues); - $em->flush(); - } else { - //Create object in database only if not default values - if ($this->sessionValues->displayedColumns != $this->getDefaultDisplayedColumns() || - $this->sessionValues->resultsPerPage != $this->defaultResultsPerPage || - $this->sessionValues->sense != $this->defaultSense || - $this->sessionValues->sort != $this->defaultSort - ) { - $objectDatabase = new UserCrudSettings(); - $objectDatabase->setUser($this->user); - $objectDatabase->setCrudName($this->sessionName); - $objectDatabase->updateFromSessionManager($this->sessionValues); - $em->persist($objectDatabase); - $em->flush(); - } - } - } - } - - /** - * Return default displayed columns - * - * @return array - */ - public function getDefaultDisplayedColumns() - { - $columns = array(); - foreach ($this->availableColumns as $column) { - if ($column->defaultDisplayed) { - $columns[] = $column->id; - } - } - if (count($columns) == 0) { - throw new \Exception('Config Crud: One column displayed is required'); - } - - return $columns; - } - - /** - * Inits the CRUD - * - */ - public function init() - { - //Cheks not empty values - $check_values = array( - 'availableColumns', - 'availableResultsPerPage', - 'defaultSort', - 'defaultSense', - 'defaultResultsPerPage', - 'queryBuilder', - 'routeName' - ); - if (!empty($this->defaultFormSearcherData)) { - $check_values[] = 'searchRouteName'; - } - foreach ($check_values as $value) { - if (empty($this->$value)) { - throw new \Exception('Config Crud: Option ' . $value . ' is required'); - } - } - - if (!empty($this->defaultFormSearcherData)) { - //Set url - $this->formSearcher->setAction($this->getSearchUrl()); - //Transform FormBuilder to Form - $this->formSearcher = $this->formSearcher->getForm(); - } - - //Loads user values inside this object - $this->load(); - - //Display or not results - if (!empty($this->defaultFormSearcherData) && $this->displayResultsOnlyIfSearch) { - $this->displayResults = $this->sessionValues->formSearcherData->isSubmitted; - } - - //Process request (resultsPerPage, sort, sense, change_columns) - $this->processRequest(); - - //Searcher form: Allocates object (Defaults values and validators) - if (!empty($this->defaultFormSearcherData) && !$this->request->query->has('raz')) { - //IMPORTANT - //We have not to allocate directelly the "$this->sessionValues->formSearcherData" object - //because otherwise it will be linked to form, and will be updated when the "bind" function will - //be called (If form is not valid, the session values will still be updated: Undesirable behavior) - $values = clone $this->sessionValues->formSearcherData; - $values->setFieldsFilter($this->defaultFormSearcherData->getFieldsFilter()); //Copy fields from defaultFormSearcherData to current data - $values->setCommonOptions($this->defaultFormSearcherData->getCommonOptions()); - $this->formSearcher->setData($values); - } - - //Saves - $this->save(); - } - - /** - * Init "fieldFilters" property in $formSearcherData object - * Inject the registry in $formSearcherData objet if implements FieldFilterDoctrineInterface - * @param AbstractFormSearcher $formSearcherData - */ - protected function initializeFieldsFilter(AbstractFormSearcher $formSearcherData) - { - foreach ($formSearcherData->getFieldsFilter($this->registry) as $field) { - if (!($field instanceof \Ecommit\CrudBundle\Form\Filter\AbstractFieldFilter)) { - throw new \Exception( - 'Crud: AbstractFormSearcher: getFieldsFilter() must only returns AbstractFieldFilter implementations' - ); - } - - if (isset($this->availableColumns[$field->getColumnId()])) { - $column = $this->availableColumns[$field->getColumnId()]; - } elseif (isset($this->availableVirtualColumns[$field->getColumnId()])) { - $column = $this->availableVirtualColumns[$field->getColumnId()]; - } else { - throw new \Exception( - 'Crud: AbstractFormSearcher: getFieldsFilter(): Column id does not exit: ' . $field->getColumnId( - ) - ); - } - - $field->setLabel($column->label, $formSearcherData->displayLabelInErrors()); - } - } - - - /** - * Load user values - * - */ - protected function load() - { - $session = $this->request->getSession(); - $object = $session->get($this->sessionName); //Load from session - - if (!empty($object)) { - $this->sessionValues = $object; - $this->checkCrudSession(); - if (!empty($this->defaultFormSearcherData)) { - $object->formSearcherData->setCommonOptions($this->defaultFormSearcherData->getCommonOptions()); - } - - return; - } - - //If session is null => Retrieve from database - //Only if persistent settings is enabled - if ($this->persistentSettings) { - $objectDatabase = $this->registry->getRepository('EcommitCrudBundle:UserCrudSettings')->findOneBy( - array( - 'user' => $this->user, - 'crudName' => $this->sessionName - ) - ); - if ($objectDatabase) { - $this->sessionValues = $objectDatabase->transformToCrudSession(new CrudSession()); - if (!empty($this->defaultFormSearcherData)) { - $this->sessionValues->formSearcherData = clone $this->defaultFormSearcherData; - } - $this->checkCrudSession(); - - return; - } - } - - //Session and database values are null: Default values; - $this->sessionValues->displayedColumns = $this->getDefaultDisplayedColumns(); - $this->sessionValues->resultsPerPage = $this->defaultResultsPerPage; - $this->sessionValues->sense = $this->defaultSense; - $this->sessionValues->sort = $this->defaultSort; - if (!empty($this->defaultFormSearcherData)) { - $this->sessionValues->formSearcherData = clone $this->defaultFormSearcherData; - } - } - - /** - * Checks user values - */ - protected function checkCrudSession() - { - //Forces change => checks - $this->changeNumberResultsDisplayed($this->sessionValues->resultsPerPage); - $this->changeColumnsDisplayed($this->sessionValues->displayedColumns); - $this->changeSort($this->sessionValues->sort); - $this->changeSense($this->sessionValues->sense); - $this->changeFilterValues($this->sessionValues->formSearcherData); - $this->changePage($this->sessionValues->page); - } - - /** - * User Action: Changes number of displayed results - * - * @param int $value - */ - protected function changeNumberResultsDisplayed($value) - { - $oldValue = $this->sessionValues->resultsPerPage; - if (is_scalar($value) && in_array($value, $this->availableResultsPerPage)) { - $this->sessionValues->resultsPerPage = $value; - } else { - $this->sessionValues->resultsPerPage = $this->defaultResultsPerPage; - } - $this->testIfDatabaseMustMeUpdated($oldValue, $value); - } - - protected function testIfDatabaseMustMeUpdated($oldValue, $new_value) - { - if ($oldValue != $new_value) { - $this->updateDatabase = true; - } - } - - /** - * User Action: Changes displayed columns - * - * @param array $value (columns id) - */ - protected function changeColumnsDisplayed($value) - { - $oldValue = $this->sessionValues->displayedColumns; - if (!is_array($value)) { - $value = $this->getDefaultDisplayedColumns(); - } - $newDisplayedColumns = array(); - $availableColumns = $this->availableColumns; - foreach ($value as $column_name) { - if (is_scalar($column_name) && array_key_exists($column_name, $availableColumns)) { - $newDisplayedColumns[] = $column_name; - } - } - if (count($newDisplayedColumns) == 0) { - $newDisplayedColumns = $this->getDefaultDisplayedColumns(); - } - $this->sessionValues->displayedColumns = $newDisplayedColumns; - $this->testIfDatabaseMustMeUpdated($oldValue, $newDisplayedColumns); - } - - /** - * User Action: Changes sort - * - * @param string $value Column id - */ - protected function changeSort($value) - { - $oldValue = $this->sessionValues->sort; - $availableColumns = $this->availableColumns; - if ((is_scalar($value) && array_key_exists($value, $availableColumns) && $availableColumns[$value]->sortable) - || (is_scalar($value) && $value == 'defaultPersonalizedSort' && $this->defaultPersonalizedSort)) { - $this->sessionValues->sort = $value; - $this->testIfDatabaseMustMeUpdated($oldValue, $value); - } else { - $this->sessionValues->sort = $this->defaultSort; - $this->testIfDatabaseMustMeUpdated($oldValue, $this->defaultSort); - } - } - - /** - * User action: Changes sense - * - * @param const $value Sens (ASC / DESC) - */ - protected function changeSense($value) - { - $oldValue = $this->sessionValues->sense; - if (is_scalar($value) && ($value == self::ASC || $value == self::DESC)) { - $this->sessionValues->sense = $value; - $this->testIfDatabaseMustMeUpdated($oldValue, $value); - } else { - $this->sessionValues->sense = $this->defaultSense; - $this->testIfDatabaseMustMeUpdated($oldValue, $this->defaultSense); - } - } - - /** - * Process request - * - */ - protected function processRequest() - { - if ($this->request->query->has('razsettings')) { - //Reset display settings - $this->razDisplaySettings(); - - return; - } - if ($this->request->query->has('raz')) { - $this->raz(); - - return; - } - $displaySettingsFormName = sprintf('crud_display_settings_%s', $this->sessionName); - if ($this->request->request->has($displaySettingsFormName)) { - $displaySettings = $this->request->request->get($displaySettingsFormName); - if (is_array($displaySettings) && isset($displaySettings['displayedColumns'])) { - $this->changeColumnsDisplayed($displaySettings['displayedColumns']); - } - if (is_array($displaySettings) && isset($displaySettings['resultsPerPage'])) { - $this->changeNumberResultsDisplayed($displaySettings['resultsPerPage']); - } - } - if ($this->request->query->has('sort')) { - $this->changeSort($this->request->query->get('sort')); - } - if ($this->request->query->has('sense')) { - $this->changeSense($this->request->query->get('sense')); - } - if ($this->request->query->has('page')) { - $this->changePage($this->request->query->get('page')); - } - } - - /** - * Reset display settings - * - */ - protected function razDisplaySettings() - { - $this->sessionValues->displayedColumns = $this->getDefaultDisplayedColumns(); - $this->sessionValues->resultsPerPage = $this->defaultResultsPerPage; - $this->sessionValues->sense = $this->defaultSense; - $this->sessionValues->sort = $this->defaultSort; - - if ($this->persistentSettings) { - //Remove settings in database - $qb = $this->registry->getManager()->createQueryBuilder(); - $qb->delete('EcommitCrudBundle:UserCrudSettings', 's') - ->andWhere('s.user = :user AND s.crudName = :crud_name') - ->setParameters(array('user' => $this->user, 'crud_name' => $this->sessionName)) - ->getQuery() - ->execute(); - } - } - - /** - * Reset search form values - * - */ - public function raz() - { - if ($this->defaultFormSearcherData) { - $newValue = clone $this->defaultFormSearcherData; - $this->changeFilterValues($newValue); - $this->formSearcher->setData(clone $newValue); - } - $this->changePage(1); - $this->save(); - - if ($this->displayResultsOnlyIfSearch) { - $this->displayResults = false; - } - } - - /** - * Reset sort - * - */ - public function razSort() - { - $this->sessionValues->sense = $this->defaultSense; - $this->sessionValues->sort = $this->defaultSort; - $this->save(); - } - - /** - * Builds the query - * - */ - public function buildQuery() - { - //Builds query - $columnSortId = $this->sessionValues->sort; - if ($columnSortId == 'defaultPersonalizedSort') { - //Default personalised sort is used - foreach ($this->defaultPersonalizedSort as $key => $value) { - if (is_int($key)) { - $sort = $value; - $sense = $this->defaultSense; - } else { - $sort = $key; - $sense = $value; - } - $this->queryBuilder->addOrderBy($sort, $sense); - } - } else { - $columnSortAlias = $this->availableColumns[$columnSortId]->aliasSort; - if (empty($columnSortAlias)) { - //Sort alias is not defined. Alias is used - $columnSortAlias = $this->availableColumns[$columnSortId]->alias; - $this->queryBuilder->orderBy($columnSortAlias, $this->sessionValues->sense); - } elseif (is_array($columnSortAlias)) { - //Sort alias is defined in many columns - foreach ($columnSortAlias as $oneColumnSortAlias) { - $this->queryBuilder->addOrderBy($oneColumnSortAlias, $this->sessionValues->sense); - } - } else { - //Sort alias is defined in one column - $this->queryBuilder->orderBy($columnSortAlias, $this->sessionValues->sense); - } - } - - //Adds form searcher filters - if (!empty($this->defaultFormSearcherData)) { - foreach ($this->defaultFormSearcherData->getFieldsFilter() as $field) { - if (!($this->queryBuilder instanceof \Doctrine\ORM\QueryBuilder) && - !($this->queryBuilder instanceof \Doctrine\DBAL\Query\QueryBuilder)) { - throw new \Exception('getFieldsFilter can not be used'); - } - - if (isset($this->availableColumns[$field->getColumnId()])) { - $column = $this->availableColumns[$field->getColumnId()]; - } elseif (isset($this->availableVirtualColumns[$field->getColumnId()])) { - $column = $this->availableVirtualColumns[$field->getColumnId()]; - } else { - throw new \Exception( - 'Crud: AbstractFormSearcher: getFieldsFilter(): Column id does not exit: ' . $field->getColumnId( - ) - ); - } - - //Get alias search - if (empty($column->aliasSearch)) { - $aliasSearch = $column->alias; - } else { - $aliasSearch = $column->aliasSearch; - } - - $this->queryBuilder = $field->changeQuery( - $this->queryBuilder, - $this->sessionValues->formSearcherData, - $aliasSearch - ); - } - - //Global change Query - $this->queryBuilder = $this->sessionValues->formSearcherData->globalChangeQuery($this->queryBuilder); - } - - - //Builds paginator - if ($this->displayResults) { - if (is_object($this->buildPaginator) && $this->buildPaginator instanceof \Closure) { - //Case: Manual paginator (by closure) is enabled - $this->paginator = $this->buildPaginator->__invoke( - $this->queryBuilder, - $this->sessionValues->page, - $this->sessionValues->resultsPerPage - ); - } elseif (true === $this->buildPaginator || is_array($this->buildPaginator)) { - //Case: Auto paginator is enabled - $paginatorOptions = array(); - if (is_array($this->buildPaginator)) { - $paginatorOptions = $this->buildPaginator; - } - - $this->paginator = Paginate::createDoctrinePaginator( - $this->queryBuilder, - $this->sessionValues->page, - $this->sessionValues->resultsPerPage, - $paginatorOptions - ); - $this->paginator->init(); - } - } - } - - /** - * Return default results per page - * @return int - */ - public function getDefaultResultsPerPage() - { - return $this->defaultResultsPerPage; - } - - /** - * Clears this object, before sending it to template - * - */ - public function clearTemplate() - { - $this->queryBuilder = null; - $this->formFactory = null; - $this->request = null; - $this->registry = null; - if (empty($this->defaultFormSearcherData)) { - $this->formSearcher = null; - } else { - $this->formSearcher = $this->formSearcher->createView(); - } - $this->defaultFormSearcherData = null; - } - - /** - * Returns availabled columns - * - * @return array - */ - public function getColumns() - { - return $this->availableColumns; - } - - /** - * Returns one column - * - * @return CrudColumn $columnId - */ - public function getColumn($columnId) - { - if (isset($this->availableColumns[$columnId])) { - return $this->availableColumns[$columnId]; - } - throw new \Exception('Crud: Column ' . $columnId . ' does not exist'); - } - - /** - * Returns user values - * - * @return CrudSession - */ - public function getSessionValues() - { - return $this->sessionValues; - } - - /** - * Returns the paginator - * @return Object - */ - public function getPaginator() - { - return $this->paginator; - } - - /** - * Sets the paginator - * - * @param Object $value - */ - public function setPaginator($value) - { - $this->paginator = $value; - - return $this; - } - - /** - * Returns the search form - * - * @return FormBuilder (before init) or Form (before clearTemplate) or FormView (after clearTemplate) - */ - public function getSearcherForm() - { - return $this->formSearcher; - } - - /** - * Returns the div id search - * - * @return string - */ - public function getDivIdSearch() - { - return $this->divIdSearch; - } - - /** - * Sets the div id search - * - * @param string - * @return Crud - */ - public function setDivIdSearch($divIdSearch) - { - $this->divIdSearch = $divIdSearch; - - return $this; - } - - /** - * Returns the div id list - * - * @return string - */ - public function getDivIdList() - { - return $this->divIdList; - } - - /** - * Sets the div id list - * - * @param string - * @return Crud - */ - public function setDivIdList($divIdList) - { - $this->divIdList = $divIdList; - - return $this; - } - - /** - * Gets session name - * - * @return string - */ - public function getSessionName() - { - return $this->sessionName; - } - - /** - * @return boolean - */ - public function getDisplayResultsOnlyIfSearch() - { - return $this->displayResultsOnlyIfSearch; - } - - /** - * @param boolean $displayResultsOnlyIfSearch - * @return Crud - */ - public function setDisplayResultsOnlyIfSearch($displayResultsOnlyIfSearch) - { - $this->displayResultsOnlyIfSearch = $displayResultsOnlyIfSearch; - - return $this; - } - - /** - * @return boolean - */ - public function getDisplayResults() - { - return $this->displayResults; - } - - /** - * @param bool $displayResults - * @return Crud - */ - public function setDisplayResults($displayResults) - { - $this->displayResults = $displayResults; - - return $this; - } - - /** - * @param string $functionName - * @param mixed $value - */ - public function configureTemplate($functionName, $value) - { - if (!self::validateConfigureTemplateFunctionName($functionName)) { - throw new \Exception(\sprintf('%s method is not allowed in configureTemplate', $functionName)); - } - - if (isset($this->templateConfiguration[$functionName])) { - $this->templateConfiguration[$functionName] = array_merge( - $this->templateConfiguration[$functionName], - $value - ); - } else { - $this->templateConfiguration[$functionName] = $value; - } - } - - /** - * @param string $functionName - * @return bool - */ - public static function validateConfigureTemplateFunctionName($functionName) - { - $functionsAllowed = array( - 'paginator_links', - 'crud_paginator_links', - 'crud_th', - 'crud_td', - 'crud_search_reset', - 'crud_remote_modal', - 'crud_display_settings', - ); - - return in_array($functionName, $functionsAllowed); - } - - /** - * @param string $functionName - * @return array - */ - public function getTemplateConfiguration($functionName) - { - if (isset($this->templateConfiguration[$functionName])) { - return $this->templateConfiguration[$functionName]; - } - - return array(); - } -} diff --git a/Crud/CrudColumn.php b/Crud/CrudColumn.php deleted file mode 100644 index 799af5d..0000000 --- a/Crud/CrudColumn.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Crud; - -class CrudColumn -{ - public $id; - public $alias; - public $aliasSearch; - public $aliasSort; - public $label; - public $sortable; - public $defaultDisplayed; - - /** - * Constructor - * - * @param string $id Column id (used everywhere inside the crud) - * @param string $alias Column SQL alias - * @param string $label Column label (used in the header table) - * @param bool $sortable If the column is sortable - * @param bool $defaultDisplayed If the column is displayed, by default - * @param string $aliasSearch Column SQL alias, used during searchs - * @param string $aliasSort Column(s) SQL alias (string or array of strings), used during sorting - */ - public function __construct($id, $alias, $label, $sortable, $defaultDisplayed, $aliasSearch, $aliasSort) - { - $this->id = $id; - $this->alias = $alias; - $this->label = $label; - $this->sortable = $sortable; - $this->defaultDisplayed = $defaultDisplayed; - $this->aliasSearch = $aliasSearch; - $this->aliasSort = $aliasSort; - } -} diff --git a/Crud/CrudFactory.php b/Crud/CrudFactory.php deleted file mode 100644 index d66fd6d..0000000 --- a/Crud/CrudFactory.php +++ /dev/null @@ -1,83 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Crud; - -use Symfony\Bridge\Doctrine\ManagerRegistry; -use Symfony\Component\Form\FormFactoryInterface; -use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\Routing\RouterInterface; -use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; - -class CrudFactory -{ - /** - * @var RouterInterface - */ - protected $router; - - /** - * @var FormFactoryInterface - */ - protected $formFactory; - - /** - * @var RequestStack - */ - protected $requestStack; - - /** - * @var ManagerRegistry - */ - protected $registry; - - /** - * @var TokenStorageInterface - */ - protected $tokenStorage; - - /** - * @var array - */ - protected $templateConfiguration; - - public function __construct( - RouterInterface $router, - FormFactoryInterface $formFactory, - RequestStack $requestStack, - ManagerRegistry $registry, - TokenStorageInterface $tokenStorage, - array $templateConfiguration - ) { - $this->router = $router; - $this->formFactory = $formFactory; - $this->requestStack = $requestStack; - $this->registry = $registry; - $this->tokenStorage = $tokenStorage; - $this->templateConfiguration = $templateConfiguration; - } - - /** - * @param $sessionName - * @return Crud - */ - public function create($sessionName) - { - return new Crud( - $sessionName, - $this->router, - $this->formFactory, - $this->requestStack->getCurrentRequest(), - $this->registry, - $this->tokenStorage->getToken()->getUser(), - $this->templateConfiguration - ); - } -} diff --git a/Crud/CrudSession.php b/Crud/CrudSession.php deleted file mode 100644 index 7b2931a..0000000 --- a/Crud/CrudSession.php +++ /dev/null @@ -1,60 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Crud; - -use Ecommit\CrudBundle\Form\Searcher\AbstractFormSearcher; - -class CrudSession -{ - /** - * Search's object (used by "setData" inside the form). Used to - * save the data of the search form - * - * @var AbstractFormSearcher - */ - public $formSearcherData = null; - - /** - * Number of results, in one page - * - * @var int - */ - public $resultsPerPage = null; - - /** - * Displayed colums - * - * @var type - */ - public $displayedColumns = array(); - - /** - * Sortable: Sort (Column id) - * - * @var type - */ - public $sort = null; - - /** - * Sortable: Sens (ASC / DESC) - * - * @var type - */ - public $sense = null; - - /** - * Page number - * - * @var int - */ - public $page = 1; -} diff --git a/Crud/Rest/RestQueryBuilder.php b/Crud/Rest/RestQueryBuilder.php deleted file mode 100644 index 7dcaa3e..0000000 --- a/Crud/Rest/RestQueryBuilder.php +++ /dev/null @@ -1,190 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Crud\Rest; - -use Ecommit\CrudBundle\Crud\QueryBuilderInterface; -use Ecommit\CrudBundle\Crud\QueryBuilderParameterInterface; - -class RestQueryBuilder implements QueryBuilderInterface -{ - /** - * @var string - */ - protected $url; - - /** - * @var string - */ - protected $method; - - /** - * @var array - */ - protected $queryParameters = array(); - - /** - * @var array - */ - protected $formParameters = array(); - - /** - * @var string - */ - protected $bodyParameter; - - /** - * @var \Closure - */ - protected $orderBuilder; - - /** - * @var \Closure - */ - protected $paginationBuilder; - - /** - * @var array - */ - protected $orders = array(); - - /** - * RestQueryBuilder constructor. - * @param string $url - * @param string $method - * @param array $defaultParameters Array of RestQueryBuilderParameter objects - */ - public function __construct($url, $method, $defaultParameters = array()) - { - $this->url = $url; - $this->method = $method; - foreach ($defaultParameters as $defaultParameter) { - $this->addParameter($defaultParameter); - } - } - - /** - * @param \Closure $orderBuilder - * @return $this - */ - public function setOrderBuilder(\Closure $orderBuilder) - { - $this->orderBuilder = $orderBuilder; - - return $this; - } - - /** - * @param \Closure $paginationBuilder - * @return $this - */ - public function setPaginationBuilder(\Closure $paginationBuilder) - { - $this->paginationBuilder = $paginationBuilder; - - return $this; - } - - /** - * @param string $parameter - * @param string $value - * @param string $method - * @return $this - * @throws \Exception - */ - public function addParameter(QueryBuilderParameterInterface $parameter) - { - if (!($parameter instanceof RestQueryBuilderParameter)) { - throw new \Exception('Bad class'); - } - - switch ($parameter->method) { - case 'get': - $this->queryParameters[$parameter->name] = $parameter->value; - break; - case 'post': - $this->formParameters[$parameter->name] = $parameter->value; - break; - case 'body': - $this->bodyParameter = $parameter->value; - break; - default: - throw new \Exception('Bad parameter method'); - } - - return $this; - } - - /** - * @param string $sort - * @param string $sense - * @return $this - */ - public function addOrderBy($sort, $sense) - { - $this->orders[$sort] = $sense; - - return $this; - } - - /** - * @param string $sort - * @param string $sense - * @return $this - */ - public function orderBy($sort, $sense) - { - $this->orders = array(); - $this->addOrderBy($sort, $sense); - - return $this; - } - - /** - * @param int $page - * @param int $resultsPerPage - * @param array $options - * @return mixed|\Psr\Http\Message\ResponseInterface - * @throws \Exception - */ - public function getResponse($page, $resultsPerPage, $options = array()) - { - $client = new \GuzzleHttp\Client(); - - //Add paginator parameters - if ($this->paginationBuilder && $this->paginationBuilder instanceof \Closure) { - $parameters = $this->paginationBuilder->__invoke($page, $resultsPerPage); - foreach ($parameters as $parameter) { - $this->addParameter($parameter); - } - } - - //Add sort parameters - if (count($this->orders) > 0 && $this->orderBuilder && $this->orderBuilder instanceof \Closure) { - $parameters = $this->orderBuilder->__invoke($this->orders); - foreach ($parameters as $parameter) { - $this->addParameter($parameter); - } - } - - //Add parameters to GuzzleHttp options - foreach ($this->queryParameters as $parameterName => $parameterValue) { - $options['query'][$parameterName] = $parameterValue; - } - foreach ($this->formParameters as $parameterName => $parameterValue) { - $options['form_params'][$parameterName] = $parameterValue; - } - if ($this->bodyParameter) { - $options['body'] = $this->bodyParameter; - } - - return $client->request($this->method, $this->url, $options); - } -} diff --git a/Crud/Rest/RestQueryBuilderParameter.php b/Crud/Rest/RestQueryBuilderParameter.php deleted file mode 100644 index 5b7d24e..0000000 --- a/Crud/Rest/RestQueryBuilderParameter.php +++ /dev/null @@ -1,29 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Crud\Rest; - -use Ecommit\CrudBundle\Crud\QueryBuilderParameterInterface; - -class RestQueryBuilderParameter implements QueryBuilderParameterInterface -{ - public $name; - - public $value; - - public $method; - - public function __construct($name, $value, $method) - { - $this->name = $name; - $this->value = $value; - $this->method = $method; - } -} diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php deleted file mode 100644 index aea2c09..0000000 --- a/DependencyInjection/Configuration.php +++ /dev/null @@ -1,63 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\DependencyInjection; - -use Ecommit\CrudBundle\Crud\Crud; -use Symfony\Component\Config\Definition\Builder\TreeBuilder; -use Symfony\Component\Config\Definition\ConfigurationInterface; - -/** - * This is the class that validates and merges configuration from your config files - * - * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class} - */ -class Configuration implements ConfigurationInterface -{ - /** - * {@inheritDoc} - */ - public function getConfigTreeBuilder() - { - $treeBuilder = new TreeBuilder('ecommit_crud'); - $rootNode = $treeBuilder->getRootNode(); - - $rootNode - ->children() - ->arrayNode('template_configuration') - ->treatNullLike(array()) - ->prototype('variable') - ->end() - ->validate() - ->ifTrue(function ($v) { - foreach ($v as $functionName => $value) { - if (!Crud::validateConfigureTemplateFunctionName($functionName)) { - return true; - } - } - }) - ->thenInvalid('Function name in template_configuration is invalid.') - ->end() - ->end() - ->arrayNode('images') - ->addDefaultsIfNotSet() - ->children() - ->scalarNode('th_image_up')->defaultValue('/bundles/ecommitcrud/images/i16/sort_incr.png')->end() - ->scalarNode('th_image_down')->defaultValue('/bundles/ecommitcrud/images/i16/sort_decrease.png')->end() - ->end() - ->end() - ->end() - ; - - return $treeBuilder; - } -} - diff --git a/DependencyInjection/EcommitCrudExtension.php b/DependencyInjection/EcommitCrudExtension.php deleted file mode 100644 index 3d06ea1..0000000 --- a/DependencyInjection/EcommitCrudExtension.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\DependencyInjection; - -use Symfony\Component\HttpKernel\DependencyInjection\Extension; -use Symfony\Component\Config\FileLocator; -use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; -use Symfony\Component\DependencyInjection\ContainerBuilder; - -class EcommitCrudExtension extends Extension -{ - - /** - * Loads a specific configuration. - * - * @param array $config An array of configuration values - * @param ContainerBuilder $container A ContainerBuilder instance - * - * @throws \InvalidArgumentException When provided tag is not defined in this extension - */ - public function load(array $config, ContainerBuilder $container) - { - $configuration = new Configuration(); - $config = $this->processConfiguration($configuration, $config); - - $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); - $loader->load('services.xml'); - - $container->setParameter('ecommit_crud.template_configuration', $config['template_configuration']); - $container->setParameter('ecommit_crud.images', array('th_image_up' => $config['images']['th_image_up'], 'th_image_down' => $config['images']['th_image_down'])); - } -} diff --git a/DoctrineExtension/Paginate.php b/DoctrineExtension/Paginate.php deleted file mode 100644 index 0b40e58..0000000 --- a/DoctrineExtension/Paginate.php +++ /dev/null @@ -1,270 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\DoctrineExtension; - -use Doctrine\ORM\NativeQuery; -use Doctrine\ORM\Query; -use Doctrine\ORM\Query\Parameter; -use Doctrine\ORM\Query\ResultSetMapping; -use Doctrine\ORM\Tools\Pagination\Paginator; -use Ecommit\CrudBundle\Paginator\AbstractPaginator; -use Ecommit\CrudBundle\Paginator\ArrayPaginator; -use Ecommit\CrudBundle\Paginator\DoctrineDBALPaginator; -use Ecommit\CrudBundle\Paginator\DoctrineORMPaginator; -use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; -use Symfony\Component\OptionsResolver\OptionsResolver; - -class Paginate -{ - /** - * Returns total results (SQL function "count") - * - * @param Query $query - * @param bool $simplifiedRequest Use simplified request (not subrequest and not order by) or not - * @return int - * @deprecated Deprecated since version 2.4. Use countQueryBuilder or Doctrine\ORM\Tools\Pagination\Paginator::count method instead. - */ - static public function count(Query $query, $simplifiedRequest = true) - { - trigger_error('Paginate::count is deprecated since 2.4 version. Use countQueryBuilder or Doctrine\ORM\Tools\Pagination\Paginator::count method instead.', E_USER_DEPRECATED); - - $doctrinePaginator = new Paginator($query); - $doctrinePaginator->setUseOutputWalkers(!$simplifiedRequest); - - return $doctrinePaginator->count(); - } - - /** - * @param \Doctrine\ORM\QueryBuilde|\Doctrine\DBAL\Query\QueryBuilder $queryBuilder - * @param array $options - * @return int - * @throws \Exception - */ - static public function countQueryBuilder($queryBuilder, array $options = array()) - { - if ($queryBuilder instanceof \Doctrine\ORM\QueryBuilder) { - $useORM = true; - } elseif ($queryBuilder instanceof \Doctrine\DBAL\Query\QueryBuilder) { - $useORM = false; - } else { - throw new \Exception('Bad QueryBuilder'); - } - - $resolver = new OptionsResolver(); - $resolver->setDefaults(array( - //Behavior. Availabled values: - // - count_by_alias: Use alias. Option "alias" is required - // - count_by_sub_request: Use sub request - // - orm: Use Doctrine Paginator - 'behavior' => self::getDefaultCountBehavior($queryBuilder), - //Used when behavior=count_by_alias - 'alias' => null, - //Used when behavior=count_by_alias - 'distinct_alias' => true, - )); - $resolver->setAllowedTypes('distinct_alias', array('boolean')); - if ($useORM) { - $resolver->setAllowedValues('behavior', array('count_by_alias', 'count_by_sub_request', 'orm')); - //Use only when ORM and behavior=orm - $resolver->setDefault('simplified_request', true); - } else { - $resolver->setAllowedValues('behavior', array('count_by_alias', 'count_by_sub_request')); - } - $options = $resolver->resolve($options); - if ('count_by_alias' === $options['behavior'] && null === $options['alias']) { - throw new MissingOptionsException('Option "alias" is required'); - } - - if ($useORM) { - if ('orm' === $options['behavior']) { - $cloneQueryBuilder = clone $queryBuilder; - $doctrinePaginator = new Paginator($cloneQueryBuilder->getQuery()); - $doctrinePaginator->setUseOutputWalkers(!$options['simplified_request']); - - return (int) $doctrinePaginator->count(); - } elseif ('count_by_alias' === $options['behavior']) { - /** @var \Doctrine\ORM\QueryBuilder $countQueryBuilder */ - $countQueryBuilder = clone $queryBuilder; - $distinct = ($options['distinct_alias'])? 'DISTINCT ' : ''; - $countQueryBuilder->select(\sprintf('count(%s%s)', $distinct, $options['alias'])); - $countQueryBuilder->resetDQLPart('orderBy'); - - return (int) $countQueryBuilder->getQuery()->getSingleScalarResult(); - } elseif ('count_by_sub_request' === $options['behavior']) { - /** @var \Doctrine\ORM\QueryBuilder $cloneQueryBuilder */ - $cloneQueryBuilder = clone $queryBuilder; - $cloneQueryBuilder->resetDQLPart('orderBy'); - $rsm = new ResultSetMapping(); - $rsm->addScalarResult('cnt', 'cnt'); - $countSql = \sprintf('SELECT count(*) as cnt FROM (%s) mainquery', $cloneQueryBuilder->getQuery()->getSQL()); - /** @var NativeQuery $countQuery */ - $countQuery = $queryBuilder->getEntityManager()->createNativeQuery($countSql, $rsm); - $i = 0; - /** @var Parameter $parameter */ - foreach ($queryBuilder->getParameters() as $parameter) { - $i++; - $countQuery->setParameter($i, $parameter->getValue(), $parameter->getType()); - } - - return (int) $countQuery->getSingleScalarResult(); - } - } else { - if ('count_by_alias' === $options['behavior']) { - /** @var \Doctrine\DBAL\Query\QueryBuilder $countQueryBuilder */ - $countQueryBuilder = clone $queryBuilder; - $distinct = ($options['distinct_alias'])? 'DISTINCT ' : ''; - $countQueryBuilder->select(\sprintf('count(%s%s)', $distinct, $options['alias'])); - $countQueryBuilder->resetQueryPart('orderBy'); - - return (int) $countQueryBuilder->execute()->fetchColumn(0); - } elseif ('count_by_sub_request' === $options['behavior']) { - $queryBuilderCount = clone $queryBuilder; - $queryBuilderClone = clone $queryBuilder; - - $queryBuilderClone->resetQueryPart('orderBy'); //Disable sort (> performance) - - $queryBuilderCount->resetQueryParts(); //Remove Query Parts - $queryBuilderCount->select('count(*)') - ->from('(' . $queryBuilderClone->getSql() . ')', 'mainquery'); - - return (int) $queryBuilderCount->execute()->fetchColumn(0); - } - } - } - - /** - * @param \Doctrine\ORM\QueryBuilde|\Doctrine\DBAL\Query\QueryBuilder $queryBuilder - * @return string - */ - public static function getDefaultCountBehavior($queryBuilder) - { - if ($queryBuilder instanceof \Doctrine\ORM\QueryBuilder) { - return 'orm'; - } elseif ($queryBuilder instanceof \Doctrine\DBAL\Query\QueryBuilder) { - return 'count_by_sub_request'; - } - - return null; - } - - /** - * @param \Doctrine\ORM\QueryBuilde|\Doctrine\DBAL\Query\QueryBuilder $queryBuilder - * @param int $page Page number to display - * @param int $perPage Results per page - * @param array $options - * @return AbstractPaginator - * @throws \Exception - */ - static public function createDoctrinePaginator($queryBuilder, $page, $perPage, array $options = array()) - { - if ($queryBuilder instanceof \Doctrine\ORM\QueryBuilder) { - $useORM = true; - } elseif ($queryBuilder instanceof \Doctrine\DBAL\Query\QueryBuilder) { - $useORM = false; - } else { - throw new \Exception('Bad QueryBuilder'); - } - - $resolver = new OptionsResolver(); - $resolver->setDefaults(array( - //Behavior for create paginator. Availabled values : - // - doctrine_paginator: Return DoctrineORMPaginator or DoctrineDBALPaginator object - // - identifier_by_sub_request: Primary keys are found by sub request. Return ArrayPaginator. Option "identifier" is required - 'behavior' => 'doctrine_paginator', - //Manual value for the number of results - 'count_manual_value' => null, - //Count options. See countQueryBuilder. Used only if count_manual_value = null - 'count_options' => array(), - //Identifier used when behavior=identifier_by_sub_request - 'identifier' => null, - //Used only when ORM and behavior=doctrine_paginator - 'simplified_request' => true, - 'fetch_join_collection' => false, - )); - $resolver->setAllowedValues('behavior', array('doctrine_paginator', 'identifier_by_sub_request')); - $options = $resolver->resolve($options); - - if ('identifier_by_sub_request' === $options['behavior']) { - if (null === $options['identifier']) { - throw new MissingOptionsException('Option "identifier" is required'); - } - } - - if ('doctrine_paginator' === $options['behavior']) { - if ($useORM) { - $paginator = new DoctrineORMPaginator($perPage); - $paginator->setSimplifiedRequest($options['simplified_request']); - $paginator->setFetchJoinCollection($options['fetch_join_collection']); - } else { - $paginator = new DoctrineDBALPaginator($perPage); - } - $paginator->setQueryBuilder($queryBuilder); - $paginator->setPage($page); - if (null === $options['count_manual_value']) { - $paginator->setCountOptions($options['count_options']); - } else { - $paginator->setManualCountResults($options['count_manual_value']); - } - - return $paginator; - } elseif ('identifier_by_sub_request' === $options['behavior']) { - $result = array(); - - if (null === $options['count_manual_value']) { - $countResults = self::countQueryBuilder($queryBuilder, $options['count_options']); - } else { - $countResults = $options['count_manual_value']; - } - - if ($countResults) { - $idsQueryBuilder = clone $queryBuilder; - $idsQueryBuilder->select(\sprintf('DISTINCT %s as pk', $options['identifier'])); - - if ($useORM) { - $tmpPaginator = new DoctrineORMPaginator($perPage); - $tmpPaginator->setSimplifiedRequest(false); - $tmpPaginator->setFetchJoinCollection(false); - } else { - $tmpPaginator = new DoctrineDBALPaginator($perPage); - } - $tmpPaginator->setQueryBuilder($idsQueryBuilder); - $tmpPaginator->setPage($page); - $tmpPaginator->setManualCountResults($countResults); - $tmpPaginator->init(); - - $ids = array(); - foreach ($tmpPaginator->getResults() as $line) { - $ids[] = $line['pk']; - } - - $finalQueryBuilder = clone $queryBuilder; - if ($useORM) { - $finalQueryBuilder->resetDQLPart('where'); - $finalQueryBuilder->setParameters(array()); - QueryBuilderFilter::addMultiFilter($finalQueryBuilder, QueryBuilderFilter::SELECT_IN, $ids, $options['identifier'], 'paginate_pks'); - $result = $finalQueryBuilder->getQuery()->getResult(); - } else { - $finalQueryBuilder->resetQueryPart('where'); - $finalQueryBuilder->setParameters(array()); - QueryBuilderFilter::addMultiFilter($finalQueryBuilder, QueryBuilderFilter::SELECT_IN, $ids, $options['identifier'], 'paginate_pks'); - $result = $finalQueryBuilder->execute()->fetchAll(); - } - } - - $paginator = new ArrayPaginator($perPage); - $paginator->setPage($page); - $paginator->setDataWithoutSlice($result, $countResults); - - return $paginator; - } - } -} diff --git a/DoctrineExtension/QueryBuilderFilter.php b/DoctrineExtension/QueryBuilderFilter.php deleted file mode 100644 index 003c8d6..0000000 --- a/DoctrineExtension/QueryBuilderFilter.php +++ /dev/null @@ -1,229 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\DoctrineExtension; - -class QueryBuilderFilter -{ - const SELECT_IN = 'IN'; //WHERE IN - const SELECT_NOT_IN = 'NIN'; //WHERE NOT IN - const SELECT_ALL = 'ALL'; //No Filter (all values) - const SELECT_AUTO = 'AUT'; //WHERE IN. If filter values are empty, no filter (all values) - const SELECT_NO = 'NO'; //Must return no result - - /** - * Add SQL WHERE IN or WHERE NOT IN filter - * @param \Doctrine\DBAL\Query\QueryBuilder|\Doctrine\ORM\QueryBuilder $queryBuilder - * @param string $filterSign ALL (no filter), IN (WHERE IN), NIN (WHERE NOT IN), AUT (WHERE IN if $filterValues is not empty. No filter else), NO (no result) - * @param array $filterValues Values - * @param string $sqlField SQL field name - * @param string $paramName SQL parameter name - * @return \Doctrine\DBAL\Query\QueryBuilder|\Doctrine\ORM\QueryBuilder - */ - public static function addMultiFilter($queryBuilder, $filterSign, $filterValues, $sqlField, $paramName) - { - if (self::SELECT_NO == $filterSign) { - //Must return no result - $queryBuilder->andWhere('0 = 1'); - - return $queryBuilder; - } - if ($filterSign != self::SELECT_IN && $filterSign != self::SELECT_NOT_IN && $filterSign != self::SELECT_AUTO) { - return $queryBuilder; - } - if (null === $filterValues || 0 === count($filterValues)) { - if (self::SELECT_NOT_IN == $filterSign || self::SELECT_AUTO == $filterSign) { - return $queryBuilder; - } - - //Must return no result - $queryBuilder->andWhere('0 = 1'); - - return $queryBuilder; - } - - if (count($filterValues) > 1000) { - return self::addGroupMultiFilter($queryBuilder, $filterSign, $filterValues, $sqlField, $paramName); - } - - return self::addSimpleMultiFilter($queryBuilder, $filterSign, $filterValues, $sqlField, $paramName); - } - - /** - * Add SQL WHERE IN or WHERE NOT IN filter without group - * @param \Doctrine\DBAL\Query\QueryBuilder|\Doctrine\ORM\QueryBuilder $queryBuilder - * @param string $filterSign ALL (no filter), IN (WHERE IN), NIN (WHERE NOT IN), AUT (WHERE IN if $filterValues is not empty. No filter else), NO (no result) - * @param array $filterValues Values - * @param string $sqlField SQL field name - * @param string $paramName SQL parameter name - * @return \Doctrine\DBAL\Query\QueryBuilder|\Doctrine\ORM\QueryBuilder - */ - protected static function addSimpleMultiFilter($queryBuilder, $filterSign, $filterValues, $sqlField, $paramName) - { - $clauseSql = ($filterSign == self::SELECT_IN || $filterSign == self::SELECT_AUTO)? 'IN' : 'NOT IN'; - - $queryBuilder->andWhere(\sprintf('%s %s (:%s)', $sqlField, $clauseSql, $paramName)); - $queryBuilder->setParameter($paramName, $filterValues, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY); - - return $queryBuilder; - } - - /** - * Add SQL WHERE IN or WHERE NOT IN filter with group - * @param \Doctrine\DBAL\Query\QueryBuilder|\Doctrine\ORM\QueryBuilder $queryBuilder - * @param string $filterSign ALL (no filter), IN (WHERE IN), NIN (WHERE NOT IN), AUT (WHERE IN if $filterValues is not empty. No filter else), NO (no result) - * @param array $filterValues Values - * @param string $sqlField SQL field name - * @param string $paramName SQL parameter name - * @return \Doctrine\DBAL\Query\QueryBuilder|\Doctrine\ORM\QueryBuilder - */ - protected static function addGroupMultiFilter($queryBuilder, $filterSign, $filterValues, $sqlField, $paramName) - { - $clauseSql = ($filterSign == self::SELECT_IN || $filterSign == self::SELECT_AUTO)? 'IN' : 'NOT IN'; - $separatorClauseSql = ($filterSign == self::SELECT_IN || $filterSign == self::SELECT_AUTO)? 'OR' : 'AND'; - - $groupNumber = 0; - $groups = array(); - foreach (\array_chunk($filterValues, 1000) as $filterValuesGroup) { - $groupNumber++; - $groups[] = \sprintf('%s %s (:%s%s)', $sqlField, $clauseSql, $paramName, $groupNumber); - $queryBuilder->setParameter($paramName.$groupNumber, $filterValuesGroup, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY); - } - - $queryBuilder->andWhere(implode(' '.$separatorClauseSql.' ', $groups)); - - return $queryBuilder; - } - - /** - * Add SQL WHERE IN or WHERE NOT IN filter. And result MUST BE in the whitelist (if $restrictSign=IN) or MUST NOT BE in the blacklist (if $restrictSign=NIN) - * @param \Doctrine\DBAL\Query\QueryBuilder|\Doctrine\ORM\QueryBuilder $queryBuilder - * @param string $filterSign ALL (no filter), IN (WHERE IN), NIN (WHERE NOT IN), AUT (WHERE IN if $filterValues is not empty. No filter else), NO (no result) - * @param array $filterValues Values - * @param string $sqlField SQL field name - * @param string $paramName SQL parameter name - * @param string $restrictSign IN (WHERE IN), NIN (WHERE NOT IN), AUT (WHERE IN if $restrictValues is not empty. No filter else), NO (no result) - * @param array $restrictValues Whitelist (if $restrictSign=IN or AUT) or blacklist (if $restrictSign=NIN) - * @return \Doctrine\DBAL\Query\QueryBuilder|\Doctrine\ORM\QueryBuilder - */ - public static function addMultiFilterWithRestrictValues($queryBuilder, $filterSign, $filterValues, $sqlField, $paramName, $restrictSign, $restrictValues) - { - if (self::SELECT_NO == $filterSign || self::SELECT_NO == $restrictSign) { - //Must return no result - $queryBuilder->andWhere('0 = 1'); - - return $queryBuilder; - } - - if (in_array($restrictSign, array(self::SELECT_IN, self::SELECT_AUTO)) && in_array($filterSign, array(self::SELECT_IN, self::SELECT_AUTO)) && count($filterValues) > 0 && count($restrictValues) > 0) { - //We can simplify the query - - //Data cleaning - $cleanValues = array(); - foreach ($filterValues as $value) { - if (in_array($value, $restrictValues)) { - $cleanValues[] = $value; - } - } - - $queryBuilder = self::addMultiFilter($queryBuilder, self::SELECT_IN, $cleanValues, $sqlField, $paramName); - } elseif (self::SELECT_NOT_IN === $restrictSign && self::SELECT_NOT_IN === $filterSign && count($filterValues) > 0 && count($restrictValues) > 0) { - //We can simplify the query - - //Data fusion - $cleanValues = $restrictValues; - foreach ($filterValues as $value) { - if (!in_array($value, $restrictValues)) { - $cleanValues[] = $value; - } - } - - $queryBuilder = self::addMultiFilter($queryBuilder, self::SELECT_NOT_IN, $cleanValues, $sqlField, $paramName); - } else { - //Two filters - self::addMultiFilter($queryBuilder, $filterSign, $filterValues, $sqlField, $paramName); - self::addMultiFilter($queryBuilder, $restrictSign, $restrictValues, $sqlField, $paramName.'Restrict'); - } - - return $queryBuilder; - } - - /** - * Add SQL "equal" or "not equal" filter - * @param \Doctrine\DBAL\Query\QueryBuilder|\Doctrine\ORM\QueryBuilder $queryBuilder - * @param bool $equal Equal or not - * @param string $filterValue Value - * @param string $sqlField SQL field name - * @param string $paramName SQL parameter name - * @return \Doctrine\DBAL\Query\QueryBuilder|\Doctrine\ORM\QueryBuilder - */ - public static function addEqualFilter($queryBuilder, $equal, $filterValue, $sqlField, $paramName) - { - if (null === $filterValue || '' === $filterValue) { - return $queryBuilder; - } - - if ($equal) { - $queryBuilder->andWhere($sqlField.' = :'.$paramName); - } else { - $queryBuilder->andWhere($sqlField.' != :'.$paramName); - } - $queryBuilder->setParameter($paramName, $filterValue); - - return $queryBuilder; - } - - /** - * Add SQL comparator filter - * @param \Doctrine\DBAL\Query\QueryBuilder|\Doctrine\ORM\QueryBuilder $queryBuilder - * @param string $sign Comparator sign (< > <= >=) - * @param string $filterValue Value - * @param string $sqlField SQL field name - * @param string $paramName SQL parameter name - * @return \Doctrine\DBAL\Query\QueryBuilder|\Doctrine\ORM\QueryBuilder - */ - public static function addComparatorFilter($queryBuilder, $sign, $filterValue, $sqlField, $paramName) - { - if (null === $filterValue || '' === $filterValue) { - return $queryBuilder; - } - - $queryBuilder->andWhere(\sprintf('%s %s :%s', $sqlField, $sign, $paramName)); - $queryBuilder->setParameter($paramName, $filterValue); - - return $queryBuilder; - } - - /** - * Add SQL "LIKE" or "NOT LIKE" filter - * @param \Doctrine\DBAL\Query\QueryBuilder|\Doctrine\ORM\QueryBuilder $queryBuilder - * @param bool $contain Contain or not - * @param string $filterValue Value - * @param string $sqlField SQL field name - * @param string $paramName SQL parameter name - * @return \Doctrine\DBAL\Query\QueryBuilder|\Doctrine\ORM\QueryBuilder - */ - public static function addContainFilter($queryBuilder, $contain, $filterValue, $sqlField, $paramName) - { - if (null === $filterValue || '' === $filterValue) { - return $queryBuilder; - } - - $filterValue = addcslashes($filterValue, '%_'); - if ($contain) { - $queryBuilder->andWhere($queryBuilder->expr()->like($sqlField, ':' . $paramName)); - } else { - $queryBuilder->andWhere($queryBuilder->expr()->notLike($sqlField, ':' . $paramName)); - } - $queryBuilder->setParameter($paramName, '%'.$filterValue.'%'); - - return $queryBuilder; - } -} diff --git a/Entity/UserCrudSettings.php b/Entity/UserCrudSettings.php deleted file mode 100644 index 436f94d..0000000 --- a/Entity/UserCrudSettings.php +++ /dev/null @@ -1,221 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Entity; - -use Doctrine\ORM\Mapping as ORM; -use Ecommit\CrudBundle\Crud\CrudSession; - -/** - * @ORM\Entity() - * @ORM\Table(name="user_crud_settings") - */ -class UserCrudSettings -{ - /** - * @ORM\Id - * @ORM\ManyToOne(targetEntity="Ecommit\CrudBundle\Entity\UserCrudInterface") - */ - protected $user; - - /** - * @ORM\Id - * @ORM\Column(type="string", length=255, name="crud_name") - */ - protected $crudName; - - /** - * @ORM\Column(type="integer", name="results_displayed") - */ - protected $resultsDisplayed; - - /** - * @ORM\Column(type="array", name="displayed_columns") - */ - protected $displayedColumns = array(); - - /** - * @ORM\Column(type="string", length=30) - */ - protected $sort; - - /** - * @ORM\Column(type="string", length=4) - */ - protected $sense; - - /** - * Set crudName - * - * @param string $crudName - * @return UserCrudSettings - */ - public function setCrudName($crudName) - { - $this->crudName = $crudName; - - return $this; - } - - /** - * Get crudName - * - * @return string - */ - public function getCrudName() - { - return $this->crudName; - } - - /** - * Set resultsDisplayed - * - * @param integer $resultsDisplayed - * @return UserCrudSettings - */ - public function setResultsDisplayed($resultsDisplayed) - { - $this->resultsDisplayed = $resultsDisplayed; - - return $this; - } - - /** - * Get resultsDisplayed - * - * @return integer - */ - public function getResultsDisplayed() - { - return $this->resultsDisplayed; - } - - /** - * Set displayedColumns - * - * @param array $displayedColumns - * @return UserCrudSettings - */ - public function setDisplayedColumns($displayedColumns) - { - $this->displayedColumns = $displayedColumns; - - return $this; - } - - /** - * Get displayedColumns - * - * @return array - */ - public function getDisplayedColumns() - { - return $this->displayedColumns; - } - - /** - * Set sort - * - * @param string $sort - * @return UserCrudSettings - */ - public function setSort($sort) - { - $this->sort = $sort; - - return $this; - } - - /** - * Get sort - * - * @return string - */ - public function getSort() - { - return $this->sort; - } - - /** - * Set sense - * - * @param string $sense - * @return UserCrudSettings - */ - public function setSense($sense) - { - $this->sense = $sense; - - return $this; - } - - /** - * Get sense - * - * @return string - */ - public function getSense() - { - return $this->sense; - } - - /** - * Set user - * - * @param \Ecommit\CrudBundle\Entity\UserCrudInterface $user - * @return UserCrudSettings - */ - public function setUser(\Ecommit\CrudBundle\Entity\UserCrudInterface $user) - { - $this->user = $user; - - return $this; - } - - /** - * Get user - * - * @return \Ecommit\CrudBundle\Entity\UserCrudInterface - */ - public function getUser() - { - return $this->user; - } - - /** - * Create CrudSession from this object - * - * @param \Ecommit\CrudBundle\Crud\CrudSession $crudSessionManager - * @return \Ecommit\CrudBundle\Crud\CrudSession - */ - public function transformToCrudSession(CrudSession $crudSessionManager) - { - $crudSessionManager->displayedColumns = $this->displayedColumns; - $crudSessionManager->resultsPerPage = $this->resultsDisplayed; - $crudSessionManager->sense = $this->sense; - $crudSessionManager->sort = $this->sort; - - return $crudSessionManager; - } - - /** - * Update this object from CrudSession - * - * @param \Ecommit\CrudBundle\Crud\CrudSession $crudSessionManager - */ - public function updateFromSessionManager(CrudSession $crudSessionManager) - { - $this->displayedColumns = $crudSessionManager->displayedColumns; - $this->resultsDisplayed = $crudSessionManager->resultsPerPage; - $this->sense = $crudSessionManager->sense; - $this->sort = $crudSessionManager->sort; - } -} diff --git a/Form/Filter/AbstractFieldFilter.php b/Form/Filter/AbstractFieldFilter.php deleted file mode 100644 index d6481ce..0000000 --- a/Form/Filter/AbstractFieldFilter.php +++ /dev/null @@ -1,169 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Form\Filter; - -use Ecommit\CrudBundle\Form\Searcher\AbstractFormSearcher; -use Symfony\Component\Form\FormBuilder; -use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Context\ExecutionContextInterface; - -abstract class AbstractFieldFilter -{ - protected $columnId; - protected $property; - protected $options; - protected $typeOptions; - protected $isInitiated = false; - - /** - * @param string $columnId Column id - * @param string $property Field Name (search form) - * @param array $options Options - * @param array $typeOptions Type options - */ - public function __construct($columnId, $property, $options = array(), $typeOptions = array()) - { - $this->columnId = $columnId; - $this->property = $property; - - $this->options = $options; - if (!isset($typeOptions['required'])) { - $typeOptions['required'] = false; - } - $this->typeOptions = $typeOptions; - } - - public function init() - { - if ($this->isInitiated) { - return; - } - - // Define options - $resolver = new OptionsResolver(); - $this->configureCommonOptions($resolver); - $this->configureOptions($resolver); - $this->options = $resolver->resolve($this->options); - - // Define type options - $this->typeOptions = $this->configureTypeOptions($this->typeOptions); - - $this->isInitiated = true; - } - - /** - * @param OptionsResolver $resolver - */ - protected function configureCommonOptions(OptionsResolver $resolver) - { - $resolver->setDefaults( - array( - 'validate' => true, - ) - ); - } - - /** - * @param OptionsResolver $resolver - */ - protected function configureOptions(OptionsResolver $resolver) - { - } - - /** - * @param array $typeOptions - * @return array - */ - protected function configureTypeOptions($typeOptions) - { - return $typeOptions; - } - - /** - * Adds the field into the form - * @param FormBuilder $formBuilder - * @return FormBuilder - */ - abstract public function addField(FormBuilder $formBuilder); - - /** - * Changes the query - * @param QueryBuilder $queryBuilder - * @param AbstractFormSearcher $formData - * @param string $aliasSearch - * @return QueryBuilder - */ - abstract public function changeQuery($queryBuilder, AbstractFormSearcher $formData, $aliasSearch); - - /** - * Add auto validation - * @param $value - * @param ExecutionContextInterface $context - */ - public function autoValidate(AbstractFormSearcher $value, ExecutionContextInterface $context) - { - if (!$this->options['validate']) { - return; - } - $autoConstraints = $this->getAutoConstraints(); - if (count($autoConstraints) == 0) { - return; - } - $context->getValidator() - ->inContext($context) - ->atPath($this->getProperty()) - ->validate($value->get($this->getProperty()), $autoConstraints); - } - - /** - * Gets auto constraints list - * @return array - */ - protected function getAutoConstraints() - { - return array(); - } - - /** - * Returns the column id associated at this object - * - * @return string - */ - public function getColumnId() - { - return $this->columnId; - } - - /** - * Returns the property associated at this object - * - * @return string - */ - public function getProperty() - { - return $this->property; - } - - /** - * @param string $label - * @param bool $displayLabelInErrors - */ - public function setLabel($label, $displayLabelInErrors = false) - { - if (!empty($label) && !isset($this->typeOptions['label'])) { - $this->typeOptions['label'] = $label; - } - if (!isset($this->typeOptions['label_attr']['data-display-in-errors']) && $displayLabelInErrors) { - $this->typeOptions['label_attr']['data-display-in-errors'] = '1'; - } - } -} diff --git a/Form/Filter/FieldFilterBoolean.php b/Form/Filter/FieldFilterBoolean.php deleted file mode 100644 index 949bbd3..0000000 --- a/Form/Filter/FieldFilterBoolean.php +++ /dev/null @@ -1,124 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Form\Filter; - -use Ecommit\CrudBundle\Form\Searcher\AbstractFormSearcher; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; -use Symfony\Component\Form\FormBuilder; -use Symfony\Component\OptionsResolver\OptionsResolver; - -class FieldFilterBoolean extends AbstractFieldFilter -{ - /** - * {@inheritDoc} - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults( - array( - 'value_true' => 1, - 'value_false' => 0, - 'not_null_is_true' => false, - 'null_is_false' => true, - ) - ); - } - - /** - * {@inheritDoc} - */ - protected function configureTypeOptions($typeOptions) - { - $typeOptions['multiple'] = false; - $typeOptions['choices'] = self::getChoices(); - if (!isset($typeOptions['placeholder']) && !$typeOptions['required']) { - $typeOptions['placeholder'] = 'filter.choices.placeholder'; - } - - return $typeOptions; - } - - /** - * {@inheritDoc} - */ - public function addField(FormBuilder $formBuilder) - { - $formBuilder->add($this->property, ChoiceType::class, $this->typeOptions); - - return $formBuilder; - } - - /** - * {@inheritDoc} - */ - public function changeQuery($queryBuilder, AbstractFormSearcher $formData, $aliasSearch) - { - $value = $formData->get($this->property); - if (empty($value) || !is_scalar($value)) { - return $queryBuilder; - } - - if ($value == 'T') { - $parameterName = 'value_true' . str_replace(' ', '', $this->property); - if ($this->options['not_null_is_true']) { - $parameterNameFalse = 'value_false' . str_replace(' ', '', $this->property); - $queryBuilder->andWhere( - sprintf( - '(%s = :%s OR (%s IS NOT NULL AND %s != :%s))', - $aliasSearch, - $parameterName, - $aliasSearch, - $aliasSearch, - $parameterNameFalse - ) - ) - ->setParameter($parameterName, $this->options['value_true']) - ->setParameter($parameterNameFalse, $this->options['value_false']); - } else { - $queryBuilder->andWhere(sprintf('%s = :%s',$aliasSearch, $parameterName)) - ->setParameter($parameterName, $this->options['value_true']); - } - - return $queryBuilder; - } elseif ($value == 'F') { - $parameterName = 'value_false' . str_replace(' ', '', $this->property); - if (is_null($this->options['value_false'])) { - $queryBuilder->andWhere(sprintf('%s IS NULL', $aliasSearch)); - } elseif ($this->options['null_is_false']) { - $queryBuilder->andWhere( - sprintf( - '(%s = :%s OR %s IS NULL)', - $aliasSearch, - $parameterName, - $aliasSearch - ) - ) - ->setParameter($parameterName, $this->options['value_false']); - } else { - $queryBuilder->andWhere(sprintf('%s = :%s', $aliasSearch, $parameterName)) - ->setParameter($parameterName, $this->options['value_false']); - } - - return $queryBuilder; - } else { - return $queryBuilder; - } - } - - public static function getChoices() - { - return array( - 'filter.true' => 'T', - 'filter.false' => 'F', - ); - } -} diff --git a/Form/Filter/FieldFilterChoice.php b/Form/Filter/FieldFilterChoice.php deleted file mode 100644 index 3faeff8..0000000 --- a/Form/Filter/FieldFilterChoice.php +++ /dev/null @@ -1,129 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Form\Filter; - -use Ecommit\CrudBundle\DoctrineExtension\QueryBuilderFilter; -use Ecommit\CrudBundle\Form\Searcher\AbstractFormSearcher; -use Ecommit\ScalarValues\ScalarValues; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; -use Symfony\Component\Form\FormBuilder; -use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints as Assert; - -class FieldFilterChoice extends AbstractFieldFilter -{ - /** - * {@inheritDoc} - */ - protected function configureCommonOptions(OptionsResolver $resolver) - { - parent::configureCommonOptions($resolver); - - $resolver->setDefaults( - array( - 'multiple' => false, - 'min' => null, - 'max' => 99, - ) - ); - } - - /** - * {@inheritDoc} - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults( - array( - 'choices' => null, - ) - ); - } - - /** - * {@inheritDoc} - */ - protected function configureTypeOptions($typeOptions) - { - $typeOptions['choices'] = $this->options['choices']; - $typeOptions['multiple'] = $this->options['multiple']; - if (!isset($typeOptions['placeholder']) && !$typeOptions['required']) { - $typeOptions['placeholder'] = 'filter.choices.placeholder'; - } - - return $typeOptions; - } - - - /** - * {@inheritDoc} - */ - public function addField(FormBuilder $formBuilder) - { - $formBuilder->add($this->property, ChoiceType::class, $this->typeOptions); - - return $formBuilder; - } - - /** - * {@inheritDoc} - */ - protected function getAutoConstraints() - { - if ($this->options['multiple']) { - return array( - new Assert\Count( - array( - 'min' => $this->options['min'], - 'max' => $this->options['max'], - ) - ), - ); - } else { - return array(); - } - } - - /** - * {@inheritDoc} - */ - public function changeQuery($queryBuilder, AbstractFormSearcher $formData, $aliasSearch) - { - $value = $formData->get($this->property); - $parameterName = 'value_choice' . str_replace(' ', '', $this->property); - if (null === $value || '' === $value || array() === $value) { - return $queryBuilder; - } - - if ($this->options['multiple']) { - if (!is_array($value)) { - $value = array($value); - } - $value = ScalarValues::filterScalarValues($value); - if (count($value) > $this->options['max'] || 0 === count($value)) { - return $queryBuilder; - } - if ($this->options['min'] && count($value) < $this->options['min']) { - return $queryBuilder; - } - QueryBuilderFilter::addMultiFilter($queryBuilder, QueryBuilderFilter::SELECT_IN, $value, $aliasSearch, $parameterName); - } else { - if (!is_scalar($value)) { - return $queryBuilder; - } - $queryBuilder->andWhere(sprintf('%s = :%s', $aliasSearch, $parameterName)) - ->setParameter($parameterName, $value); - } - - return $queryBuilder; - } -} diff --git a/Form/Filter/FieldFilterDate.php b/Form/Filter/FieldFilterDate.php deleted file mode 100644 index c20ff08..0000000 --- a/Form/Filter/FieldFilterDate.php +++ /dev/null @@ -1,156 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Form\Filter; - -use Ecommit\CrudBundle\Form\Searcher\AbstractFormSearcher; -use Ecommit\JavascriptBundle\Form\Type\JqueryDatePickerType; -use Symfony\Component\Form\Extension\Core\Type\DateTimeType; -use Symfony\Component\Form\Extension\Core\Type\DateType; -use Symfony\Component\Form\FormBuilder; -use Symfony\Component\OptionsResolver\Options; -use Symfony\Component\OptionsResolver\OptionsResolver; - -class FieldFilterDate extends AbstractFieldFilter -{ - const GREATER_THAN = '>'; - const GREATER_EQUAL = '>='; - const SMALLER_THAN = '<'; - const SMALLER_EQUAL = '<='; - const EQUAL = '='; - - /** - * {@inheritDoc} - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults( - array( - 'type' => JqueryDatePickerType::class, - 'with_time' => function (Options $options) { - if (DateTimeType::class === $options['type']) { - return true; - } - - return false; - }, - ) - ); - - $resolver->setRequired( - array( - 'comparator', - ) - ); - - $resolver->setAllowedValues( - 'comparator', - array( - self::EQUAL, - self::GREATER_EQUAL, - self::GREATER_THAN, - self::SMALLER_EQUAL, - self::SMALLER_THAN, - ) - ); - - $resolver->setAllowedValues( - 'type', - array( - DateType::class, - DateTimeType::class, - JqueryDatePickerType::class, - ) - ); - } - - protected function configureTypeOptions($typeOptions) - { - $typeOptions['input'] = 'datetime'; - - if (JqueryDatePickerType::class === $this->options['type'] && $this->options['with_time'] && empty($this->typeOptions['time_format'])) { - $typeOptions['time_format'] = 'H:i:s'; - } - - return $typeOptions; - } - - /** - * {@inheritDoc} - */ - public function addField(FormBuilder $formBuilder) - { - $formBuilder->add($this->property, $this->options['type'], $this->typeOptions); - - return $formBuilder; - } - - /** - * {@inheritDoc} - */ - public function changeQuery($queryBuilder, AbstractFormSearcher $formData, $aliasSearch) - { - $value = $formData->get($this->property); - if (!empty($value) && $value instanceof \DateTime) { - $parameterName = 'value_date_' . str_replace(' ', '', $this->property); - - switch ($this->options['comparator']): - case FieldFilterDate::SMALLER_THAN: - case FieldFilterDate::GREATER_EQUAL: - if (!$this->options['with_time']) { - $value->setTime(0, 0, 0); - } - $value = $value->format('Y-m-d H:i:s'); - $queryBuilder->andWhere( - sprintf('%s %s :%s', $aliasSearch, $this->options['comparator'], $parameterName) - ) - ->setParameter($parameterName, $value); - break; - case FieldFilterDate::SMALLER_EQUAL: - case FieldFilterDate::GREATER_THAN: - if (!$this->options['with_time']) { - $value->setTime(23, 59, 59); - } - $value = $value->format('Y-m-d H:i:s'); - $queryBuilder->andWhere( - sprintf('%s %s :%s', $aliasSearch, $this->options['comparator'], $parameterName) - ) - ->setParameter($parameterName, $value); - break; - default: - $valueDateInf = clone $value; - $valueDateSup = clone $value; - if (!$this->options['with_time']) { - $valueDateInf->setTime(0, 0, 0); - $valueDateSup->setTime(23, 59, 59); - } - $valueDateInf = $valueDateInf->format('Y-m-d H:i:s'); - $valueDateSup = $valueDateSup->format('Y-m-d H:i:s'); - $parameterNameInf = 'value_date_inf_' . str_replace(' ', '', $this->property); - $parameterNameSup = 'value_date_sup_' . str_replace(' ', '', $this->property); - $queryBuilder->andWhere( - sprintf( - '%s >= :%s AND %s <= :%s', - $aliasSearch, - $parameterNameInf, - $aliasSearch, - $parameterNameSup - ) - ) - ->setParameter($parameterNameInf, $valueDateInf) - ->setParameter($parameterNameSup, $valueDateSup); - break; - endswitch; - } - - return $queryBuilder; - } -} diff --git a/Form/Filter/FieldFilterEmpty.php b/Form/Filter/FieldFilterEmpty.php deleted file mode 100644 index 0b88551..0000000 --- a/Form/Filter/FieldFilterEmpty.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Form\Filter; - -use Ecommit\CrudBundle\Form\Searcher\AbstractFormSearcher; -use Symfony\Component\Form\Extension\Core\Type\CheckboxType; -use Symfony\Component\Form\FormBuilder; - -class FieldFilterEmpty extends AbstractFieldFilter -{ - /** - * {@inheritDoc} - */ - protected function configureTypeOptions($typeOptions) - { - $typeOptions['value'] = 1; - - return $typeOptions; - } - - /** - * {@inheritDoc} - */ - public function addField(FormBuilder $formBuilder) - { - $formBuilder->add($this->property, CheckboxType::class, $this->typeOptions); - - return $formBuilder; - } - - /** - * {@inheritDoc} - */ - public function changeQuery($queryBuilder, AbstractFormSearcher $formData, $aliasSearch) - { - $value = $formData->get($this->property); - if (empty($value) || !is_scalar($value)) { - return $queryBuilder; - } - - if ($value == 1) { - $queryBuilder->andWhere( - sprintf('(%s IS NULL OR %s = \'\')', $aliasSearch, $aliasSearch) - ); - } - - return $queryBuilder; - } -} diff --git a/Form/Filter/FieldFilterEntity.php b/Form/Filter/FieldFilterEntity.php deleted file mode 100644 index 9850ed1..0000000 --- a/Form/Filter/FieldFilterEntity.php +++ /dev/null @@ -1,115 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Form\Filter; - -use Doctrine\Persistence\ManagerRegistry; -use Ecommit\JavascriptBundle\Form\Type\EntityNormalizerTrait; -use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader; -use Symfony\Component\OptionsResolver\Options; -use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\PropertyAccess\PropertyAccess; - -class FieldFilterEntity extends FieldFilterChoice implements FieldFilterDoctrineInterface -{ - use EntityNormalizerTrait; - - /** - * @var ManagerRegistry - */ - protected $registry; - - /** - * {@inheritDoc} - */ - protected function configureOptions(OptionsResolver $resolver) - { - parent::configureOptions($resolver); - - $resolver->setDefaults( - array( - 'property' => null, // deprecated since 2.2, use "choice_label" - 'choice_label' => function (Options $options) { - // BC with the "property" option - if ($options['property']) { - trigger_error('The "property" option is deprecated since version 2.2. Use "choice_label" instead.', E_USER_DEPRECATED); - - return $options['property']; - } - - return null; - }, - 'em' => null, - 'query_builder' => null, - 'identifier' => null, - ) - ); - - $resolver->setRequired( - array( - 'class', - ) - ); - - $resolver->setNormalizer('em', $this->getEmNormalizer($this->registry)); - $resolver->setNormalizer('query_builder', $this->getQueryBuilderNormalizer()); - $resolver->setNormalizer('identifier', $this->getIdentifierNormalizer()); - } - - protected function configureTypeOptions($typeOptions) - { - $typeOptions = parent::configureTypeOptions($typeOptions); - - $queryBuilderLoader = new ORMQueryBuilderLoader($this->options['query_builder']); - - $accessor = PropertyAccess::createPropertyAccessor(); - $choices = array(); - foreach ($queryBuilderLoader->getEntities() as $entity) { - $id = $accessor->getValue($entity, $this->options['identifier']); - $choices[$this->extractLabel($entity)] = $id; - } - - $typeOptions['choices'] = $choices; - if (!isset($typeOptions['placeholder']) && !$typeOptions['required']) { - $typeOptions['placeholder'] = 'filter.choices.placeholder'; - } - - return $typeOptions; - } - - /** - * Extract property that should be used for displaying the entities as text in the HTML element - * @param object $object - * @throws \Exception - */ - protected function extractLabel($object) - { - if ($this->options['choice_label']) { - $accessor = PropertyAccess::createPropertyAccessor(); - - return $accessor->getValue($object, $this->options['choice_label']); - } elseif (method_exists($object, '__toString')) { - return (string)$object; - } else { - throw new \Exception('"choice_label" option or "__toString" method must be defined"'); - } - } - - public function getRegistry() - { - return $this->registry; - } - - public function setRegistry(ManagerRegistry $registry) - { - $this->registry = $registry; - } -} diff --git a/Form/Filter/FieldFilterInteger.php b/Form/Filter/FieldFilterInteger.php deleted file mode 100644 index 49cdb14..0000000 --- a/Form/Filter/FieldFilterInteger.php +++ /dev/null @@ -1,76 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Form\Filter; - -use Ecommit\CrudBundle\Form\Searcher\AbstractFormSearcher; -use Symfony\Component\Form\Extension\Core\Type\IntegerType; -use Symfony\Component\Form\FormBuilder; -use Symfony\Component\OptionsResolver\OptionsResolver; - -class FieldFilterInteger extends AbstractFieldFilter -{ - const GREATER_THAN = '>'; - const GREATER_EQUAL = '>='; - const SMALLER_THAN = '<'; - const SMALLER_EQUAL = '<='; - const EQUAL = '='; - - /** - * {@inheritDoc} - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setRequired( - array( - 'comparator', - ) - ); - - $resolver->setAllowedValues( - 'comparator', - array( - self::EQUAL, - self::GREATER_EQUAL, - self::GREATER_THAN, - self::SMALLER_EQUAL, - self::SMALLER_THAN, - ) - ); - } - - /** - * {@inheritDoc} - */ - public function addField(FormBuilder $formBuilder) - { - $formBuilder->add($this->property, IntegerType::class, $this->typeOptions); - - return $formBuilder; - } - - /** - * {@inheritDoc} - */ - public function changeQuery($queryBuilder, AbstractFormSearcher $formData, $aliasSearch) - { - $value = $formData->get($this->property); - if (!is_null($value) && is_numeric($value)) { //Important: Is_null but not is_empty - $parameterName = 'value_integer_' . str_replace(' ', '', $this->property); - $queryBuilder->andWhere( - sprintf('%s %s :%s', $aliasSearch, $this->options['comparator'], $parameterName) - ) - ->setParameter($parameterName, $value); - } - - return $queryBuilder; - } -} diff --git a/Form/Filter/FieldFilterJqueryAutocompleteEntityAjax.php b/Form/Filter/FieldFilterJqueryAutocompleteEntityAjax.php deleted file mode 100644 index 860f14c..0000000 --- a/Form/Filter/FieldFilterJqueryAutocompleteEntityAjax.php +++ /dev/null @@ -1,110 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Form\Filter; - -use Ecommit\CrudBundle\Form\Searcher\AbstractFormSearcher; -use Ecommit\JavascriptBundle\Form\Type\JqueryAutocompleteEntityAjaxType; -use Symfony\Component\Form\FormBuilder; -use Symfony\Component\OptionsResolver\Options; -use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints as Assert; - -class FieldFilterJqueryAutocompleteEntityAjax extends AbstractFieldFilter -{ - /** - * {@inheritDoc} - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults( - array( - 'em' => null, - 'query_builder' => null, - 'property' => null, // deprecated since 2.2, use "choice_label" - 'choice_label' => function (Options $options) { - // BC with the "property" option - if ($options['property']) { - trigger_error('The "property" option is deprecated since version 2.2. Use "choice_label" instead.', E_USER_DEPRECATED); - - return $options['property']; - } - - return null; - }, - 'identifier' => null, - 'url' => null, //Required in FormType if route_name is empty - 'route_name' => null, - 'route_params' => null, - 'max_length' => 255, - ) - ); - - $resolver->setRequired( - array( - 'class', - ) - ); - } - - /** - * {@inheritDoc} - */ - protected function configureTypeOptions($typeOptions) - { - foreach ($this->options as $optionName => $optionValue) { - if (!empty($optionValue) && !in_array($optionName, array('validate', 'max_length'))) { - $typeOptions[$optionName] = $optionValue; - } - } - $typeOptions['input'] = 'key'; - - return $typeOptions; - } - - /** - * {@inheritDoc} - */ - public function addField(FormBuilder $formBuilder) - { - $formBuilder->add($this->property, JqueryAutocompleteEntityAjaxType::class, $this->typeOptions); - - return $formBuilder; - } - - /** - * {@inheritDoc} - */ - protected function getAutoConstraints() - { - return array( - new Assert\Length( - array( - 'max' => $this->options['max_length'], - ) - ), - ); - } - - /** - * {@inheritDoc} - */ - public function changeQuery($queryBuilder, AbstractFormSearcher $formData, $aliasSearch) - { - $value = $formData->get($this->property); - $parameterName = 'value_jquery_auto' . str_replace(' ', '', $this->property); - if (null === $value || '' === $value || !is_scalar($value)) { - return $queryBuilder; - } - - return $queryBuilder->andWhere(sprintf('%s = :%s', $aliasSearch, $parameterName)) - ->setParameter($parameterName, $value); - } -} diff --git a/Form/Filter/FieldFilterNumber.php b/Form/Filter/FieldFilterNumber.php deleted file mode 100644 index 5de370a..0000000 --- a/Form/Filter/FieldFilterNumber.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Form\Filter; - -use Symfony\Component\Form\Extension\Core\Type\NumberType; -use Symfony\Component\Form\FormBuilder; - -class FieldFilterNumber extends FieldFilterInteger -{ - /** - * {@inheritDoc} - */ - public function addField(FormBuilder $formBuilder) - { - $formBuilder->add($this->property, NumberType::class, $this->typeOptions); - - return $formBuilder; - } -} diff --git a/Form/Filter/FieldFilterSelect2Choice.php b/Form/Filter/FieldFilterSelect2Choice.php deleted file mode 100644 index af909b9..0000000 --- a/Form/Filter/FieldFilterSelect2Choice.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Form\Filter; - -use Ecommit\JavascriptBundle\Form\Type\Select2\Select2ChoiceType; -use Symfony\Component\Form\FormBuilder; - -class FieldFilterSelect2Choice extends FieldFilterChoice -{ - /** - * {@inheritDoc} - */ - public function addField(FormBuilder $formBuilder) - { - $formBuilder->add($this->property, Select2ChoiceType::class , $this->typeOptions); - - return $formBuilder; - } -} diff --git a/Form/Filter/FieldFilterSelect2Country.php b/Form/Filter/FieldFilterSelect2Country.php deleted file mode 100644 index b57932a..0000000 --- a/Form/Filter/FieldFilterSelect2Country.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Form\Filter; - -use Ecommit\JavascriptBundle\Form\Type\Select2\Select2CountryType; -use Symfony\Component\Form\FormBuilder; - -class FieldFilterSelect2Country extends FieldFilterChoice -{ - /** - * {@inheritDoc} - */ - public function addField(FormBuilder $formBuilder) - { - $formBuilder->add($this->property, Select2CountryType::class , $this->typeOptions); - - return $formBuilder; - } -} diff --git a/Form/Filter/FieldFilterSelect2Entity.php b/Form/Filter/FieldFilterSelect2Entity.php deleted file mode 100644 index 05ef219..0000000 --- a/Form/Filter/FieldFilterSelect2Entity.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Form\Filter; - -use Ecommit\JavascriptBundle\Form\Type\Select2\Select2ChoiceType; -use Symfony\Component\Form\FormBuilder; - -class FieldFilterSelect2Entity extends FieldFilterEntity -{ - /** - * {@inheritDoc} - */ - public function addField(FormBuilder $formBuilder) - { - $formBuilder->add($this->property, Select2ChoiceType::class, $this->typeOptions); - - return $formBuilder; - } -} diff --git a/Form/Filter/FieldFilterSelect2EntityAjax.php b/Form/Filter/FieldFilterSelect2EntityAjax.php deleted file mode 100644 index 0b3af32..0000000 --- a/Form/Filter/FieldFilterSelect2EntityAjax.php +++ /dev/null @@ -1,78 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Form\Filter; - -use Ecommit\JavascriptBundle\Form\Type\Select2\Select2EntityAjaxType; -use Symfony\Component\Form\FormBuilder; -use Symfony\Component\OptionsResolver\Options; -use Symfony\Component\OptionsResolver\OptionsResolver; - -class FieldFilterSelect2EntityAjax extends FieldFilterChoice -{ - /** - * {@inheritDoc} - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults( - array( - 'em' => null, - 'query_builder' => null, - 'property' => null, // deprecated since 2.2, use "choice_label" - 'choice_label' => function (Options $options) { - // BC with the "property" option - if ($options['property']) { - trigger_error('The "property" option is deprecated since version 2.2. Use "choice_label" instead.', E_USER_DEPRECATED); - - return $options['property']; - } - - return null; - }, - 'identifier' => null, - 'url' => null, //Required in FormType if route_name is empty - 'route_name' => null, - 'route_params' => null, - ) - ); - - $resolver->setRequired( - array( - 'class', - ) - ); - } - - /** - * {@inheritDoc} - */ - protected function configureTypeOptions($typeOptions) - { - foreach ($this->options as $optionName => $optionValue) { - if (!empty($optionValue) && !in_array($optionName, array('validate', 'min'))) { - $typeOptions[$optionName] = $optionValue; - } - } - $typeOptions['input'] = 'key'; - - return $typeOptions; - } - - /** - * {@inheritDoc} - */ - public function addField(FormBuilder $formBuilder) - { - $formBuilder->add($this->property, Select2EntityAjaxType::class, $this->typeOptions); - - return $formBuilder; - } -} diff --git a/Form/Filter/FieldFilterText.php b/Form/Filter/FieldFilterText.php deleted file mode 100644 index 8dfd7b3..0000000 --- a/Form/Filter/FieldFilterText.php +++ /dev/null @@ -1,89 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Form\Filter; - -use Ecommit\CrudBundle\Form\Searcher\AbstractFormSearcher; -use Symfony\Component\Form\Extension\Core\Type\TextType; -use Symfony\Component\Form\FormBuilder; -use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints as Assert; - -class FieldFilterText extends AbstractFieldFilter -{ - /** - * {@inheritDoc} - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults( - array( - 'must_begin' => false, - 'must_end' => false, - 'min_length' => null, - 'max_length' => 255, - ) - ); - } - - /** - * {@inheritDoc} - */ - public function addField(FormBuilder $formBuilder) - { - $formBuilder->add($this->property, TextType::class, $this->typeOptions); - - return $formBuilder; - } - - /** - * {@inheritDoc} - */ - protected function getAutoConstraints() - { - return array( - new Assert\Length( - array( - 'min' => $this->options['min_length'], - 'max' => $this->options['max_length'], - ) - ), - ); - } - - /** - * {@inheritDoc} - */ - public function changeQuery($queryBuilder, AbstractFormSearcher $formData, $aliasSearch) - { - $value = $formData->get($this->property); - $parameterName = 'value_text_' . str_replace(' ', '', $this->property); - if (null === $value || '' === $value || !is_scalar($value)) { - return $queryBuilder; - } - - if ($this->options['must_begin'] && $this->options['must_end']) { - $queryBuilder->andWhere(sprintf('%s = :%s', $aliasSearch, $parameterName)) - ->setParameter($parameterName, $value); - } else { - $after = ($this->options['must_begin']) ? '' : '%'; - $before = ($this->options['must_end']) ? '' : '%'; - $value = addcslashes($value, '%_'); - $like = $after . $value . $before; - $queryBuilder->andWhere( - $queryBuilder->expr()->like($aliasSearch, ':' . $parameterName) - ) - ->setParameter($parameterName, $like); - } - - return $queryBuilder; - } -} diff --git a/Form/Filter/FieldFilterTokenInputEntitiesAjax.php b/Form/Filter/FieldFilterTokenInputEntitiesAjax.php deleted file mode 100644 index 99edf83..0000000 --- a/Form/Filter/FieldFilterTokenInputEntitiesAjax.php +++ /dev/null @@ -1,123 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Form\Filter; - -use Ecommit\CrudBundle\Form\Searcher\AbstractFormSearcher; -use Ecommit\JavascriptBundle\Form\Type\TokenInputEntitiesAjaxType; -use Ecommit\ScalarValues\ScalarValues; -use Symfony\Component\Form\FormBuilder; -use Symfony\Component\OptionsResolver\Options; -use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints as Assert; - -class FieldFilterTokenInputEntitiesAjax extends AbstractFieldFilter -{ - /** - * {@inheritDoc} - */ - protected function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults( - array( - 'em' => null, - 'query_builder' => null, - 'property' => null, // deprecated since 2.2, use "choice_label" - 'choice_label' => function (Options $options) { - // BC with the "property" option - if ($options['property']) { - trigger_error('The "property" option is deprecated since version 2.2. Use "choice_label" instead.', E_USER_DEPRECATED); - - return $options['property']; - } - - return null; - }, - 'identifier' => null, - 'url' => null, //Required in FormType if route_name is empty - 'route_name' => null, - 'route_params' => null, - 'min' => null, - 'max' => 99, - ) - ); - - $resolver->setRequired( - array( - 'class', - ) - ); - } - - /** - * {@inheritDoc} - */ - protected function configureTypeOptions($typeOptions) - { - foreach ($this->options as $optionName => $optionValue) { - if (!empty($optionValue) && !in_array($optionName, array('validate', 'min'))) { - $typeOptions[$optionName] = $optionValue; - } - } - $typeOptions['input'] = 'key'; - - return $typeOptions; - } - - /** - * {@inheritDoc} - */ - public function addField(FormBuilder $formBuilder) - { - $formBuilder->add($this->property, TokenInputEntitiesAjaxType::class, $this->typeOptions); - - return $formBuilder; - } - - /** - * {@inheritDoc} - */ - protected function getAutoConstraints() - { - return array( - new Assert\Count( - array( - 'min' => $this->options['min'], - 'max' => $this->options['max'], - ) - ), - ); - } - - /** - * {@inheritDoc} - */ - public function changeQuery($queryBuilder, AbstractFormSearcher $formData, $aliasSearch) - { - $value = $formData->get($this->property); - $parameterName = 'value_choice' . str_replace(' ', '', $this->property); - if (null === $value || '' === $value || !is_array($value)) { - return $queryBuilder; - } - $value = ScalarValues::filterScalarValues($value); - if (0 === count($value)) { - return $queryBuilder; - } - - if (count($value) > $this->options['max']) { - return $queryBuilder; - } - if ($this->options['min'] && count($value) < $this->options['min']) { - return $queryBuilder; - } - return $queryBuilder->andWhere($queryBuilder->expr()->in($aliasSearch, ':' . $parameterName)) - ->setParameter($parameterName, $value); - } -} diff --git a/Form/Searcher/AbstractFormSearcher.php b/Form/Searcher/AbstractFormSearcher.php deleted file mode 100644 index fc22029..0000000 --- a/Form/Searcher/AbstractFormSearcher.php +++ /dev/null @@ -1,202 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Form\Searcher; - -use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\PropertyAccess\Exception\AccessException; -use Symfony\Component\PropertyAccess\PropertyAccess; -use Symfony\Component\PropertyAccess\PropertyAccessor; -use Symfony\Component\Validator\Constraints\Callback; -use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Mapping\ClassMetadata; - -abstract class AbstractFormSearcher -{ - protected $fieldFilters; - - /** - * @var PropertyAccessor - */ - protected $accessor; - - protected $commonOptions = array(); - - public $isSubmitted = false; - - /** - * Declares fields - * - * @return array - */ - public function configureFieldsFilter() - { - return array(); - } - - /** - * Gets field value - * - * @param string $field Field Name - * @return mixed - */ - public function get($field) - { - try { - $value = $this->getAccessor()->getValue($this, $field); - } catch (AccessException $e) { - $value = null; - } - - return $value; - } - - /** - * Clears this objet - * Used before storing this object in session - * By default, If one property is not public and it doesn't begin - * by "field", it will be deleted - */ - public function clear() - { - unset($this->fieldFilters); - unset($this->accessor); - $this->clearExceptFields(); - } - - /** - * Sub method of "clear" method - * Remove properties if they are not fields - * If one property is not public and it doesn't begin - * by "field", it will be deleted - */ - protected function clearExceptFields() - { - foreach ($this as $key => $value) { - $variable = new \ReflectionProperty($this, $key); - if (!$variable->isPublic() && !\preg_match('/^field/', $key)) { - unset($this->$key); - } - } - } - - /** - * Returns fields - * - * @return array - */ - public function getFieldsFilter($registry = null) - { - if (!$this->fieldFilters) { - $this->fieldFilters = array(); - foreach ($this->configureFieldsFilter() as $field) { - $this->fieldFilters[] = $field; - if (!empty($registry) && $field instanceof \Ecommit\CrudBundle\Form\Filter\FieldFilterDoctrineInterface) { - $field->setRegistry($registry); - } - $field->init(); - } - } - - return $this->fieldFilters; - } - - /** - * Sets fields - * - * @param array $filters - */ - public function setFieldsFilter(array $filters) - { - $this->fieldFilters = $filters; - } - - /** - * Changes the form (global change) - * - * @param \Symfony\Component\Form\FormBuilderInterface $formBuilder - * @return \Symfony\Component\Form\FormBuilderInterface - */ - public function globalBuildForm(FormBuilderInterface $formBuilder) - { - return $formBuilder; - } - - /** - * Changes the query (global change) - * - * @param QueryBuilder $queryBuilder - * @return QueryBuilder - */ - public function globalChangeQuery($queryBuilder) - { - return $queryBuilder; - } - - /** - * Returns true if auto validation is enabled - * @return bool - */ - public function automaticValidationIsEnabled() - { - return true; - } - - /** - * Returns true if labels are displayed in errors messages - * @return bool - */ - public function displayLabelInErrors() - { - return false; - } - - public static function loadValidatorMetadata(ClassMetadata $metadata) - { - $metadata->addConstraint(new Callback('validateFormSearcher')); - } - - public static function validateFormSearcher($value, ExecutionContextInterface $context) - { - if ($value->automaticValidationIsEnabled()) { - foreach ($value->getFieldsFilter() as $field) { - $field->autoValidate($value, $context); - } - } - } - - /** - * @return PropertyAccessor - */ - protected function getAccessor() - { - if (!isset($this->accessor) || !$this->accessor) { - $this->accessor = PropertyAccess::createPropertyAccessor(); - } - return $this->accessor; - } - - /** - * @return array - */ - public function getCommonOptions() - { - return $this->commonOptions; - } - - /** - * @param array $commonOptions - */ - public function setCommonOptions($commonOptions) - { - $this->commonOptions = $commonOptions; - } -} diff --git a/Form/Type/DisplaySettingsType.php b/Form/Type/DisplaySettingsType.php deleted file mode 100644 index ce314d1..0000000 --- a/Form/Type/DisplaySettingsType.php +++ /dev/null @@ -1,76 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Form\Type; - -use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; -use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\OptionsResolver\OptionsResolver; - -class DisplaySettingsType extends AbstractType -{ - /** - * {@inheritDoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) - { - //Field "resultsPerPage" - $builder->add( - 'resultsPerPage', - ChoiceType::class, - array( - 'choices' => array_flip($options['resultsPerPageChoices']), - 'label' => 'Number of results per page', - 'choice_translation_domain' => false, - ) - ); - - //Field "displayedColumns" - $builder->add( - 'displayedColumns', - ChoiceType::class, - array( - 'choices' => array_flip($options['columnsChoices']), - 'multiple' => true, - 'expanded' => true, - 'label' => 'Columns to be shown' - ) - ); - } - - /** - * {@inheritDoc} - */ - public function configureOptions(OptionsResolver $resolver) - { - $resolver->setDefaults( - array( - 'csrf_protection' => false, - ) - ); - - $resolver->setRequired( - array( - 'resultsPerPageChoices', - 'columnsChoices', - ) - ); - } - - /** - * {@inheritDoc} - */ - public function getBlockPrefix() - { - return 'crud_display_settings'; - } -} diff --git a/Helper/CrudHelper.php b/Helper/CrudHelper.php deleted file mode 100644 index 41024e3..0000000 --- a/Helper/CrudHelper.php +++ /dev/null @@ -1,712 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Helper; - -use Ecommit\CrudBundle\Crud\Crud; -use Ecommit\CrudBundle\Form\Type\DisplaySettingsType; -use Ecommit\CrudBundle\Paginator\AbstractPaginator; -use Ecommit\JavascriptBundle\Helper\JqueryHelper; -use Ecommit\JavascriptBundle\Overlay\AbstractOverlay; -use Ecommit\UtilBundle\Helper\UtilHelper; -use Symfony\Component\Form\FormFactoryInterface; -use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Routing\RouterInterface; -use Symfony\Contracts\Translation\TranslatorInterface; -use Twig\Markup; -use Twig_Environment; - -class CrudHelper -{ - /** - * @var UtilHelper - */ - protected $util; - - /** - * @var JqueryHelper - */ - protected $javascriptManager; - - /** - * @var FormFactoryInterface - */ - protected $formFactory; - - /** - * @var RouterInterface - */ - protected $router; - - /** - * @var TranslatorInterface - */ - protected $translator; - - /** - * @var Twig_Environment - */ - protected $templating; - - /** - * @var AbstractOverlay - */ - protected $overlay; - - /** - * @var bool - */ - protected $useBootstrap; - - /** - * @var array - */ - protected $parameters; - - /** - * @var array - */ - protected $lastValues = array(); - - /** - * Constructor - * - * @param UtilHelper $util - * @param Manager $javascriptManager - * @param FormFactoryInterface $formFactory - * @param bool $useBootstrap - */ - public function __construct( - UtilHelper $util, - JqueryHelper $javascriptManager, - FormFactoryInterface $formFactory, - RouterInterface $router, - TranslatorInterface $translator, - Twig_Environment $templating, - AbstractOverlay $overlay, - $parameters - ) { - $this->util = $util; - $this->javascriptManager = $javascriptManager; - $this->formFactory = $formFactory; - $this->router = $router; - $this->translator = $translator; - $this->templating = $templating; - $this->overlay = $overlay; - $this->parameters = $parameters; - $this->useBootstrap = $parameters['use_bootstrap']; - } - - /** - * @return bool - */ - public function useBootstrap() - { - return $this->useBootstrap; - } - - /** - * @return AbstractOverlay - */ - public function getOverlayService() - { - return $this->overlay; - } - - /** - * Returns links paginator - * - * @param AbstractPaginator $paginator - * @param string $routeName Route name - * @param array $routeParams Route parameters - * @param array $options Options: - * * ajax_options: Ajax Options. If null, Ajax is not used. Default: null - * * attribute_page: Attribute inside url. Default: page - * * type: Type of links paginator: elastic (all links) or sliding. Default: sliding - * * max_pages_before: Max links before current page (only if sliding type is used). Default: 3 - * * max_pages_after: Max links after current page (only if sliding type is used). Default: 3 - * * buttons: Type of buttons: text or image. Default: text - * * image_first: Url image "<<" (only if image buttons is used) - * * image_previous: Url image "<" (only if image buttons is used) - * * image_next: Url image ">" (only if image buttons is used) - * * image_last: Url image ">>" (only if image buttons is used) - * * text_first: Text "<<" (only if text buttons is used) - * * text_previous: Text "<" (only if text buttons is used) - * * text_next: Text ">" (only if text buttons is used) - * * text_last: Text ">>" (only if text buttons is used) - * * use_bootstrap: Use or not bootstap - * * bootstrap_size: Bootstrap nav size (lg sm or null) - * * template: Template used. If null, default template is used - * @return string - */ - public function paginatorLinks(AbstractPaginator $paginator, $routeName, $routeParams, $options) - { - if (isset($this->parameters['template_configuration']['paginator_links'])) { - $options = array_merge($this->parameters['template_configuration']['paginator_links'], $options); - } - $resolver = new OptionsResolver(); - $resolver->setDefaults( - array( - 'ajax_options' => null, - 'attribute_page' => 'page', - 'type' => 'sliding', - 'max_pages_before' => 3, - 'max_pages_after' => 3, - 'buttons' => 'text', - 'image_first' => '/bundles/ecommitcrud/images/i16/resultset_first.png', - 'image_previous' => '/bundles/ecommitcrud/images/i16/resultset_previous.png', - 'image_next' => '/bundles/ecommitcrud/images/i16/resultset_next.png', - 'image_last' => '/bundles/ecommitcrud/images/i16/resultset_last.png', - 'text_first' => '«', - 'text_previous' => '‹', - 'text_next' => '›', - 'text_last' => '»', - 'use_bootstrap' => $this->useBootstrap, - 'bootstrap_size' => null, - 'template' => null, - ) - ); - $resolver->setAllowedTypes('ajax_options', array('null', 'array')); - $resolver->setAllowedTypes('max_pages_before', 'int'); - $resolver->setAllowedTypes('max_pages_after', 'int'); - $resolver->setAllowedValues('type', array('sliding', 'elastic')); - $resolver->setAllowedValues('buttons', array('text', 'image')); - $resolver->setAllowedTypes('use_bootstrap', 'bool'); - $resolver->setAllowedValues('bootstrap_size', array('lg', 'sm', null)); - $options = $resolver->resolve($options); - - if ($options['template']) { - return $this->templating->render( - $options['template'], - array( - 'paginator' => $paginator, - 'routeName' => $routeName, - 'routeParams' => $routeParams, - 'options' => $options, - ) - ); - } - - $navigation = ''; - if ($paginator->haveToPaginate()) { - $navigationClass = $options['use_bootstrap']? 'pagination': 'pagination_nobootstrap'; - if ($options['use_bootstrap'] && $options['bootstrap_size']) { - $navigationClass .= ' pagination-'.$options['bootstrap_size']; - } - $navigation .= \sprintf(''; - } - - return $navigation; - } - - /** - * Paginator links: Display one element - * - * @param int $page Page number - * @param array $options Options - * @param string $elementName first, previous, page, next or last - * @param string $routeName - * @param array $routeParams - * @param bool $current If element is the current page - */ - protected function elementPaginatorLinks( - $page, - $options, - $elementName, - $routeName, - $routeParams, - $current = false - ) { - $url = $this->router->generate( - $routeName, - \array_merge($routeParams, array($options['attribute_page'] => $page)) - ); - if ($elementName == 'page') { - if ($current) { - $class = ($options['use_bootstrap'])? 'active': 'pagination_current'; - if ($options['use_bootstrap']) { - $content = $this->listePrivateLink($page, '', array('onclick' => 'return false;')); //bootstrap expects link - } else { - $content = $page; - } - } else { - $class = ($options['use_bootstrap'])? '': 'pagination_no_current'; - $content = $this->listePrivateLink($page, $url, array(), $options['ajax_options']); - } - } else { - $class = $options['buttons'] . ' ' . $elementName; - $buttonName = $options['buttons'] . '_' . $elementName; - $button = $options[$buttonName]; - if ($options['buttons'] == 'text' || $options['use_bootstrap']) { - $content = $this->listePrivateLink($button, $url, array(), $options['ajax_options']); - } else { - $image = $this->util->tag('img', array('src' => $button, 'alt' => $elementName)); - $content = $this->listePrivateLink($image, $url, array(), $options['ajax_options']); - } - } - - return \sprintf('
  • %s
  • ', $class, $content); - } - - /** - * Returns CRUD links paginator - * - * @param Crud $crud - * @param array $options Options. See CrudHelper::paginatorLinks (ajax_options is ignored) - * @param array $ajaxOptions Ajax Options - * @return string - */ - public function crudPaginatorLinks(Crud $crud, $options, $ajaxOptions) - { - $options = array_merge($crud->getTemplateConfiguration('crud_paginator_links'), $options); - if (!isset($ajaxOptions['update'])) { - $ajaxOptions['update'] = $crud->getDivIdList(); - } - $options['ajax_options'] = $ajaxOptions; - - return $this->paginatorLinks($crud->getPaginator(), $crud->getRouteName(), $crud->getRouteParams(), $options); - } - - /** - * Returns one colunm, inside "header" CRUD - * - * @param string $column_id Column id - * @param Crud $crud - * @param array $options Options : - * * label: Label. If null, default label is displayed - * * image_up: Url image "^" - * * image_down: Url image "V" - * * template: Template used. If null, default template is used - * @param array $thOptions Html options - * @param array $ajaxOptions Ajax Options - * @return string - */ - public function th($column_id, Crud $crud, $options, $thOptions, $ajaxOptions) - { - $options = array_merge($crud->getTemplateConfiguration('crud_th'), $options); - $resolver = new OptionsResolver(); - $resolver->setDefaults( - array( - 'label' => null, - 'image_up' => $this->parameters['images']['th_image_up'], - 'image_down' => $this->parameters['images']['th_image_down'], - 'template' => null, - ) - ); - $options = $resolver->resolve($options); - - if ($options['template']) { - return $this->templating->render( - $options['template'], - array( - 'column_id' => $column_id, - 'crud' => $crud, - 'th_options' => $thOptions, - 'ajax_options' => $ajaxOptions, - ) - ); - } - - if (!isset($ajaxOptions['update'])) { - $ajaxOptions['update'] = $crud->getDivIdList(); - } - $image_up = $options['image_up']; - $image_down = $options['image_down']; - - //If the column is not to be shown, returns empty - $session_values = $crud->getSessionValues(); - if (!\in_array($column_id, $session_values->displayedColumns)) { - return ''; - } - - //If the label was not defined, we take default label - $column = $crud->getColumn($column_id); - $label = $options['label']; - if (\is_null($label)) { - $label = $column->label; - } - //I18N label - $label = $this->translator->trans($label); - //XSS protection - $label = \htmlentities($label, ENT_QUOTES, 'UTF-8'); - - //Case n°1: We cannot sort this column, we just show the label - if (!$column->sortable) { - return $this->util->tag('th', $thOptions, $label); - } - - //Case n°2: We can sort on this column, but the sorting is not active on her at present - if ($session_values->sort != $column_id) { - $content = $this->listePrivateLink( - $label, - $crud->getUrl(array('sort' => $column_id)), - array(), - $ajaxOptions - ); - - return $this->util->tag('th', $thOptions, $content); - } - - //Case n°3: We can sort on this column, and the sorting is active on her at present - $image_src = ($session_values->sense == Crud::ASC) ? $image_up : $image_down; - $image_alt = ($session_values->sense == Crud::ASC) ? '^' : 'V'; - $new_sense = ($session_values->sense == Crud::ASC) ? Crud::DESC : Crud::ASC; - $image = $this->util->tag('img', array('src' => $image_src, 'alt' => $image_alt)); - $link = $this->listePrivateLink($label, $crud->getUrl(array('sense' => $new_sense)), array(), $ajaxOptions); - - return $this->util->tag('th', $thOptions, $link . $image); - } - - /** - * Returns one colunm, inside "body" CRUD - * - * @param string $column_id Column id - * @param Crud $crud - * @param string $value Value - * @param array $options Options : - * * escape: Escape (or not) the value - * * template: Template used. If null, default template is used - * @param array $tdOptions Html options - * @return string - */ - public function td($column_id, Crud $crud, $value, $options, $tdOptions) - { - $options = array_merge($crud->getTemplateConfiguration('crud_td'), $options); - $resolver = new OptionsResolver(); - $resolver->setDefaults( - array( - 'escape' => true, - 'template' => null, - 'repeated_values_string' => null, - 'repeated_values_add_title' => true, - ) - ); - $options = $resolver->resolve($options); - - if ($options['template']) { - return $this->templating->render( - $options['template'], - array( - 'column_id' => $column_id, - 'crud' => $crud, - 'value' => $value, - 'options' => $options, - 'td_options' => $tdOptions, - ) - ); - } - - //If the column is not to be shown, returns empty - $session_values = $crud->getSessionValues(); - if (!\in_array($column_id, $session_values->displayedColumns)) { - return ''; - } - - //Repeated values - if (null !== $options['repeated_values_string']) { - if ($value instanceof Markup) { - $value = $value->__toString(); - } - if (null === $value) { - $value = ''; - } - - if (isset($this->lastValues[$column_id]) && $this->lastValues[$column_id] === $value) { - if ('' !== $value) { - if ($options['repeated_values_add_title']) { - $tdOptions['title'] = $value; - } - $value = $options['repeated_values_string']; - } - } else { - $this->lastValues[$column_id] = $value; - } - } - - //XSS protection - if ($options['escape']) { - $value = \htmlentities($value, ENT_QUOTES, 'UTF-8'); - } - - return $this->util->tag('td', $tdOptions, $value); - } - - /** - * Returns "Display Settings" form - * - * @param Crud $crud - * @return FormView - */ - public function getFormDisplaySettings(Crud $crud) - { - $form_name = sprintf('crud_display_settings_%s', $crud->getSessionName()); - $resultsPerPageChoices = array(); - foreach ($crud->getAvailableResultsPerPage() as $number) { - $resultsPerPageChoices[$number] = $number; - } - $columnsChoices = array(); - foreach ($crud->getColumns() as $column) { - $columnsChoices[$column->id] = $column->label; - } - $data = array( - 'resultsPerPage' => $crud->getSessionValues()->resultsPerPage, - 'displayedColumns' => $crud->getSessionValues()->displayedColumns, - ); - - $form = $this->formFactory->createNamed( - $form_name, - DisplaySettingsType::class, - $data, - array( - 'resultsPerPageChoices' => $resultsPerPageChoices, - 'columnsChoices' => $columnsChoices, - 'action' => $crud->getUrl(), - ) - ); - - return $form->createView(); - } - - /** - * Returns search form tag - * - * @param Crud $crud - * @param array $ajaxOptions Ajax Options - * @param type $htmlOptions Html options - * @return string - */ - public function searchFormTag(Crud $crud, $ajaxOptions, $htmlOptions) - { - if (!isset($ajaxOptions['update'])) { - $ajaxOptions['update'] = 'js_holder_for_multi_update_' . $crud->getSessionName(); - } - - if (!isset($htmlOptions['novalidate'])) { - $htmlOptions['novalidate'] = 'novalidate'; - } - - return $this->javascriptManager->jQueryFormToRemote($crud->getSearcherForm(), $ajaxOptions, $htmlOptions); - } - - /** - * Returns search reset button - * - * @param Crud $crud - * @param array $options Options : - * * label: Label. Défault: Reset - * * template: Template used. If null, default template is used - * @param array $ajaxOptions Ajax options - * @param array $htmlOptions Html options - * @return string - */ - public function searchResetButton(Crud $crud, $options, $ajaxOptions, $htmlOptions) - { - $options = array_merge($crud->getTemplateConfiguration('crud_search_reset'), $options); - $resolver = new OptionsResolver(); - $resolver->setDefaults( - array( - 'label' => 'Reset', - 'template' => null, - ) - ); - $options = $resolver->resolve($options); - - if ($options['template']) { - return $this->templating->render( - $options['template'], - array( - 'crud' => $crud, - 'options' => $options, - 'ajax_options' => $ajaxOptions, - 'html_options' => $htmlOptions, - ) - ); - } - - if (!isset($ajaxOptions['update'])) { - $ajaxOptions['update'] = 'js_holder_for_multi_update_' . $crud->getSessionName(); - } - if (!isset($htmlOptions['class'])) { - $htmlOptions['class'] = ($this->useBootstrap) ? 'raz-bootstrap btn btn-default btn-sm' : 'raz'; - } - $label = $this->translator->trans($options['label']); - if ($this->useBootstrap) { - $label = ' ' . $label; - } - - return $this->javascriptManager->jQueryButtonToRemote( - $label, - $crud->getSearchUrl(array('raz' => 1)), - $ajaxOptions, - $htmlOptions - ); - } - - /** - * Returns declaration of modal - * - * @param string $modalId Modal id - * @param array $options - * @return string - */ - public function declareModal($modalId, $options = array()) - { - return $this->overlay->declareHtmlModal($modalId, $options); - } - - /** - * Returns JS code to open modal window - * - * @param string $modalId Modal id - * @param string $url Url - * @param array $options Array options - * @param array $ajaxOptions Ajax options - * @return string - */ - public function remoteModal($modalId, $url, $options, $ajaxOptions) - { - if (isset($this->parameters['template_configuration']['crud_remote_modal'])) { - $options = array_merge($this->parameters['template_configuration']['crud_remote_modal'], $options); - } - $modalId = str_replace(' ', '', $modalId); - - $resolver = new OptionsResolver(); - $resolver->setDefaults( - array( - 'js_on_close' => null, - 'close_div_class' => 'overlay-close', - ) - ); - $options = $resolver->resolve($options); - - //Create Callback (Opening window) - $jsModal = "$('#$modalId .contentWrap').html(data); "; - $jsModal .= $this->overlay->declareJavascriptModal($modalId, array('js_on_close' => $options['js_on_close'], 'close_div_class' => $options['close_div_class'])); - $jsModal .= $this->overlay->openModal($modalId); - - //Add callback - if (isset($ajaxOptions['success'])) { - $ajaxOptions['success'] = $jsModal . ' ' . $ajaxOptions['success']; - } else { - $ajaxOptions['success'] = $jsModal; - } - - //Method - if (!isset($ajaxOptions['method'])) { - $ajaxOptions['method'] = 'GET'; - } - - return $this->javascriptManager->jQueryRemoteFunction($url, $ajaxOptions); - } - - /** - * Returns modal form tag - * - * @param string $modalId Modal id - * @param FormView|string $form The form or the url. Url is deprecated since 2.2 version - * @param array $ajaxOptions Ajax options - * @param array $htmlOptions Html options - * @return string - */ - public function formModal($modalId, $form, $ajaxOptions, $htmlOptions) - { - $modalId = str_replace(' ', '', $modalId); - if (!isset($ajaxOptions['update'])) { - $ajaxOptions['update'] = $modalId . ' .contentWrap'; - } - - return $this->javascriptManager->jQueryFormToRemote($form, $ajaxOptions, $htmlOptions); - } - - /** - * Creates a link - * - * @param string $name Name link - * @param string $url Url link - * @param array $linkToOptions Html options - * @param array $ajaxOptions Ajax options (if null, Ajax is disabled) - * @return string - */ - protected function listePrivateLink($name, $url, $linkToOptions = array(), $ajaxOptions = null) - { - if (is_null($ajaxOptions)) { - //No Ajax, Simple link - $linkToOptions['href'] = $url; - - return $this->util->tag('a', $linkToOptions, $name); - } else { - //Ajax Request - return $this->javascriptManager->jQueryLinkToRemote($name, $url, $ajaxOptions, $linkToOptions); - } - } -} diff --git a/LICENSE b/LICENSE index b2a515f..a6b1d0f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014 E-COMMIT +Copyright (c) 2011-present E-COMMIT Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Paginator/AbstractDoctrinePaginator.php b/Paginator/AbstractDoctrinePaginator.php deleted file mode 100644 index 5e770d4..0000000 --- a/Paginator/AbstractDoctrinePaginator.php +++ /dev/null @@ -1,117 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Paginator; - -abstract class AbstractDoctrinePaginator extends AbstractPaginator -{ - protected $query = null; - protected $manualCountResults = null; - protected $countOptions = array(); - - /** - * @return string - */ - abstract protected function getQueryBuilderClass(); - - abstract protected function initPaginator(); - - public function init() - { - if (is_null($this->query)) { - throw new \Exception('QueryBuilder must be defined.'); - } - - $this->initPaginator(); - - $this->initLastPage(); - $this->query->setFirstResult(0); - $this->query->setMaxResults(0); - - if ($this->getCountResults() > 0) { - $offset = ($this->getPage() - 1) * $this->getMaxPerPage(); - $this->query->setFirstResult($offset); - $this->query->setMaxResults($this->getMaxPerPage()); - } - } - - /** - * Returns QueryBuilder - * - * @return mixed - */ - public function getQueryBuilder() - { - return $this->query; - } - - /** - * Sets the QueryBuilder - * - * @param mixed $query - * @return AbstractDoctrinePaginator - */ - public function setQueryBuilder($query) - { - $queryBuilderClass = $this->getQueryBuilderClass(); - if (!($query instanceof $queryBuilderClass)) { - throw new \Exception('QueryBuilder must be an instance of '.$queryBuilderClass); - } - $this->query = $query; - - return $this; - } - - /** - * Returns manual total results - * - * @return Int - */ - public function getManualCountResults() - { - return $this->manualCountResults; - } - - /** - * Sets manual total results - * - * @param Int $manualCountResults - * @return AbstractDoctrinePaginator - */ - public function setManualCountResults($manualCountResults) - { - $this->manualCountResults = $manualCountResults; - - return $this; - } - - /** - * Returns count options - * - * @return array - */ - public function getCountOptions() - { - return $this->countOptions; - } - - /** - * Sets count options - * - * @param array $countOptions - * @return AbstractDoctrinePaginator - */ - public function setCountOptions(array $countOptions) - { - $this->countOptions = $countOptions; - - return $this; - } -} diff --git a/Paginator/AbstractPaginator.php b/Paginator/AbstractPaginator.php deleted file mode 100644 index 07624b9..0000000 --- a/Paginator/AbstractPaginator.php +++ /dev/null @@ -1,265 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Paginator; - -abstract class AbstractPaginator implements \IteratorAggregate, \Countable -{ - - protected $page = 1; - protected $maxPerPage = 1; - protected $lastPage = 1; - protected $countResults = 0; - protected $results = null; - - /** - * Constructor. - * - * @param string $class The model class - * @param integer $maxPerPage Number of records to display per page - * @return AbstractPaginator - */ - public function __construct($maxPerPage = 10) - { - $this->setMaxPerPage($maxPerPage); - - return $this; - } - - /** - * Initializes the pager. - */ - abstract public function init(); - - /** - * Returns an array of results on the given page. - * - * @return \ArrayIterator - */ - abstract public function getResults(); - - /** - * Returns true if the current query requires pagination. - * - * @return boolean - */ - public function haveToPaginate() - { - return $this->getCountResults() > $this->getMaxPerPage(); - } - - /** - * Returns the first index on the current page. - * - * @return integer - */ - public function getFirstIndice() - { - if ($this->getCountResults() == 0) { - return 0; - } - - return ($this->page - 1) * $this->maxPerPage + 1; - } - - /** - * Returns the last index on the current page. - * - * @return integer - */ - public function getLastIndice() - { - if ($this->page * $this->maxPerPage >= $this->countResults) { - return $this->countResults; - } else { - return $this->page * $this->maxPerPage; - } - } - - /** - * Returns the number of results. - * - * @return integer - */ - public function getCountResults() - { - return $this->countResults; - } - - /** - * Sets the number of results. - * - * @param integer $nb - */ - protected function setCountResults($nb) - { - $this->countResults = $nb; - } - - /** - * Returns the first page number. - * - * @return integer - */ - public function getFirstPage() - { - return 1; - } - - /** - * Returns the last page number. - * - * @return integer - */ - public function getLastPage() - { - return $this->lastPage; - } - - /** - * Init the last page number. - * - */ - protected function initLastPage() - { - if ($this->getCountResults() > 0) { - $lastPage = (int) \ceil($this->getCountResults() / $this->getMaxPerPage()); - } else { - $lastPage = 1; - } - $this->lastPage = $lastPage; - - if ($this->getPage() > $lastPage) { - $this->setPage($lastPage); - } - } - - /** - * Returns the current page. - * - * @return integer - */ - public function getPage() - { - return $this->page; - } - - /** - * Returns the next page. - * - * @return integer - */ - public function getNextPage() - { - return min($this->getPage() + 1, $this->getLastPage()); - } - - /** - * Returns the previous page. - * - * @return integer - */ - public function getPreviousPage() - { - return max($this->getPage() - 1, $this->getFirstPage()); - } - - /** - * Sets the current page. - * - * @param integer $page - * @return AbstractPaginator - */ - public function setPage($page) - { - $this->page = intval($page); - - if ($this->page <= 0) { - $this->page = 1; - } - - return $this; - } - - /** - * Returns the maximum number of results per page. - * - * @return integer - */ - public function getMaxPerPage() - { - return $this->maxPerPage; - } - - /** - * Sets the maximum number of results per page. - * - * @param integer $max - * @return AbstractPaginator - */ - public function setMaxPerPage($max) - { - $max = intval($max); - - if ($max <= 0) { - throw new \Exception('Max results value must be positive'); - } - $this->maxPerPage = $max; - - return $this; - } - - /** - * Returns true if on the first page. - * - * @return boolean - */ - public function isFirstPage() - { - return 1 == $this->page; - } - - /** - * Returns true if on the last page. - * - * @return boolean - */ - public function isLastPage() - { - return $this->page == $this->lastPage; - } - - /** - * Returns true if the properties used for iteration have been initialized. - * - * @return boolean - */ - protected function isIteratorInitialized() - { - return null !== $this->results; - } - - /** - * {@inheritdoc} - */ - public function count() - { - return $this->getCountResults(); - } - - /** - * {@inheritdoc} - */ - public function getIterator() - { - return $this->getResults(); - } -} diff --git a/Paginator/ArrayPaginator.php b/Paginator/ArrayPaginator.php deleted file mode 100644 index 42843b0..0000000 --- a/Paginator/ArrayPaginator.php +++ /dev/null @@ -1,118 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Paginator; - -class ArrayPaginator extends AbstractPaginator -{ - - protected $initialObjects = null; - protected $manualCountResults = null; - - /** - * {@inheritDoc} - */ - public function init() - { - if ($this->initialObjects === null) { - throw new \Exception('Results are required'); - } - - if (is_null($this->manualCountResults)) { - $this->setCountResults(\count($this->initialObjects)); - $this->initLastPage(); - - $offset = 0; - $limit = 0; - - if ($this->getCountResults() > 0) { - $offset = ($this->getPage() - 1) * $this->getMaxPerPage(); - $limit = $this->getMaxPerPage(); - } - - $this->results = \array_slice($this->initialObjects, $offset, $limit); - } else { - $this->setCountResults($this->manualCountResults); - $this->initLastPage(); - - $this->results = $this->initialObjects; - } - } - - /** - * Set an array of results - * - * @param array|ArrayIterator $results - * @return ArrayPaginator - * @deprecated Deprecated since version 2.2. Use setData method instead. - */ - public function setResults($results) - { - trigger_error('setResults is deprecated since 2.2 version. Use setData instead', E_USER_DEPRECATED); - - return $this->setData($results); - } - - /** - * Set an array of results without slice - * - * @param array|ArrayIterator $results - * @param Int $manualCountResults - * @deprecated Deprecated since version 2.2. Use setDataWithoutSlice method instead. - */ - public function setResultsWithoutSlice($results, $manualCountResults) - { - trigger_error('setResultsWithoutSlice is deprecated since 2.2 version. Use setDataWithoutSlice instead', E_USER_DEPRECATED); - - return $this->setDataWithoutSlice($results, $manualCountResults); - } - - /** - * Set an array of results - * - * @param array|ArrayIterator $results - * @return ArrayPaginator - */ - public function setData($results) - { - if ($results instanceof \ArrayIterator) { - $this->initialObjects = $results->getArrayCopy(); - } elseif (is_array($results)) { - $this->initialObjects = $results; - } else { - throw new \Exception('Results must be an array'); - } - - return $this; - } - - /** - * Set an array of results without slice - * - * @param array|ArrayIterator $results - * @param Int $manualCountResults - */ - public function setDataWithoutSlice($results, $manualCountResults) - { - $this->setData($results); - $this->manualCountResults = $manualCountResults; - - return $this; - } - - /** - * {@inheritDoc} - */ - public function getResults() - { - return new \ArrayIterator($this->results); - } -} diff --git a/Paginator/DoctrineDBALPaginator.php b/Paginator/DoctrineDBALPaginator.php deleted file mode 100644 index 3b07fa9..0000000 --- a/Paginator/DoctrineDBALPaginator.php +++ /dev/null @@ -1,66 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Paginator; - -use Ecommit\CrudBundle\DoctrineExtension\Paginate; - -class DoctrineDBALPaginator extends AbstractDoctrinePaginator -{ - /** - * {@inheritDoc} - */ - protected function getQueryBuilderClass() - { - return 'Doctrine\DBAL\Query\QueryBuilder'; - } - - /** - * {@inheritDoc} - */ - public function initPaginator() - { - //Calculation of the number of lines - if (is_null($this->manualCountResults)) { - $count = Paginate::countQueryBuilder($this->query, $this->countOptions); - $this->setCountResults($count); - } else { - $this->setCountResults($this->manualCountResults); - } - } - - /** - * Sets the QueryBuilder - * - * @param mixed $query - * @return DoctrineDBALPaginator - * @deprecated Deprecated since version 2.2. Use setQueryBuilder method instead. - */ - public function setDbalQueryBuilder($query) - { - trigger_error('setDbalQueryBuilder is deprecated since 2.2 version. Use setQueryBuilder instead', E_USER_DEPRECATED); - - return $this->setQueryBuilder($query); - } - - /** - * {@inheritDoc} - */ - public function getResults() - { - if (is_null($this->results)) { - $results = $this->query->execute()->fetchAll(); - $this->results = new \ArrayIterator($results); - } - - return $this->results; - } -} diff --git a/Paginator/DoctrineORMPaginator.php b/Paginator/DoctrineORMPaginator.php deleted file mode 100644 index 07fef2b..0000000 --- a/Paginator/DoctrineORMPaginator.php +++ /dev/null @@ -1,107 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Paginator; - -use Doctrine\ORM\Tools\Pagination\Paginator; -use Ecommit\CrudBundle\DoctrineExtension\Paginate; - -class DoctrineORMPaginator extends AbstractDoctrinePaginator -{ - protected $simplifiedRequest = true; - protected $fetchJoinCollection = false; - - /** - * {@inheritDoc} - */ - protected function getQueryBuilderClass() - { - return 'Doctrine\ORM\QueryBuilder'; - } - - /** - * {@inheritDoc} - */ - public function initPaginator() - { - //Calculation of the number of lines - if (is_null($this->manualCountResults)) { - $countOptions = $this->countOptions; - if (!isset($countOptions['behavior'])) { - $countOptions['behavior'] = Paginate::getDefaultCountBehavior($this->query); - } - if ('orm' === $countOptions['behavior'] && !isset($countOptions['simplified_request'])) { - $countOptions['simplified_request'] = $this->simplifiedRequest; - } - - $count = Paginate::countQueryBuilder($this->query, $countOptions); - $this->setCountResults($count); - } else { - $this->setCountResults($this->manualCountResults); - } - } - - /** - * @return boolean - */ - public function isSimplifiedRequest() - { - return $this->simplifiedRequest; - } - - /** - * Use simplified request (not subrequest and not order by) or not when count results - * @param boolean $simplifiedRequest - * @return DoctrineORMPaginator - */ - public function setSimplifiedRequest($simplifiedRequest) - { - $this->simplifiedRequest = $simplifiedRequest; - - return $this; - } - - /** - * @return boolean - */ - public function isFetchJoinCollection() - { - return $this->fetchJoinCollection; - } - - /** - * Set to true when fetch join a to-many collection - * In that case 3 instead of the 2 queries described are executed - * @param boolean $fetchJoinCollection - * @return DoctrineORMPaginator - */ - public function setFetchJoinCollection($fetchJoinCollection) - { - $this->fetchJoinCollection = $fetchJoinCollection; - - return $this; - } - - /** - * {@inheritDoc} - */ - public function getResults() - { - if (is_null($this->results)) { - $query = $this->query->getQuery(); - $paginator = new Paginator($query, $this->fetchJoinCollection); - $paginator->setUseOutputWalkers(!$this->simplifiedRequest); - $this->results = $paginator->getIterator(); - } - - return $this->results; - } -} diff --git a/README.md b/README.md index 694692c..eb79480 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,20 @@ -EcommitCrudBundle -================== +# EcommitCrudBundle -[![SensioLabsInsight](https://insight.sensiolabs.com/projects/ee0677db-2ad9-4248-9b61-ab00020317b5/big.png)](https://insight.sensiolabs.com/projects/ee0677db-2ad9-4248-9b61-ab00020317b5) + +![Tests](https://github.com/e-commit/EcommitCrudBundle/workflows/Tests/badge.svg) +[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard) + +**WARNING: This branch is under development. Not use in production. You can use stable versions or 2.6 branch.** + +## Documentation ## + +[Read the Documentation (French documentation 🇫🇷)](doc/index.md) + +## Security ## + +If you think that you have found a security issue, do **NOT** use the bug tracker. Instead, please email +contact@e-commit.fr with information about the issue. + +## License ## + +This bundle is available under the MIT license. See the complete license in the *LICENSE* file. diff --git a/Resources/config/services.xml b/Resources/config/services.xml deleted file mode 100644 index 5f17468..0000000 --- a/Resources/config/services.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - %ecommit_crud.template_configuration% - - - - - - - - - - - - - %ecommit_javascript.use_bootstrap% - %ecommit_crud.template_configuration% - %ecommit_crud.images% - - - - - - - - - - - - - - - diff --git a/Resources/doc/config.sample.yml b/Resources/doc/config.sample.yml deleted file mode 100644 index c6801d5..0000000 --- a/Resources/doc/config.sample.yml +++ /dev/null @@ -1,5 +0,0 @@ -ecommit_crud: - images: - #Images used in th method - th_image_up: "/bundles/ecommitcrud/images/i16/sort_incr.png" - th_image_down: "/bundles/ecommitcrud/images/i16/sort_decrease.png" diff --git a/Resources/public/images/i16/LICENSE b/Resources/public/images/i16/LICENSE deleted file mode 100644 index a065aca..0000000 --- a/Resources/public/images/i16/LICENSE +++ /dev/null @@ -1,32 +0,0 @@ -Icons used: - --------------------------------------------------------- -FAM FAM FAM - -Mark James -http://www.famfamfam.com/lab/icons/silk/ - -This work is licensed under a -Creative Commons Attribution 2.5 License. -[ http://creativecommons.org/licenses/by/2.5/ ] - -This means you may use it for any purpose, -and make any changes you like. -All I ask is that you include a link back -to this page in your credits. - -Are you using this icon set? Send me an email -(including a link or picture if available) to -mjames@gmail.com - -Any other questions about this icon set please -contact mjames@gmail.com - --------------------------------------------------------- - -TITLE: Crystal Project Icons -AUTHOR: Everaldo Coelho -SITE: http://www.everaldo.com -CONTACT: everaldo@everaldo.com - -Copyright (c) 2006-2007 Everaldo Coelho. diff --git a/Resources/public/images/i16/list.png b/Resources/public/images/i16/list.png deleted file mode 100644 index 2c9c154..0000000 Binary files a/Resources/public/images/i16/list.png and /dev/null differ diff --git a/Resources/public/images/i16/resultset_first.png b/Resources/public/images/i16/resultset_first.png deleted file mode 100644 index b03eaf8..0000000 Binary files a/Resources/public/images/i16/resultset_first.png and /dev/null differ diff --git a/Resources/public/images/i16/resultset_last.png b/Resources/public/images/i16/resultset_last.png deleted file mode 100644 index 8ec8947..0000000 Binary files a/Resources/public/images/i16/resultset_last.png and /dev/null differ diff --git a/Resources/public/images/i16/resultset_next.png b/Resources/public/images/i16/resultset_next.png deleted file mode 100644 index e252606..0000000 Binary files a/Resources/public/images/i16/resultset_next.png and /dev/null differ diff --git a/Resources/public/images/i16/resultset_previous.png b/Resources/public/images/i16/resultset_previous.png deleted file mode 100644 index 18f9cc1..0000000 Binary files a/Resources/public/images/i16/resultset_previous.png and /dev/null differ diff --git a/Resources/public/images/i16/sort_decrease.png b/Resources/public/images/i16/sort_decrease.png deleted file mode 100644 index 3d58baf..0000000 Binary files a/Resources/public/images/i16/sort_decrease.png and /dev/null differ diff --git a/Resources/public/images/i16/sort_incr.png b/Resources/public/images/i16/sort_incr.png deleted file mode 100644 index 1fbf09d..0000000 Binary files a/Resources/public/images/i16/sort_incr.png and /dev/null differ diff --git a/Resources/public/js/scrollToFirstMessage.js b/Resources/public/js/scrollToFirstMessage.js deleted file mode 100644 index f3e37bb..0000000 --- a/Resources/public/js/scrollToFirstMessage.js +++ /dev/null @@ -1,83 +0,0 @@ -function scrollToFirstMessage(scrollToError, scrollToFlashMessage, openTableIfMessage) -{ - if( typeof(scrollToError) == 'undefined' ){ - scrollToError = true; - } - if( typeof(scrollToFlashMessage) == 'undefined' ){ - scrollToFlashMessage = true; - } - if( typeof(openTableIfMessage) == 'undefined' ){ - openTableIfMessage = true; - } - - $(document).ready(function() { - if (scrollToFlashMessage) { - var cible = $('.flash-message:first'); - if(cible.length > 0) { - if (openTableIfMessage) { - openTable(cible); - } - var popupWrapper = $(cible).parents('.popup_wrapper_visible:first'); - if (popupWrapper.length > 0) { - height = getPositionTocenter(getCiblePositionInModal(cible)); - $(popupWrapper).animate({scrollTop:height}); - } else { - height = getPositionTocenter(cible.offset().top); - $('html,body').animate({scrollTop:height}); - } - return; - } - } - - if (scrollToError) { - var cible = $('li.form_error_message:first'); - if(cible.length > 0) { - if (openTableIfMessage) { - openTable(cible); - } - - var popupWrapper = $(cible).parents('.popup_wrapper_visible:first'); - if (popupWrapper.length > 0) { - height = getPositionTocenter(getCiblePositionInModal(cible)); - $(popupWrapper).animate({scrollTop:height}); - } else { - height = getPositionTocenter(cible.offset().top); - $('html,body').animate({scrollTop:height}); - } - return; - } - } - }); -} - -function openTable(cible) -{ - pane = cible.parents('div.pane-auto-display-first-message'); - table = cible.parents('div.tab-auto-display-first-message'); - if(pane.length > 0 && table.length > 0) { - idTable = table.attr('id'); - indexPane = pane.prevAll("#"+idTable+" div.pane-auto-display-first-message").length; - api = $("#"+idTable).tabs(); - api.click(indexPane); - } -} - -function getCiblePositionInModal(cible) -{ - var divModal = $(cible).parents('.crud_modal:first'); - var cibleOffset = parseInt(cible.offset().top); - var divModalOffset = parseInt(divModal.offset().top); - var divModalMarginTop = parseInt(divModal.css('margin-top')); - - return cibleOffset - divModalOffset + divModalMarginTop; -} - -function getPositionTocenter(errorPosition) -{ - var positionToCenter = errorPosition - ($(window).height() / 2); - if (positionToCenter < 0) { - positionToCenter = 0; - } - - return positionToCenter; -} diff --git a/Resources/translations/messages.en.yml b/Resources/translations/messages.en.yml deleted file mode 100644 index eb3826a..0000000 --- a/Resources/translations/messages.en.yml +++ /dev/null @@ -1,19 +0,0 @@ -Display Settings: Display Settings -Number of results per page: Number of results per page -Columns to be shown: Columns to be shown -You need to select at least one column to display: You need to select at least one column to display -Save: Save -Search: Search -Reset: Reset -'{0} No results|{1} 1 result found|]1,Inf] %count% results found': '{0} No results|{1} 1 result found|]1,Inf] %count% results found' -Page %firstPage%/%lastPage%: Page %page%/%lastPage% -Results %first%-%last%: Results %first%-%last% -Page %page%/%lastPage%: Page %page%/%lastPage% -filter.true: Yes -filter.false: No -filter.choices.placeholder: All -picker.add: Add -picker.list: Select -reset_display_settings: Reset display settings -check_all: Check all -uncheck_all: Uncheck all diff --git a/Resources/translations/messages.fr.yml b/Resources/translations/messages.fr.yml deleted file mode 100644 index 355a14d..0000000 --- a/Resources/translations/messages.fr.yml +++ /dev/null @@ -1,19 +0,0 @@ -Display Settings: Propriétés d'affichage -Number of results per page: Nombre de résultats par page -Columns to be shown: Colonnes à afficher -You need to select at least one column to display: Vous devez sélectionner au moins une colonne à afficher -Save: Enregistrer -Search: Rechercher -Reset: R.A.Z -'{0} No results|{1} 1 result found|]1,Inf] %count% results found': '{0} Pas de résultat|{1} 1 résultat trouvé|]1,Inf] %count% résultats trouvés' -Page %firstPage%/%lastPage%: Page %page%/%lastPage% -Results %first%-%last%: Résultats %first%-%last% -Page %page%/%lastPage%: Page %page%/%lastPage% -filter.true: Oui -filter.false: Non -filter.choices.placeholder: Tous -picker.add: Ajouter -picker.list: Sélectionner -reset_display_settings: Réinitialiser paramètres par défaut -check_all: Tout sélectionner -uncheck_all: Tout désélectionner diff --git a/Resources/views/Crud/double_search.html.twig b/Resources/views/Crud/double_search.html.twig deleted file mode 100644 index 420c644..0000000 --- a/Resources/views/Crud/double_search.html.twig +++ /dev/null @@ -1,8 +0,0 @@ -{% autoescape 'js' %} - -{% endautoescape %} diff --git a/Resources/views/Crud/form_settings_modal.html.twig b/Resources/views/Crud/form_settings_modal.html.twig deleted file mode 100644 index a872172..0000000 --- a/Resources/views/Crud/form_settings_modal.html.twig +++ /dev/null @@ -1,68 +0,0 @@ -{% set ajax_options = ajax_options|merge({ 'before': 'if(processCrudDisplaySettings_'~suffix~'() == false) { return false;}' }) %} -{% set reset_ajax_options = ajax_options|merge({ 'before': 'resetDisplaySettings'~suffix~'();' }) %} -{% set modalId = 'display_settings_' ~suffix %} - -{% macro contentDisplaySettings(vars) %} -

    {% trans %}Display Settings{% endtrans %}

    - {{ ecommit_javascript_jquery_ajax_form(vars.form, vars.ajax_options) }} - {{ form_label(vars.form.resultsPerPage) }} :
    - {{ form_widget(vars.form.resultsPerPage) }}

    - - {{ form_label(vars.form.displayedColumns) }} :
    - {% for choice, child in vars.form.displayedColumns %} - {{ form_widget(child) }} {{ form_label(child) }}
    - {% endfor %} - - - {{ form_rest(vars.form) }} - -
    - - {{ ecommit_javascript_jquery_ajax_button('reset_display_settings'|trans, vars.reset_settings_url, vars.reset_ajax_options, {'class': 'raz'}) }} -
    - -{% endmacro %} - -{% import _self as macro %} - - - -{% if display_button %} -{% trans %}Display Settings{% endtrans %} -{% endif %} - - diff --git a/Resources/views/Crud/form_settings_modal_bootstrap.html.twig b/Resources/views/Crud/form_settings_modal_bootstrap.html.twig deleted file mode 100644 index fe5eb0a..0000000 --- a/Resources/views/Crud/form_settings_modal_bootstrap.html.twig +++ /dev/null @@ -1,75 +0,0 @@ -{% set ajax_options = ajax_options|merge({ 'before': 'if(processCrudDisplaySettings_'~suffix~'() == false) { return false;}' }) %} -{% set reset_ajax_options = ajax_options|merge({ 'before': 'resetDisplaySettings'~suffix~'();' }) %} -{% set modalId = 'display_settings_' ~suffix %} - -{% macro contentDisplaySettings(vars) %} - {% form_theme vars.form 'bootstrap_3_layout.html.twig' %} - - - - {{ ecommit_javascript_jquery_ajax_form(vars.form, vars.ajax_options) }} - - - - -{% endmacro %} - -{% import _self as macro %} - - - -{% if display_button %} - -{% endif %} - - diff --git a/Resources/views/Crud/form_settings_nomodal.html.twig b/Resources/views/Crud/form_settings_nomodal.html.twig deleted file mode 100644 index 21a0253..0000000 --- a/Resources/views/Crud/form_settings_nomodal.html.twig +++ /dev/null @@ -1,63 +0,0 @@ -{% set reset_ajax_options = ajax_options %} -{% set ajax_options = ajax_options|merge({ 'before': 'if(processCrudDisplaySettings'~suffix~'() == false) { return false;}' }) %} - - - - - - -{% if display_button %} -{% trans %}Display Settings{% endtrans %} -{% endif %} - - diff --git a/Resources/views/Crud/form_settings_nomodal_bootstrap.html.twig b/Resources/views/Crud/form_settings_nomodal_bootstrap.html.twig deleted file mode 100644 index a93841a..0000000 --- a/Resources/views/Crud/form_settings_nomodal_bootstrap.html.twig +++ /dev/null @@ -1,66 +0,0 @@ -{% set reset_ajax_options = ajax_options %} -{% set ajax_options = ajax_options|merge({ 'before': 'if(processCrudDisplaySettings'~suffix~'() == false) { return false;}' }) %} -{% form_theme form 'bootstrap_3_layout.html.twig' %} - - - - - - -{% if display_button %} - -{% endif %} - - diff --git a/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig b/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig deleted file mode 100644 index 23117f1..0000000 --- a/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig +++ /dev/null @@ -1,47 +0,0 @@ -{% extends 'bootstrap_3_horizontal_layout.html.twig' %} - -{% block form_row -%} - {% spaceless %} -
    - {{ form_label(form) }} -
    - {{ form_errors(form) }} - {{ form_widget(form) }} -
    -
    - {% endspaceless %} -{%- endblock form_row %} - -{% block form_errors %} - {% spaceless %} - {% if errors|length > 0 %} - {% if form.parent %} - -
      - {% for error in errors %} -
    • - {% if 'data-display-in-errors' in form.vars.label_attr|keys and form.vars.label_attr['data-display-in-errors'] == '1' %} - {% if 'label-in-errors' in form.vars.label_attr|keys %} - {% set labelDisplayed = form.vars.label_attr['label-in-errors'] %} - {% else %} - {% set labelDisplayed = form.vars.label %} - {% endif %} - {{ labelDisplayed|trans }} : {{ error.message }} - {% else %} - {{ error.message }} - {% endif %} -
    • - {% endfor %} -
    -
    - {% else %} - {% for error in errors %} -
    - - {{ error.message }} -
    - {% endfor %} - {% endif %} - {% endif %} - {% endspaceless %} -{% endblock form_errors %} diff --git a/Resources/views/Form/bootstrap_3_layout.html.twig b/Resources/views/Form/bootstrap_3_layout.html.twig deleted file mode 100644 index 5ed44f4..0000000 --- a/Resources/views/Form/bootstrap_3_layout.html.twig +++ /dev/null @@ -1,45 +0,0 @@ -{% extends 'bootstrap_3_layout.html.twig' %} - -{% block form_row -%} - {% spaceless %} -
    - {{ form_label(form) }} - {{ form_errors(form) }} - {{ form_widget(form) }} -
    - {% endspaceless %} -{%- endblock form_row %} - -{% block form_errors %} - {% spaceless %} - {% if errors|length > 0 %} - {% if form.parent %} - -
      - {% for error in errors %} -
    • - {% if 'data-display-in-errors' in form.vars.label_attr|keys and form.vars.label_attr['data-display-in-errors'] == '1' %} - {% if 'label-in-errors' in form.vars.label_attr|keys %} - {% set labelDisplayed = form.vars.label_attr['label-in-errors'] %} - {% else %} - {% set labelDisplayed = form.vars.label %} - {% endif %} - {{ labelDisplayed|trans }} : {{ error.message }} - {% else %} - {{ error.message }} - {% endif %} -
    • - {% endfor %} -
    -
    - {% else %} - {% for error in errors %} -
    - - {{ error.message }} -
    - {% endfor %} - {% endif %} - {% endif %} - {% endspaceless %} -{% endblock form_errors %} diff --git a/Resources/views/Form/div_layout.html.twig b/Resources/views/Form/div_layout.html.twig deleted file mode 100644 index e48a354..0000000 --- a/Resources/views/Form/div_layout.html.twig +++ /dev/null @@ -1,23 +0,0 @@ -{% extends 'form_div_layout.html.twig' %} -{% block form_errors %} -{% spaceless %} - {% if errors|length > 0 %} -
      - {% for error in errors %} -
    • - {% if 'data-display-in-errors' in form.vars.label_attr|keys and form.vars.label_attr['data-display-in-errors'] == '1' %} - {% if 'label-in-errors' in form.vars.label_attr|keys %} - {% set labelDisplayed = form.vars.label_attr['label-in-errors'] %} - {% else %} - {% set labelDisplayed = form.vars.label %} - {% endif %} - {{ labelDisplayed|trans }} : {{ error.message }} - {% else %} - {{ error.message }} - {% endif %} -
    • - {% endfor %} -
    - {% endif %} -{% endspaceless %} -{% endblock form_errors %} diff --git a/Twig/CrudExtension.php b/Twig/CrudExtension.php deleted file mode 100644 index 49ac925..0000000 --- a/Twig/CrudExtension.php +++ /dev/null @@ -1,280 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Ecommit\CrudBundle\Twig; - -use Ecommit\CrudBundle\Crud\Crud; -use Ecommit\CrudBundle\Helper\CrudHelper; -use Ecommit\CrudBundle\Paginator\AbstractPaginator; -use Symfony\Component\OptionsResolver\OptionsResolver; -use Twig_Environment; -use Twig_Extension; -use Twig_SimpleFunction; - -class CrudExtension extends Twig_Extension -{ - /** - * @var CrudHelper - */ - protected $crudHelper; - - /** - * @var Twig_Environment - */ - protected $templating; - - - /** - * Constructor - * - * @param CrudHelper $crudHelper - */ - public function __construct(CrudHelper $crudHelper, Twig_Environment $templating) - { - $this->crudHelper = $crudHelper; - $this->templating = $templating; - } - - /** - * Returns the name of the extension. - * - * @return string The extension name - */ - public function getName() - { - return 'ecommit_crud_crud_extension'; - } - - /** - * Returns a list of global functions to add to the existing list. - * - * @return array An array of global functions - */ - public function getFunctions() - { - return array( - new Twig_SimpleFunction( - 'paginator_links', - array($this, 'paginatorLinks'), - array('is_safe' => array('all')) - ), - new Twig_SimpleFunction( - 'crud_paginator_links', - array($this, 'crudPaginatorLinks'), - array('is_safe' => array('all')) - ), - new Twig_SimpleFunction( - 'crud_th', - array($this, 'th'), - array('is_safe' => array('all')) - ), - new Twig_SimpleFunction( - 'crud_td', - array($this, 'td'), - array('is_safe' => array('all')) - ), - new Twig_SimpleFunction( - 'crud_display_settings', - array($this, 'displaySettings'), - array('is_safe' => array('all')) - ), - new Twig_SimpleFunction( - 'crud_search_form', - array($this, 'searchForm'), - array('is_safe' => array('all')) - ), - new Twig_SimpleFunction( - 'crud_search_reset', - array($this, 'searchReset'), - array('is_safe' => array('all')) - ), - new Twig_SimpleFunction( - 'crud_declare_modal', - array($this, 'declareModal'), - array('is_safe' => array('all')) - ), - new Twig_SimpleFunction( - 'crud_remote_modal', - array($this, 'remoteModal'), - array('is_safe' => array('all')) - ), - new Twig_SimpleFunction( - 'crud_form_modal', - array($this, 'formModal'), - array('is_safe' => array('all')) - ), - ); - } - - /** - * Twig function: "paginator_links" - * - * @see CrudHelper:paginatorLinks - */ - public function paginatorLinks( - AbstractPaginator $paginator, - $routeName, - $routeParams = array(), - $options = array() - ) { - return $this->crudHelper->paginatorLinks($paginator, $routeName, $routeParams, $options); - } - - /** - * Twig function: "crud_paginator_links" - * - * @see CrudHelper:crudPaginatorLinks - */ - public function crudPaginatorLinks(Crud $crud, $options = array(), $ajaxOptions = array()) - { - return $this->crudHelper->crudPaginatorLinks($crud, $options, $ajaxOptions); - } - - /** - * Twig function: "crud_th" - * - * @see CrudHelper:th - */ - public function th($columnId, Crud $crud, $options = array(), $thOptions = array(), $ajaxOptions = array()) - { - return $this->crudHelper->th($columnId, $crud, $options, $thOptions, $ajaxOptions); - } - - /** - * Twig function: "crud_td" - * - * @see CrudHelper:td - */ - public function td($columnId, Crud $crud, $value, $options = array(), $tdOptions = array()) - { - return $this->crudHelper->td($columnId, $crud, $value, $options, $tdOptions); - } - - /** - * Twig function: "crud_display_config" - * - * @param Crud $crud - * @param array $options Options : - * * modal: Include (or not) inside a modal. Default: true - * * image_url: Url image (button) - * * use_bootstrap: Use Bootstrap or not - * * modal_close_div_class: Close Div CSS Class - * * template: Template used. If null, default template is used - * @param array $ajaxOptions Ajax options - * @return string - */ - public function displaySettings(Crud $crud, $options = array(), $ajaxOptions = array()) - { - $options = array_merge($crud->getTemplateConfiguration('crud_display_settings'), $options); - $resolver = new OptionsResolver(); - $resolver->setDefaults( - array( - 'modal' => true, - 'image_url' => '/bundles/ecommitcrud/images/i16/list.png', - 'use_bootstrap' => $this->crudHelper->useBootstrap(), - 'modal_close_div_class' => 'overlay-close', - 'template' => null, - ) - ); - $resolver->setAllowedTypes('modal', 'bool'); - $resolver->setAllowedTypes('use_bootstrap', 'bool'); - $options = $resolver->resolve($options); - - if (!isset($ajaxOptions['update'])) { - $ajaxOptions['update'] = $crud->getDivIdList(); - } - - $form = $this->crudHelper->getFormDisplaySettings($crud); - - if (!empty($options['template'])) { - $templateName = $options['template']; - } elseif ($options['modal']) { - if ($options['use_bootstrap']) { - $templateName = '@EcommitCrud/Crud/form_settings_modal_bootstrap.html.twig'; - } else { - $templateName = '@EcommitCrud/Crud/form_settings_modal.html.twig'; - } - } else { - if ($options['use_bootstrap']) { - $templateName = '@EcommitCrud/Crud/form_settings_nomodal_bootstrap.html.twig'; - } else { - $templateName = '@EcommitCrud/Crud/form_settings_nomodal.html.twig'; - } - } - - return $this->templating->render( - $templateName, - array( - 'form' => $form, - 'url' => $crud->getUrl(), - 'reset_settings_url' => $crud->getUrl(array('razsettings' => 1)), - 'ajax_options' => $ajaxOptions, - 'image_url' => $options['image_url'], - 'suffix' => $crud->getSessionName(), - 'use_bootstrap' => $options['use_bootstrap'], - 'close_div_class' => $options['modal_close_div_class'], - 'overlay_service' => $this->crudHelper->getOverlayService(), - 'display_button' => $crud->getDisplayResults(), - 'div_id_list' => $crud->getDivIdList(), - ) - ); - } - - /** - * Twig function: "crud_search_form" - * - * @see CrudHelper:searchFormTag - */ - public function searchForm(Crud $crud, $ajaxOptions = array(), $htmlOptions = array()) - { - return $this->crudHelper->searchFormTag($crud, $ajaxOptions, $htmlOptions); - } - - /** - * Twig function: "crud_search_reset" - * - * @see CrudHelper:searchResetButton - */ - public function searchReset(Crud $crud, $options = array(), $ajaxOptions = array(), $htmlOptions = array()) - { - return $this->crudHelper->searchResetButton($crud, $options, $ajaxOptions, $htmlOptions); - } - - /** - * Twig function: "crud_declare_modal" - * - * @see CrudHelper:declareModal - */ - public function declareModal($modalId, $options = array()) - { - return $this->crudHelper->declareModal($modalId, $options); - } - - /** - * Twig function: "crud_remote_modal" - * - * @see CrudHelper:remoteModal - */ - public function remoteModal($modalId, $url, $options = array(), $ajaxOptions = array()) - { - return $this->crudHelper->remoteModal($modalId, $url, $options, $ajaxOptions); - } - - /** - * Twig function: "crud_form_modal" - * - * @see CrudHelper:formModal - */ - public function formModal($modalId, $form, $ajaxOptions = array(), $htmlOptions = array()) - { - return $this->crudHelper->formModal($modalId, $form, $ajaxOptions, $htmlOptions); - } -} diff --git a/UPGRADE.md b/UPGRADE.md new file mode 100644 index 0000000..b3ebb23 --- /dev/null +++ b/UPGRADE.md @@ -0,0 +1,3 @@ +# Upgrade + +[Read the Documentation (French documentation)](doc/index.md#migrations) diff --git a/assets/js/ajax.js b/assets/js/ajax.js new file mode 100644 index 0000000..a231b06 --- /dev/null +++ b/assets/js/ajax.js @@ -0,0 +1,432 @@ +/* + * This file is part of the EcommitCrudBundle package. + * + * (c) E-commit + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import * as optionsResolver from './options-resolver' +import runCallback from './callback' + +const ready = (callback) => { + if (document.readyState !== 'loading') callback() + else document.addEventListener('DOMContentLoaded', callback) +} + +ready(function () { + document.addEventListener('click', function (event) { + if (event.target.closest('[data-ec-crud-toggle="ajax-click"]')) { + onClickAuto(event) + } + + if (event.target.closest('a[data-ec-crud-toggle="ajax-link"]')) { + onClickLinkAuto(event) + } + }) + + document.addEventListener('submit', function (event) { + if (event.target.matches('form[data-ec-crud-toggle="ajax-form"]')) { + onSubmitFormAuto(event) + } + }) +}) + +function onClickAuto (event) { + event.preventDefault() + const element = event.target.closest('[data-ec-crud-toggle="ajax-click"]') + const eventBefore = new CustomEvent('ec-crud-ajax-click-auto-before', { + bubbles: true, + cancelable: true + }) + element.dispatchEvent(eventBefore) + if (eventBefore.defaultPrevented) { + return + } + + click(element).catch((error) => console.error(error)) +} + +function onClickLinkAuto (event) { + event.preventDefault() + const aLink = event.target.closest('a[data-ec-crud-toggle="ajax-link"]') + const eventBefore = new CustomEvent('ec-crud-ajax-link-auto-before', { + bubbles: true, + cancelable: true + }) + aLink.dispatchEvent(eventBefore) + if (eventBefore.defaultPrevented) { + return + } + + link(aLink).catch((error) => console.error(error)) +} + +function onSubmitFormAuto (event) { + event.preventDefault() + const eventBefore = new CustomEvent('ec-crud-ajax-form-auto-before', { + bubbles: true, + cancelable: true + }) + event.target.dispatchEvent(eventBefore) + if (eventBefore.defaultPrevented) { + return + } + + sendForm(event.target).catch((error) => console.error(error)) +} + +export function sendRequest (options) { + const eventBeginning = new CustomEvent('ec-crud-ajax', { + cancelable: true, + detail: { + options + } + }) + document.dispatchEvent(eventBeginning) + if (eventBeginning.defaultPrevented) { + return new Promise((resolve, reject) => { + resolve(null) + }) + } + + options = optionsResolver.resolve( + { + url: null, + update: null, + updateMode: 'update', + onBeforeSend: null, + onSuccess: null, + onError: null, + onComplete: null, + responseDataType: 'text', + method: 'POST', + query: {}, + body: null, + successfulResponseRequired: false, + cache: false, + options: {} + }, + options + ) + + if (optionsResolver.isNotBlank(options.url) === false) { + return new Promise((resolve, reject) => { + reject(new TypeError('Value required: url')) + }) + } + + options.urlResolved = resolveUrl(options) + + const callbacksSuccess = [] + if (optionsResolver.isNotBlank(options.update)) { + callbacksSuccess.push({ + priority: 10, + callback: (data, response) => updateDom(options.update, options.updateMode, data) + }) + } + if (optionsResolver.isNotBlank(options.onSuccess)) { + callbacksSuccess.push(options.onSuccess) + } + + let fetchOptions = { + method: options.method + } + if (options.body) { + if (typeof options.body === 'string' || options.body instanceof String || options.body instanceof FormData) { + fetchOptions.body = options.body + } else if (options.body instanceof Object) { + const formData = new FormData() + generateParameters([], '', '', options.body).forEach(entry => { + formData.append(entry[0], entry[1]) + }) + fetchOptions.body = formData + } else if (options.body !== null) { + return new Promise((resolve, reject) => { + reject(new TypeError('Bad type for option "body"')) + }) + } + } + + const eventBeforeSend = new CustomEvent('ec-crud-ajax-before-send', { + cancelable: true, + detail: { + options + } + }) + document.dispatchEvent(eventBeforeSend) + if (eventBeforeSend.defaultPrevented) { + return new Promise((resolve, reject) => { + resolve(null) + }) + } + if (optionsResolver.isNotBlank(options.onBeforeSend)) { + runCallback(options.onBeforeSend, options) + } + if (options.stop !== undefined && options.stop === true) { + return new Promise((resolve, reject) => { + resolve(null) + }) + } + + fetchOptions = optionsResolver.extend(fetchOptions, options.options) + + const fetchPromise = fetch(options.urlResolved, fetchOptions) + const ajaxPromise = new Promise((resolve, reject) => { + fetchPromise.then(response => { + if (response.ok) { + // Response OK (status in the range 200 – 299) + + let dataPromise + if (options.responseDataType === 'text' || options.responseDataType === 'json') { + // Using a clone avoids "TypeError: Already read" when response read is read a 2nd time later + const responseCloned = response.clone() + + try { + if (options.responseDataType === 'text') { + dataPromise = responseCloned.text() + } else if (options.responseDataType === 'json') { + dataPromise = responseCloned.json() + } + } catch (e) { + } + } else { + dataPromise = new Promise((resolve, reject) => { + resolve(null) + }) + } + + dataPromise.then(data => { + executeEventsAndCallbacksSuccess(callbacksSuccess, options, data, response) + resolve(response) + }).catch(error => { + error = 'Error during fetching response body: ' + error + executeEventsAndCallbacksError(options, error, response) + reject(error) + }) + } else { + // Response not OK (status not in the range 200 – 299) + executeEventsAndCallbacksError(options, response.statusText, response) + if (options.successfulResponseRequired) { + reject(new Error('The response is not successful: ' + response.statusText)) + } else { + resolve(response) + } + } + }).catch(error => { + error = 'Error during query execution: ' + error + executeEventsAndCallbacksError(options, error, null) + reject(error) + }) + }) + + return ajaxPromise +} + +export function click (element, options) { + // Options in data-* override options argument + options = optionsResolver.resolve( + options, + optionsResolver.getDataAttributes(element, 'ecCrudAjax') + ) + + return sendRequest(options) +} + +export function link (link, options) { + link = optionsResolver.getElement(link) + // Options in data-* override options argument + // Option argument override href + options = optionsResolver.resolve( + { + url: link.getAttribute('href') + }, + optionsResolver.resolve( + options, + optionsResolver.getDataAttributes(link, 'ecCrudAjax') + ) + ) + + return sendRequest(options) +} + +export function sendForm (form, options) { + const eventBefore = new CustomEvent('ec-crud-ajax-form-before', { + bubbles: true, + cancelable: true, + detail: { + form, + options + } + }) + document.dispatchEvent(eventBefore) + if (eventBefore.defaultPrevented) { + return new Promise((resolve, reject) => { + resolve(null) + }) + } + + form = optionsResolver.getElement(form) + // Options in data-* override options argument + // Option argument override action, method and data form + options = optionsResolver.resolve( + { + url: form.getAttribute('action'), + method: form.getAttribute('method'), + body: new FormData(form) + }, + optionsResolver.resolve( + options, + optionsResolver.getDataAttributes(form, 'ecCrudAjax') + ) + ) + + const callbacksComplete = [] + callbacksComplete.push({ + priority: 10, + callback: (statusText, response) => { + const eventOnComplete = new CustomEvent('ec-crud-ajax-form-complete', { + detail: { + form, + statusText, + response + } + }) + document.dispatchEvent(eventOnComplete) + } + }) + if (optionsResolver.isNotBlank(options.onComplete)) { + callbacksComplete.push(options.onComplete) + } + options.onComplete = callbacksComplete + + return sendRequest(options) +} + +export function updateDom (element, updateMode, content) { + const originElement = element + element = optionsResolver.getElement(element) + const eventBefore = new CustomEvent('ec-crud-ajax-update-dom-before', { + bubbles: true, + cancelable: true, + detail: { + element: originElement, + updateMode, + content + } + }) + element.dispatchEvent(eventBefore) + if (eventBefore.defaultPrevented) { + return + } + updateMode = eventBefore.detail.updateMode + content = eventBefore.detail.content + + if (updateMode === 'update') { + element.innerHTML = content + } else if (updateMode === 'before') { + element.outerHTML = content + element.outerHTML + } else if (updateMode === 'after') { + element.outerHTML = element.outerHTML + content + } else if (updateMode === 'prepend') { + element.innerHTML = content + element.innerHTML + } else if (updateMode === 'append') { + element.innerHTML = element.innerHTML + content + } else { + console.error('Bad updateMode: ' + updateMode) + + return + } + + const eventAfter = new CustomEvent('ec-crud-ajax-update-dom-after', { + bubbles: true, + detail: { + element: originElement, + updateMode, + content + } + }) + element.dispatchEvent(eventAfter) +} + +function resolveUrl (options) { + const url = new URL(options.url, window.location.origin) + const searchParams = url.searchParams + + generateParameters([], '', '', options.query).forEach(entry => { + searchParams.set(entry[0], entry[1]) + }) + + if (!options.cache && !searchParams.has('_')) { + searchParams.set('_', Date.now()) + } + + return url.toString() +} + +function generateParameters (result, propertyPath, property, value) { + if (/[[\]]/.test(property)) { + return result + } + + if (propertyPath === '') { + propertyPath = property + } else { + propertyPath = propertyPath + '[' + property + ']' + } + + if (typeof (value) === 'undefined' || typeof (value) !== 'object') { + result.push([propertyPath, value]) + + return result + } + + for (const param in value) { + if (Object.prototype.hasOwnProperty.call(value, param)) { + result = generateParameters(result, propertyPath, param, value[param]) + } + } + + return result +} + +function executeEventsAndCallbacksSuccess (callbacksSuccess, options, data, response) { + const eventOnSuccess = new CustomEvent('ec-crud-ajax-on-success', { + detail: { + data, + response + } + }) + document.dispatchEvent(eventOnSuccess) + runCallback(callbacksSuccess, data, response) + + const eventOnComplete = new CustomEvent('ec-crud-ajax-on-complete', { + detail: { + statusText: response.statusText, + response + } + }) + document.dispatchEvent(eventOnComplete) + runCallback(options.onComplete, response.statusText, response) +} + +function executeEventsAndCallbacksError (options, statusText, response) { + const eventOnError = new CustomEvent('ec-crud-ajax-on-error', { + detail: { + statusText, + response + } + }) + document.dispatchEvent(eventOnError) + runCallback(options.onError, statusText, response) + + const eventOnComplete = new CustomEvent('ec-crud-ajax-on-complete', { + detail: { + statusText, + response + } + }) + document.dispatchEvent(eventOnComplete) + runCallback(options.onComplete, statusText, response) +} diff --git a/assets/js/callback-manager.js b/assets/js/callback-manager.js new file mode 100644 index 0000000..034c079 --- /dev/null +++ b/assets/js/callback-manager.js @@ -0,0 +1,47 @@ +/* + * This file is part of the EcommitCrudBundle package. + * + * (c) E-commit + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +const ENGINE_KEY = Symbol.for('ecommit.crudbundle.callbackengine') +const globalSymbols = Object.getOwnPropertySymbols(global) +if (globalSymbols.indexOf(ENGINE_KEY) === -1) { + global[ENGINE_KEY] = [] +} + +export function registerCallback (name, callback) { + if (typeof name !== 'string' && !(name instanceof String)) { + console.error('Bad name') + } + if (!(callback instanceof Function)) { + console.error('Invalid callback ' + name) + } + + global[ENGINE_KEY][name] = callback +} + +export function callbackIsRegistred (name) { + if (typeof name !== 'string' && !(name instanceof String)) { + console.error('Bad name') + } + + return (undefined !== global[ENGINE_KEY][name]) +} + +export function getRegistredCallback (name) { + if (!callbackIsRegistred(name)) { + console.error('Callback not found: ' + name) + + return null + } + + return global[ENGINE_KEY][name] +} + +export function clear () { + global[ENGINE_KEY] = [] +} diff --git a/assets/js/callback.js b/assets/js/callback.js new file mode 100644 index 0000000..14fa7e3 --- /dev/null +++ b/assets/js/callback.js @@ -0,0 +1,76 @@ +/* + * This file is part of the EcommitCrudBundle package. + * + * (c) E-commit + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import * as callbackManager from './callback-manager' + +export default function (callbacks, ...args) { + if (undefined === callbacks || callbacks === null) { + return + } + + if (typeof callbacks === 'string' || callbacks instanceof String || callbacks instanceof Function) { + callbacks = [callbacks] + } + + if (Array !== callbacks.constructor) { + return + } + + const newCallbacks = [] + callbacks.forEach((value) => { + addCallbacksToStack(value, newCallbacks) + }) + + newCallbacks.sort(function (a, b) { + if (parseInt(a.priority, 10) >= parseInt(b.priority, 10)) { + return -1 + } + + return 1 + }) + + newCallbacks.forEach((value) => { + processCallback(value.callback, args) + }) +} + +function addCallbacksToStack (value, stack) { + if (typeof value === 'string' || value instanceof String || value instanceof Function) { + stack.push({ + callback: value, + priority: 0 + }) + } else if (Array === value.constructor) { + value.forEach((subValue) => { + addCallbacksToStack(subValue, stack) + }) + } else if (undefined !== value.callback) { + stack.push({ + callback: value.callback, + priority: (value.priority !== undefined) ? value.priority : 0 + }) + } +} + +function processCallback (subject, args) { + if (subject instanceof Function) { + subject(...args) + + return + } + + if (typeof subject !== 'string' && !(subject instanceof String)) { + return + } + + subject = callbackManager.getRegistredCallback(subject) + if (subject) { + subject(...args) + } +} diff --git a/assets/js/crud.js b/assets/js/crud.js new file mode 100644 index 0000000..9f17954 --- /dev/null +++ b/assets/js/crud.js @@ -0,0 +1,168 @@ +/* + * This file is part of the EcommitCrudBundle package. + * + * (c) E-commit + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { click, sendForm, sendRequest, updateDom } from './ajax' +import { closeModal, openModal } from './modal/modal-manager' +import { getElement } from './options-resolver' + +const ready = (callback) => { + if (document.readyState !== 'loading') callback() + else document.addEventListener('DOMContentLoaded', callback) +} + +ready(function () { + document.addEventListener('submit', function (event) { + if (event.target.matches('form[data-ec-crud-toggle="search-form"]')) { + onSubmitCrudSearchForm(event) + } + + if (event.target.matches('[data-ec-crud-toggle="display-settings"] form')) { + onCrudDisplaySettingsSubmit(event) + } + }) + + document.addEventListener('click', function (event) { + if (event.target.closest('button[data-ec-crud-toggle="search-reset"]')) { + onResetCrudSearchForm(event) + } + + if (event.target.closest('button[data-ec-crud-toggle="display-settings-button"]')) { + onCrudDisplaySettingsOpen(event) + } + + if (event.target.closest('button[data-ec-crud-toggle="display-settings-check-all-columns"]')) { + onCrudDisplaySettingsCheckAllColumns(event) + } + + if (event.target.closest('button[data-ec-crud-toggle="display-settings-uncheck-all-columns"]')) { + onCrudDisplaySettingsUncheckAllColumns(event) + } + + if (event.target.closest('button[data-ec-crud-toggle="display-settings-reset"]')) { + onCrudDisplaySettingsReset(event) + } + }) +}) + +function onSubmitCrudSearchForm (event) { + event.preventDefault() + + const form = event.target + const searchContainer = getElement('#' + form.getAttribute('data-crud-search-id')) + const listContainer = getElement('#' + form.getAttribute('data-crud-list-id')) + + sendForm(form, { + responseDataType: 'json', + onSuccess: function (json, response) { + updateDom(searchContainer, 'update', json.render_search) + updateDom(listContainer, 'update', json.render_list) + } + }) +} + +function onResetCrudSearchForm (event) { + const button = event.target.closest('button[data-ec-crud-toggle="search-reset"]') + const searchContainer = getElement('#' + button.getAttribute('data-crud-search-id')) + const listContainer = getElement('#' + button.getAttribute('data-crud-list-id')) + + click(button, { + responseDataType: 'json', + onSuccess: function (json, response) { + updateDom(searchContainer, 'update', json.render_search) + updateDom(listContainer, 'update', json.render_list) + } + }) +} + +function onCrudDisplaySettingsOpen (event) { + const button = event.target.closest('button[data-ec-crud-toggle="display-settings-button"]') + const displaySettingsContainer = getElement('#' + button.getAttribute('data-display-settings')) + const isModal = displaySettingsContainer.getAttribute('data-modal') === '1' + + if (isModal) { + openDisplaySettings(displaySettingsContainer) + + return + } + + if (displaySettingsContainer.offsetWidth > 0 && displaySettingsContainer.offsetHeight > 0) { // is visible ? + closeDisplaySettings(displaySettingsContainer) + } else { + openDisplaySettings(displaySettingsContainer) + } +} + +function onCrudDisplaySettingsCheckAllColumns (event) { + const button = event.target.closest('button[data-ec-crud-toggle="display-settings-check-all-columns"]') + button.parentNode.closest('div[data-ec-crud-toggle="display-settings"]').querySelectorAll('input[type=checkbox]').forEach(checkbox => { + checkbox.checked = true + }) +} + +function onCrudDisplaySettingsUncheckAllColumns (event) { + const button = event.target.closest('button[data-ec-crud-toggle="display-settings-uncheck-all-columns"]') + button.parentNode.closest('div[data-ec-crud-toggle="display-settings"]').querySelectorAll('input[type=checkbox]').forEach(checkbox => { + checkbox.checked = false + }) +} + +function onCrudDisplaySettingsReset (event) { + const button = event.target.closest('button[data-ec-crud-toggle="display-settings-reset"]') + const displaySettingsContainer = button.parentNode.closest('div[data-ec-crud-toggle="display-settings"]') + const listContainer = getElement('#' + displaySettingsContainer.getAttribute('data-crud-list-id')) + + closeDisplaySettings(displaySettingsContainer) + + sendRequest({ + url: button.getAttribute('data-reset-url'), + update: listContainer + }) +} + +function onCrudDisplaySettingsSubmit (event) { + event.preventDefault() + const form = event.target + + const displaySettingsContainer = form.parentNode.closest('div[data-ec-crud-toggle="display-settings"]') + const listContainer = getElement('#' + displaySettingsContainer.getAttribute('data-crud-list-id')) + + closeDisplaySettings(displaySettingsContainer) + + sendForm(form, { + responseDataType: 'json', + onSuccess: function (json, response) { + const displaySettingsContainerId = displaySettingsContainer.getAttribute('id') // Backup before deletion (by updateDom) + updateDom(listContainer, 'update', json.render_list) + if (!json.form_is_valid) { + openDisplaySettings(getElement('#' + displaySettingsContainerId)) + } + } + }) +} + +function openDisplaySettings (displaySettingsContainer) { + const isModal = displaySettingsContainer.getAttribute('data-modal') === '1' + if (isModal) { + openModal({ + element: displaySettingsContainer + }) + } else { + displaySettingsContainer.style.display = 'block' + } +} + +function closeDisplaySettings (displaySettingsContainer) { + const isModal = displaySettingsContainer.getAttribute('data-modal') === '1' + + if (isModal) { + closeModal(displaySettingsContainer) + } else { + displaySettingsContainer.style.display = 'none' + } +} diff --git a/assets/js/modal/engine/bootstrap3.js b/assets/js/modal/engine/bootstrap3.js new file mode 100644 index 0000000..572112f --- /dev/null +++ b/assets/js/modal/engine/bootstrap3.js @@ -0,0 +1,34 @@ +/* + * This file is part of the EcommitCrudBundle package. + * + * (c) E-commit + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import $ from 'jquery' +import runCallback from '../../callback' + +export function openModal (options) { + // Suppression des événements + $(options.element).off('shown.bs.modal') + $(options.element).off('hide.bs.modal') + + $(options.element).on('shown.bs.modal', function (e) { + runCallback(options.onOpen, $(options.element)) + }) + + $(options.element).on('hide.bs.modal', function (e) { + runCallback(options.onClose, $(options.element)) + }) + + $(options.element).modal({ + show: true, + focus: true + }) +} + +export function closeModal (element) { + $(element).modal('hide') +} diff --git a/assets/js/modal/engine/bootstrap4.js b/assets/js/modal/engine/bootstrap4.js new file mode 100644 index 0000000..804ab3c --- /dev/null +++ b/assets/js/modal/engine/bootstrap4.js @@ -0,0 +1,18 @@ +/* + * This file is part of the EcommitCrudBundle package. + * + * (c) E-commit + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import * as bootstrap3 from './bootstrap3' + +export function openModal (options) { + bootstrap3.openModal(options) +} + +export function closeModal (element) { + bootstrap3.closeModal(element) +} diff --git a/assets/js/modal/engine/bootstrap5.js b/assets/js/modal/engine/bootstrap5.js new file mode 100644 index 0000000..08e56b2 --- /dev/null +++ b/assets/js/modal/engine/bootstrap5.js @@ -0,0 +1,34 @@ +/* + * This file is part of the EcommitCrudBundle package. + * + * (c) E-commit + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Modal } from 'bootstrap' +import runCallback from '../../callback' +import { getElement } from '../../options-resolver' + +export function openModal (options) { + const element = getElement(options.element) + + element.addEventListener('shown.bs.modal', e => { + runCallback(options.onOpen, element) + }, { once: true }) + + element.addEventListener('hide.bs.modal', e => { + runCallback(options.onClose, element) + }, { once: true }) + + const modal = new Modal(element, { + focus: true + }) + modal.show() +} + +export function closeModal (element) { + const modal = Modal.getInstance(getElement(element)) + modal.hide() +} diff --git a/assets/js/modal/engine/empty.js b/assets/js/modal/engine/empty.js new file mode 100644 index 0000000..8865495 --- /dev/null +++ b/assets/js/modal/engine/empty.js @@ -0,0 +1,18 @@ +/* + * This file is part of the EcommitCrudBundle package. + * + * (c) E-commit + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import runCallback from '../../callback' + +export function openModal (options) { + runCallback(options.onOpen, options.element) +} + +export function closeModal (element) { + runCallback(element) +} diff --git a/assets/js/modal/modal-manager.js b/assets/js/modal/modal-manager.js new file mode 100644 index 0000000..83dec11 --- /dev/null +++ b/assets/js/modal/modal-manager.js @@ -0,0 +1,178 @@ +/* + * This file is part of the EcommitCrudBundle package. + * + * (c) E-commit + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import * as optionsResolver from '../options-resolver' +import * as ajax from '../ajax' + +const ENGINE_KEY = Symbol.for('ecommit.crudbundle.modalengine') +const globalSymbols = Object.getOwnPropertySymbols(global) +if (globalSymbols.indexOf(ENGINE_KEY) === -1) { + global[ENGINE_KEY] = null +} + +const ready = (callback) => { + if (document.readyState !== 'loading') callback() + else document.addEventListener('DOMContentLoaded', callback) +} + +ready(function () { + document.addEventListener('click', function (event) { + if (event.target.closest('[data-ec-crud-toggle="modal"]')) { + onClickModalAuto(event) + } + + if (event.target.closest('button[data-ec-crud-toggle="remote-modal"]')) { + onClickButtonRemoteModalAuto(event) + } + + if (event.target.closest('a[data-ec-crud-toggle="remote-modal"]')) { + onClickLinkRemoteModalAuto(event) + } + }) +}) + +function onClickModalAuto (event) { + event.preventDefault() + const element = event.target.closest('[data-ec-crud-toggle="modal"]') + const eventBefore = new CustomEvent('ec-crud-modal-auto-before', { + bubbles: true, + cancelable: true + }) + element.dispatchEvent(eventBefore) + if (eventBefore.defaultPrevented) { + return + } + + openModal(optionsResolver.getDataAttributes(element, 'ecCrudModal')) +} + +function onClickButtonRemoteModalAuto (event) { + event.preventDefault() + const button = event.target.closest('button[data-ec-crud-toggle="remote-modal"]') + const eventBefore = new CustomEvent('ec-crud-remote-modal-auto-before', { + bubbles: true, + cancelable: true + }) + button.dispatchEvent(eventBefore) + if (eventBefore.defaultPrevented) { + return + } + + openRemoteModal(optionsResolver.getDataAttributes(button, 'ecCrudModal')) +} + +function onClickLinkRemoteModalAuto (event) { + event.preventDefault() + const aLink = event.target.closest('a[data-ec-crud-toggle="remote-modal"]') + const eventBefore = new CustomEvent('ec-crud-remote-modal-auto-before', { + bubbles: true, + cancelable: true + }) + aLink.dispatchEvent(eventBefore) + if (eventBefore.defaultPrevented) { + return + } + + // Options in data-* override href + const options = optionsResolver.resolve( + { + url: aLink.getAttribute('href') + }, + optionsResolver.getDataAttributes(aLink, 'ecCrudModal') + ) + + openRemoteModal(options) +} + +export function defineEngine (newEngine) { + global[ENGINE_KEY] = newEngine +} + +export function getEngine () { + if (global[ENGINE_KEY] === null) { + console.error('Engine not defined') + + return + } + + return global[ENGINE_KEY] +} + +export function openModal (options) { + options = optionsResolver.resolve( + { + element: null, + onOpen: null, + onClose: null + }, + options + ) + + if (optionsResolver.isNotBlank(options.element) === false) { + console.error('Value required: element') + + return + } + + getEngine().openModal(options) +} + +export function openRemoteModal (options) { + options = optionsResolver.resolve( + { + url: null, + element: null, + elementContent: null, + onOpen: null, + onClose: null, + method: 'POST', + ajaxOptions: {} + }, + options + ) + + let hasError = false; + ['url', 'element', 'elementContent', 'method'].forEach((value) => { + if (optionsResolver.isNotBlank(options[value]) === false) { + console.error('Value required: ' + value) + hasError = true + } + }) + if (hasError === true) { + return + } + + const ajaxOptions = optionsResolver.resolve( + { + url: options.url, + method: options.method, + update: options.elementContent + }, + options.ajaxOptions + ) + + const callbacksSuccess = [ + { + priority: 1, + callback: function (data, textStatus, jqXHR) { + openModal(options) + } + } + ] + if (optionsResolver.isNotBlank(ajaxOptions.onSuccess)) { + callbacksSuccess.push(ajaxOptions.onSuccess) + } + ajaxOptions.onSuccess = callbacksSuccess + + ajax.sendRequest(ajaxOptions) +} + +export function closeModal (element) { + getEngine().closeModal(element) +} diff --git a/assets/js/options-resolver.js b/assets/js/options-resolver.js new file mode 100644 index 0000000..6425220 --- /dev/null +++ b/assets/js/options-resolver.js @@ -0,0 +1,95 @@ +/* + * This file is part of the EcommitCrudBundle package. + * + * (c) E-commit + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export function resolve (defaultOptions, options) { + Object.keys(options).forEach(key => options[key] === undefined ? delete options[key] : {}) + + return extend({}, defaultOptions, options) +} + +export function getDataAttributes (element, prefix) { + const prefixLength = prefix.length + const attributes = {} + + element = getElement(element) + if (!element) { + return attributes + } + + Object.entries(element.dataset).forEach((property) => { + const index = property[0] + const value = property[1] + if (index.length > prefixLength && index.substr(0, prefixLength) === prefix) { + let newIndex = index.substr(prefixLength) + newIndex = newIndex.charAt(0).toLowerCase() + newIndex.slice(1) + attributes[newIndex] = transformDataValue(value) + } + }) + + return attributes +} + +function transformDataValue (value) { + if (value.length === 0) { + return null + } + + if (/^\d+$/.test(value)) { + return parseInt(value, 10) + } else if (/^true$/i.test(value)) { + return true + } else if (/^false$/i.test(value)) { + return false + } else if (/^[[{]/i.test(value)) { + try { + return JSON.parse(value) + } catch (e) { + return value + } + } + + return value +} + +export function isNotBlank (value) { + if (undefined === value || value === null || value.length === 0) { + return false + } + + return true +} + +export function getElement (element) { + if (element === null) { + return null + } else if (typeof element === 'string' || element instanceof String) { + return document.querySelector(element) + } else if (element instanceof Element) { + return element + } else if (typeof element === 'object' && element.jquery !== undefined) { + const elementByJquery = element.get(0) + if (elementByJquery instanceof Element) { + return elementByJquery + } + } + + return null +} + +export function extend () { + for (let i = 1; i < arguments.length; i++) { + for (const key in arguments[i]) { + if (Object.prototype.hasOwnProperty.call(arguments[i], key)) { + arguments[0][key] = arguments[i][key] + } + } + } + + return arguments[0] +} diff --git a/assets/package.json b/assets/package.json new file mode 100644 index 0000000..f50c0d2 --- /dev/null +++ b/assets/package.json @@ -0,0 +1,6 @@ +{ + "name": "@ecommit/crud-bundle", + "description": "ECOMMIT CRUD BUNDLE", + "license": "MIT", + "version": "3.0.0" +} diff --git a/composer.json b/composer.json index 3606c75..06ec917 100644 --- a/composer.json +++ b/composer.json @@ -10,36 +10,66 @@ } ], "require": { - "doctrine/common": "*", - "doctrine/doctrine-bundle": "^1.12|^2.0", - "doctrine/orm": "~2.3", - "doctrine/persistence": "^1.0|^2.0", - "ecommit/javascript-bundle": "2.6.*@dev", - "ecommit/util-bundle": "3.0.*@dev", - "symfony/config": "^4.2|^5.0", - "symfony/dependency-injection": "^4.2|^5.0", - "symfony/doctrine-bridge": "^4.2|^5.0", - "symfony/form": "^4.2|^5.0", - "symfony/framework-bundle": "^4.2|^5.0", - "symfony/http-foundation": "^4.2|^5.0", - "symfony/http-kernel": "^4.2|^5.0", - "symfony/options-resolver": "^4.2|^5.0", - "symfony/property-access": "^4.2|^5.0", - "symfony/security-bundle": "^4.2|^5.0", - "symfony/translation": "^4.2|^5.0", - "symfony/twig-bridge": "^4.2|^5.0", - "symfony/twig-bundle": "^4.2|^5.0", - "symfony/validator": "^4.2|^5.0" + "php": "^8.1", + "ext-json": "*", + "ext-mbstring": "*", + "doctrine/collections": "^1.5|^2.0", + "doctrine/dbal": "^3.2|^4.0", + "doctrine/doctrine-bundle": "^2.4.5|^3.0", + "doctrine/orm": "^2.9|^3.0", + "doctrine/persistence": "^2.0|^3.0|^4.0", + "ecommit/doctrine-utils": "^1.0|^2.0", + "ecommit/paginator": "^1.0", + "ecommit/scalar-values": "^1.0", + "psr/container": "^1.1|^2.0", + "symfony/asset": "^6.4|^7.4|^8.0", + "symfony/config": "^6.4|^7.4|^8.0", + "symfony/dependency-injection": "^6.4|^7.4|^8.0", + "symfony/doctrine-bridge": "^6.4|^7.4|^8.0", + "symfony/form": "^6.4|^7.4|^8.0", + "symfony/framework-bundle": "^6.4|^7.4|^8.0", + "symfony/http-client": "^6.4|^7.4|^8.0", + "symfony/http-client-contracts": "^2.4|^3.0", + "symfony/http-foundation": "^6.4|^7.4|^8.0", + "symfony/http-kernel": "^6.4|^7.4|^8.0", + "symfony/intl": "^6.4|^7.4|^8.0", + "symfony/options-resolver": "^6.4|^7.4|^8.0", + "symfony/property-access": "^6.4|^7.4|^8.0", + "symfony/routing": "^6.4|^7.4|^8.0", + "symfony/security-bundle": "^6.4|^7.4|^8.0", + "symfony/security-core": "^6.4|^7.4|^8.0", + "symfony/security-csrf": "^6.4|^7.4|^8.0", + "symfony/service-contracts": "^1.1.6|^2|^3", + "symfony/translation": "^6.4|^7.4|^8.0", + "symfony/translation-contracts": "^2.3|^3.0", + "symfony/twig-bridge": "^6.4|^7.4|^8.0", + "symfony/twig-bundle": "^6.4|^7.4|^8.0", + "symfony/validator": "^6.4|^7.4|^8.0", + "twig/twig": "^2.12.0|^3.0" }, - "conflict": { - "symfony/doctrine-bridge": "4.4.19|5.2.2|5.2.3" + "require-dev": { + "dbrekelmans/bdi": "^1.0", + "dg/bypass-finals": "^1.3", + "doctrine/data-fixtures": "^1.5.3|^2.0", + "doctrine/doctrine-fixtures-bundle": "^3.4.2|^4.0", + "friendsofphp/php-cs-fixer": "^3.88", + "phpunit/phpunit": "^10.0", + "symfony/dom-crawler": "^6.4|^7.4|^8.0", + "symfony/panther": "^2.3", + "symfony/webpack-encore-bundle": "^1.7.3|^2.4", + "symfony/yaml": "^6.4|^7.4|^8.0", + "vimeo/psalm": "^5.0" }, "autoload": { - "psr-4": { "Ecommit\\CrudBundle\\": "" } + "psr-4": { "Ecommit\\CrudBundle\\": "src/" } }, - "extra": { - "branch-alias": { - "dev-master": "2.5.x-dev" + "autoload-dev": { + "psr-4": { "Ecommit\\CrudBundle\\Tests\\": "tests/" } + }, + "config": { + "sort-packages": true, + "allow-plugins": { + "phpstan/extension-installer": true } } } diff --git a/config/crud.php b/config/crud.php new file mode 100644 index 0000000..09c6d21 --- /dev/null +++ b/config/crud.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Doctrine\Persistence\ManagerRegistry; +use Ecommit\CrudBundle\Crud\CrudFactory; +use Ecommit\CrudBundle\Crud\CrudResponseGenerator; +use Ecommit\CrudBundle\EventListener\MappingEntities; +use Ecommit\CrudBundle\Form\Type\EntityAjaxType; +use Ecommit\CrudBundle\Twig\CrudExtension; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Routing\RouterInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; + +return static function (ContainerConfigurator $container): void { + $container->services() + + ->set('ecommit_crud.locator', ServiceLocator::class) + ->args([[ + 'router' => service(RouterInterface::class), + 'form.factory' => service(FormFactoryInterface::class), + 'request_stack' => service(RequestStack::class), + 'doctrine' => service(ManagerRegistry::class), + 'security.token_storage' => service(TokenStorageInterface::class), + 'ecommit_crud.filters' => service('ecommit_crud.filters'), + ]]) + ->tag('container.service_locator') + + ->set('ecommit_crud.factory', CrudFactory::class) + ->args([service('ecommit_crud.locator')]) + ->alias(CrudFactory::class, 'ecommit_crud.factory') + + ->set('ecommit_crud.response_generatror', CrudResponseGenerator::class) + ->tag('container.service_subscriber') + ->alias(CrudResponseGenerator::class, 'ecommit_crud.response_generatror') + + ->set('ecommit_crud.twig.crud_extension', CrudExtension::class) + ->args([ + service('twig.form.renderer'), + param('ecommit_crud.theme'), + param('ecommit_crud.icon_theme'), + param('ecommit_crud.twig_functions_configuration'), + ]) + ->tag('twig.extension') + + ->set('ecommit_crud.event_listener.mapping_entities', MappingEntities::class) + ->tag('doctrine.event_listener', ['event' => 'loadClassMetadata']) + + ->set('ecommit_crud.type.entity_ajax', EntityAjaxType::class) + ->args([ + service(ManagerRegistry::class), + service(RouterInterface::class), + ]) + ->tag('form.type') + ; +}; diff --git a/config/filters.php b/config/filters.php new file mode 100644 index 0000000..bdf5b2c --- /dev/null +++ b/config/filters.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Ecommit\CrudBundle\Form\Filter; + +return static function (ContainerConfigurator $container): void { + $container->services() + + ->set('ecommit_crud.filter.boolean', Filter\BooleanFilter::class) + ->tag('ecommit_crud.filter') + + ->set('ecommit_crud.filter.choice', Filter\ChoiceFilter::class) + ->tag('ecommit_crud.filter') + + ->set('ecommit_crud.filter.date', Filter\DateFilter::class) + ->tag('ecommit_crud.filter') + + ->set('ecommit_crud.filter.entity_ajax', Filter\EntityAjaxFilter::class) + ->tag('ecommit_crud.filter') + + ->set('ecommit_crud.filter.entity', Filter\EntityFilter::class) + ->tag('ecommit_crud.filter') + + ->set('ecommit_crud.filter.integer', Filter\IntegerFilter::class) + ->tag('ecommit_crud.filter') + + ->set('ecommit_crud.filter.not_null', Filter\NotNullFilter::class) + ->tag('ecommit_crud.filter') + + ->set('ecommit_crud.filter.null', Filter\NullFilter::class) + ->tag('ecommit_crud.filter') + + ->set('ecommit_crud.filter.number', Filter\NumberFilter::class) + ->tag('ecommit_crud.filter') + + ->set('ecommit_crud.filter.text', Filter\TextFilter::class) + ->tag('ecommit_crud.filter') + ; +}; diff --git a/doc/cookbook/advanced-searcher.md b/doc/cookbook/advanced-searcher.md new file mode 100644 index 0000000..f947a3e --- /dev/null +++ b/doc/cookbook/advanced-searcher.md @@ -0,0 +1,189 @@ +# Personnalisation avancée de la classe Searcher + +## Personnalisation des filtres de recherche + +### Solution 1: Utilisation de addField et updateQueryBuilder + +```php +addFilter('id', Filter\IntegerFilter::class, [ + 'comparator' => Filter\IntegerFilter::EQUAL, + ]); + + //Ajout manuel d'un champ dans filtre de recherche + $builder->addField('name', TextType::class, [ + 'required' => false, + ]); + } + + public function updateQueryBuilder(mixed $queryBuilder, array $options): void + { + //Traitement du filtre de recherche "name" + if (null !== $this->name) { + $queryBuilder->andWhere('c1.name = :name') + ->setParameter('name', $this->name); + } + } +} +``` + +### Solution 2: Utilisation d'une classe FormType + +```php +name) { + $queryBuilder->andWhere('c1.name = :name') + ->setParameter('name', $this->name); + } + } +} +``` + +> **_REMARQUE:_** Pensez à la validation de vos champs. + + +```php +addField('name', TextType::class, [ + 'required' => false, + ]); + } +} +``` + +```diff +createCrudConfig('my_crud'); //Passé en argument: Nom du CRUD + $crudConfig->addColumn(['id' => 'id', 'alias' => 'c1.id', 'label' => 'Id']) + //... + ->setRoute('my_crud_ajax') +- ->createSearchForm(new CarSearcher()) ++ ->createSearchForm(new CarSearcher(), CarSearcherType::class , [ ++ //Searcher options ++ 'form_options' => [ ++ //Form type options ++ ], ++ ]) + //... + + return $crudConfig->getOptions(); + } +} +``` + +### Solution 3: Création d'un filtre de recherche + +Voir [Création d'un filtre de recherche](create_filter.md) + + +## Définir les options du formulaire de recherche + +La classe `configureOptions` de `SearcherInterface` peut être utilisée pour définir des options au formulaire de recherche : + +```php +//src/Form/Searcher/CarSearcher +namespace App\Form\Searcher; + +use Ecommit\CrudBundle\Form\Searcher\AbstractSearcher; +use Symfony\Component\OptionsResolver\OptionsResolver; + +class CarSearcher extends AbstractSearcher +{ + //... + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'my_option' => null, + ]); + } +} +``` + +Ces options doivent être passées au 3ème argument (`$options`) de la méthode `createSearchForm` de `Crud` : + +```diff +createCrudConfig('my_crud'); //Passé en argument: Nom du CRUD + $crudConfig->addColumn(['id' => 'id', 'alias' => 'c1.id', 'label' => 'Id']) + //... + ->setRoute('my_crud_ajax') +- ->createSearchForm(new CarSearcher()) ++ ->createSearchForm(new CarSearcher(), null , [ ++ 'my_option' => 'my_value', ++ ]) + //... + + return $crudConfig->getOptions(); + } +} +``` + +Ces options peuvent être enfin récupérées dans les arguments `$options` des méthodes `buildForm` et `updateQueryBuilder` de `SearcherInterface`. diff --git a/doc/cookbook/create_filter.md b/doc/cookbook/create_filter.md new file mode 100644 index 0000000..0a815ae --- /dev/null +++ b/doc/cookbook/create_filter.md @@ -0,0 +1,20 @@ +# Création d'un filtre de recherche + +> **_ALTERNATIVES :_** +> +> * [Utilisation d'un filtre existant](../references/filters.md) +> * [Personnalisation avancée de la classe Searcher](../cookbook/advanced-searcher.md) + + +Un filtre doit être une classe qui hérite de `AbstractFilter` ou implémente `FilterInterface` : +* La méthode `buildForm` ajoute le filtre dans le fromulaire de recherche (utiliser `$builder->addField()`). +* La méthode `updateQueryBuilder` modifie le QueryBuilder en fonction de la valeur saisie dans le filtre. +* La méthode `configureOptions` définie les éventuelles options. + +> **_REMARQUE :_** Il est conseillé de regarder le code source des filtres existants. + + +La classe doit être déclarée comme service ayant le [tag](https://symfony.com/doc/current/service_container/tags.html) `ecommit_crud.filter`. + +> **_REMARQUE :_** Avec l'option [autoconfigure](https://symfony.com/doc/current/service_container.html#services-autoconfigure) +> de Symfony, le tag est automatiquement ajouté aux services. diff --git a/doc/cookbook/data-template.md b/doc/cookbook/data-template.md new file mode 100644 index 0000000..fdc534d --- /dev/null +++ b/doc/cookbook/data-template.md @@ -0,0 +1,29 @@ +# Ajout de données aux templates + +Par défaut, un contrôleur héritant de `AbstractCrudController` ou utilisant `CrudControllerTrait` retourne aux templates l'objet Crud (nom de variable Twig: `crud`). + +Pour passer des variables supplémentaires aux templates Twig, peuvent être surchargées les méthodes : +* `beforeBuild` : Ajouter des données avant la génération de la requête Doctrine et du paginator +* `afterBuild` : Ajouter des données après la génération de la requête Doctrine et du paginator + +Exemple : + +```php +createCrudConfig('my_crud'); //Passé en argument: Nom du CRUD + $crudConfig->addColumn(['id' => 'id', 'alias' => 'c1.id', 'label' => 'Id']) + //... + ->setRoute('my_crud_ajax') ++ ->setDisplayResultsOnlyIfSearch(true) + //... + + return $crudConfig->getOptions(); + } + + //... +} +``` diff --git a/doc/cookbook/div_ids.md b/doc/cookbook/div_ids.md new file mode 100644 index 0000000..5ad7087 --- /dev/null +++ b/doc/cookbook/div_ids.md @@ -0,0 +1,38 @@ +# Personnaliser les IDs des Divs + +Par défaut, les IDs des DIVs de liste et recherche sont : + +| DIV | ID | +| --- | --- | +| DIV liste des résultats | crud_list | +| DIV formulaire de recherche | crud_search | + +Ces IDs peuvent être changés par les méthodes `setDivIdList` et `setDivIdSearch`: + +```diff +createCrudConfig('my_crud'); //Passé en argument: Nom du CRUD + $crudConfig->addColumn(['id' => 'id', 'alias' => 'c1.id', 'label' => 'Id']) + //... + ->setRoute('my_crud_ajax') ++ ->setDivIdList('my_div1') ++ ->setDivIdSearch('my_div2') + //... + + return $crudConfig->getOptions(); + } + + //... +} +``` diff --git a/doc/cookbook/http.md b/doc/cookbook/http.md new file mode 100644 index 0000000..6d43cc2 --- /dev/null +++ b/doc/cookbook/http.md @@ -0,0 +1,172 @@ +# Utilisation d'un CRUD avec une API HTTP + +Exemple avec [l'API Opendatasoft Correspondance Code INSEE - Code Postal](https://public.opendatasoft.com/explore/dataset/correspondance-code-insee-code-postal/api/) : + +```php +get(HttpClientInterface::class)); + + /* + * Définition du paramètre "dataset" ajouté en GET à la requête. + * + * A différents moments (dans cette méthode et dans la classe du formulaire de recherche), on peut modifier l'objet $queryBuilder : + * + * Pour ajouter des éléments à la requête HTTP, on peut appeler la méthode "addParameter" en lui passant en paramètre + * une instance de l'une des classes suivantes : + * Ecommit\CrudBundle\Crud\Http\QueryBuilderQueryParameter : Paramètre passé en GET (query) + * Ecommit\CrudBundle\Crud\Http\QueryBuilderBodyParameter : Paramètre passé dans le body + * Ecommit\CrudBundle\Crud\Http\QueryBuilderBody : Paramètre unique (sans nom) passé dans le body + * Ecommit\CrudBundle\Crud\Http\QueryBuilderHeaderParameter : Paramètre passé dans l'entête + * + * La méthode "setBodyIsJson" peut être appelée pour définir une requête au format JSON. + */ + $queryBuilder->addParameter(new QueryBuilderQueryParameter('dataset', 'correspondance-code-insee-code-postal')); + + //FACULTATIF - Ajout dans la requête HTTP la gestion de la pagination + $queryBuilder->setPaginationBuilder(function (QueryBuilder $queryBuilder, $page, $resultsPerPage): void { + $start = ($page - 1) * $resultsPerPage; + + $queryBuilder->addParameter(new QueryBuilderQueryParameter('rows', $resultsPerPage)); + $queryBuilder->addParameter(new QueryBuilderQueryParameter('start', $start)); + }); + + //FACULTATIF - Ajout dans la requête HTTP la gestion du tri + $queryBuilder->setOrderBuilder(function (QueryBuilder $queryBuilder, $orders): void { + foreach ($orders as $sort => $sortDirection) { + $sortDirectionParameter = ($sortDirection === Crud::ASC)? '-' : ''; + $queryBuilder->addParameter(new QueryBuilderQueryParameter('sort', $sortDirection.$sort)); + } + }); + + $crudConfig = $this->createCrudConfig('my_http_crud'); + $crudConfig->addColumn(['id' => 'name', 'alias' => 'fields.nom_comm', 'label' => 'Nom', 'sortable' => false]) + ->addColumn(['id' => 'postal_code', 'alias' => 'fields.postal_code', 'label' => 'Postal code', 'sortable' => false]) + ->addColumn(['id' => 'population', 'alias' => 'fields.population', 'label' => 'Population', 'alias_sort' => 'population']) + ->addColumn(['id' => 'timestamp', 'alias' => 'record_timestamp', 'label' => 'Timestamp', 'sortable' => false]) + ->setQueryBuilder($queryBuilder) + ->setMaxPerPage([2, 5, 10], 5) + ->setDefaultSort('population', Crud::DESC) + ->setRoute('my_http_crud_ajax') + ->setBuildPaginator(function (QueryBuilder $queryBuilder, $page, $resultsPerPage) { + //Appel HTTP + décode du JSON de la réponse + $response = json_decode($queryBuilder->getResponse($page, $resultsPerPage)->getContent()); + + //Création du paginator + $paginator = new ArrayPaginator([ + 'page' => $page, + 'max_per_page' => $resultsPerPage, + 'data' => $response->records, + 'count' => $response->nhits, + ]); + + return $paginator; + }) + ->createSearchForm(new CitySearcher()) + ->setPersistentSettings(true); + + return $crudConfig->getOptions(); + } + + protected function getTemplateName(string $action): string + { + return sprintf('my_http_crud/%s.html.twig', $action); + } + + /** + * @Route("/my-http-crud", name="my_http_crud") + */ + public function crudAction() + { + return $this->getCrudResponse(); + } + + /** + * @Route("/my-http-crud/ajax", name="my_http_crud_ajax") + */ + public function ajaxCrudAction() + { + return $this->getAjaxCrudResponse(); + } + + public static function getSubscribedServices() + { + return array_merge(parent::getSubscribedServices(), [ + HttpClientInterface::class, + ]); + } +} +``` + + +```php +addField('text', TextType::class, [ + 'label' => 'Query', + 'required' => false, + ]); + } + + public function updateQueryBuilder(mixed $queryBuilder, array $options): void + { + //Traitement de la recherche + + if (null !== $this->text && is_scalar($this->text)) { + $queryBuilder->addParameter(new QueryBuilderQueryParameter('q', $this->text)); + } + } +} +``` diff --git a/doc/cookbook/paginator.md b/doc/cookbook/paginator.md new file mode 100644 index 0000000..ff8b93e --- /dev/null +++ b/doc/cookbook/paginator.md @@ -0,0 +1,80 @@ +# Création manuelle du paginator + +Par défaut, un paginator `Ecommit\DoctrineUtils\Paginator\AbstractDoctrinePaginator` est automatiquement créé. Le comportement par défaut peut être modifié par +la méthode `setBuildPaginator`. + +**Exemple 1 - Options de génération :** + +```diff +createCrudConfig('my_crud'); //Passé en argument: Nom du CRUD + $crudConfig->addColumn(['id' => 'id', 'alias' => 'c1.id', 'label' => 'Id']) + //... + ->setRoute('my_crud_ajax') ++ ->setBuildPaginator([ ++ //Options de Ecommit\DoctrineUtils\Paginator\DoctrinePaginatorBuilder::createDoctrinePaginator ++ 'by_identifier' => 'c1.id', ++ 'count' => [ ++ 'behavior' => 'count_by_alias', ++ 'alias' => 'c1.id', ++ ], ++ ]) + //... + + return $crudConfig->getOptions(); + } + + //... +} +``` + +**Exemple 2 - Création manuelle du paginator :** + +```diff +createCrudConfig('my_crud'); //Passé en argument: Nom du CRUD + $crudConfig->addColumn(['id' => 'id', 'alias' => 'c1.id', 'label' => 'Id']) + //... + ->setRoute('my_crud_ajax') ++ ->setBuildPaginator(function ($queryBuilder, $page, $resultsPerPage) { ++ $queryBuilder->andWhere('c1.active = 1'); ++ $paginator = new DoctrineORMPaginator([ ++ 'query_builder' => $queryBuilder, ++ 'page' => $page, ++ 'max_per_page' => $resultsPerPage, ++ ]); ++ ++ return $paginator; ++ }) + //... + + return $crudConfig->getOptions(); + } + + //... +} +``` diff --git a/doc/cookbook/sort.md b/doc/cookbook/sort.md new file mode 100644 index 0000000..21f6603 --- /dev/null +++ b/doc/cookbook/sort.md @@ -0,0 +1,94 @@ +# Tri par défaut personnalisé + +La méthode `setDefaultSort` permet de définir le tri par défaut : + +```php +createCrudConfig('my_crud'); //Passé en argument: Nom du CRUD + $crudConfig->addColumn(['id' => 'id', 'alias' => 'c1.id', 'label' => 'Id']) + //... + ->setRoute('my_crud_ajax') + ->setDefaultSort('id', Crud::ASC) + //... + + return $crudConfig->getOptions(); + } + + //... +} +``` + +Sur deux colonnes (ici va trier sur c1.id puis c1.name) : + +```php +createCrudConfig('my_crud'); //Passé en argument: Nom du CRUD + $crudConfig->addColumn(['id' => 'id', 'alias' => 'c1.id', 'label' => 'Id', 'alias_sort' => ['c1.id', 'c1.name']]) + //... + ->setRoute('my_crud_ajax') + ->setDefaultSort('id', Crud::ASC) + //... + + return $crudConfig->getOptions(); + } + + //... +} +``` + +Il est aussi possible de définir un tri par défaut personnalisé grâce à la méthode `setDefaultPersonalizedSort` : + +```php +createCrudConfig('my_crud'); //Passé en argument: Nom du CRUD + $crudConfig->addColumn(['id' => 'id', 'alias' => 'c1.id', 'label' => 'Id']) + //... + ->setRoute('my_crud_ajax') + ->setDefaultPersonalizedSort([ + 'c1.purchaseDate' => Crud::DESC, + 'c1.id' => Crud::ASC, + ]) + //... + + return $crudConfig->getOptions(); + } + + //... +} +``` diff --git a/doc/cookbook/template_configuration.md b/doc/cookbook/template_configuration.md new file mode 100644 index 0000000..fdcbd95 --- /dev/null +++ b/doc/cookbook/template_configuration.md @@ -0,0 +1,69 @@ +# Configuration par défaut des templates des CRUD + +Il est possible de définir les options par défaut des fonctions Twig suivantes : +* paginator_links +* crud_paginator_links +* crud_th +* crud_td +* crud_display_settings +* crud_search_form_start +* crud_search_form_submit +* crud_search_form_reset + +Ces options par défaut peuvent être définies : +* Pour l'application +* Pour un CRUD + + +L'ordre de priorité prise en compte pour les options est le suivant : +* Options définies lors de l'appel de la méthode Twig +* Options définies dans les options du CRUD +* Options définies dans l'application +* Options par défaut de EcommitCrudBundle + +## Options définies dans l'application + +```yaml +#config/packages/ecommit_crud.yaml +ecommit_crud: + twig_functions_configuration: + #Nom de la fonction Twig + crud_td: + #Options par défaut + escape: false +``` + +## Options définies dans le CRUD + +```diff +createCrudConfig('my_crud'); //Passé en argument: Nom du CRUD + $crudConfig->addColumn(['id' => 'id', 'alias' => 'c1.id', 'label' => 'Id']) + //... + ->setRoute('my_crud_ajax') ++ ->setTwigFunctionsConfiguration([ ++ //Nom de la fonction Twig ++ 'crud_td' => [ ++ //Options par défaut ++ 'escape' => false, ++ ], ++ ]) + //... + + return $crudConfig->getOptions(); + } + + //... +} +``` diff --git a/doc/cookbook/virtual_columns.md b/doc/cookbook/virtual_columns.md new file mode 100644 index 0000000..c7302c6 --- /dev/null +++ b/doc/cookbook/virtual_columns.md @@ -0,0 +1,37 @@ +# Colonnes virtuelles + +Des colonnes virtuelles peuvent être ajoutées au CRUD. Une colonne virtuelle ne s'affiche pas dans la liste des +résultats mais les utilisateurs peuvent faire des recherches dessus. + +Pour ajouter une colonne virtuelle, la méthode `addVirtualColumn` doit être utilisée. + +Cette méthode prend comme paramètre un tableau d'options : +* **id** : Id de la colonne (Nous donnons un ID à la colonne). Cet ID est totalement indépendant des noms de colonnes DQL/SQL. **Dans ce document, à chaque fois que nous parlerons de l'ID d'une colonne, c'est ce paramètre qui sera concerné. Requis** +* **alias** : Alias Doctrine (du query builder) utilisé pour la requête Doctrine **Requis** + +```diff +createCrudConfig('my_crud'); //Passé en argument: Nom du CRUD + $crudConfig->addColumn(['id' => 'id', 'alias' => 'c1.id', 'label' => 'Id']) + //... ++ ->addVirtualColumn(['id' => my_virtual_column', 'alias' => c1.name']) + ->setRoute('my_crud_ajax') + //... + + return $crudConfig->getOptions(); + } + + //... +} +``` diff --git a/doc/crud.md b/doc/crud.md new file mode 100644 index 0000000..521a677 --- /dev/null +++ b/doc/crud.md @@ -0,0 +1,412 @@ +# Création d'un CRUD + +## Introduction + +Supposons que notre projet possède ces 2 entités Doctrine : + +```php +name; + } + + //Getters and Setters + + //... +} +``` + +## Création du contrôleur + +Nous devons créer une classe contrôleur héritant la classe abstraite `Ecommit\CrudBundle\Controller\AbstractCrudController`. + +Notre classe doit surcharger les méthodes abstraites : +* getCrudOptions +* getTemplateName + +```php +getDoctrine()->getManager(); + $queryBuilder = $em->createQueryBuilder(); + $queryBuilder->from(Car::class, 'c1') + ->select('c1, c2') + ->innerJoin('c1.category', 'c2'); + + $crudConfig = $this->createCrudConfig('my_crud'); //Passé en argument: Nom du CRUD + $crud->addColumn(['id' => 'id', 'alias' => 'c1.id', 'label' => 'Id']) + ->addColumn(['id' => 'name', 'alias' => 'c1.name', 'label' => 'Name']) + ->addColumn(['id' => 'category', 'alias' => 'c2.name', 'label' => 'Category', 'alias_search' => 'c2.id']) + ->addColumn(['id' => 'purchase_date', 'alias' => 'c1.purchaseDate', 'label' => 'Purchase date') + ->addColumn(['id' => 'active', 'alias' => 'c1.active', 'label' => 'Active') + ->setQueryBuilder($queryBuilder) + ->setMaxPerPage([2, 5, 10], 5) + ->setDefaultSort('id', Crud::ASC) + ->setRoute('my_crud_ajax') + ->setPersistentSettings(true); //Enregistre les paramètres d'affichage en base de données. Par défaut: false + + return $crudConfig->getOptions(); + } + + protected function getTemplateName(string $action): string + { + return sprintf('my_crud/%s.html.twig', $action); + } + + /** + * @Route("/my-crud", name="my_crud") + */ + public function crudAction() + { + return $this->getCrudResponse(); + } + + /** + * @Route("/my-crud/ajax", name="my_crud_ajax") + */ + public function ajaxCrudAction() + { + return $this->getAjaxCrudResponse(); + } +} +``` + +Explications de getCrudOptions(): + +* Avec `$this->createCrudConfig('my_crud')`, nous créons une configuration de CRUD (instance de `Ecommit\CrudBundle\Crud\CrudConfig`) dont le nom est `my_crud`. +* Nous déclarons chaque colonne du CRUD par la méthode `addColumn()`. Cette méthode prend en paramètre un tableau d'options : + * **id** : Id de la colonne (Nous donnons un ID à la colonne). Cet ID est totalement indépendant des noms de colonnes DQL/SQL. **Dans ce document, à chaque fois que nous parlerons de l'ID d'une colonne, c'est ce paramètre qui sera concerné. Requis** + * **alias** : Alias Doctrine (du query builder) utilisé pour la requête Doctrine **Requis** + * **label** : Label donné à la colonne (le label sera traduit si traduction est active sur le projet) + * **sortable**: Booléen qui définit si on active (ou non) le tri sur cette colonne **Défaut: True** + * **displayed_by_default**: Booléen qui définit si on affiche (ou non) cette colonne par défaut **Défaut: True** + * **alias_search**: Alias Doctrine utilisé lors de la recherche DQL/SQL. Si pas défini, utilise l'alias Doctrine défini par l'option `alias` + * **alias_sort**: Alias Doctrine (chaine de caractères ou tableau de chaines de caractères) utilisé(s) lors du tri sur cette colonne. Si pas défini, utilise l'alias Doctrine défini par l'option `alias` +* Nous donnons la requête Doctrine (sous forme d'objet QueryBuilder ou d'une fonction anonyme) avec la méthode setQueryBuilder() Le moteur du CRUD modifiera automatiquement cette requête, en fonction des actions demandées par l'utilisateur +* Nous définissions les paramètres du nombre de pages avec la méthode `setMaxPerPage()`. Cette méthode prend 2 paramètres : + * Un tableau contenant les différents nombres possibles du nombre de résultats par page. **Requis** + * Le nombre de résultats par page, par défaut. **Requis** +* Nous définissions le tri par défaut par la méthode `setDefaultSort`. Cette méthode prend 2 paramètres : + * L'id de la colonne utilisée pour le tri par défaut **Requis** + * Le sens du tri par défaut (`Crud::ASC` ou `Crud::DESC`). **Requis** +* On définit par la fonction `setRoute` la route Ajax utilisée pour mettre à jour notre liste +* On active par la fonction `setPersistentSettings` l'enregistrement des propriétés d'affichage des utilisateurs en base de données. Par défaut, désactivé. +* On retourne notre objet créé pour la création du CRUD + +## Ajout templates + +```twig +{# templates/my_crud/index.html.twig #} +{% extends 'layout.html.twig' %} + +{% block content %} +
    + {% include 'my_crud/list.html.twig' %} +
    +{% endblock %} +``` + +```twig +{# templates/my_crud/list.html.twig #} +{% if crud.paginator %} +
    + + + + {{ crud_th('id', crud) }} + {{ crud_th('name', crud) }} + {{ crud_th('category', crud) }} + {{ crud_th('purchase_date', crud) }} + {{ crud_th('active', crud) }} + + + + {% for car in crud.paginator %} + + {{ crud_td('id', crud, car.id) }} + {{ crud_td('name', crud, car.name) }} + {{ crud_td('category', crud, car.category.name) }} + {{ crud_td('purchase_date', crud, car.purchaseDate|date('Y-m-d')) }} + {{ crud_td('active', crud, (car.active) ? 'yes' : 'no') }} + + {% endfor %} + +
    +
    + + +
    + {{ 'pagination_count_results'|trans({'count': crud.paginator.countResults}) }}
    + {{ 'pagination_indices'|trans({'first': crud.paginator.firstIndice, 'last': crud.paginator.lastIndice}) }} - + {{ 'pagination_pages'|trans({'page': crud.paginator.page, 'lastPage': crud.paginator.lastPage}) }} +
    + + {{ crud_paginator_links(crud) }} +{% endif %} + +{{ crud_display_settings(crud) }} +``` + +Vous pouvez ajouter par exemple les traductions suivantes (+intl-icu) à votre projet : + +```yaml +# translations/messages+intl-icu.fr.yaml +pagination_count_results: > + {count, plural, + =0 {Pas de résultat} + one {1 résultat trouvé} + other {# résultats trouvés} + } +pagination_indices: Résultats {first}-{last} +pagination_pages: Page {page}/{lastPage} +``` + +## Ajout formulaire de recherche (FACULTATIF) + +La classe Searcher représente les champs du formulaire de recherche. + +Si vous ne désirez pas activer le formulaire de recherche sur le CRUD, passez ce paragraphe. + +Notre classe doit hériter de `Ecommit\CrudBundle\Form\Searcher\AbstractSearcher` (ou implémenter `Ecommit\CrudBundle\Form\Searcher\SearcherInterface`) : + +```php +addFilter('id', Filter\IntegerFilter::class, [ + 'comparator' => Filter\IntegerFilter::EQUAL, + ]); + + $builder->addFilter('name', Filter\TextFilter::class, [ + 'must_begin' => true, + ]); + + $builder->addFilter('nameEmpty', Filter\NullFilter::class, [ + //On ne souhaite pas relier le filtre "nameEmpty" à la colonne "nameEmpty" du CRUD (qui n'existe pas). + //On précise donc manuellement l'ID de la colonne + 'column_id' => 'name', + 'type_options' => [ + 'label' => 'Name empty', + ], + ]); + + $builder->addFilter('purchaseBeginningDate', Filter\DateFilter::class, [ + 'comparator' => Filter\DateFilter::GREATER_EQUAL, + 'column_id' => 'purchase_date', + 'type_options' => [ + 'label' => 'Purchase date - From' + ], + ]); + + $builder->addFilter('purchaseEndDate', Filter\DateFilter::class, [ + 'comparator' => Filter\DateFilter::SMALLER_EQUAL, + 'column_id' => 'purchase_date', + 'type_options' => [ + 'label' => 'Purchase date - To' + ], + ]); + + $builder->addFilter('active', Filter\BooleanFilter::class); + + $builder->addFilter('category', Filter\EntityFilter::class, [ + 'class' => Category::class, + 'multiple' => 'true', + ]); + } +} +``` + +> **_REMARQUE:_** La liste des différents filtres disponibles (ainsi que leurs configurations) est disponible [ici](references/filters.md) + +> **_REMARQUE:_** Il est aussi possible de faire des recherches plus complexes sans utiliser les filtres pré-définis. [En savoir plus](cookbook/advanced-searcher.md) + +Une fois la classe Searcher créée, nous devons modifier notre contrôleur : + +```diff +createCrudConfig('my_crud'); //Passé en argument: Nom du CRUD + $crudConfig->addColumn(['id' => 'id', 'alias' => 'c1.id', 'label' => 'Id']) + //... + ->setRoute('my_crud_ajax') ++ ->createSearchForm(new CarSearcher()) + //... + + return $crudConfig->getOptions(); + } +} +``` + +Nous devons ensuite créer un nouveau template Twig : + +```twig +{# templates/my_crud/search.html.twig #} +{{ crud_search_form_start(crud) }} + {{ form_row(crud.searchForm.id) }} + {{ form_row(crud.searchForm.name) }} + {{ form_row(crud.searchForm.nameEmpty) }} + {{ form_row(crud.searchForm.purchaseBeginningDate) }} + {{ form_row(crud.searchForm.purchaseEndDate) }} + {{ form_row(crud.searchForm.active) }} + {{ form_row(crud.searchForm.category) }} + +
    + {{ crud_search_form_submit(crud) }} + {{ crud_search_form_reset(crud) }} +
    + +``` + +Et enfin modifier le template principal : + +```twig +{# templates/my_crud/index.html.twig #} +{% extends 'layout.html.twig' %} + +{% block content %} + + +
    + {% include 'my_crud/list.html.twig' %} +
    +{% endblock %} +``` + +> **_REMARQUE:_** [En savoir plus sur la configuration du formulaire de recherche](references/searcher.md) + + + +> **_REMARQUE:_** [En savoir plus sur l'utilisation avancée du CRUD](index.md#fonctionnalités-avancées) diff --git a/doc/index.md b/doc/index.md new file mode 100644 index 0000000..d429ba4 --- /dev/null +++ b/doc/index.md @@ -0,0 +1,33 @@ +# EcommitCrudBundle - Documentation + +## Pour commmencer + +* [Installation](install.md) +* [Création d'un CRUD](crud.md) + +## Références + +* [Options du formulaire de recherche](references/searcher.md) +* [Filtres du formulaire de recherche](references/filters.md) +* [EntityAjaxType](references/entity_ajax_type.md) +* [Fonctions Twig](references/twig.md) +* [Fonctions Ajax](references/ajax.md) +* [Gestionnaire de fenêtre modale](references/modal.md) +* [Callbacks JavaScript](references/js-callbacks.md) + +## Fonctionnalités avancées + +* [Personnalisation avancée de la classe Searcher](cookbook/advanced-searcher.md) +* [Création d'un filtre de recherche](cookbook/create_filter.md) +* [Ajout de données aux templates](cookbook/data-template.md) +* [Affichage des résultats uniquement si recherche envoyée](cookbook/display_results.md) +* [Personnaliser les IDs des Divs](cookbook/div_ids.md) +* [Création manuelle du paginator](cookbook/paginator.md) +* [Utilisation d'un CRUD avec une API HTTP](cookbook/http.md) +* [Tri par défaut personnalisé](cookbook/sort.md) +* [Configuration par défaut des templates des CRUD](cookbook/template_configuration.md) +* [Colonnes virtuelles](cookbook/virtual_columns.md) + +## Migrations + +* [Mise à jour de 2.6 vers 3.0](upgrade/3.0.md) diff --git a/doc/install.md b/doc/install.md new file mode 100644 index 0000000..b83ebf9 --- /dev/null +++ b/doc/install.md @@ -0,0 +1,116 @@ +# Installation + +Prérequis : +* Projet Symfony fonctionnel +* Entité utilisateur avec Doctrine +* NodeJS +* Webpack Encore +* Pour une compatibilité avec un maximum de navigateurs Internet, il est conseillé : + * De configurer Webpack Encore pour activer Babel et core-js + * D'utiliser un [polyfill pour Fetch](https://github.com/github/fetch) +* Un gestionnaire de thème chargé par Webpack Encore parmi : + * Bootstrap 3 + * Bootstrap 4 + * Bootstrap 5 + * Votre thème personnalisé (créer un thème Twig qui hérite `@EcommitCrud/Theme/base.html.twig`) +* Un gestionnaire d'icones chargé par Webpack Encore parmi : + * Fontawesome 4 + * Fontawesome 5 Solid + * Fontawesome 6 Solid + * Votre thème personnalisé (créer un thème Twig qui hérite `@EcommitCrud/IconTheme/base.html.twig`) + +Installez le bundle avec Composer : A la racine de votre projet Symfony, éxécutez la commande suivante : + +```bash +$ composer require ecommit/crud-bundle:3.*@dev +$ npm install --save-dev @ecommit/crud-bundle@file:vendor/ecommit/crud-bundle/assets +``` + +Activez le bundle dans le fichier de configuration `config/bundles.php` de votre projet : + +```php +return [ + //... + Ecommit\CrudBundle\EcommitCrudBundle::class => ['all' => true], + //... +]; +``` + +Ajoutez à votre projet le fichier de configuration `config/packages/ecommit_crud.yaml` : + +```yaml +ecommit_crud: + #Theme + #Themes disponibles : + #@EcommitCrud/Theme/base.html.twig + #@EcommitCrud/Theme/bootstrap3.html.twig (boostrap3 requis) + #@EcommitCrud/Theme/bootstrap4.html.twig (boostrap4 requis) + #@EcommitCrud/Theme/bootstrap5.html.twig (boostrap5 requis) + #Ou faire son propre terme (doit hériter de l'un des thèmes précédents) + theme: '@EcommitCrud/Theme/bootstrap5.html.twig' + + #Theme pour les icones + #Themes disponibles : + #@EcommitCrud/IconTheme/base.html.twig + #@EcommitCrud/IconTheme/fontawesome4.html.twig (fontawesome4 requis) + #@EcommitCrud/IconTheme/fontawesome5_solid.html.twig (fontawesome5 Solid requis) + #@EcommitCrud/IconTheme/fontawesome6_solid.html.twig (fontawesome5 Solid requis) + #Ou faire son propre terme (doit hériter de l'un des thèmes précédents) + icon_theme: '@EcommitCrud/IconTheme/fontawesome6_solid.html.twig' +``` + +Votre entité Doctrine "utilisateur" doit implémenter l'interface `Ecommit\CrudBundle\Entity\UserCrudInterface`. Exemple : + +```php +
  • `data`: Donnée de la réponse. Voir option `responseDataType` pour le format attendu
  • `response`: Objet de type [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response)
  • | Non | | +| onError | [Callback(s)](js-callbacks.md#définition-des-callbacks) lancé(s) en cas d'erreur (code de réponse HTTP ≠ 200-299 ou erreur avant la réponse). `Function(statusText, response)`
    • `statusText`: [statusText](https://developer.mozilla.org/en-US/docs/Web/API/Response/statusText) de la réponse (si réponse). Sinon mesage d'erreur au format string
    • `response`: Objet de type [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) si réponse, nulm sinon
    | Non | | +| onComplete | [Callback(s)](js-callbacks.md#définition-des-callbacks) lancé(s) après les callbacks `onSuccess` ou `onError`. `Function(statusText, response)`. Voir options `onSuccess` et `onError` pour détails sur `statusText` et `response` | Non | | +| successfulResponseRequired | En cas de code de réponse HTTP ≠ 200-299:
    • Si cette option est vraie, la promesse est alors rejetée
    • Sinon elle est résolue
    | Non | false | +| responseDataType | Format de donnée de réponse attendu dans le callback `OnSuccess`. Valeurs disponibles:
    • `text`: Format string
    • `json`: Objet JavaScript converti depuis une réponse JSON
    • Autre valeur: NULL (donnée de réponse à récupérer manuellement dans l'objet Response)
    | Non | text | +| method | Méthode HTTP | Non | POST | +| body | Données envoyées dans le corps de la requête. Types de données acceptés:
    • String
    • [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData)
    • Objet
    | Non | | +| query | Paramètres à rajouter dans l'URL | Non | { } | +| cache | Utilise ou non le cache | Non | false | +| options | Tableau d'options de la fonction [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/fetch) | Non | { } | + +**Promesses :** + +La fonction `sendRequest` retourne une promesse. +* En cas de code de réponse HTTP 200-299, la promesse résout l'objet [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) représentant la réponse à la requête. +* En cas de code de réponse HTTP autre 200-299: + * Si l'option `successfulResponseRequired` est à `false`, alors la promesse résout l'objet [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) représentant la réponse à la requête. + * Sinon la promesse est rejetée. +* En cas d'annulation de la requête (par le callback `onBeforeSend` ou les événements `ec-crud-ajax` / `ec-crud-ajax-before-send`), la promesse résout une valeur nulle. +* En cas d'erreur lors de l'exécution de la requête, la promesse est rejetée. +* En cas d'erreur lors la lecture de la réponse, la promesse est rejetée. +* En cas d'erreur de configuration, la promesse est rejetée. + + +**Événements :** + +| Événement | Objet | Description | Propriétés disponibles | +|--------------------------|------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------| +| ec-crud-ajax | [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) (cancelable) | Appelé avant l'exécution de la requête, avant la résolution des options. Peut annuler la requête avec `event.preventDefault()` |
    • event.detail.options
    | +| ec-crud-ajax-before-send | [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) (cancelable) | Appelé avant l'exécution de la requête, après la résolution des options. Peut annuler la requête avec `event.preventDefault()` |
    • event.detail.options
    | +| ec-crud-ajax-on-success | [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) | Même fonctionnement que callback `onSuccess` |
    • event.detail.data
    • event.detail.response
    | +| ec-crud-ajax-on-error | [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) | Même fonctionnement que callback `onError` |
    • event.detail.statusText
    • event.detail.response
    | +| ec-crud-ajax-on-complete | [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent) | Même fonctionnement que callback `onComplete` |
    • event.detail.statusText
    • event.detail.response
    | + + + +### click + +Fonction permettant de faire une requête AJAX lors d'un clic sur élément du DOM. + +L'élément du DOM doit avoir l'attribut HTML `data-ec-crud-toggle="ajax-click"`. + +Toutes les options de la fonction `sendRequest` peuvent être utilisées en les passant par les attributs `data-`. Pour cela: +* Préfixer chaque option par `data-ec-crud-ajax-` +* Les options d'origine (en JavaScript) de `sendRequest` sont en camelCase. Pour leur écriture par des attributs HTML, remplacer chaque nouveau mot en majuscule par un tiret. +Exemple: L'équivalent de l'option `updateMode` est `data-ec-crud-ajax-update-mode` en attribut HTML. + +Exemple : + +```html + +``` + +### link + +Fonction permettant de faire une requête AJAX lors d'un clic sur lien. + +Toutes les options de la fonction `sendRequest` peuvent être utilisées en les passant par les attributs `data-`. Pour cela: +* Préfixer chaque option par `data-ec-crud-ajax-` +* Les options d'origine (en JavaScript) de `sendRequest` sont en camelCase. Pour leur écriture par des attributs HTML, remplacer chaque nouveau mot en majuscule par un tiret. + Exemple: L'équivalent de l'option `updateMode` est `data-ec-crud-ajax-update-mode` en attribut HTML. + +#### Mode automatique + +Le lien doit avoir l'attribut HTML `data-ec-crud-toggle="ajax-link"`. + +Exemple : + +```html +Go ! +``` + +L'URL utilisée pour la requête Ajax est: +* La valeur de l'attribut `data-ec-crud-ajax-url` (si présent) +* Ou la valeur de `href` + +#### Mode manuel + +```html +Go ! +``` + +```js +import * as ajax from '@ecommit/crud-bundle/js/ajax'; + +//Argument n°1: Lien +//Argument n°2: Options de sendRequest +ajax.link($('#linkToTest'), { + //Options de sendRequest + method: 'GET', +}); +``` + +* L'URL utilisée pour la requête Ajax est: + * La valeur de l'attribut `data-ec-crud-ajax-url` (si présent) + * Ou la valeur de l'option `url` de la fonction `link` (si présent) + * Ou la valeur de `href` +* Les attributs `data-ec-crud-ajax-*` (si présents) écrasent les options de la fonction `link` (si présent) +* La fonction retourne la promesse générée par `sendRequest` + +### sendForm + +Fonction permettant de faire une requête AJAX depuis l'envoi d'un formulaire. + +Toutes les options de la fonction `sendRequest` peuvent être utilisées en les passant par les attributs `data-`. Pour cela: +* Préfixer chaque option par `data-ec-crud-ajax-` +* Les options d'origine (en JavaScript) de `sendRequest` sont en camelCase. Pour leur écriture par des attributs HTML, remplacer chaque nouveau mot en majuscule par un tiret. + Exemple: L'équivalent de l'option `updateMode` est `data-ec-crud-ajax-update-mode` en attribut HTML. + +#### Mode automatique + +Le formulaire doit avoir l'attribut HTML `data-ec-crud-toggle="ajax-form"`. + +Exemple : + +```html +
    +``` + +* L'URL utilisée pour la requête Ajax est: + * La valeur de l'attribut `data-ec-crud-ajax-url` (si présent) + * Ou la valeur de `action` +* La méthode utilisée pour la requête Ajax est: + * La valeur de l'attribut `data-ec-crud-ajax-method` (si présent) + * Ou la valeur de `method` +* Les données envoyées lors de la requête Ajax sont: + * La valeur de l'attribut `data-ec-crud-ajax-data` (si présent) + * Ou les données du formulaire + +#### Mode manuel + +```html + +``` + +```js +import * as ajax from '@ecommit/crud-bundle/js/ajax'; + +//Argument n°1: Formulaire +//Argument n°2: Options de sendRequest +ajax.sendForm($('#formToTest'), { + //Options de sendRequest + cache: true, +}); +``` + +* L'URL utilisée pour la requête Ajax est: + * La valeur de l'attribut `data-ec-crud-ajax-url` (si présent) + * Ou la valeur de l'option `url` de la fonction `sendForm` (si présent) + * Ou la valeur de `action` +* La méthode utilisée pour la requête Ajax est: + * La valeur de l'attribut `data-ec-crud-ajax-method` (si présent) + * Ou la valeur de l'option `method` de la fonction `sendForm` (si présent) + * Ou la valeur de `method` +* Les données envoyées lors de la requête Ajax sont: + * La valeur de l'attribut `data-ec-crud-ajax-data` (si présent) + * Ou la valeur de l'option `data` de la fonction `sendForm` (si présent) + * Ou les données du formulaire +* Les attributs `data-ec-crud-ajax-*` (si présents) écrasent les options de la fonction `sendForm` (si présent) +* La fonction retourne la promesse générée par `sendRequest` + + +### updateDom + +Permet la mise à jour du DOM. + +```js +import * as ajax from '@ecommit/crud-bundle/js/ajax'; + +//Argument n°1: Element à mettre à jour +//Argument n°2: Méthode de mise à jour +//Argument n°3: Contenu +ajax.updateDom($('#myDiv'), 'update', 'Hello world'); +``` + +Méthodes disponibles pour la mise à jour : + +| Méthode | Description | +|---------|-------------------------------------------------------------------------| +| update | Modifie le contenu de l'élément par le nouveau contenu | +| before | Ajoute le nouveau contenu avant l'élément | +| after | Ajoute le nouveau contenu après l'élément | +| prepend | Modifie le contenu de l'élément en ajoutant le nouveau contenu au début | +| append | Modifie le contenu de l'élément en ajoutant le nouveau contenu à la fin | + +La mise à jour du DOM utilise la fonction [innerHTML](https://developer.mozilla.org/en-US/docs/Web/API/Element/innerHTML). +Si le nouveau contenu contient des balises ` + {{ encore_entry_link_tags('app') }} + {% block stylesheets %}{% endblock %} + + + {% block content %}{% endblock %} + {{ encore_entry_script_tags('app') }} + {% block javascripts %}{% endblock %} + + diff --git a/tests/Functional/App/templates/login.html.twig b/tests/Functional/App/templates/login.html.twig new file mode 100644 index 0000000..8a596a2 --- /dev/null +++ b/tests/Functional/App/templates/login.html.twig @@ -0,0 +1,9 @@ + + + + + + + + +
    diff --git a/tests/Functional/App/templates/render.html.twig b/tests/Functional/App/templates/render.html.twig new file mode 100644 index 0000000..d86bac9 --- /dev/null +++ b/tests/Functional/App/templates/render.html.twig @@ -0,0 +1 @@ +OK diff --git a/tests/Functional/App/templates/theme.html.twig b/tests/Functional/App/templates/theme.html.twig new file mode 100644 index 0000000..a705f98 --- /dev/null +++ b/tests/Functional/App/templates/theme.html.twig @@ -0,0 +1,3 @@ +{% block hello_world %} + Hello world +{% endblock %} diff --git a/tests/Functional/App/templates/user/index.html.twig b/tests/Functional/App/templates/user/index.html.twig new file mode 100644 index 0000000..cfafad4 --- /dev/null +++ b/tests/Functional/App/templates/user/index.html.twig @@ -0,0 +1,11 @@ +{% extends 'layout.html.twig' %} + +{% block content %} + + +
    + {% include 'user/list.html.twig' %} +
    +{% endblock %} diff --git a/tests/Functional/App/templates/user/list.html.twig b/tests/Functional/App/templates/user/list.html.twig new file mode 100644 index 0000000..dadff8a --- /dev/null +++ b/tests/Functional/App/templates/user/list.html.twig @@ -0,0 +1,33 @@ +{% if crud.paginator %} +
    + + + + {{ crud_th('username', crud) }} + {{ crud_th('firstName', crud) }} + {{ crud_th('lastName', crud) }} + + + + {% for user in crud.paginator %} + + {{ crud_td('username', crud, user.username|lower) }} + {{ crud_td('firstName', crud, user.firstName|capitalize) }} + {{ crud_td('lastName', crud, user.lastName|capitalize) }} + + {% endfor %} + +
    +
    + + {% if test_before_after_build is defined %}
    TEST BEFORE AFTER BUILD {{ test_before_after_build }}
    {% endif %} + +
    + {% trans with {'%first%': crud.paginator.firstIndice, '%last%' : crud.paginator.lastIndice} %}Results %first%-%last%{% endtrans %} - + {% trans with {'%page%': crud.paginator.page, '%lastPage%' : crud.paginator.lastPage} %}Page %page%/%lastPage%{% endtrans %} +
    + + {{ crud_paginator_links(crud) }} +{% endif %} + +{{ crud_display_settings(crud, {modal: false}) }} diff --git a/tests/Functional/App/templates/user/search.html.twig b/tests/Functional/App/templates/user/search.html.twig new file mode 100644 index 0000000..197d222 --- /dev/null +++ b/tests/Functional/App/templates/user/search.html.twig @@ -0,0 +1,10 @@ +{{ crud_search_form_start(crud, {}, {'class': 'form-inline'}) }} + {{ form_row(crud.searchForm.username) }} + {{ form_row(crud.searchForm.firstName) }} + {{ form_row(crud.searchForm.lastName) }} + +
    + {{ crud_search_form_submit(crud) }} + {{ crud_search_form_reset(crud) }} +
    + diff --git a/tests/Functional/Controller/TestCrudControllerTest.php b/tests/Functional/Controller/TestCrudControllerTest.php new file mode 100644 index 0000000..0c0cd40 --- /dev/null +++ b/tests/Functional/Controller/TestCrudControllerTest.php @@ -0,0 +1,433 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Ecommit\CrudBundle\Tests\Functional\Controller; + +use Ecommit\CrudBundle\Crud\Crud; +use Symfony\Component\Panther\Client; +use Symfony\Component\Panther\DomCrawler\Crawler; +use Symfony\Component\Panther\PantherTestCase; + +class TestCrudControllerTest extends PantherTestCase +{ + use TestTrait; + + public const URL = '/user/'; + public const SESSION_NAME = 'user'; + public const SEARCH_IN_LIST = null; + + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + + static::$defaultOptions['browser'] = static::FIREFOX; + } + + public function testList(): Client + { + $client = static::createPantherClient(); + $client->request('GET', static::URL); + + $this->assertSame([5, 2], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame([1, 3], $this->getPagination($client->getCrawler())); + $this->assertSame(['first_name', Crud::ASC], $this->getSort($client->getCrawler())); + $this->assertSame('AudeJavel', $this->getFirstUsername($client->getCrawler())); + $this->checkBeforeAndAfterBuild($client->getCrawler()); + + return $client; + } + + /** + * @depends testList + */ + public function testChangeSortDirection(Client $client): Client + { + $link = $client->getCrawler()->filterXPath('//table[@class="result"]/thead/tr/th/a[contains(text(), "first_name")]'); + $link->click(); + $this->waitForAjax($client); + + $this->assertSame([5, 2], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame([1, 3], $this->getPagination($client->getCrawler())); + $this->assertSame(['first_name', Crud::DESC], $this->getSort($client->getCrawler())); + $this->assertSame('YvonEmbavé', $this->getFirstUsername($client->getCrawler())); + $this->checkBeforeAndAfterBuild($client->getCrawler()); + + return $client; + } + + /** + * @depends testChangeSortDirection + */ + public function testChangeSortColumn(Client $client): Client + { + $link = $client->getCrawler()->filterXPath('//table[@class="result"]/thead/tr/th/a[text()="last_name"]'); + $link->click(); + $this->waitForAjax($client); + + $this->assertSame([5, 2], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame([1, 3], $this->getPagination($client->getCrawler())); + $this->assertSame(['last_name', Crud::DESC], $this->getSort($client->getCrawler())); + $this->assertSame('ClémentTine', $this->getFirstUsername($client->getCrawler())); + $this->checkBeforeAndAfterBuild($client->getCrawler()); + + return $client; + } + + /** + * @depends testChangeSortColumn + */ + public function testChangeDisplayedColumns(Client $client): Client + { + $button = $client->getCrawler()->filterXPath('//button[contains(.,"Display Settings")]'); + $button->first()->click(); + $client->waitForVisibility('#ec-crud-display-settings-'.static::SESSION_NAME); + + $client->getCrawler()->filterXPath('//form[@name="crud_display_settings_'.static::SESSION_NAME.'"]/descendant::input[@value="username"]')->click(); + $client->getCrawler()->filterXPath('//form[@name="crud_display_settings_'.static::SESSION_NAME.'"]/descendant::button[@type="submit"]')->click(); + $this->waitForAjax($client); + $client->waitForInvisibility('#ec-crud-display-settings-'.static::SESSION_NAME); + + $this->assertSame([5, 3], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame([1, 3], $this->getPagination($client->getCrawler())); + $this->assertSame(['last_name', Crud::DESC], $this->getSort($client->getCrawler())); + $this->assertSame('ClémentTine', $this->getFirstUsername($client->getCrawler())); + $this->checkBeforeAndAfterBuild($client->getCrawler()); + + return $client; + } + + /** + * @depends testChangeDisplayedColumns + */ + public function testChangePerPage(Client $client): Client + { + $button = $client->getCrawler()->filterXPath('//button[contains(., "Display Settings")]'); + $button->first()->click(); + + $form = $client->getCrawler()->filterXPath('//form[@name="crud_display_settings_'.static::SESSION_NAME.'"]/descendant::button[@type="submit"]')->form(); + $form['crud_display_settings_'.static::SESSION_NAME.'[resultsPerPage]'] = 10; + $client->submit($form); + $this->waitForAjax($client); + + $this->assertSame([10, 3], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame([1, 2], $this->getPagination($client->getCrawler())); + $this->assertSame(['last_name', Crud::DESC], $this->getSort($client->getCrawler())); + $this->assertSame('ClémentTine', $this->getFirstUsername($client->getCrawler())); + $this->checkBeforeAndAfterBuild($client->getCrawler()); + + return $client; + } + + /** + * @depends testChangePerPage + */ + public function testSessionValuesAfterChangeSortAndSettings(Client $client): Client + { + $client->request('GET', static::URL); + + $this->assertSame([10, 3], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame([1, 2], $this->getPagination($client->getCrawler())); + $this->assertSame(['last_name', Crud::DESC], $this->getSort($client->getCrawler())); + $this->assertSame('ClémentTine', $this->getFirstUsername($client->getCrawler())); + $this->checkBeforeAndAfterBuild($client->getCrawler()); + + return $client; + } + + /** + * @depends testSessionValuesAfterChangeSortAndSettings + */ + public function testChangePage(Client $client): Client + { + $page = $client->getCrawler()->filterXPath('//ul[@class="ec-crud-pagination"]/li/a[text()="2"]'); + $page->first()->click(); + $this->waitForAjax($client); + + $this->assertSame([1, 3], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame([2, 2], $this->getPagination($client->getCrawler())); + $this->assertSame(['last_name', Crud::DESC], $this->getSort($client->getCrawler())); + $this->assertSame('JudieCieux', $this->getFirstUsername($client->getCrawler())); + $this->checkBeforeAndAfterBuild($client->getCrawler()); + + return $client; + } + + /** + * @depends testChangePage + */ + public function testSessionValuesAfterChangePage(Client $client): Client + { + $client->request('GET', static::URL); + + $this->assertSame([1, 3], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame([2, 2], $this->getPagination($client->getCrawler())); + $this->assertSame(['last_name', Crud::DESC], $this->getSort($client->getCrawler())); + $this->assertSame('JudieCieux', $this->getFirstUsername($client->getCrawler())); + $this->checkBeforeAndAfterBuild($client->getCrawler()); + + return $client; + } + + /** + * @depends testSessionValuesAfterChangePage + */ + public function testSearch(Client $client): Client + { + $form = $client->getCrawler()->filterXPath('//div[@id="crud_search"]/descendant::button[@type="submit" and contains(text(), "Search")]')->form(); + $form['crud_search_'.static::SESSION_NAME.'[firstName]'] = 'Henri'; + $client->submit($form); + $this->waitForAjax($client); + + $this->assertSame([2, 3], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame([1, 1], $this->getPagination($client->getCrawler())); + $this->assertSame(['last_name', Crud::DESC], $this->getSort($client->getCrawler())); + $this->assertSame('HenriPoste', $this->getFirstUsername($client->getCrawler())); + $this->checkBeforeAndAfterBuild($client->getCrawler()); + + return $client; + } + + /** + * @depends testSearch + */ + public function testSessionValuesAfterSearch(Client $client): Client + { + $client->request('GET', static::URL); + + $this->assertSame([2, 3], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame([1, 1], $this->getPagination($client->getCrawler())); + $this->assertSame(['last_name', Crud::DESC], $this->getSort($client->getCrawler())); + $this->assertSame('HenriPoste', $this->getFirstUsername($client->getCrawler())); + $this->checkBeforeAndAfterBuild($client->getCrawler()); + + return $client; + } + + /** + * @depends testSessionValuesAfterSearch + */ + public function testSearchWithoutFilter(Client $client): Client + { + $form = $client->getCrawler()->filterXPath('//div[@id="crud_search"]/descendant::button[@type="submit" and contains(text(), "Search")]')->form(); + $form['crud_search_'.static::SESSION_NAME.'[lastName]'] = 'Plait'; + $client->submit($form); + $this->waitForAjax($client); + + $this->assertSame([1, 3], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame([1, 1], $this->getPagination($client->getCrawler())); + $this->assertSame(['last_name', Crud::DESC], $this->getSort($client->getCrawler())); + $this->assertSame('HenriPlait', $this->getFirstUsername($client->getCrawler())); + $this->checkBeforeAndAfterBuild($client->getCrawler()); + + return $client; + } + + /** + * @depends testSearchWithoutFilter + */ + public function testResetSearch(Client $client): Client + { + $button = $client->getCrawler()->filterXPath('//div[@id="crud_search"]/descendant::button[contains(text(), "Reset")]'); + $button->first()->click(); + $this->waitForAjax($client); + + $this->assertSame([10, 3], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame([1, 2], $this->getPagination($client->getCrawler())); + $this->assertSame(['last_name', Crud::DESC], $this->getSort($client->getCrawler())); + $this->assertSame('ClémentTine', $this->getFirstUsername($client->getCrawler())); + $this->checkBeforeAndAfterBuild($client->getCrawler()); + + return $client; + } + + /** + * @depends testResetSearch + */ + public function testSessionValuesAfterResetSearch(Client $client): Client + { + $client->request('GET', static::URL); + + $this->assertSame([10, 3], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame([1, 2], $this->getPagination($client->getCrawler())); + $this->assertSame(['last_name', Crud::DESC], $this->getSort($client->getCrawler())); + $this->assertSame('ClémentTine', $this->getFirstUsername($client->getCrawler())); + $this->checkBeforeAndAfterBuild($client->getCrawler()); + + return $client; + } + + /** + * @depends testSessionValuesAfterResetSearch + */ + public function testResetSettings(Client $client): Client + { + $this->assertSame(['username', 'firstName', 'lastName'], $this->getCheckedColumns($client->getCrawler())); + + $button = $client->getCrawler()->filterXPath('//button[contains(., "Display Settings")]'); + $button->first()->click(); + + $button = $client->getCrawler()->filterXPath('//form[@name="crud_display_settings_'.static::SESSION_NAME.'"]/descendant::button[contains(., "Reset display settings")]'); + $button->first()->click(); + $this->waitForAjax($client); + + $this->assertSame([5, 2], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame([1, 3], $this->getPagination($client->getCrawler())); + $this->assertSame(['first_name', Crud::ASC], $this->getSort($client->getCrawler())); + $this->assertSame('AudeJavel', $this->getFirstUsername($client->getCrawler())); + $this->checkBeforeAndAfterBuild($client->getCrawler()); + $this->assertSame(['firstName', 'lastName'], $this->getCheckedColumns($client->getCrawler())); + + return $client; + } + + /** + * @depends testResetSettings + */ + public function testSessionValuesAfterResetSettings(Client $client): Client + { + $client->request('GET', static::URL); + + $this->assertSame(['firstName', 'lastName'], $this->getCheckedColumns($client->getCrawler())); + $this->assertSame([5, 2], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame([1, 3], $this->getPagination($client->getCrawler())); + $this->assertSame(['first_name', Crud::ASC], $this->getSort($client->getCrawler())); + $this->assertSame('AudeJavel', $this->getFirstUsername($client->getCrawler())); + $this->checkBeforeAndAfterBuild($client->getCrawler()); + + return $client; + } + + /** + * @depends testSessionValuesAfterResetSettings + */ + public function testCheckAndUncheckAllColumns(Client $client): Client + { + $client->request('GET', static::URL); + + $button = $client->getCrawler()->filterXPath('//button[contains(.,"Display Settings")]'); + $button->first()->click(); + $form = $client->getCrawler()->selectButton('Save')->form(); + $this->assertCount(2, $form->get('crud_display_settings_'.static::SESSION_NAME.'[displayedColumns]')->getValue()); + + // Check all + $client->getCrawler()->filterXPath('//form[@name="crud_display_settings_'.static::SESSION_NAME.'"]/descendant::button[contains(.,"Check all")]')->click(); + $client->wait(5, 30)->until(static fn () => 3 === \count($form->get('crud_display_settings_'.static::SESSION_NAME.'[displayedColumns]')->getValue())); + $this->assertCount(3, $form->get('crud_display_settings_'.static::SESSION_NAME.'[displayedColumns]')->getValue()); + + // Uncheck all + $client->getCrawler()->filterXPath('//form[@name="crud_display_settings_'.static::SESSION_NAME.'"]/descendant::button[contains(.,"Uncheck all")]')->click(); + $client->wait(5, 30)->until(static fn () => null === $form->get('crud_display_settings_'.static::SESSION_NAME.'[displayedColumns]')->getValue()); + $this->assertNull($form->get('crud_display_settings_'.static::SESSION_NAME.'[displayedColumns]')->getValue()); + + // Save and error (save not done) + $client->getCrawler()->filterXPath('//form[@name="crud_display_settings_'.static::SESSION_NAME.'"]/descendant::button[@type="submit"]')->click(); + $this->waitForAjax($client); + $client->waitForVisibility('#ec-crud-display-settings-'.static::SESSION_NAME); + $this->assertCount(1, $client->getCrawler()->filterXPath('//div[@id="ec-crud-display-settings-'.static::SESSION_NAME.'"]/descendant::li[contains(text(), "This collection should contain 1 element or more")]')); + $this->assertSame([5, 2], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame([1, 3], $this->getPagination($client->getCrawler())); + + // Save not done after reload + $client->request('GET', static::URL); + $this->assertSame([5, 2], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame([1, 3], $this->getPagination($client->getCrawler())); + + return $client; + } + + /** + * @depends testCheckAndUncheckAllColumns + */ + public function testManualReset(Client $client): Client + { + $client->request('GET', static::URL); + + // Display username column + $button = $client->getCrawler()->filterXPath('//button[contains(.,"Display Settings")]'); + $button->first()->click(); + $client->getCrawler()->filterXPath('//form[@name="crud_display_settings_'.static::SESSION_NAME.'"]/descendant::input[@value="username"]')->click(); + $client->getCrawler()->filterXPath('//form[@name="crud_display_settings_'.static::SESSION_NAME.'"]/descendant::button[@type="submit"]')->click(); + $this->waitForAjax($client); + $this->assertSame([5, 3], $this->countRowsAndColumns($client->getCrawler())); + + // Search + $form = $client->getCrawler()->filterXPath('//div[@id="crud_search"]/descendant::button[@type="submit" and contains(text(), "Search")]')->form(); + $form['crud_search_'.static::SESSION_NAME.'[firstName]'] = 'Henri'; + $client->submit($form); + $this->waitForAjax($client); + $this->assertSame([2, 3], $this->countRowsAndColumns($client->getCrawler())); + + // Manuel RESET (before CRUD initialization) + if (parse_url(static::URL, \PHP_URL_QUERY)) { + $resetUrl = static::URL.'&manual-reset=1'; + } else { + $resetUrl = static::URL.'?manual-reset=1'; + } + $client->request('GET', $resetUrl); + + $this->assertSame([5, 3], $this->countRowsAndColumns($client->getCrawler())); // Reset rows but not columns + + return $client; + } + + /** + * @depends testManualReset + */ + public function testManualResetSort(Client $client): Client + { + $client->request('GET', static::URL); + + // Search + $form = $client->getCrawler()->filterXPath('//div[@id="crud_search"]/descendant::button[@type="submit" and contains(text(), "Search")]')->form(); + $form['crud_search_'.static::SESSION_NAME.'[firstName]'] = 'Henri'; + $client->submit($form); + $this->waitForAjax($client); + $this->assertSame([2, 3], $this->countRowsAndColumns($client->getCrawler())); + + // Sort + $link = $client->getCrawler()->filterXPath('//table[@class="result"]/thead/tr/th/a[text()="last_name"]'); + $link->click(); + $this->waitForAjax($client); + $this->assertSame([2, 3], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame(['last_name', Crud::ASC], $this->getSort($client->getCrawler())); + + // Manuel RESET (before CRUD initialization) + if (parse_url(static::URL, \PHP_URL_QUERY)) { + $resetUrl = static::URL.'&manual-reset-sort=1'; + } else { + $resetUrl = static::URL.'?manual-reset-sort=1'; + } + $client->request('GET', $resetUrl); + + $this->assertSame([2, 3], $this->countRowsAndColumns($client->getCrawler())); // Not reset display settings and search + $this->assertSame(['first_name', Crud::ASC], $this->getSort($client->getCrawler())); + + return $client; + } + + protected function checkBeforeAndAfterBuild(Crawler $crawler): void + { + if (null === static::SEARCH_IN_LIST) { + $this->assertCount(0, $crawler->filterXPath('//div[contains(text(), "TEST BEFORE AFTER BUILD")]')); + + return; + } + + $this->assertCount(1, $crawler->filterXPath('//div[contains(text(), "TEST BEFORE AFTER BUILD '.static::SEARCH_IN_LIST.'")]')); + } + + protected function getCheckedColumns(Crawler $crawler): array + { + return $crawler->filterXPath('//form[@name="crud_display_settings_'.static::SESSION_NAME.'"]/descendant::input[@type="checkbox" and @checked]')->extract(['value']); + } +} diff --git a/tests/Functional/Controller/TestCrudControllerWithPersistentSettingsTest.php b/tests/Functional/Controller/TestCrudControllerWithPersistentSettingsTest.php new file mode 100644 index 0000000..ecf7fd9 --- /dev/null +++ b/tests/Functional/Controller/TestCrudControllerWithPersistentSettingsTest.php @@ -0,0 +1,331 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Ecommit\CrudBundle\Tests\Functional\Controller; + +use Ecommit\CrudBundle\Crud\Crud; +use Ecommit\CrudBundle\Entity\UserCrudSettings; +use Symfony\Component\Panther\Client; +use Symfony\Component\Panther\PantherTestCase; + +class TestCrudControllerWithPersistentSettingsTest extends PantherTestCase +{ + use TestTrait; + + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + + static::$defaultOptions['browser'] = static::FIREFOX; + } + + public function testListWithUserWithoutPersistent(): Client + { + $client = static::createPantherClient(); + $this->login($client, 'EveReste'); + $client->request('GET', '/user-with-persistent-settings/private/no'); + + // Check default values + $this->assertSame([5, 2], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame(['first_name', Crud::ASC], $this->getSort($client->getCrawler())); + + $this->checkUserCrudSettingsDatabase( + username: 'EveReste', + maxPerPage: 50, + displayedColumns: ['username', 'firstName'], + sort: 'firstName', + sortDirection: Crud::DESC + ); + + return $client; + } + + /** + * @depends testListWithUserWithoutPersistent + */ + public function testChangeSettingsWithUserWithoutPersistent($client): Client + { + $this->changeSettings( + client: $client, + headerColumnClicks: ['last_name', 'last_name'], + displayColumnClicks: ['username'], + resultsPerPage: 10 + ); + + $this->assertSame([10, 3], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame(['last_name', Crud::DESC], $this->getSort($client->getCrawler())); + + $this->checkUserCrudSettingsDatabase( + username: 'EveReste', + maxPerPage: 50, + displayedColumns: ['username', 'firstName'], + sort: 'firstName', + sortDirection: Crud::DESC + ); + + return $client; + } + + /** + * @depends testChangeSettingsWithUserWithoutPersistent + */ + public function testResetSettingsWithUserWithoutPersistent($client): Client + { + $this->resetSettings($client); + + $this->checkUserCrudSettingsDatabase( + username: 'EveReste', + maxPerPage: 50, + displayedColumns: ['username', 'firstName'], + sort: 'firstName', + sortDirection: Crud::DESC + ); + + $this->logout($client); + + return $client; + } + + /** + * @depends testResetSettingsWithUserWithoutPersistent + */ + public function testListWithUserWithPersistent(Client $client): Client + { + $this->login($client, 'EveReste'); + $client->request('GET', '/user-with-persistent-settings/private/yes'); + + // Check persistent values + $this->assertSame([11, 2], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame(['first_name', Crud::DESC], $this->getSort($client->getCrawler())); + + $this->checkUserCrudSettingsDatabase( + username: 'EveReste', + maxPerPage: 50, + displayedColumns: ['username', 'firstName'], + sort: 'firstName', + sortDirection: Crud::DESC + ); + + return $client; + } + + /** + * @depends testListWithUserWithPersistent + */ + public function testChangeSettingsWithUserWithPersistent($client): Client + { + $this->changeSettings( + client: $client, + headerColumnClicks: ['username', 'username'], + displayColumnClicks: ['lastName'], + resultsPerPage: 10 + ); + + $this->assertSame([10, 3], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame(['username', Crud::ASC], $this->getSort($client->getCrawler())); + + $this->checkUserCrudSettingsDatabase( + username: 'EveReste', + maxPerPage: 10, + displayedColumns: ['username', 'firstName', 'lastName'], + sort: 'username', + sortDirection: Crud::ASC + ); + + return $client; + } + + /** + * @depends testChangeSettingsWithUserWithPersistent + */ + public function testResetSettingsWithUserWithPersistent($client): Client + { + $this->resetSettings($client); + + // Check default values + $this->assertSame([5, 2], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame(['first_name', Crud::ASC], $this->getSort($client->getCrawler())); + + $this->assertNull($this->getUserCrudSettings('EveReste')); + + return $client; + } + + /** + * @depends testResetSettingsWithUserWithPersistent + */ + public function testCreateSettingsWithUserWithPersistent($client): Client + { + $this->changeSettings( + client: $client, + headerColumnClicks: ['last_name', 'last_name'], + displayColumnClicks: ['username'], + resultsPerPage: 10 + ); + + $this->assertSame([10, 3], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame(['last_name', Crud::DESC], $this->getSort($client->getCrawler())); + + $this->checkUserCrudSettingsDatabase( + username: 'EveReste', + maxPerPage: 10, + displayedColumns: ['firstName', 'lastName', 'username'], + sort: 'lastName', + sortDirection: Crud::DESC + ); + + $this->logout($client); + + return $client; + } + + /** + * @depends testCreateSettingsWithUserWithPersistent + */ + public function testListWithoutUserWithPersistent(Client $client): Client + { + $client->request('GET', '/user-with-persistent-settings/public/yes'); + + // Check default values + $this->assertSame([5, 2], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame(['first_name', Crud::ASC], $this->getSort($client->getCrawler())); + + $this->checkUserCrudSettingsDatabase( + username: 'EveReste', + maxPerPage: 10, + displayedColumns: ['firstName', 'lastName', 'username'], + sort: 'lastName', + sortDirection: Crud::DESC + ); + + return $client; + } + + /** + * @depends testListWithoutUserWithPersistent + */ + public function testChangeSettingsWithoutUserWithPersistent($client): Client + { + $this->changeSettings( + client: $client, + headerColumnClicks: ['last_name', 'last_name'], + displayColumnClicks: ['username'], + resultsPerPage: 10 + ); + + $this->assertSame([10, 3], $this->countRowsAndColumns($client->getCrawler())); + $this->assertSame(['last_name', Crud::DESC], $this->getSort($client->getCrawler())); + + $this->checkUserCrudSettingsDatabase( + username: 'EveReste', + maxPerPage: 10, + displayedColumns: ['firstName', 'lastName', 'username'], + sort: 'lastName', + sortDirection: Crud::DESC + ); + + return $client; + } + + /** + * @depends testChangeSettingsWithoutUserWithPersistent + */ + public function testResetSettingsWithoutUserWithPersistent($client): Client + { + $this->resetSettings($client); + + $this->checkUserCrudSettingsDatabase( + username: 'EveReste', + maxPerPage: 10, + displayedColumns: ['firstName', 'lastName', 'username'], + sort: 'lastName', + sortDirection: Crud::DESC + ); + + return $client; + } + + protected function login(Client $client, string $username): void + { + $client->request('GET', '/login'); + + $form = $client->getCrawler()->filterXPath('//button[@type="submit"]')->form(); + $form['_username'] = $username; + $form['_password'] = $username; + $client->submit($form); + } + + protected function logout(Client $client): void + { + $client->request('GET', '/logout'); + } + + protected function changeSettings(Client $client, array $headerColumnClicks = [], array $displayColumnClicks = [], ?int $resultsPerPage = null): void + { + foreach ($headerColumnClicks as $column) { + $link = $client->getCrawler()->filterXPath('//table[@class="result"]/thead/tr/th/a[contains(text(), "'.$column.'")]'); + $link->click(); + $this->waitForAjax($client); + } + + if (\count($displayColumnClicks) > 0 || null !== $resultsPerPage) { + $button = $client->getCrawler()->filterXPath('//button[contains(.,"Display Settings")]'); + $button->first()->click(); + $form = $client->getCrawler()->filterXPath('//form[@name="crud_display_settings_crud_persistent_settings"]/descendant::button[@type="submit"]')->form(); + + foreach ($displayColumnClicks as $column) { + $client->getCrawler()->filterXPath('//form[@name="crud_display_settings_crud_persistent_settings"]/descendant::input[@value="'.$column.'"]')->click(); + } + + if (null !== $resultsPerPage) { + $form['crud_display_settings_crud_persistent_settings[resultsPerPage]'] = $resultsPerPage; + } + + $client->submit($form); + $this->waitForAjax($client); + } + } + + protected function resetSettings(Client $client): void + { + $button = $client->getCrawler()->filterXPath('//button[contains(., "Display Settings")]'); + $button->first()->click(); + + $button = $client->getCrawler()->filterXPath('//form[@name="crud_display_settings_crud_persistent_settings"]/descendant::button[contains(., "Reset display settings")]'); + $button->first()->click(); + $this->waitForAjax($client); + } + + protected function checkUserCrudSettingsDatabase(string $username, int $maxPerPage, array $displayedColumns, string $sort, string $sortDirection): void + { + $userCrudSettings = $this->getUserCrudSettings($username); + $this->assertSame($maxPerPage, $userCrudSettings->getMaxPerPage()); + $this->assertSame($displayedColumns, $userCrudSettings->getDisplayedColumns()); + $this->assertSame($sort, $userCrudSettings->getSort()); + $this->assertSame($sortDirection, $userCrudSettings->getSortDirection()); + } + + protected function getUserCrudSettings(string $username): ?UserCrudSettings + { + $queryBuilder = static::getContainer()->get('doctrine')->getRepository(UserCrudSettings::class)->createQueryBuilder('ucs'); + + return $queryBuilder->select('ucs') + ->leftJoin('ucs.user', 'u') + ->andWhere('u.username = :username') + ->setParameter('username', $username) + ->andWhere('ucs.crudName = :crudName') + ->setParameter('crudName', 'crud_persistent_settings') + ->getQuery() + ->getOneOrNullResult(); + } +} diff --git a/tests/Functional/Controller/TestCrudControllerWithoutTraitTest.php b/tests/Functional/Controller/TestCrudControllerWithoutTraitTest.php new file mode 100644 index 0000000..e02fc36 --- /dev/null +++ b/tests/Functional/Controller/TestCrudControllerWithoutTraitTest.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Ecommit\CrudBundle\Tests\Functional\Controller; + +class TestCrudControllerWithoutTraitTest extends TestCrudControllerTest +{ + public const URL = '/user-without-trait/'; + public const SESSION_NAME = 'user_without_trait'; +} diff --git a/tests/Functional/Controller/TestCrudControllerWithoutTraitWithDataTest.php b/tests/Functional/Controller/TestCrudControllerWithoutTraitWithDataTest.php new file mode 100644 index 0000000..336cde3 --- /dev/null +++ b/tests/Functional/Controller/TestCrudControllerWithoutTraitWithDataTest.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Ecommit\CrudBundle\Tests\Functional\Controller; + +class TestCrudControllerWithoutTraitWithDataTest extends TestCrudControllerTest +{ + public const URL = '/user-without-trait?test-before-and-after-build-query=1'; + public const SESSION_NAME = 'user_without_trait_with_data'; + public const SEARCH_IN_LIST = 'BEFORE AFTER'; +} diff --git a/tests/Functional/Controller/TestTrait.php b/tests/Functional/Controller/TestTrait.php new file mode 100644 index 0000000..dafeb23 --- /dev/null +++ b/tests/Functional/Controller/TestTrait.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Ecommit\CrudBundle\Tests\Functional\Controller; + +use Ecommit\CrudBundle\Crud\Crud; +use Symfony\Component\Panther\Client; +use Symfony\Component\Panther\DomCrawler\Crawler; + +trait TestTrait +{ + protected function countRowsAndColumns(Crawler $crawler): array + { + $rows = $crawler->filterXPath('//table[@class="result"]/tbody/tr'); + $countRows = \count($rows); + $columns = $rows->first()->filterXPath('td'); + $countColumns = \count($columns); + + return [$countRows, $countColumns]; + } + + protected function getPagination(Crawler $crawler): array + { + $infos = $crawler->filterXPath('//div[@class="info-pagination"]')->text(); + + preg_match('/^Results \d+\-\d+ \- Page (\d+)\/(\d+)$/', $infos, $groups); + + $page = (int) $groups[1]; + $countPages = (int) $groups[2]; + + return [$page, $countPages]; + } + + protected function getSort(Crawler $crawler): array + { + $iSort = $crawler->filterXPath('//table[@class="result"]/thead/tr/th/a/i'); + if (0 === \count($iSort)) { + return []; + } + $iSort = $iSort->first(); + + switch ($iSort->text()) { + case '^': + $sortDirection = Crud::ASC; + break; + case 'v': + $sortDirection = Crud::DESC; + break; + default: + throw new \Exception('Bad sort direction'); + } + + $column = $iSort->filterXPath('ancestor::th')->last()->text(); + $column = str_replace(' '.$iSort->text(), '', $column); + + return [$column, $sortDirection]; + } + + protected function getFirstUsername(Crawler $crawler): ?string + { + $rows = $crawler->filterXPath('//table[@class="result"]/tbody/tr'); + if (0 === \count($rows)) { + return null; + } + + return $rows->first()->getAttribute('data-username'); + } + + protected function waitForAjax(Client $client, int $timeout = 5): void + { + $driver = $client->getWebDriver(); + + $driver->wait($timeout, 500)->until(static fn ($driver) => !$driver->executeScript('return (typeof jQuery !== "undefined" && jQuery.active);')); + + usleep(500000); + } +} diff --git a/tests/Twig/CrudExtensionTest.php b/tests/Twig/CrudExtensionTest.php new file mode 100644 index 0000000..bc9b09f --- /dev/null +++ b/tests/Twig/CrudExtensionTest.php @@ -0,0 +1,772 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Ecommit\CrudBundle\Tests\Twig; + +use Ecommit\CrudBundle\Crud\Crud; +use Ecommit\CrudBundle\Crud\CrudColumn; +use Ecommit\CrudBundle\Crud\CrudSession; +use Ecommit\CrudBundle\Twig\CrudExtension; +use Ecommit\Paginator\ArrayPaginator; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Symfony\Component\DomCrawler\Crawler; +use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException; +use Twig\Environment; +use Twig\Markup; + +class CrudExtensionTest extends KernelTestCase +{ + /** + * @var CrudExtension + */ + protected $crudExtension; + + /** + * @var FormFactoryInterface + */ + protected $formFactory; + + /** + * @var Environment + */ + protected $environment; + + protected function setUp(): void + { + self::bootKernel(); + + $this->crudExtension = self::getContainer()->get('ecommit_crud.twig.crud_extension'); + $this->formFactory = self::getContainer()->get(FormFactoryInterface::class); + $this->environment = self::getContainer()->get(Environment::class); + } + + public function testFormStartAjax(): void + { + $html = $this->crudExtension->formStartAjax($this->createFormView()); + + $xpath = '//form[@method="post" and @action="/url" and @data-ec-crud-toggle="ajax-form"]'; + $this->assertMatchesXpath($html, $xpath); + } + + public function testFormStartAjaxWithAjaxOptions(): void + { + $html = $this->crudExtension->formStartAjax($this->createFormView(), [ + 'ajax_options' => [ + 'on_success' => 'a=b', + ], + ]); + + $xpath = '//form[@data-ec-crud-toggle="ajax-form" and @data-ec-crud-ajax-on-success="a=b"]'; + $this->assertMatchesXpath($html, $xpath); + } + + public function testFormStartAjaxWithAttrAndAjaxOptions(): void + { + $html = $this->crudExtension->formStartAjax($this->createFormView(), [ + 'attr' => [ + 'data-custom' => 'custom', + ], + 'ajax_options' => [ + 'on_success' => 'a=b', + ], + ]); + + $xpath = '//form[@data-ec-crud-toggle="ajax-form" and @data-ec-crud-ajax-on-success="a=b" and @data-custom="custom"]'; + $this->assertMatchesXpath($html, $xpath); + } + + public function testFormStartAjaxWithBadAjaxOptions(): void + { + $this->expectException(UndefinedOptionsException::class); + $this->crudExtension->formStartAjax($this->createFormView(), [ + 'ajax_options' => [ + 'badOption' => 'value', + ], + ]); + } + + public function testFormStartAjaxWithFormStartOptions(): void + { + $html = $this->crudExtension->formStartAjax($this->createFormView(), [ + 'method' => 'get', + ]); + + $xpath = '//form[@method="get" and @action="/url" and @data-ec-crud-toggle="ajax-form"]'; + $this->assertMatchesXpath($html, $xpath); + } + + /** + * @dataProvider getTestAjaxAttributesProvider + */ + public function testAjaxAttributes(array $ajaxOptions, string $expected): void + { + $result = $this->crudExtension->ajaxAttributes( + $this->environment, + $ajaxOptions + ); + + $this->assertSame($expected, $result); + } + + public static function getTestAjaxAttributesProvider(): array + { + return [ + [[], ''], + [['url' => '/url?id=a'], ' data-ec-crud-ajax-url="/url?id=a"'], + [['url' => '/url?id=a', 'method' => 'GET'], ' data-ec-crud-ajax-url="/url?id=a" data-ec-crud-ajax-method="GET"'], + [['body' => ''], ' data-ec-crud-ajax-body="<script>ValueToEscape</script>"'], + [['cache' => true], ' data-ec-crud-ajax-cache="true"'], + [['cache' => false], ' data-ec-crud-ajax-cache="false"'], + [['body' => ['var1' => 'value1']], ' data-ec-crud-ajax-body="{"var1":"value1"}"'], + ]; + } + + public function testAjaxAttributesWithBadAjaxOptions(): void + { + $this->expectException(UndefinedOptionsException::class); + $this->crudExtension->ajaxAttributes( + $this->environment, + ['badOption' => 'value'] + ); + } + + public function testPaginatorLinksWithNoResult(): void + { + $paginator = new ArrayPaginator([ + 'max_per_page' => 5, + 'data' => [], + ]); + + $result = $this->crudExtension->paginatorLinks($this->environment, $paginator, 'user_crud'); + + $this->assertEmpty($result); + } + + public function testPaginatorLinksWithOnePage(): void + { + $paginator = new ArrayPaginator([ + 'max_per_page' => 5, + 'data' => ['val'], + ]); + + $result = $this->crudExtension->paginatorLinks($this->environment, $paginator, 'user_crud'); + + $this->assertEmpty($result); + } + + /** + * @dataProvider getTestPaginatorWithDefaultOptionsProvider + */ + public function testPaginatorWithDefaultOptions(int $page, string $expected): void + { + $paginator = new ArrayPaginator([ + 'max_per_page' => 5, + 'data' => range(1, 100), + 'page' => $page, + ]); + + $result = $this->crudExtension->paginatorLinks($this->environment, $paginator, 'user_crud'); + + $this->assertSame($expected, $result); + } + + public static function getTestPaginatorWithDefaultOptionsProvider(): array + { + return [ + [1, ''], + [2, ''], + [10, ''], + [19, ''], + [20, ''], + ]; + } + + /** + * @dataProvider getTestPaginatorWithMaxPagesOptionsProvider + */ + public function testPaginatorWithMaxPagesOptions(int $page, string $expected): void + { + $paginator = new ArrayPaginator([ + 'max_per_page' => 5, + 'data' => range(1, 100), + 'page' => $page, + ]); + + $result = $this->crudExtension->paginatorLinks($this->environment, $paginator, 'user_crud', [], [ + 'max_pages_before' => 4, + 'max_pages_after' => 2, + ]); + + $this->assertSame($expected, $result); + } + + public static function getTestPaginatorWithMaxPagesOptionsProvider(): array + { + return [ + [1, ''], + [2, ''], + [10, ''], + [19, ''], + [20, ''], + ]; + } + + /** + * @dataProvider getTestPaginatorWithTypeOptionProvider + */ + public function testPaginatorWithTypeOption(int $page, string $type, string $expected): void + { + $paginator = new ArrayPaginator([ + 'max_per_page' => 5, + 'data' => range(1, 100), + 'page' => $page, + ]); + + $result = $this->crudExtension->paginatorLinks($this->environment, $paginator, 'user_crud', [], [ + 'type' => $type, + ]); + + $this->assertSame($expected, $result); + } + + public static function getTestPaginatorWithTypeOptionProvider(): array + { + $full = static function (int $currentPage) { + $result = ''; + for ($i = 1; $i <= 20; ++$i) { + $class = ($i === $currentPage) ? ' class="current"' : ''; + $result .= \sprintf('%s', $class, $i, $i); + } + + return $result; + }; + + return [ + [1, 'sliding', ''], + [2, 'sliding', ''], + [10, 'sliding', ''], + [19, 'sliding', ''], + [20, 'sliding', ''], + + [1, 'elastic', ''], + [2, 'elastic', ''], + [10, 'elastic', ''], + [19, 'elastic', ''], + [20, 'elastic', ''], + ]; + } + + public function testPaginatorWithEmptyArrayAjaxOption(): void + { + $paginator = new ArrayPaginator([ + 'max_per_page' => 5, + 'data' => range(1, 100), + 'page' => 1, + ]); + + $expected = ''; + $result = $this->crudExtension->paginatorLinks($this->environment, $paginator, 'user_crud', [], [ + 'ajax_options' => [], + 'max_pages_before' => 1, + 'max_pages_after' => 1, + ]); + + $this->assertSame($expected, $result); + } + + public function testPaginatorWithAjaxOption(): void + { + $paginator = new ArrayPaginator([ + 'max_per_page' => 5, + 'data' => range(1, 100), + 'page' => 1, + ]); + + $expected = ''; + $result = $this->crudExtension->paginatorLinks($this->environment, $paginator, 'user_crud', [], [ + 'ajax_options' => [ + 'update' => '#myId', + ], + 'max_pages_before' => 1, + 'max_pages_after' => 1, + ]); + + $this->assertSame($expected, $result); + } + + public function testPaginatorWithAttributePageOption(): void + { + $paginator = new ArrayPaginator([ + 'max_per_page' => 5, + 'data' => range(1, 100), + 'page' => 1, + ]); + + $expected = ''; + $result = $this->crudExtension->paginatorLinks($this->environment, $paginator, 'user_crud', [], [ + 'attribute_page' => 'p', + 'max_pages_before' => 1, + 'max_pages_after' => 1, + ]); + + $this->assertSame($expected, $result); + } + + public function testPaginatorWithAttrOptions(): void + { + $paginator = new ArrayPaginator([ + 'max_per_page' => 5, + 'data' => range(1, 100), + 'page' => 10, + ]); + + $expected = ''; + $result = $this->crudExtension->paginatorLinks($this->environment, $paginator, 'user_crud', [], [ + 'nav_attr' => ['class' => 'navattr', 'data-nav' => 'val'], + 'ul_attr' => ['class' => 'ulattr', 'data-ul' => 'val'], + 'li_attr' => [ + 'first_page' => ['class' => 'lifirstpageattr', 'data-li-first' => 'val'], + 'previous_page' => ['class' => 'lipreviouspageattr'], + 'current_page' => ['class' => 'liccurentpageattr'], + 'next_page' => ['class' => 'linextpageattr'], + 'last_page' => ['class' => 'lilastpageattr'], + 'other_page' => ['class' => 'liotherpageattr'], + ], + 'a_attr' => [ + 'first_page' => ['class' => 'afirstpageattr', 'data-a-first' => 'val'], + 'previous_page' => ['class' => 'apreviouspageattr'], + 'current_page' => ['class' => 'accurentpageattr'], + 'next_page' => ['class' => 'anextpageattr'], + 'last_page' => ['class' => 'alastpageattr'], + 'other_page' => ['class' => 'aotherpageattr'], + ], + 'max_pages_before' => 1, + 'max_pages_after' => 1, + ]); + + $this->assertSame($expected, $result); + } + + public function testPaginatorWithRenderOption(): void + { + $paginator = new ArrayPaginator([ + 'max_per_page' => 5, + 'data' => range(1, 100), + 'page' => 1, + ]); + + $result = $this->crudExtension->paginatorLinks($this->environment, $paginator, 'user_crud', [], [ + 'render' => 'render.html.twig', + ]); + + $this->assertMatchesRegularExpression('/OK/', $result); + } + + public function testPaginatorWithThemeAndBlockOptions(): void + { + $paginator = new ArrayPaginator([ + 'max_per_page' => 5, + 'data' => range(1, 100), + 'page' => 1, + ]); + + $result = $this->crudExtension->paginatorLinks($this->environment, $paginator, 'user_crud', [], [ + 'theme' => 'theme.html.twig', + 'block' => 'hello_world', + ]); + + $this->assertMatchesRegularExpression('/Hello world/', $result); + } + + public function testPaginatorWithBadOptions(): void + { + $this->expectException(UndefinedOptionsException::class); + $paginator = new ArrayPaginator([ + 'max_per_page' => 5, + 'data' => range(1, 100), + 'page' => 1, + ]); + + $this->crudExtension->paginatorLinks($this->environment, $paginator, 'user_crud', [], [ + 'bad_option' => 'bad', + ]); + } + + public function testCrudPaginatorLinks(): void + { + $paginator = new ArrayPaginator([ + 'max_per_page' => 5, + 'data' => range(1, 100), + 'page' => 1, + ]); + + $crud = $this->getMockBuilder(Crud::class) + ->disableOriginalConstructor() + ->onlyMethods(['getDivIdList', 'getPaginator', 'getRouteName', 'getRouteParameters']) + ->getMock(); + $crud->expects($this->once())->method('getDivIdList')->willReturn('myId'); + $crud->expects($this->exactly(2))->method('getPaginator')->willReturn($paginator); + $crud->expects($this->once())->method('getRouteName')->willReturn('user_crud'); + $crud->expects($this->once())->method('getRouteParameters')->willReturn([]); + + $expected = ''; + $result = $this->crudExtension->crudPaginatorLinks($this->environment, $crud, [ + 'max_pages_before' => 1, + 'max_pages_after' => 1, + ]); + + $this->assertSame($expected, $result); + } + + public function testCrudPaginatorLinksWithoutPaginator(): void + { + $crud = $this->getMockBuilder(Crud::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('The paginator is not defined'); + $this->crudExtension->crudPaginatorLinks($this->environment, $crud); + } + + public function testThColumnNotDisplayed(): void + { + $html = $this->crudExtension->th($this->environment, 'column4', $this->createCrud()); + $this->assertSame('', $html); + } + + public function testThColumnNotSortable(): void + { + $html = $this->crudExtension->th($this->environment, 'column3', $this->createCrud()); + $this->assertSame('label3', $html); + } + + public function testThColumnSortableNotActive(): void + { + $html = $this->crudExtension->th($this->environment, 'column2', $this->createCrud()); + $this->assertSame('label2', $html); + } + + public function testThColumnSortableActiveAsc(): void + { + $html = $this->crudExtension->th($this->environment, 'column1', $this->createCrud()); + $this->assertSame('label1 ^', $html); + } + + public function testThColumnSortableActiveDesc(): void + { + $html = $this->crudExtension->th($this->environment, 'column1', $this->createCrud('column1', CRUD::DESC)); + $this->assertSame('label1 v', $html); + } + + public function testThWithAjaxOptions(): void + { + $html = $this->crudExtension->th($this->environment, 'column2', $this->createCrud(), [ + 'ajax_options' => [ + 'update' => '#div2', + ], + ]); + $this->assertSame('label2', $html); + } + + public function testThWithLabel(): void + { + $html = $this->crudExtension->th($this->environment, 'column2', $this->createCrud(), [ + 'label' => 'My label', + ]); + $this->assertSame('My label', $html); + } + + /** + * @dataProvider getTestThWithAttrOptionsProvider + */ + public function testThWithAttrOptions(string $columnId, string $sort, string $sortDirection, string $expected): void + { + $html = $this->crudExtension->th($this->environment, $columnId, $this->createCrud($sort, $sortDirection), [ + 'th_attr' => [ + 'not_sortable' => ['class' => 'a', 'data-a' => 'val'], + 'sortable_active_asc' => ['class' => 'b', 'data-b' => 'val'], + 'sortable_active_desc' => ['class' => 'c', 'data-c' => 'val'], + 'sortable_not_active' => ['class' => 'd', 'data-d' => 'val'], + ], + 'a_attr' => [ + 'sortable_active_asc' => ['class' => 'e', 'data-e' => 'val'], + 'sortable_active_desc' => ['class' => 'f', 'data-f' => 'val'], + 'sortable_not_active' => ['class' => 'g', 'data-g' => 'val'], + ], + ]); + + $this->assertSame($expected, $html); + } + + public static function getTestThWithAttrOptionsProvider(): array + { + return [ + ['column3', 'column1', Crud::ASC, 'label3'], + ['column1', 'column1', Crud::ASC, 'label1 ^'], + ['column1', 'column1', Crud::DESC, 'label1 v'], + ['column2', 'column1', Crud::ASC, 'label2'], + ]; + } + + public function testThWithRenderOption(): void + { + $html = $this->crudExtension->th($this->environment, 'column1', $this->createCrud(), [ + 'render' => 'render.html.twig', + ]); + + $this->assertMatchesRegularExpression('/OK/', $html); + } + + public function testThWithThemeAndBlockOptions(): void + { + $html = $this->crudExtension->th($this->environment, 'column1', $this->createCrud(), [ + 'theme' => 'theme.html.twig', + 'block' => 'hello_world', + ]); + + $this->assertMatchesRegularExpression('/Hello world/', $html); + } + + public function testThWithBadOptions(): void + { + $this->expectException(UndefinedOptionsException::class); + $this->crudExtension->th($this->environment, 'column1', $this->createCrud(), [ + 'bad_option' => 'bad', + ]); + } + + public function testTdColumnNotDisplayed(): void + { + $html = $this->crudExtension->td($this->environment, 'column4', $this->createCrud(), 'value4'); + $this->assertSame('', $html); + } + + public function testTdWithEscape(): void + { + $html = $this->crudExtension->td($this->environment, 'column1', $this->createCrud(), 'value1 é&>"'); + $this->assertSame('value1 é&>"', $html); + } + + public function testTdWithoutEscape(): void + { + $html = $this->crudExtension->td($this->environment, 'column1', $this->createCrud(), 'value1 é&', [ + 'escape' => false, + ]); + $this->assertSame('value1 é&', $html); + } + + public function testRepeatedValuesStringFirst(): CrudExtension + { + $html = $this->crudExtension->td($this->environment, 'column1', $this->createCrud(), 'value1 é&>"', [ + 'repeated_values_string' => '"', + ]); + $this->assertSame('value1 é&>"', $html); + + return $this->crudExtension; + } + + /** + * @depends testRepeatedValuesStringFirst + */ + public function testRepeatedValuesStringRepeat(CrudExtension $crudExtension): CrudExtension + { + $html = $crudExtension->td($this->environment, 'column1', $this->createCrud(), 'value1 é&>"', [ + 'repeated_values_string' => 'Bis', + ]); + $this->assertSame('Bis', $html); + + return $crudExtension; + } + + /** + * @depends testRepeatedValuesStringRepeat + */ + public function testRepeatedValuesStringOtherColumn(CrudExtension $crudExtension): CrudExtension + { + $html = $crudExtension->td($this->environment, 'column2', $this->createCrud(), 'value1 é&>"', [ + 'repeated_values_string' => 'Bis', + ]); + $this->assertSame('value1 é&>"', $html); + + return $crudExtension; + } + + /** + * @depends testRepeatedValuesStringOtherColumn + */ + public function testRepeatedValuesStringOtherColumnRepeat(CrudExtension $crudExtension): CrudExtension + { + $html = $crudExtension->td($this->environment, 'column2', $this->createCrud(), 'value1 é&>"', [ + 'repeated_values_string' => 'Bis', + ]); + $this->assertSame('Bis', $html); + + return $crudExtension; + } + + /** + * @depends testRepeatedValuesStringOtherColumnRepeat + */ + public function testRepeatedValuesStringRepeatWithMarkup(CrudExtension $crudExtension): CrudExtension + { + $markup = new Markup('value1 é&>"', 'UTF-8'); + $html = $crudExtension->td($this->environment, 'column1', $this->createCrud(), $markup, [ + 'repeated_values_string' => 'Bis', + ]); + $this->assertSame('Bis', $html); + + return $crudExtension; + } + + /** + * @depends testRepeatedValuesStringRepeatWithMarkup + */ + public function testRepeatedValuesStringRepeatWithTitleAttr(CrudExtension $crudExtension): CrudExtension + { + $html = $crudExtension->td($this->environment, 'column1', $this->createCrud(), 'value1 é&>"', [ + 'repeated_values_string' => 'Bis', + 'td_attr' => ['title' => 'Repeated'], + ]); + $this->assertSame('Bis', $html); + + return $crudExtension; + } + + /** + * @depends testRepeatedValuesStringRepeatWithMarkup + */ + public function testRepeatedValuesStringNotRepeat(CrudExtension $crudExtension): CrudExtension + { + $html = $crudExtension->td($this->environment, 'column1', $this->createCrud(), 'value2 é&>"', [ + 'repeated_values_string' => 'Bis', + ]); + $this->assertSame('value2 é&>"', $html); + + return $crudExtension; + } + + /** + * @depends testRepeatedValuesStringNotRepeat + */ + public function testRepeatedValuesWithEmptyValueFirst(CrudExtension $crudExtension): CrudExtension + { + $html = $crudExtension->td($this->environment, 'column1', $this->createCrud(), '', [ + 'repeated_values_string' => 'Bis', + ]); + $this->assertSame('', $html); + + return $crudExtension; + } + + /** + * @depends testRepeatedValuesWithEmptyValueFirst + */ + public function testRepeatedValuesWithEmptyValueRepeat(CrudExtension $crudExtension): CrudExtension + { + $html = $crudExtension->td($this->environment, 'column1', $this->createCrud(), '', [ + 'repeated_values_string' => 'Bis', + ]); + $this->assertSame('', $html); + + return $crudExtension; + } + + public function testTdWithAttrOptions(): void + { + $html = $this->crudExtension->td($this->environment, 'column1', $this->createCrud(), 'value1', [ + 'td_attr' => ['class' => 'a', 'data-a' => 'val'], + ]); + $this->assertSame('value1', $html); + } + + public function testTdWithRenderOption(): void + { + $html = $this->crudExtension->td($this->environment, 'column1', $this->createCrud(), 'value1', [ + 'render' => 'render.html.twig', + ]); + + $this->assertMatchesRegularExpression('/OK/', $html); + } + + public function testTdWithThemeAndBlockOptions(): void + { + $html = $this->crudExtension->td($this->environment, 'column1', $this->createCrud(), 'value1', [ + 'theme' => 'theme.html.twig', + 'block' => 'hello_world', + ]); + + $this->assertMatchesRegularExpression('/Hello world/', $html); + } + + public function testTdWithBadOptions(): void + { + $this->expectException(UndefinedOptionsException::class); + $this->crudExtension->td($this->environment, 'column1', $this->createCrud(), 'value1', [ + 'bad_option' => 'bad', + ]); + } + + protected function createCrud(string $sort = 'column1', string $sortDirection = Crud::ASC): Crud + { + $columns = [ + 'column1' => new CrudColumn(['id' => 'column1', 'alias' => 'alias1', 'label' => 'label1']), + 'column2' => new CrudColumn(['id' => 'column2', 'alias' => 'alias2', 'label' => 'label2', 'displayed_by_default' => false]), + 'column3' => new CrudColumn(['id' => 'column3', 'alias' => 'alias3', 'label' => 'label3', 'sortable' => false]), + 'column4' => new CrudColumn(['id' => 'column4', 'alias' => 'alias4', 'label' => 'label4']), + ]; + $crudSession = new CrudSession( + 10, + [ + 'column1', + 'column2', + 'column3', + ], + $sort, + $sortDirection, + ); + + $crud = $this->getMockBuilder(Crud::class) + ->disableOriginalConstructor() + ->onlyMethods(['getSessionName', 'getDivIdList', 'getRouteName', 'getRouteParameters', 'getSessionValues', 'getColumn']) + ->getMock(); + $crud->expects($this->any())->method('getSessionName')->willReturn('sessionName'); + $crud->expects($this->any())->method('getDivIdList')->willReturn('myId'); + $crud->expects($this->any())->method('getRouteName')->willReturn('user_crud'); + $crud->expects($this->any())->method('getRouteParameters')->willReturn([]); + $crud->expects($this->any())->method('getSessionValues')->willReturn($crudSession); + $crud->expects($this->any())->method('getColumn')->willReturnCallback(static fn ($columnId) => $columns[$columnId]); + + return $crud; + } + + protected function createFormView(): FormView + { + $builder = $this->formFactory->createBuilder(FormType::class, null, [ + 'action' => '/url', + 'method' => 'POST', + 'csrf_protection' => false, + ]); + + return $builder->getForm()->createView(); + } + + protected function assertMatchesXpath(?string $html, string $expression, int $count = 1): void + { + $crawler = new Crawler($html); + $this->assertCount($count, $crawler->filterXPath($expression)); + } +} diff --git a/tests/assets/js/ajax.spec.js b/tests/assets/js/ajax.spec.js new file mode 100644 index 0000000..9676bfd --- /dev/null +++ b/tests/assets/js/ajax.spec.js @@ -0,0 +1,1820 @@ +/* + * This file is part of the EcommitCrudBundle package. + * + * (c) E-commit + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import * as ajax from '@ecommit/crud-bundle/js/ajax' +import * as callbackManager from '@ecommit/crud-bundle/js/callback-manager' +import $ from 'jquery' +import wait from './wait' + +describe('Test Ajax.sendRequest', function () { + beforeEach(function () { + jasmine.Ajax.install() + addJasmineAjaxFormDataSupport() + + jasmine.Ajax.stubRequest(/goodRequest/).andReturn({ + status: 200, + statusText: 'OK', + response: 'CONTENT', + responseText: 'CONTENT' + }) + + jasmine.Ajax.stubRequest(/resultJSON/).andReturn({ + status: 200, + statusText: 'OK', + response: '{"var1": "value1", "var2": "value2"}', + responseText: '{"var1": "value1", "var2": "value2"}' + }) + + jasmine.Ajax.stubRequest(/badJSON/).andReturn({ + status: 200, + statusText: 'OK', + response: '{"var1":', + responseText: '{"var1":' + }) + + jasmine.Ajax.stubRequest(/resultJavaScript/).andReturn({ + status: 200, + statusText: 'OK', + response: '
    BEFORE
    ', + responseText: '
    BEFORE
    ' + }) + + jasmine.Ajax.stubRequest(/error404/).andReturn({ + status: 404, + statusText: 'Not Found', + response: 'Page not found !', + responseText: 'Page not found !' + }) + + jasmine.Ajax.stubRequest(/failure/).andError() + }) + + afterEach(function () { + jasmine.Ajax.uninstall() + $('.html-test').remove() + callbackManager.clear() + $(document).off('ec-crud-ajax-on-success') + $(document).off('ec-crud-ajax-on-error') + $(document).off('ec-crud-ajax-on-complete') + }) + + it('Send request', async function () { + const callbackSuccess = jasmine.createSpy('success') + const callbackError = jasmine.createSpy('error') + const callbackComplete = jasmine.createSpy('complete') + const callbackCatch = jasmine.createSpy('catch') + const eventSuccess = jasmine.createSpy('event-success') + const eventError = jasmine.createSpy('event-error') + const eventComplete = jasmine.createSpy('event-complete') + + $(document).on('ec-crud-ajax-on-success', function (event) { + expect(event.detail.data).toEqual('CONTENT') + expect(event.detail.response).toBeInstanceOf(Response) + eventSuccess() + }) + $(document).on('ec-crud-ajax-on-error', function (event) { + eventError() + }) + $(document).on('ec-crud-ajax-on-complete', function (event) { + expect(event.detail.statusText).toEqual('OK') + expect(event.detail.response).toBeInstanceOf(Response) + eventComplete() + }) + + const promise = ajax.sendRequest({ + url: '/goodRequest', + onComplete: function (statusText, response) { + expect(statusText).toEqual('OK') + expect(response).toBeInstanceOf(Response) + callbackComplete() + }, + onSuccess: function (data, response) { + expect(data).toEqual('CONTENT') + expect(response).toBeInstanceOf(Response) + callbackSuccess() + }, + onError: function (statusText, response) { + callbackError() + } + }).catch(() => { + callbackCatch() + }) + expect(promise).toBeInstanceOf(Promise) + + const response = await promise + + expect(response).toBeInstanceOf(Response) + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/goodRequest') + expect(jasmine.Ajax.requests.mostRecent().method).toBe('POST') + expect(callbackSuccess).toHaveBeenCalledBefore(callbackComplete) + expect(callbackError).not.toHaveBeenCalled() + expect(callbackComplete).toHaveBeenCalled() + expect(callbackCatch).not.toHaveBeenCalled() + expect(eventSuccess).toHaveBeenCalledBefore(eventComplete) + expect(eventError).not.toHaveBeenCalled() + expect(eventComplete).toHaveBeenCalled() + }) + + it('Send bad request', async function () { + const callbackSuccess = jasmine.createSpy('success') + const callbackError = jasmine.createSpy('error') + const callbackComplete = jasmine.createSpy('complete') + const callbackCatch = jasmine.createSpy('catch') + const eventSuccess = jasmine.createSpy('event-success') + const eventError = jasmine.createSpy('event-error') + const eventComplete = jasmine.createSpy('event-complete') + + $(document).on('ec-crud-ajax-on-success', function (event) { + eventSuccess() + }) + $(document).on('ec-crud-ajax-on-error', function (event) { + expect(event.detail.statusText).toEqual('Not Found') + expect(event.detail.response).toBeInstanceOf(Response) + eventError() + }) + $(document).on('ec-crud-ajax-on-complete', function (event) { + expect(event.detail.statusText).toEqual('Not Found') + expect(event.detail.response).toBeInstanceOf(Response) + eventComplete() + }) + + const promise = ajax.sendRequest({ + url: '/error404', + onComplete: function (statusText, response) { + expect(statusText).toEqual('Not Found') + expect(response).toBeInstanceOf(Response) + callbackComplete() + }, + onSuccess: function (data, response) { + callbackSuccess() + }, + onError: function (statusText, response) { + expect(statusText).toEqual('Not Found') + expect(response).toBeInstanceOf(Response) + callbackError() + } + }).catch(() => { + callbackCatch() + }) + expect(promise).toBeInstanceOf(Promise) + + const response = await promise + + expect(response).toBeInstanceOf(Response) + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/error404') + expect(callbackSuccess).not.toHaveBeenCalled() + expect(callbackError).toHaveBeenCalledBefore(callbackComplete) + expect(callbackComplete).toHaveBeenCalled() + expect(callbackCatch).not.toHaveBeenCalled() + expect(eventSuccess).not.toHaveBeenCalled() + expect(eventError).toHaveBeenCalledBefore(eventComplete) + expect(eventComplete).toHaveBeenCalled() + }) + + it('Send bad request with successfulResponseRequired', async function () { + const callbackSuccess = jasmine.createSpy('success') + const callbackError = jasmine.createSpy('error') + const callbackComplete = jasmine.createSpy('complete') + const callbackCatch = jasmine.createSpy('catch') + const eventSuccess = jasmine.createSpy('event-success') + const eventError = jasmine.createSpy('event-error') + const eventComplete = jasmine.createSpy('event-complete') + + $(document).on('ec-crud-ajax-on-success', function (event) { + eventSuccess() + }) + $(document).on('ec-crud-ajax-on-error', function (event) { + expect(event.detail.statusText).toEqual('Not Found') + expect(event.detail.response).toBeInstanceOf(Response) + eventError() + }) + $(document).on('ec-crud-ajax-on-complete', function (event) { + expect(event.detail.statusText).toEqual('Not Found') + expect(event.detail.response).toBeInstanceOf(Response) + eventComplete() + }) + + const promise = ajax.sendRequest({ + url: '/error404', + successfulResponseRequired: true, + onComplete: function (statusText, response) { + expect(statusText).toEqual('Not Found') + expect(response).toBeInstanceOf(Response) + callbackComplete() + }, + onSuccess: function (data, response) { + callbackSuccess() + }, + onError: function (statusText, response) { + expect(statusText).toEqual('Not Found') + expect(response).toBeInstanceOf(Response) + callbackError() + } + }).catch(error => { + expect(error).toBeInstanceOf(Error) + expect(error.message).toEqual('The response is not successful: Not Found') + callbackCatch() + }) + expect(promise).toBeInstanceOf(Promise) + + const response = await promise + + expect(response).toBeUndefined() + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/error404') + expect(callbackSuccess).not.toHaveBeenCalled() + expect(callbackError).toHaveBeenCalledBefore(callbackComplete) + expect(callbackComplete).toHaveBeenCalled() + expect(callbackCatch).toHaveBeenCalled() + expect(eventSuccess).not.toHaveBeenCalled() + expect(eventError).toHaveBeenCalledBefore(eventComplete) + expect(eventComplete).toHaveBeenCalled() + }) + + it('Send bad request with fetch error', async function () { + const callbackSuccess = jasmine.createSpy('success') + const callbackError = jasmine.createSpy('error') + const callbackComplete = jasmine.createSpy('complete') + const callbackCatch = jasmine.createSpy('catch') + const eventSuccess = jasmine.createSpy('event-success') + const eventError = jasmine.createSpy('event-error') + const eventComplete = jasmine.createSpy('event-complete') + + $(document).on('ec-crud-ajax-on-success', function (event) { + eventSuccess() + }) + $(document).on('ec-crud-ajax-on-error', function (event) { + expect(event.detail.statusText).toMatch(/Error during query execution:.+failed/) + expect(event.detail.response).toBeNull() + eventError() + }) + $(document).on('ec-crud-ajax-on-complete', function (event) { + expect(event.detail.statusText).toMatch(/Error during query execution:.+failed/) + expect(event.detail.response).toBeNull() + eventComplete() + }) + + const promise = ajax.sendRequest({ + url: '/failure', + onComplete: function (statusText, response) { + expect(statusText).toMatch('failed') + expect(response).toBeNull() + callbackComplete() + }, + onSuccess: function (data, response) { + callbackSuccess() + }, + onError: function (statusText, response) { + expect(statusText).toMatch(/Error during query execution:.+failed/) + expect(response).toBeNull() + callbackError() + } + }).catch(error => { + expect(error).toMatch(/Error during query execution:.+failed/) + callbackCatch() + }) + expect(promise).toBeInstanceOf(Promise) + + const response = await promise + + expect(response).toBeUndefined() + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/failure') + expect(callbackSuccess).not.toHaveBeenCalled() + expect(callbackError).toHaveBeenCalledBefore(callbackComplete) + expect(callbackComplete).toHaveBeenCalled() + expect(callbackCatch).toHaveBeenCalled() + expect(eventSuccess).not.toHaveBeenCalled() + expect(eventError).toHaveBeenCalledBefore(eventComplete) + expect(eventComplete).toHaveBeenCalled() + }) + + it('Send request with callback priorities', async function () { + const callbackSuccess1 = jasmine.createSpy('success1') + const callbackSuccess2 = jasmine.createSpy('success2') + + await ajax.sendRequest({ + url: '/goodRequest', + onSuccess: [ + function (data, response) { + callbackSuccess1() + }, + { + priority: 99, + callback: function (data, response) { + callbackSuccess2() + } + } + ] + }) + + expect(callbackSuccess1).toHaveBeenCalled() + expect(callbackSuccess2).toHaveBeenCalledBefore(callbackSuccess1) + }) + + it('Send request without URL', async function () { + const callbackSuccess = jasmine.createSpy('success') + const callbackError = jasmine.createSpy('error') + const callbackComplete = jasmine.createSpy('complete') + const callbackCatch = jasmine.createSpy('catch') + + const promise = ajax.sendRequest({ + onComplete: function (statusText, response) { + callbackComplete() + }, + onSuccess: function (data, response) { + callbackSuccess() + }, + onError: function (statusText, response) { + callbackError() + } + }).catch(error => { + expect(error).toBeInstanceOf(TypeError) + expect(error.message).toEqual('Value required: url') + callbackCatch() + }) + expect(promise).toBeInstanceOf(Promise) + + const response = await promise + + expect(response).toBeUndefined() + expect(jasmine.Ajax.requests.mostRecent()).toBeUndefined() + expect(callbackSuccess).not.toHaveBeenCalled() + expect(callbackError).not.toHaveBeenCalled() + expect(callbackComplete).not.toHaveBeenCalled() + expect(callbackCatch).toHaveBeenCalled() + }) + + it('Send request with string body', async function () { + const callbackSuccess = jasmine.createSpy('success') + const callbackError = jasmine.createSpy('error') + const callbackComplete = jasmine.createSpy('complete') + const callbackCatch = jasmine.createSpy('catch') + + const promise = ajax.sendRequest({ + url: '/goodRequest', + body: 'body-content', + onComplete: function (statusText, response) { + callbackComplete() + }, + onSuccess: function (data, response) { + callbackSuccess() + }, + onError: function (statusText, response) { + callbackError() + } + }).catch(() => { + callbackCatch() + }) + expect(promise).toBeInstanceOf(Promise) + + const response = await promise + + expect(response).toBeInstanceOf(Response) + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/goodRequest') + expect(jasmine.Ajax.requests.mostRecent().params).toEqual('body-content') + expect(callbackSuccess).toHaveBeenCalled() + expect(callbackError).not.toHaveBeenCalled() + expect(callbackComplete).toHaveBeenCalled() + expect(callbackCatch).not.toHaveBeenCalled() + }) + + it('Send request with FormData body', async function () { + const callbackSuccess = jasmine.createSpy('success') + const callbackError = jasmine.createSpy('error') + const callbackComplete = jasmine.createSpy('complete') + const callbackCatch = jasmine.createSpy('catch') + + const formData = new FormData() + formData.append('var1', 'My value 1') + formData.append('var2[]', 'val2A') + formData.append('var2[]', 'val2C') + + const promise = ajax.sendRequest({ + url: '/goodRequest', + body: formData, + onComplete: function (statusText, response) { + callbackComplete() + }, + onSuccess: function (data, response) { + callbackSuccess() + }, + onError: function (statusText, response) { + callbackError() + } + }).catch(() => { + callbackCatch() + }) + expect(promise).toBeInstanceOf(Promise) + + const response = await promise + + expect(response).toBeInstanceOf(Response) + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/goodRequest') + expect(jasmine.Ajax.requests.mostRecent().data()).toEqual([['var1', 'My value 1'], ['var2[]', 'val2A'], ['var2[]', 'val2C']]) // Parsed by addJasmineAjaxFormDataSupport + expect(callbackSuccess).toHaveBeenCalled() + expect(callbackError).not.toHaveBeenCalled() + expect(callbackComplete).toHaveBeenCalled() + expect(callbackCatch).not.toHaveBeenCalled() + }) + + it('Send request with object body', async function () { + const callbackSuccess = jasmine.createSpy('success') + const callbackError = jasmine.createSpy('error') + const callbackComplete = jasmine.createSpy('complete') + const callbackCatch = jasmine.createSpy('catch') + + const promise = ajax.sendRequest({ + url: '/goodRequest', + body: { + var1: 'My value 1', + var2: ['val2A', 'val2C'], + 'var[': 'value', // Ignored + object: { + property1: 'A', + property2: ['B', 'C'], + subObject: { + property3: 'D' + } + } + }, + onComplete: function (statusText, response) { + callbackComplete() + }, + onSuccess: function (data, response) { + callbackSuccess() + }, + onError: function (statusText, response) { + callbackError() + } + }).catch(() => { + callbackCatch() + }) + expect(promise).toBeInstanceOf(Promise) + + const response = await promise + + expect(response).toBeInstanceOf(Response) + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/goodRequest') + expect(jasmine.Ajax.requests.mostRecent().data()).toEqual([ + ['var1', 'My value 1'], + ['var2[0]', 'val2A'], + ['var2[1]', 'val2C'], + ['object[property1]', 'A'], + ['object[property2][0]', 'B'], + ['object[property2][1]', 'C'], + ['object[subObject][property3]', 'D'] + ]) // Parsed by addJasmineAjaxFormDataSupport + expect(callbackSuccess).toHaveBeenCalled() + expect(callbackError).not.toHaveBeenCalled() + expect(callbackComplete).toHaveBeenCalled() + expect(callbackCatch).not.toHaveBeenCalled() + }) + + it('Send request with bad body', async function () { + const callbackSuccess = jasmine.createSpy('success') + const callbackError = jasmine.createSpy('error') + const callbackComplete = jasmine.createSpy('complete') + const callbackCatch = jasmine.createSpy('catch') + + const promise = ajax.sendRequest({ + url: '/goodRequest', + body: true, + onComplete: function (statusText, response) { + callbackComplete() + }, + onSuccess: function (data, response) { + callbackSuccess() + }, + onError: function (statusText, response) { + callbackError() + } + }).catch(error => { + expect(error).toBeInstanceOf(TypeError) + expect(error.message).toEqual('Bad type for option "body"') + callbackCatch() + }) + expect(promise).toBeInstanceOf(Promise) + + const response = await promise + + expect(response).toBeUndefined() + expect(jasmine.Ajax.requests.mostRecent()).toBeUndefined() + expect(callbackSuccess).not.toHaveBeenCalled() + expect(callbackError).not.toHaveBeenCalled() + expect(callbackComplete).not.toHaveBeenCalled() + expect(callbackCatch).toHaveBeenCalled() + }) + + it('Send request with query option', async function () { + const promise = ajax.sendRequest({ + url: '/goodRequest', + query: { + // Same example as https://www.php.net/manual/fr/function.http-build-query.php + user: { + name: 'Bob Smith', + age: 47, + sex: 'M', + dob: '5/12/1956' + }, + 'var[': 'value', // Ignored + pastimes: ['golf', 'opera', 'poker', 'rap'], + children: { + bobby: { + age: 12, + sex: 'M' + }, + sally: { + age: 8, + sex: 'F' + } + } + }, + cache: true + }) + expect(promise).toBeInstanceOf(Promise) + + const response = await promise + + expect(response).toBeInstanceOf(Response) + expect(jasmine.Ajax.requests.mostRecent().url).toEqual('http://localhost:9876/goodRequest?user%5Bname%5D=Bob+Smith&user%5Bage%5D=47&user%5Bsex%5D=M&user%5Bdob%5D=5%2F12%2F1956&pastimes%5B0%5D=golf&pastimes%5B1%5D=opera&pastimes%5B2%5D=poker&pastimes%5B3%5D=rap&children%5Bbobby%5D%5Bage%5D=12&children%5Bbobby%5D%5Bsex%5D=M&children%5Bsally%5D%5Bage%5D=8&children%5Bsally%5D%5Bsex%5D=F') + }) + + it('Send request with query option and override', async function () { + const promise = ajax.sendRequest({ + url: '/goodRequest?var1=value1&var2=value2', + query: { + var1: 'newValue1' + }, + cache: true + }) + expect(promise).toBeInstanceOf(Promise) + + const response = await promise + + expect(response).toBeInstanceOf(Response) + expect(jasmine.Ajax.requests.mostRecent().url).toEqual('http://localhost:9876/goodRequest?var1=newValue1&var2=value2') + }) + + it('Send request with cache', async function () { + const promise = ajax.sendRequest({ + url: '/goodRequest', + cache: true + }) + expect(promise).toBeInstanceOf(Promise) + + const response = await promise + + expect(response).toBeInstanceOf(Response) + expect(jasmine.Ajax.requests.mostRecent().url).toMatch(/goodRequest$/) + }) + + it('Send request with default cache', async function () { + const promise = ajax.sendRequest({ + url: '/goodRequest' + }) + expect(promise).toBeInstanceOf(Promise) + + const response = await promise + + expect(response).toBeInstanceOf(Response) + expect(jasmine.Ajax.requests.mostRecent().url).toMatch(/goodRequest\?_=\d+$/) + }) + + it('Send request without cache', async function () { + const promise = ajax.sendRequest({ + url: '/goodRequest', + cache: false + }) + expect(promise).toBeInstanceOf(Promise) + + const response = await promise + + expect(response).toBeInstanceOf(Response) + expect(jasmine.Ajax.requests.mostRecent().url).toMatch(/goodRequest\?_=\d+$/) + }) + + it('Send request without cache but param is already used', async function () { + const promise = ajax.sendRequest({ + url: '/goodRequest?_=val', + cache: false + }) + expect(promise).toBeInstanceOf(Promise) + + const response = await promise + + expect(response).toBeInstanceOf(Response) + expect(jasmine.Ajax.requests.mostRecent().url).toMatch(/goodRequest\?_=val$/) + }) + + it('Send request with relative URL', async function () { + const promise = ajax.sendRequest({ + url: '/goodRequest', + cache: true + }) + expect(promise).toBeInstanceOf(Promise) + + const response = await promise + + expect(response).toBeInstanceOf(Response) + expect(jasmine.Ajax.requests.mostRecent().url).toEqual('http://localhost:9876/goodRequest') + }) + + it('Send request with absolute URL', async function () { + const promise = ajax.sendRequest({ + url: 'http://test.demo/goodRequest', + cache: true + }) + expect(promise).toBeInstanceOf(Promise) + + const response = await promise + + expect(response).toBeInstanceOf(Response) + expect(jasmine.Ajax.requests.mostRecent().url).toEqual('http://test.demo/goodRequest') + }) + + it('Send request and update DOM with default mode', async function () { + $('body').append('
    ') + const callbackSuccess = jasmine.createSpy('success') + + await ajax.sendRequest({ + url: '/goodRequest', + onSuccess: { + callback: function (data, response) { + callbackSuccess($('#ajax-result').html()) + }, + priority: -99 + }, + update: '#ajax-result .content' + }) + + expect(callbackSuccess).toHaveBeenCalledWith('
    CONTENT
    ') + }) + + it('Send request and update DOM with "update" mode', async function () { + await testUpdate('update', '
    CONTENT
    ') + }) + + it('Send request and update DOM with "before" mode', async function () { + await testUpdate('before', 'CONTENT
    X
    ') + }) + + it('Send request and update DOM with "after" mode', async function () { + await testUpdate('after', '
    X
    CONTENT') + }) + + it('Send request and update DOM with "prepend" mode', async function () { + await testUpdate('prepend', '
    CONTENTX
    ') + }) + + it('Send request and update DOM with "append" mode', async function () { + await testUpdate('append', '
    XCONTENT
    ') + }) + + it('Send request and update DOM with bad mode', async function () { + spyOn(window.console, 'error') + await testUpdate('badMode', '
    X
    ') + expect(window.console.error).toHaveBeenCalledWith('Bad updateMode: badMode') + }) + + it('Send request with method option', async function () { + await ajax.sendRequest({ + url: '/goodRequest', + method: 'GET' + }) + + expect(jasmine.Ajax.requests.mostRecent().method).toEqual('GET') + }) + + it('Send request with onBeforeSend option', async function () { + const callbackSuccess = jasmine.createSpy('success') + const callbackBeforeSend = jasmine.createSpy('beforeSend') + + const promise = ajax.sendRequest({ + url: '/goodRequest', + onBeforeSend: function (options) { + expect(options).toBeInstanceOf(Object) + expect(options.url).toEqual('/goodRequest') + callbackBeforeSend() + }, + onSuccess: function (data, response) { + callbackSuccess() + } + }) + + const response = await promise + + expect(response).toBeInstanceOf(Response) + expect(callbackSuccess).toHaveBeenCalled() + expect(callbackBeforeSend).toHaveBeenCalledBefore(callbackSuccess) + }) + + it('Send request canceled by onBeforeSend option', async function () { + const callbackSuccess = jasmine.createSpy('success') + const callbackBeforeSend = jasmine.createSpy('beforeSend') + + const promise = ajax.sendRequest({ + url: '/goodRequest', + onBeforeSend: function (options) { + expect(options).toBeInstanceOf(Object) + expect(options.url).toEqual('/goodRequest') + callbackBeforeSend() + options.stop = true + }, + onSuccess: function (data, response) { + callbackSuccess() + } + }) + + const response = await promise + + expect(response).toBeNull() + expect(callbackSuccess).not.toHaveBeenCalled() + expect(callbackBeforeSend).toHaveBeenCalled() + }) + + it('Test ec-crud-ajax-before-send event', async function () { + const callbackSuccess = jasmine.createSpy('success') + const callbackBeforeSend = jasmine.createSpy('beforeSend') + + $(document).on('ec-crud-ajax-before-send', function (event) { + expect(event.detail.options).toBeInstanceOf(Object) + expect(event.detail.options.url).toEqual('/goodRequest') + callbackBeforeSend() + }) + + const promise = ajax.sendRequest({ + url: '/goodRequest', + onSuccess: function (data, response) { + callbackSuccess() + } + }) + + const response = await promise + + expect(response).toBeInstanceOf(Response) + expect(callbackSuccess).toHaveBeenCalled() + expect(callbackBeforeSend).toHaveBeenCalledBefore(callbackSuccess) + + $(document).off('ec-crud-ajax-before-send') + }) + + it('Send request canceled by ec-crud-ajax-before-send event', async function () { + const callbackSuccess = jasmine.createSpy('success') + const callbackBeforeSend = jasmine.createSpy('beforeSend') + + $(document).on('ec-crud-ajax-before-send', function (event) { + expect(event.detail.options).toBeInstanceOf(Object) + expect(event.detail.options.url).toEqual('/goodRequest') + event.preventDefault() + callbackBeforeSend() + }) + + const promise = ajax.sendRequest({ + url: '/goodRequest', + onSuccess: function (data, response) { + callbackSuccess() + } + }) + + const response = await promise + + expect(response).toBeNull() + expect(callbackSuccess).not.toHaveBeenCalled() + expect(callbackBeforeSend).toHaveBeenCalled() + + $(document).off('ec-crud-ajax-before-send') + }) + + it('Test ec-crud-ajax event', async function () { + const callbackSuccess = jasmine.createSpy('success') + const callbackBeginning = jasmine.createSpy('beginning') + + $(document).on('ec-crud-ajax', function (event) { + expect(event.detail.options).toBeInstanceOf(Object) + expect(event.detail.options.url).toEqual('/goodRequest') + callbackBeginning() + }) + + const promise = ajax.sendRequest({ + url: '/goodRequest', + onSuccess: function (data, response) { + callbackSuccess() + } + }) + + const response = await promise + + expect(response).toBeInstanceOf(Response) + expect(callbackSuccess).toHaveBeenCalled() + expect(callbackBeginning).toHaveBeenCalled() + + $(document).off('ec-crud-ajax') + }) + + it('Send request canceled by ec-crud-ajax event', async function () { + const callbackSuccess = jasmine.createSpy('success') + const callbackBeginning = jasmine.createSpy('beginning') + + $(document).on('ec-crud-ajax', function (event) { + expect(event.detail.options).toBeInstanceOf(Object) + expect(event.detail.options.url).toEqual('/goodRequest') + event.preventDefault() + callbackBeginning() + }) + + const promise = ajax.sendRequest({ + url: '/goodRequest', + onSuccess: function (data, response) { + callbackSuccess() + } + }) + + const response = await promise + + expect(response).toBeNull() + expect(callbackSuccess).not.toHaveBeenCalled() + expect(callbackBeginning).toHaveBeenCalled() + + $(document).off('ec-crud-ajax') + }) + + it('Send request with options changed by ec-crud-ajax event', async function () { + const callbackSuccess = jasmine.createSpy('success') + const callbackBeginning = jasmine.createSpy('beginning') + + $(document).on('ec-crud-ajax', function (event) { + expect(event.detail.options).toBeInstanceOf(Object) + expect(event.detail.options.body).toBeUndefined() + event.detail.options.body = 'BODY ADDED BY EVENT' + callbackBeginning() + }) + + const promise = ajax.sendRequest({ + url: '/goodRequest', + onSuccess: function (data, response) { + callbackSuccess() + } + }) + + const response = await promise + + expect(response).toBeInstanceOf(Response) + expect(jasmine.Ajax.requests.mostRecent().params).toEqual('BODY ADDED BY EVENT') + expect(callbackSuccess).toHaveBeenCalled() + expect(callbackBeginning).toHaveBeenCalled() + + $(document).off('ec-crud-ajax') + }) + + it('Send request with text responseDataType', async function () { + const callbackSuccess = jasmine.createSpy('success') + + await ajax.sendRequest({ + url: '/goodRequest', + responseDataType: 'text', + onSuccess: function (data, response) { + expect(data).toBeInstanceOf(String) + expect(data).toEqual('CONTENT') + expect(response).toBeInstanceOf(Response) + callbackSuccess() + } + }) + + expect(callbackSuccess).toHaveBeenCalled() + }) + + it('Send request with json responseDataType', async function () { + const callbackSuccess = jasmine.createSpy('success') + + await ajax.sendRequest({ + url: '/resultJSON', + responseDataType: 'json', + onSuccess: function (data, response) { + expect(data).not.toBeInstanceOf(String) + expect(data).toEqual({ var1: 'value1', var2: 'value2' }) + expect(response).toBeInstanceOf(Response) + callbackSuccess() + } + }) + + expect(callbackSuccess).toHaveBeenCalled() + }) + + it('Send request with json responseDataType and bad result', async function () { + const callbackSuccess = jasmine.createSpy('success') + const callbackError = jasmine.createSpy('error') + const callbackCatch = jasmine.createSpy('catch') + + await ajax.sendRequest({ + url: '/badJSON', + responseDataType: 'json', + onSuccess: function (data, response) { + callbackSuccess() + }, + onError: function (statusText, response) { + expect(statusText).toMatch(/Error during fetching response body:.+JSON\.parse/) + expect(response).toBeInstanceOf(Response) + callbackError() + } + }).catch(error => { + expect(error).toMatch(/Error during fetching response body:.+JSON\.parse/) + callbackCatch() + }) + + expect(callbackSuccess).not.toHaveBeenCalled() + expect(callbackError).toHaveBeenCalled() + expect(callbackCatch).toHaveBeenCalled() + }) + + it('Send request with no responseDataType', async function () { + const callbackSuccess = jasmine.createSpy('success') + + await ajax.sendRequest({ + url: '/goodRequest', + responseDataType: null, + onSuccess: function (data, response) { + expect(data).toBeNull() + expect(response).toBeInstanceOf(Response) + callbackSuccess() + } + }) + + expect(callbackSuccess).toHaveBeenCalled() + }) + + it('Send request with SCRIPT tag in response', async function () { + $('body').append('
    X
    ') + const callbackSuccess = jasmine.createSpy('success') + + await ajax.sendRequest({ + url: '/resultJavaScript', + onSuccess: { + callback: function (data, response) { + callbackSuccess($('#ajax-result').html()) + }, + priority: -99 + }, + update: '#ajax-result .content' + }) + + expect(callbackSuccess).toHaveBeenCalledWith('
    BEFORE
    ') + }) + + async function testUpdate (updateMode, expectedContent) { + $('body').append('
    X
    ') + const callbackSuccess = jasmine.createSpy('success') + + await ajax.sendRequest({ + url: '/goodRequest', + onSuccess: { + callback: function (data, response) { + callbackSuccess($('#ajax-result').html()) + }, + priority: -99 + }, + update: '#ajax-result .content', + updateMode + }) + + expect(callbackSuccess).toHaveBeenCalledWith(expectedContent) + } +}) + +describe('Test Ajax.click', function () { + beforeEach(function () { + jasmine.Ajax.install() + addJasmineAjaxFormDataSupport() + + jasmine.Ajax.stubRequest(/goodRequest/).andReturn({ + status: 200, + responseText: 'CONTENT' + }) + }) + + afterEach(function () { + jasmine.Ajax.uninstall() + $('.html-test').remove() + callbackManager.clear() + }) + + it('Send request with button', async function () { + $('body').append('') + + const callbackSuccess = jasmine.createSpy('success') + + const promise = ajax.click('#buttonToTest', { + url: '/goodRequest', + onSuccess: function (data, response) { + callbackSuccess() + } + }) + expect(promise).toBeInstanceOf(Promise) + + const response = await promise + + expect(response).toBeInstanceOf(Response) + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/goodRequest') + expect(callbackSuccess).toHaveBeenCalled() + }) + + it('Send request with button and Element', async function () { + $('body').append('') + + const callbackSuccess = jasmine.createSpy('success') + + const promise = ajax.click(document.querySelector('#buttonToTest'), { + url: '/goodRequest', + onSuccess: function (data, response) { + callbackSuccess() + } + }) + expect(promise).toBeInstanceOf(Promise) + + const response = await promise + + expect(response).toBeInstanceOf(Response) + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/goodRequest') + expect(callbackSuccess).toHaveBeenCalled() + }) + + it('Send request with button and jQuery', async function () { + $('body').append('') + + const callbackSuccess = jasmine.createSpy('success') + + const promise = ajax.click($('#buttonToTest'), { + url: '/goodRequest', + onSuccess: function (data, response) { + callbackSuccess() + } + }) + expect(promise).toBeInstanceOf(Promise) + + const response = await promise + + expect(response).toBeInstanceOf(Response) + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/goodRequest') + expect(callbackSuccess).toHaveBeenCalled() + }) + + it('Send request with button and data-*', async function () { + $('body').append('') + + const callbackSuccess = jasmine.createSpy('success') + + callbackManager.registerCallback('my_callback_on_success', function (data, response) { + callbackSuccess() + }) + + await ajax.click('#buttonToTest') + + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/goodRequest') + expect(callbackSuccess).toHaveBeenCalled() + }) + + it('Send request with button and data-* and options', async function () { + $('body').append('') + + $('#childToTest').get(0).click() + + await wait(() => { + return false + }, 500) + + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/goodRequest') + }) + + it('Send auto-request with button canceled', async function () { + $(document).on('ec-crud-ajax-click-auto-before', '#clickToTest', function (event) { + event.preventDefault() + }) + $('body').append('') + + $('#clickToTest').get(0).click() + + await wait(() => { + return false + }, 500) + + expect(jasmine.Ajax.requests.mostRecent()).toBeUndefined() + + $(document).off('ec-crud-ajax-click-auto-before', '#clickToTest') + }) + + it('Send auto-request canceled by onBeforeSend option', async function () { + $('body').append('') + + callbackManager.registerCallback('my_callback_on_before_send', function (options) { + options.stop = true + }) + + $('#clickToTest').get(0).click() + + await wait(() => { + return false + }, 500) + + expect(jasmine.Ajax.requests.mostRecent()).toBeUndefined() + + $(document).off('ec-crud-ajax-click-auto-before', '#clickToTest') + }) + + it('Send auto-request with button and error', async function () { + $('body').append('') + $('#formToTest input[name=var1]').val('My value 1') + $('#formToTest input[name=var2]').val('My value 2') + + const callbackFormBefore = jasmine.createSpy('form_before') + const callbackFormComplete = jasmine.createSpy('form_complete') + + $(document).on('ec-crud-ajax-form-before', function (event) { + callbackFormBefore() + }) + $(document).on('ec-crud-ajax-form-complete', function (event) { + callbackFormComplete() + }) + + $('#formToTest button[type="submit"]').get(0).click() + + await wait(() => { + return false + }, 500) + + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/goodRequest') + expect(jasmine.Ajax.requests.mostRecent().method).toBe('POST') + expect(jasmine.Ajax.requests.mostRecent().data()).toEqual([['var1', 'My value 1'], ['var2', 'My value 2']]) // Parsed by addJasmineAjaxFormDataSupport + expect(callbackFormBefore).toHaveBeenCalled() + expect(callbackFormComplete).toHaveBeenCalled() + }) + + it('Send auto-request with form canceled', async function () { + $(document).on('ec-crud-ajax-form-auto-before', '#formToTest', function (event) { + event.preventDefault() + }) + $('body').append('
    ') + $('#formToTest input[name=var1]').val('My value 1') + $('#formToTest input[name=var2]').val('My value 2') + + $('#formToTest button[type="submit"]').get(0).click() + + await wait(() => { + return false + }, 500) + + expect(jasmine.Ajax.requests.mostRecent()).toBeUndefined() + + $(document).off('ec-crud-ajax-form-auto-before', '#formToTest') + }) + + it('Send auto-request with form and error', async function () { + $('body').append('
    ') + spyOn(window.console, 'error') + + $('#formToTest button[type="submit"]').get(0).click() + + await wait(() => { + return false + }, 500) + + expect(jasmine.Ajax.requests.mostRecent()).toBeUndefined() + expect(window.console.error).toHaveBeenCalledWith(new TypeError('Value required: url')) + }) +}) + +describe('Test Ajax.updateDom', function () { + beforeEach(function () { + $('body').append('
    X
    ') + }) + + afterEach(function () { + $('.html-test').remove() + $(document).off('ec-crud-ajax-update-dom-before') + $(document).off('ec-crud-ajax-update-dom-after') + callbackManager.clear() + }) + + it('Update with "update" mode', function () { + testUpdateDom('update', '
    OK
    ') + }) + + it('Update with "before" mode', function () { + testUpdateDom('before', 'OK
    X
    ') + }) + + it('Update with "after" mode', function () { + testUpdateDom('after', '
    X
    OK') + }) + + it('Update with "prepend" mode', function () { + testUpdateDom('prepend', '
    OKX
    ') + }) + + it('Update with "append" mode', function () { + testUpdateDom('append', '
    XOK
    ') + }) + + it('Update with "update" mode and ec-crud-ajax-update-dom-before event - change updateMode', function () { + $(document).on('ec-crud-ajax-update-dom-before', function (event) { + event.detail.updateMode = 'append' + }) + + testUpdateDom('update', '
    XOK
    ') + }) + + it('Update with "update" mode and ec-crud-ajax-update-dom-before event - change content', function () { + $(document).on('ec-crud-ajax-update-dom-before', function (event) { + event.detail.content = 'NEW OK' + }) + + testUpdateDom('update', '
    NEW OK
    ') + }) + + it('Update with "update" mode and ec-crud-ajax-update-dom-before event - preventDefault', function () { + $(document).on('ec-crud-ajax-update-dom-before', function (event) { + event.preventDefault() + }) + + testUpdateDom('update', '
    X
    ') + }) + + it('Update with "update" mode and ec-crud-ajax-update-dom-after event', function () { + $(document).on('ec-crud-ajax-update-dom-after', function (event) { + $(event.detail.element).find('.content').html('OK') + }) + + ajax.updateDom('#container .content', 'update', '
    ') + expect($('#container').html()).toEqual('
    OK
    ') + }) + + it('Update with "update" mode and ec-crud-ajax-update-dom-after event - with scopes', function () { + const callbackCalled1 = jasmine.createSpy('called1') + const callbackCalled2 = jasmine.createSpy('called2') + const callbackNotCalled = jasmine.createSpy('not-called') + + $(document).on('ec-crud-ajax-update-dom-before', function (event) { + callbackCalled1() + }) + $(document).on('ec-crud-ajax-update-dom-before', '#container', function (event) { + callbackCalled2() + }) + $(document).on('ec-crud-ajax-update-dom-before', '#id-does-not-exit', function (event) { + callbackNotCalled() + }) + + testUpdateDom('update', '
    OK
    ') + expect(callbackCalled1).toHaveBeenCalled() + expect(callbackCalled2).toHaveBeenCalled() + expect(callbackNotCalled).not.toHaveBeenCalled() + }) + + function testUpdateDom (updateMode, expected) { + ajax.updateDom('#container .content', updateMode, 'OK') + expect($('#container').html()).toEqual(expected) + } + + it('Update with bad mode', function () { + spyOn(window.console, 'error') + ajax.updateDom('#container .content', 'badMode', 'OK') + expect(window.console.error).toHaveBeenCalledWith('Bad updateMode: badMode') + expect($('#container').html()).toEqual('
    X
    ') + }) + + it('Update with "update" mode and Element', function () { + ajax.updateDom(document.querySelector('#container .content'), 'update', 'OK') + expect($('#container').html()).toEqual('
    OK
    ') + }) + + it('Update with "update" mode and jQuery', function () { + ajax.updateDom($('#container .content'), 'update', 'OK') + expect($('#container').html()).toEqual('
    OK
    ') + }) +}) + +function addJasmineAjaxFormDataSupport () { + jasmine.Ajax.addCustomParamParser({ + test: function (xhr) { + return xhr.params instanceof FormData + }, + parse: function (params) { + const array = [] + params.forEach((value, key) => { + array.push([key, value]) + }) + + return array + } + }) +} diff --git a/tests/assets/js/callback-manager.spec.js b/tests/assets/js/callback-manager.spec.js new file mode 100644 index 0000000..48242f1 --- /dev/null +++ b/tests/assets/js/callback-manager.spec.js @@ -0,0 +1,98 @@ +/* + * This file is part of the EcommitCrudBundle package. + * + * (c) E-commit + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import * as callbackManager from '@ecommit/crud-bundle/js/callback-manager' + +describe('Test callback manager', function () { + afterEach(function () { + callbackManager.clear() + }) + + it('Test registerCallback with invalid name', function () { + spyOn(window.console, 'error') + callbackManager.registerCallback(function () {}, function () {}) + expect(window.console.error).toHaveBeenCalledWith('Bad name') + }) + + it('Test registerCallback with null name', function () { + spyOn(window.console, 'error') + callbackManager.registerCallback(null, function () {}) + expect(window.console.error).toHaveBeenCalledWith('Bad name') + }) + + it('Test registerCallback with invalid callback', function () { + spyOn(window.console, 'error') + callbackManager.registerCallback('my_callback', 'callback') + expect(window.console.error).toHaveBeenCalledWith('Invalid callback my_callback') + }) + + it('Test registerCallback with null callback', function () { + spyOn(window.console, 'error') + callbackManager.registerCallback('my_callback', null) + expect(window.console.error).toHaveBeenCalledWith('Invalid callback my_callback') + }) + + it('Test callbackIsRegistred with invalid name', function () { + spyOn(window.console, 'error') + callbackManager.callbackIsRegistred(function () {}) + expect(window.console.error).toHaveBeenCalledWith('Bad name') + }) + + it('Test callbackIsRegistred with null name', function () { + spyOn(window.console, 'error') + callbackManager.callbackIsRegistred(null) + expect(window.console.error).toHaveBeenCalledWith('Bad name') + }) + + it('Test callbackIsRegistred with found callback', function () { + callbackManager.registerCallback('my_callback', function () {}) + expect(callbackManager.callbackIsRegistred('my_callback')).toBeTrue() + }) + + it('Test callbackIsRegistred with not found callback', function () { + expect(callbackManager.callbackIsRegistred('my_callback')).toBeFalse() + }) + + it('Test getRegistredCallback with invalid name', function () { + spyOn(window.console, 'error') + callbackManager.getRegistredCallback(function () {}) + expect(window.console.error).toHaveBeenCalledWith('Bad name') + }) + + it('Test getRegistredCallback with null name', function () { + spyOn(window.console, 'error') + callbackManager.getRegistredCallback(null) + expect(window.console.error).toHaveBeenCalledWith('Bad name') + }) + + it('Test getRegistredCallback with found callback', function () { + callbackManager.registerCallback('my_callback', function () {}) + const callback = callbackManager.getRegistredCallback('my_callback') + expect(callback).toBeInstanceOf(Function) + }) + + it('Test getRegistredCallback with not found callback', function () { + spyOn(window.console, 'error') + const callback = callbackManager.getRegistredCallback('my_callback') + expect(window.console.error).toHaveBeenCalledWith('Callback not found: my_callback') + expect(callback).toBeNull() + }) + + it('Test clear', function () { + callbackManager.registerCallback('my_callback', function () {}) + expect(callbackManager.getRegistredCallback('my_callback')).toBeInstanceOf(Function) + + callbackManager.clear() + + spyOn(window.console, 'error') + const callback = callbackManager.getRegistredCallback('my_callback') + expect(window.console.error).toHaveBeenCalledWith('Callback not found: my_callback') + expect(callback).toBeNull() + }) +}) diff --git a/tests/assets/js/callback.spec.js b/tests/assets/js/callback.spec.js new file mode 100644 index 0000000..e324db2 --- /dev/null +++ b/tests/assets/js/callback.spec.js @@ -0,0 +1,146 @@ +/* + * This file is part of the EcommitCrudBundle package. + * + * (c) E-commit + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import runCallback from '@ecommit/crud-bundle/js/callback' +import * as callbackManager from '@ecommit/crud-bundle/js/callback-manager' + +describe('Test callback', function () { + it('Test callback with function', function () { + const callback = jasmine.createSpy('callback') + + runCallback(function (arg) { + callback(arg) + }, 2) + + expect(callback).toHaveBeenCalledWith(2) + }) + + it('Test callback with string', function () { + const myCallback = jasmine.createSpy('callback') + callbackManager.registerCallback('my_callback', function (arg) { + myCallback(arg) + }) + + runCallback('my_callback', 3) + + expect(myCallback).toHaveBeenCalledWith(3) + callbackManager.clear() + }) + + it('Test callback with string - Callback not registred', function () { + spyOn(window.console, 'error') + runCallback('my_callback_never_registred', 3) + expect(window.console.error).toHaveBeenCalledWith('Callback not found: my_callback_never_registred') + }) + + it('Test callback with sub array', function () { + const callback1 = jasmine.createSpy('callback1') + const callback2 = jasmine.createSpy('callback2') + const callback3 = jasmine.createSpy('callback3') + const callback4 = jasmine.createSpy('callback4') + const callback5 = jasmine.createSpy('callback5') + + runCallback([ + function () { + callback1() + }, + [ + [ + function () { + callback2() + }, + function () { + callback3() + } + ], + function () { + callback4() + } + ], + function () { + callback5() + } + ]) + + expect(callback1).toHaveBeenCalledTimes(1) + expect(callback2).toHaveBeenCalledTimes(1) + expect(callback3).toHaveBeenCalledTimes(1) + expect(callback4).toHaveBeenCalledTimes(1) + expect(callback5).toHaveBeenCalledTimes(1) + }) + + it('Test callback with priorities', function () { + const callback1 = jasmine.createSpy('callback1') + const callback2 = jasmine.createSpy('callback2') + const callback3 = jasmine.createSpy('callback3') + const callback4 = jasmine.createSpy('callback4') + const callback5 = jasmine.createSpy('callback5') + + callbackManager.registerCallback('my_callback3', function (arg) { + callback3(arg) + }) + callbackManager.registerCallback('my_callback4', function (arg) { + callback4(arg) + }) + callbackManager.registerCallback('my_callback5', function (arg) { + callback5(arg) + }) + + runCallback([ + // Called third + function (arg) { + callback1(arg) + }, + // Called first + { + priority: '99', + callback: function (arg) { + callback2(arg) + } + }, + // Called second + { + priority: 10, + callback: 'my_callback3' + }, + // Called fourth + { + callback: 'my_callback4' + }, + // Called in fifth + 'my_callback5' + ], 'myValue') + + expect(callback1).toHaveBeenCalledTimes(1) + expect(callback2).toHaveBeenCalledTimes(1) + expect(callback3).toHaveBeenCalledTimes(1) + expect(callback4).toHaveBeenCalledTimes(1) + expect(callback5).toHaveBeenCalledTimes(1) + + expect(callback1).toHaveBeenCalledWith('myValue') + expect(callback2).toHaveBeenCalledWith('myValue') + + expect(callback2).toHaveBeenCalledBefore(callback3) + expect(callback3).toHaveBeenCalledBefore(callback1) + expect(callback1).toHaveBeenCalledBefore(callback4) + expect(callback4).toHaveBeenCalledBefore(callback5) + + callbackManager.clear() + }) + + it('Test callback with many arguments', function () { + const callback = jasmine.createSpy('callback') + + runCallback(function (arg1, arg2) { + callback(arg1, arg2) + }, 2, 4) + + expect(callback).toHaveBeenCalledWith(2, 4) + }) +}) diff --git a/tests/assets/js/main.js b/tests/assets/js/main.js new file mode 100644 index 0000000..86d7edf --- /dev/null +++ b/tests/assets/js/main.js @@ -0,0 +1,15 @@ +/* + * This file is part of the EcommitCrudBundle package. + * + * (c) E-commit + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +// Use fetch with jasmine-ajax +window.fetch = undefined +require('whatwg-fetch') + +const testsContext = require.context('.', true, /\.spec\.js$/) +testsContext.keys().forEach(testsContext) diff --git a/tests/assets/js/modal/engine/test.js b/tests/assets/js/modal/engine/test.js new file mode 100644 index 0000000..53f3120 --- /dev/null +++ b/tests/assets/js/modal/engine/test.js @@ -0,0 +1,24 @@ +/* + * This file is part of the EcommitCrudBundle package. + * + * (c) E-commit + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import runCallback from '@ecommit/crud-bundle/js/callback' +import $ from 'jquery' + +export function openModal (options) { + runCallback(options.onOpen, $(options.element)) + + $(document).one('testEngineClose', options.element, function () { + runCallback(options.onClose, $(options.element)) + }) +} + +export function closeModal (element) { + $(element + ' .content').remove() + $(document).find(element).trigger('testEngineClose') +} diff --git a/tests/assets/js/modal/modal-manager.spec.js b/tests/assets/js/modal/modal-manager.spec.js new file mode 100644 index 0000000..a75240c --- /dev/null +++ b/tests/assets/js/modal/modal-manager.spec.js @@ -0,0 +1,457 @@ +/* + * This file is part of the EcommitCrudBundle package. + * + * (c) E-commit + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import * as modalManager from '@ecommit/crud-bundle/js/modal/modal-manager' +import $ from 'jquery' +import wait from './../wait' +const testEngine = require('./engine/test') +const bootstrap3Engine = require('@ecommit/crud-bundle/js/modal/engine/bootstrap3') + +it('Get engine when not defined', function () { + modalManager.defineEngine(null) + + spyOn(window.console, 'error') + + const engine = modalManager.getEngine() + expect(window.console.error).toHaveBeenCalledWith('Engine not defined') + + expect(engine).toBeUndefined() +}) + +describe('Test Modal-manager with spy engine', function () { + beforeEach(function () { + $('body').append('
    ') + this.spyEngine = { + opened: false, + openModal: function (options) { + this.opened = true + }, + closeModal: function (element) { + this.opened = false + } + } + spyOn(this.spyEngine, 'openModal').and.callThrough() + spyOn(this.spyEngine, 'closeModal').and.callThrough() + + modalManager.defineEngine(this.spyEngine) + + jasmine.Ajax.install() + jasmine.Ajax.stubRequest(/goodRequest/).andReturn({ + status: 200, + response: 'OK', + responseText: 'OK' + }) + }) + + afterEach(function () { + $('.html-test').remove() + $('#test-modal').remove() + jasmine.Ajax.uninstall() + }) + + it('Test openModal', function () { + modalManager.openModal({ + element: '#test-modal' + }) + + expect(this.spyEngine.openModal).toHaveBeenCalled() + expect(this.spyEngine.closeModal).not.toHaveBeenCalled() + }) + + it('Test openModal without element option', function () { + spyOn(window.console, 'error') + modalManager.openModal({}) + expect(window.console.error).toHaveBeenCalledWith('Value required: element') + }) + + it('Test closeModal', function () { + modalManager.closeModal('#test-modal') + + expect(this.spyEngine.openModal).not.toHaveBeenCalled() + expect(this.spyEngine.closeModal).toHaveBeenCalled() + }) + + it('Test auto openModal', function () { + $('body').append('Go !') + + $('#linkToTest').get(0).click() + + expect(this.spyEngine.openModal).toHaveBeenCalled() + expect(this.spyEngine.closeModal).not.toHaveBeenCalled() + }) + + it('Test auto openModal with child', function () { + $('body').append('Go !') + + $('#childToTest').get(0).click() + + expect(this.spyEngine.openModal).toHaveBeenCalled() + expect(this.spyEngine.closeModal).not.toHaveBeenCalled() + }) + + it('Test auto openModal canceled', function () { + $(document).on('ec-crud-modal-auto-before', '#linkToTest', function (event) { + event.preventDefault() + }) + $('body').append('Go !') + + $('#linkToTest').get(0).click() + + expect(this.spyEngine.openModal).not.toHaveBeenCalled() + expect(this.spyEngine.closeModal).not.toHaveBeenCalled() + + $(document).off('ec-crud-modal-auto-before', '#linkToTest') + }) + + it('Test auto openRemoteModal - link', async function () { + $('body').append('Go !') + + $('#linkToTest').get(0).click() + await wait(() => { + return this.spyEngine.opened + }) + + expect(this.spyEngine.openModal).toHaveBeenCalled() + expect(this.spyEngine.closeModal).not.toHaveBeenCalled() + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/goodRequest') + expect(jasmine.Ajax.requests.mostRecent().method).toBe('POST') + }) + + it('Test auto openRemoteModal - link with child', async function () { + $('body').append('Go !') + + $('#childToTest').get(0).click() + await wait(() => { + return this.spyEngine.opened + }) + + expect(this.spyEngine.openModal).toHaveBeenCalled() + expect(this.spyEngine.closeModal).not.toHaveBeenCalled() + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/goodRequest') + expect(jasmine.Ajax.requests.mostRecent().method).toBe('POST') + }) + + it('Test auto openRemoteModal - link with href', async function () { + $('body').append('Go !') + + $('#linkToTest').get(0).click() + await wait(() => { + return this.spyEngine.opened + }) + + expect(this.spyEngine.openModal).toHaveBeenCalled() + expect(this.spyEngine.closeModal).not.toHaveBeenCalled() + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/goodRequest') + expect(jasmine.Ajax.requests.mostRecent().method).toBe('POST') + }) + + it('Test auto openRemoteModal - link - prioriry url attr', async function () { + $('body').append('Go !') + + $('#linkToTest').get(0).click() + await wait(() => { + return this.spyEngine.opened + }) + + expect(this.spyEngine.openModal).toHaveBeenCalled() + expect(this.spyEngine.closeModal).not.toHaveBeenCalled() + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/goodRequest') + expect(jasmine.Ajax.requests.mostRecent().method).toBe('POST') + }) + + it('Test auto openRemoteModal - link - canceled', async function () { + $(document).on('ec-crud-remote-modal-auto-before', '#linkToTest', function (event) { + event.preventDefault() + }) + $('body').append('Go !') + + $('#linkToTest').get(0).click() + await wait(() => { + return this.spyEngine.opened + }, 500) + + expect(this.spyEngine.openModal).not.toHaveBeenCalled() + expect(this.spyEngine.closeModal).not.toHaveBeenCalled() + expect(jasmine.Ajax.requests.mostRecent()).toBeUndefined() + + $(document).off('ec-crud-remote-modal-auto-before', '#linkToTest') + }) + + it('Test auto openRemoteModal - button', async function () { + $('body').append('') + + $('#buttonToTest').get(0).click() + await wait(() => { + return this.spyEngine.opened + }) + + expect(this.spyEngine.openModal).toHaveBeenCalled() + expect(this.spyEngine.closeModal).not.toHaveBeenCalled() + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/goodRequest') + expect(jasmine.Ajax.requests.mostRecent().method).toBe('POST') + }) + + it('Test auto openRemoteModal - button with child', async function () { + $('body').append('') + + $('#childToTest').get(0).click() + await wait(() => { + return this.spyEngine.opened + }) + + expect(this.spyEngine.openModal).toHaveBeenCalled() + expect(this.spyEngine.closeModal).not.toHaveBeenCalled() + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/goodRequest') + expect(jasmine.Ajax.requests.mostRecent().method).toBe('POST') + }) + + it('Test auto openRemoteModal - button - canceled', async function () { + $(document).on('ec-crud-remote-modal-auto-before', '#buttonToTest', function (event) { + event.preventDefault() + }) + $('body').append('') + + $('#buttonToTest').get(0).click() + await wait(() => { + return this.spyEngine.opened + }, 500) + + expect(this.spyEngine.openModal).not.toHaveBeenCalled() + expect(this.spyEngine.closeModal).not.toHaveBeenCalled() + expect(jasmine.Ajax.requests.mostRecent()).toBeUndefined() + + $(document).off('ec-crud-remote-modal-auto-before', '#buttonToTest') + }) +}) + +describe('Test Modal-manager with test engine', function () { + beforeEach(function () { + modalManager.defineEngine(testEngine) + $('body').append('
    ') + + jasmine.Ajax.install() + jasmine.Ajax.stubRequest(/goodRequest/).andReturn({ + status: 200, + response: 'OK', + responseText: 'OK' + }) + jasmine.Ajax.stubRequest(/error404/).andReturn({ + status: 404, + response: 'Page not found !', + responseText: 'Page not found !' + }) + }) + + afterEach(function () { + $('#test-modal').remove() + jasmine.Ajax.uninstall() + }) + + describe('Test Modal-manager.defineEngine/getEngine', function () { + it('getEngine is testEngine', function () { + expect(modalManager.getEngine()).toEqual(testEngine) + }) + + it('Define bootstrap3 engine', function () { + modalManager.defineEngine(bootstrap3Engine) + expect(modalManager.getEngine()).toEqual(bootstrap3Engine) + }) + + it('Test openModal with onOpen and onClose options', async function () { + const callbackOpen = jasmine.createSpy('open') + const callbackClose = jasmine.createSpy('close') + let opened = false + + modalManager.openModal({ + element: '#test-modal', + onOpen: function (element) { + callbackOpen(element) + opened = true + }, + onClose: function (element) { + callbackClose(element) + opened = false + } + }) + + await wait(() => { + return opened + }) + + expect(callbackOpen).toHaveBeenCalledWith($('#test-modal')) + expect(callbackClose).not.toHaveBeenCalled() + + modalManager.closeModal('#test-modal') + + expect(callbackOpen).toHaveBeenCalledTimes(1) + expect(callbackClose).toHaveBeenCalledWith($('#test-modal')) + }) + }) + + describe('Test Modal-manager.openRemoteModal', function () { + it('Test openRemoteModal', async function () { + const callbackOpen = jasmine.createSpy('open') + const callbackClose = jasmine.createSpy('close') + + modalManager.openRemoteModal({ + url: '/goodRequest', + element: '#test-modal', + elementContent: '#test-modal .content', + onOpen: function (element) { + callbackOpen(element) + }, + onClose: function (element) { + callbackClose(element) + } + }) + + await wait(() => { + return $('#test-modal .content').text().length > 0 + }) + + expect(callbackOpen).toHaveBeenCalledWith($('#test-modal')) + expect(callbackClose).not.toHaveBeenCalled() + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/goodRequest') + expect(jasmine.Ajax.requests.mostRecent().method).toBe('POST') + expect($('#test-modal .content').html()).toBe('OK') + }) + + it('Test openRemoteModal without option', function () { + spyOn(window.console, 'error') + modalManager.openRemoteModal({}) + expect(window.console.error).toHaveBeenCalledWith('Value required: url') + expect(window.console.error).toHaveBeenCalledWith('Value required: element') + expect(window.console.error).toHaveBeenCalledWith('Value required: elementContent') + }) + + it('Test openRemoteModal without element option', function () { + spyOn(window.console, 'error') + const callbackOpen = jasmine.createSpy('open') + const callbackClose = jasmine.createSpy('close') + + modalManager.openRemoteModal({ + url: '/goodRequest', + elementContent: '#test-modal .content', + onOpen: function (element) { + callbackOpen(element) + }, + onClose: function (element) { + callbackClose(element) + } + }) + + expect(window.console.error).toHaveBeenCalledWith('Value required: element') + expect(callbackOpen).not.toHaveBeenCalled() + expect(callbackClose).not.toHaveBeenCalled() + expect(jasmine.Ajax.requests.mostRecent()).toBeUndefined() + }) + + it('Test openRemoteModal with ajaxOptions.onSuccess', async function () { + const callbackOpen = jasmine.createSpy('open') + const callbackClose = jasmine.createSpy('close') + const callbackSuccess1 = jasmine.createSpy('success1') + const callbackSuccess2 = jasmine.createSpy('success2') + + modalManager.openRemoteModal({ + url: '/goodRequest', + element: '#test-modal', + elementContent: '#test-modal .content', + onOpen: function (element) { + callbackOpen(element) + }, + onClose: function (element) { + callbackClose(element) + }, + ajaxOptions: { + onSuccess: [ + { + priority: 6, + callback: function (data, textStatus, jqXHR) { + callbackSuccess1() + } + }, + { + priority: -2, + callback: function (data, textStatus, jqXHR) { + callbackSuccess2() + } + } + ] + } + }) + + await wait(() => { + return $('#test-modal .content').text().length > 0 + }) + + expect(callbackOpen).toHaveBeenCalledWith($('#test-modal')) + expect(callbackSuccess1).toHaveBeenCalledBefore(callbackOpen) + expect(callbackOpen).toHaveBeenCalledBefore(callbackSuccess2) + expect(callbackClose).not.toHaveBeenCalled() + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/goodRequest') + expect(jasmine.Ajax.requests.mostRecent().method).toBe('POST') + expect($('#test-modal .content').html()).toBe('OK') + }) + + it('Test openRemoteModal with method option', async function () { + const callbackOpen = jasmine.createSpy('open') + const callbackClose = jasmine.createSpy('close') + + modalManager.openRemoteModal({ + url: '/goodRequest', + element: '#test-modal', + elementContent: '#test-modal .content', + onOpen: function (element) { + callbackOpen(element) + }, + onClose: function (element) { + callbackClose(element) + }, + method: 'PUT' + }) + + await wait(() => { + return $('#test-modal .content').text().length > 0 + }) + + expect(callbackOpen).toHaveBeenCalledWith($('#test-modal')) + expect(callbackClose).not.toHaveBeenCalled() + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/goodRequest') + expect(jasmine.Ajax.requests.mostRecent().method).toBe('PUT') + expect($('#test-modal .content').html()).toBe('OK') + }) + + it('Test openRemoteModal with bad request', async function () { + const callbackOpen = jasmine.createSpy('open') + const callbackClose = jasmine.createSpy('close') + + modalManager.openRemoteModal({ + url: '/error404', + element: '#test-modal', + elementContent: '#test-modal .content', + onOpen: function (element) { + callbackOpen(element) + }, + onClose: function (element) { + callbackClose(element) + } + }) + + await wait(() => { + return false + }, 500) + + expect(callbackOpen).not.toHaveBeenCalled() + expect(callbackClose).not.toHaveBeenCalled() + expect(jasmine.Ajax.requests.mostRecent().url).toMatch('/error404') + expect(jasmine.Ajax.requests.mostRecent().method).toBe('POST') + expect($('#test-modal .content').html()).toBe('') + }) + }) +}) diff --git a/tests/assets/js/options-resolver.spec.js b/tests/assets/js/options-resolver.spec.js new file mode 100644 index 0000000..bb9ccae --- /dev/null +++ b/tests/assets/js/options-resolver.spec.js @@ -0,0 +1,241 @@ +/* + * This file is part of the EcommitCrudBundle package. + * + * (c) E-commit + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import * as optionsRevolser from '@ecommit/crud-bundle/js/options-resolver' +import $ from 'jquery' + +describe('Test options-resolver.resolve', function () { + const defaultOptions = { + var1: 'hello', + var2: null, + var3: true, + var4: 1, + var5: 'world', + var6: null + } + + it('Empty options', function () { + expect(optionsRevolser.resolve(defaultOptions, {})).toEqual(defaultOptions) + }) + + it('Options with values', function () { + const options = { + var3: null, + var5: 'world', + var6: 'hello', + var7: 'extra', + var8: null + } + + const expected = { + var1: 'hello', + var2: null, + var3: null, + var4: 1, + var5: 'world', + var6: 'hello', + var7: 'extra', + var8: null + } + + expect(optionsRevolser.resolve(defaultOptions, options)).toEqual(expected) + }) +}) + +describe('Test options-resolver.getDataAttributes', function () { + afterEach(function () { + $('.html-test').remove() + }) + + it('Element doesn\'t exist', function () { + expect(optionsRevolser.getDataAttributes('#badId', 'myPrefix')).toEqual({}) + }) + + it('Element exists', function () { + $('body').append('
    ') + + const expected = { + var1: 'value1', + var2: 'value2' + } + + expect(optionsRevolser.getDataAttributes('#myDiv', 'myPrefix')).toEqual(expected) + }) + + it('Element without attribute', function () { + $('body').append('
    ') + + expect(optionsRevolser.getDataAttributes('#myDiv', 'myPrefix')).toEqual({}) + }) + + it('Element with different data types', function () { + $('body').append('
    ') + + const expected = { + var1: 'value1', + var2: 16, + var3: false, + var4: [8], + var5: { + result: true, + count: 100 + }, + var6: true, + var7: '[a', + var8: null + } + + expect(optionsRevolser.getDataAttributes('#myDiv', 'myPrefix')).toEqual(expected) + }) + + it('Element with Element', function () { + $('body').append('
    ') + const element = document.querySelector('#myDiv') + + expect(optionsRevolser.getDataAttributes(element, 'myPrefix')).toEqual({ var1: 'value1' }) + }) + + it('Element with jQuery', function () { + $('body').append('
    ') + const element = $('#myDiv') + + expect(optionsRevolser.getDataAttributes(element, 'myPrefix')).toEqual({ var1: 'value1' }) + }) +}) + +describe('Test options-resolver.isNotBlank', function () { + it('Undefined is blank', function () { + expect(optionsRevolser.isNotBlank(undefined)).toBe(false) + }) + + it('Null is blank', function () { + expect(optionsRevolser.isNotBlank(null)).toBe(false) + }) + + it('Empty string is blank', function () { + expect(optionsRevolser.isNotBlank('')).toBe(false) + }) + + it('String is not blank', function () { + expect(optionsRevolser.isNotBlank('string')).toBe(true) + }) + + it('Int is not blank', function () { + expect(optionsRevolser.isNotBlank(8)).toBe(true) + }) + + it('Empty array is blank', function () { + expect(optionsRevolser.isNotBlank([])).toBe(false) + }) + + it('Array is not blank', function () { + expect(optionsRevolser.isNotBlank(['val'])).toBe(true) + }) +}) + +describe('Test options-resolver.getElement', function () { + beforeEach(function () { + $('body').append('
    ') + }) + + afterEach(function () { + $('.html-test').remove() + }) + + it('Test with null', function () { + const result = optionsRevolser.getElement(null) + + expect(result).toBeNull() + }) + + it('Test with string - found', function () { + const result = optionsRevolser.getElement('#myDiv1') + + expect(result).toBeInstanceOf(Element) + expect(result.getAttribute('id')).toEqual('myDiv1') + }) + + it('Test with string - found (multiple)', function () { + const result = optionsRevolser.getElement('.myDiv') + + expect(result).toBeInstanceOf(Element) + expect(result.getAttribute('id')).toEqual('myDiv1') + }) + + it('Test with string - not found', function () { + const result = optionsRevolser.getElement('#myDiv3') + + expect(result).toBeNull() + }) + + it('Test with element', function () { + const element = document.querySelector('#myDiv1') + const result = optionsRevolser.getElement(element) + + expect(result).toBeInstanceOf(Element) + expect(result.getAttribute('id')).toEqual('myDiv1') + expect(result).toBe(element) + }) + + // If "jquery" dependency deleted in the future, the "Test with fake jQuery" test must be kept (replacement test) + it('Test with jQuery', function () { + const result = optionsRevolser.getElement($('#myDiv1')) + + expect(result).toBeInstanceOf(Element) + expect(result.getAttribute('id')).toEqual('myDiv1') + }) + + // If "jquery" dependency deleted in the future, the "Test with fake empty jQuery" test must be kept (replacement test) + it('Test with empty jQuery', function () { + const result = optionsRevolser.getElement($('#notFound')) + + expect(result).toBeNull() + }) + + it('Test with fake jQuery', function () { + const object = { + get: function (index) { + if (index === 0) { + return document.querySelector('#myDiv1') + } + + return undefined + }, + jquery: 'fake' + } + const result = optionsRevolser.getElement(object) + + expect(result).toBeInstanceOf(Element) + expect(result.getAttribute('id')).toEqual('myDiv1') + }) + + it('Test with fake empty jQuery', function () { + const object = { + get: function (index) { + return undefined + }, + jquery: 'fake' + } + const result = optionsRevolser.getElement(object) + + expect(result).toBeNull() + }) + + it('Test with other type', function () { + const result = optionsRevolser.getElement({}) + + expect(result).toBeNull() + }) + + it('Test with undefined', function () { + const result = optionsRevolser.getElement(undefined) + + expect(result).toBeNull() + }) +}) diff --git a/tests/assets/js/wait.js b/tests/assets/js/wait.js new file mode 100644 index 0000000..f148ec9 --- /dev/null +++ b/tests/assets/js/wait.js @@ -0,0 +1,34 @@ +/* + * This file is part of the EcommitCrudBundle package. + * + * (c) E-commit + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +export default function (stopCondition, timeout = 10000) { + const dateTimeout = Date.now() + timeout + + return new Promise(resolve => { + testCondition(stopCondition, dateTimeout, resolve) + }) +} + +function testCondition (stopCondition, dateTimeout, resolve) { + if (Date.now() > dateTimeout) { + resolve('timeout') + + return + } + + if (stopCondition()) { + resolve('ok') + + return + } + + setTimeout(() => { + testCondition(stopCondition, dateTimeout, resolve) + }, 1000) +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..a684089 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use DG\BypassFinals; + +require __DIR__.'/Functional/App/config/bootstrap.php'; + +function bootstrap(): void +{ + BypassFinals::enable(); + + $kernel = new \Ecommit\CrudBundle\Tests\Functional\App\Kernel('test', true); + $kernel->boot(); + + $application = new \Symfony\Bundle\FrameworkBundle\Console\Application($kernel); + $application->setAutoExit(false); + + $application->run(new \Symfony\Component\Console\Input\ArrayInput([ + 'command' => 'doctrine:database:drop', + '--force' => true, + ])); + + $application->run(new \Symfony\Component\Console\Input\ArrayInput([ + 'command' => 'doctrine:database:create', + ])); + + $application->run(new \Symfony\Component\Console\Input\ArrayInput([ + 'command' => 'doctrine:schema:update', + '--force' => true, + ])); + + $application->run(new \Symfony\Component\Console\Input\ArrayInput([ + 'command' => 'doctrine:fixtures:load', + '--no-interaction' => true, + ])); +} + +bootstrap(); diff --git a/translations/EcommitCrudBundle.en.yml b/translations/EcommitCrudBundle.en.yml new file mode 100644 index 0000000..0edb125 --- /dev/null +++ b/translations/EcommitCrudBundle.en.yml @@ -0,0 +1,16 @@ +display_settings: + check_all: Check all + displayed_columns: Columns to be shown + reset_display_settings: Reset display settings + results_per_page: Number of results per page + save: Save + title: Display Settings + uncheck_all: Uncheck all + +filter: + 'false': No + 'true': Yes + +search: + reset: Reset + submit: Search diff --git a/translations/EcommitCrudBundle.fr.yml b/translations/EcommitCrudBundle.fr.yml new file mode 100644 index 0000000..22824a1 --- /dev/null +++ b/translations/EcommitCrudBundle.fr.yml @@ -0,0 +1,16 @@ +display_settings: + check_all: Tout sélectionner + displayed_columns: Colonnes à afficher + reset_display_settings: Réinitialiser paramètres par défaut + results_per_page: Nombre de résultats par page + save: Enregistrer + title: Propriétés d'affichage + uncheck_all: Tout désélectionner + +filter: + 'false': Non + 'true': Oui + +search: + reset: R.A.Z + submit: Rechercher diff --git a/webpack-encore-config.js b/webpack-encore-config.js new file mode 100644 index 0000000..b8467e5 --- /dev/null +++ b/webpack-encore-config.js @@ -0,0 +1,25 @@ +module.exports = function (outputPath) { + var Encore = require('@symfony/webpack-encore'); + + if (!Encore.isRuntimeEnvironmentConfigured()) { + Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev'); + } + + Encore + .setOutputPath(outputPath) + .setPublicPath('/build') + .addEntry('app', './tests/Functional/App/assets/js/app.js') + .splitEntryChunks() + .enableSingleRuntimeChunk() + .cleanupOutputBeforeBuild() + .enableBuildNotifications() + .enableSourceMaps(!Encore.isProduction()) + .enableVersioning(false) + .configureBabelPresetEnv((config) => { + config.useBuiltIns = 'usage'; + config.corejs = '3.38'; + }) + ; + + return Encore; +} diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..2302a3a --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,3 @@ +var encoreConfig = require('./webpack-encore-config'); + +module.exports = encoreConfig('tests/Functional/App/public/build/').getWebpackConfig();