diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..732a2df --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,175 @@ +name: ci + +on: + push: + branches: [ main ] + pull_request: + +jobs: + tests: + name: Tests on a fresh Thelia 3 shop (PHP ${{ matrix.php }}, Thelia ${{ matrix.thelia }}) + runs-on: ubuntu-latest + # A green run takes ~10 minutes; anything beyond this is a hang, fail fast. + timeout-minutes: 30 + # The `main` axis proves the module against the tip of the core, the way the + # thelia/thelia repository tests itself. The core moves faster than this module + # releases, so that axis is informative: it annotates the run without failing it. + continue-on-error: ${{ matrix.thelia == 'main' }} + strategy: + fail-fast: false + matrix: + php: [ '8.3', '8.4' ] + # `release`: what `composer create-project thelia/thelia-project` gives a shop today. + # `main`: the core, its schema and the themes resolved from their development branch. + thelia: [ release ] + include: + - php: '8.4' + thelia: main + env: + SHOP_DIR: ${{ github.workspace }}/shop + # Without a token, Flex silently skips the thelia/thelia-recipes endpoint and + # the GitHub archives the `main` axis downloads from are rate limited per runner IP. + COMPOSER_AUTH: '{"github-oauth":{"github.com":"${{ github.token }}"}}' + # The runner ships MySQL pre-installed (root/root on 127.0.0.1:3306). Real + # environment variables win over the .env files, so every step that touches the + # database (bin/install, the Thelia console, PHPUnit) sees the same credentials. + DATABASE_HOST: 127.0.0.1 + DATABASE_PORT: '3306' + DATABASE_USER: root + DATABASE_PASSWORD: root + DATABASE_NAME: test + steps: + - name: Checkout the module + uses: actions/checkout@v5 + with: + path: module + + - name: Start MySQL + run: sudo systemctl start mysql.service + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + ini-values: post_max_size=20M + extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite, mysql, gd, zip + + - name: Validate the module composer manifest + run: composer validate --strict --no-check-publish + working-directory: module + + - name: Create a fresh shop + # --no-install: the dependencies are resolved once, below, after the module and + # the Thelia packages under test are added to the manifest. + run: composer create-project thelia/thelia-project "$SHOP_DIR" --no-interaction --no-install --no-progress + + - name: Resolve the Thelia packages from their development branch + if: matrix.thelia == 'main' + working-directory: ${{ env.SHOP_DIR }} + # The core, its database schema (thelia/setup) and its configuration + # (thelia/config) live in the same monorepo and are released together: they are + # resolved from the same branch tip so the Propel models and the SQL schema + # describe the same tables. The themes follow, as in the core CI: a core ahead + # of its release against released themes is a combination nobody runs. + # + # The `as` alias keeps the released constraints of thelia/thelia-skeleton and + # of every package that requires the core or a theme by version satisfiable. + run: | + composer require --no-update --no-scripts --no-audit \ + "thelia/core:dev-main as 3.99.0" \ + "thelia/setup:dev-main as 3.99.0" \ + "thelia/config:dev-main as 3.99.0" \ + "thelia/flexy:dev-main as 1.99.0" \ + "thelia/backoffice-default-twig-template:dev-main as 1.99.0" \ + "thelia/email-default-template:dev-main as 1.99.0" \ + "thelia/pdf-default-template:dev-main as 1.99.0" + + - name: Pin the database schema to the released core + if: matrix.thelia == 'release' + working-directory: ${{ env.SHOP_DIR }} + # thelia/core requires that thelia/setup and thelia/config be pinned to the minor + # version of the core that will be installed. Thus, a new installation uses the + # most recent tag for each. The schema and demo data for thelia/setup are + # generated from the Propel models in the core at the time of the same commit: + # a more recent configuration describes columns that the released version of + # thelia/core does not recognize. + run: | + # The listing puts the branch alias (3.0.x-dev) first: keep the newest stable tag, + # the one prefer-stable will pick. + core=$(composer show thelia/core --available --format=json | php -r '$stable = array_values(array_filter(json_decode(stream_get_contents(STDIN), true)["versions"], static fn (string $v): bool => (bool) preg_match("/^\d+\.\d+\.\d+$/", $v))); echo $stable[0] ?? "";') + [ -n "$core" ] || { echo "::error::no stable release of thelia/core found"; exit 1; } + minor=$(php -r 'preg_match("/^(\d+\.\d+)\./", $argv[1], $m); echo $m[1];' "$core") + echo "thelia/core will resolve to $core: pinning thelia/setup and thelia/config to ~$minor.0" + composer require --no-update --no-scripts --no-audit \ + "thelia/setup:~$minor.0" \ + "thelia/config:~$minor.0" + + - name: Add the module from the checkout and install the shop dependencies + working-directory: ${{ env.SHOP_DIR }} + # The fresh shop locks thecodingmachine/safe at v3 while the bundle needs ^2.5: + # --with-all-dependencies allows the downgrade. The shop has no committed lock + # file, so one `update` resolves everything at once. + run: | + composer config repositories.query-builder-module path "$GITHUB_WORKSPACE/module" + composer require --no-update --no-scripts --no-audit "thelia/query-builder-module:@dev" + composer require --no-update --no-scripts --no-audit --dev "phpunit/phpunit:^11.5" + composer update --with-all-dependencies --no-interaction --no-progress --prefer-dist + + - name: Check that the core, its schema and its configuration are consistent + working-directory: ${{ env.SHOP_DIR }} + # The failure this guards against surfaces late and cryptically otherwise: a + # Propel "Unable to execute INSERT statement" in the middle of the demo import. + # `main`: every package must be a development version (a package that declares a + # branch-alias reports the alias, e.g. 3.0.x-dev, not dev-main). + # `release`: thelia/setup and thelia/config must share the minor of thelia/core. + run: | + show_version() { composer show "$1" --format=json | php -r 'echo json_decode(stream_get_contents(STDIN), true)["versions"][0];'; } + minor_of() { php -r 'preg_match("/^v?(\d+\.\d+)\./", $argv[1], $m); echo $m[1] ?? $argv[1];' "$1"; } + for package in core setup config flexy backoffice-default-twig-template email-default-template pdf-default-template; do + echo "thelia/$package -> $(show_version thelia/$package)" + done + if [ "${{ matrix.thelia }}" = "main" ]; then + for package in core setup config flexy backoffice-default-twig-template email-default-template pdf-default-template; do + version=$(show_version "thelia/$package") + case "$version" in dev-*|*-dev) ;; *) echo "::error::thelia/$package resolved to $version, expected a development version"; exit 1 ;; esac + done + else + core=$(minor_of "$(show_version thelia/core)") + for package in setup config; do + version=$(show_version "thelia/$package") + if [ "$(minor_of "$version")" != "$core" ]; then + echo "::error::thelia/$package $version does not match thelia/core $core.x: the SQL schema and the Propel models would describe different tables" + exit 1 + fi + done + fi + + - name: Unit tests + # The module is symlinked into the shop: the bootstrap gets the shop autoloader explicitly. + # + # -d auto_prepend_file: the PHPUnit entry script requires the Composer autoloader + # before it reads any configuration, and the autoloader pulls in + # vendor/thelia/core/bootstrap.php, which derives THELIA_ROOT from its own location + # under vendor/. The shop root bootstrap.php holds the real path constants and only + # wins by being loaded first, which no PHPUnit bootstrap file can do: a prepended + # file is the one hook that runs before the entry script. + working-directory: ${{ env.SHOP_DIR }} + env: + THELIA_VENDOR_AUTOLOAD: ${{ env.SHOP_DIR }}/vendor/autoload.php + run: php -d auto_prepend_file=./bootstrap.php vendor/bin/phpunit -c "$GITHUB_WORKSPACE/module/phpunit.xml.dist" --testsuite unit + + - name: Install the shop + working-directory: ${{ env.SHOP_DIR }} + env: + APP_ENV: test + run: php bin/install --frontoffice_theme=flexy --backoffice_theme=default-twig --pdf_theme=default --email_theme=default --with-demo --skip-demo-images --with-admin --admin_login=thelia --admin_password=thelia --admin_first_name=thelia --admin_last_name=thelia --admin_email=thelia@example.com + + - name: Integration tests + working-directory: ${{ env.SHOP_DIR }} + # -d auto_prepend_file: see the unit step. Without it the kernel boots on a + # THELIA_ROOT pointing inside vendor/ and Propel looks for the core schema under + # vendor/thelia/vendor/thelia/config/. + env: + APP_ENV: test + THELIA_VENDOR_AUTOLOAD: ${{ env.SHOP_DIR }}/vendor/autoload.php + run: php -d auto_prepend_file=./bootstrap.php vendor/bin/phpunit -c "$GITHUB_WORKSPACE/module/phpunit.xml.dist" --testsuite integration diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80d8e16 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/Resources/node_modules/ +.DS_Store +*.log +/.phpunit.cache/ +/.phpunit.result.cache +/vendor/ +/composer.lock diff --git a/Action/ActionInterface.php b/Action/ActionInterface.php new file mode 100644 index 0000000..452bf62 --- /dev/null +++ b/Action/ActionInterface.php @@ -0,0 +1,37 @@ + $actions */ + public function __construct( + #[AutowireIterator(ActionInterface::TAG)] + private iterable $actions, + ) { + } + + public function get(string $code): ?ActionInterface + { + foreach ($this->actions as $action) { + if ($action::getCode() === $code) { + return $action; + } + } + + return null; + } + + /** @return array keyed by action code */ + public function forContext(Context $context): array + { + $actions = []; + + foreach ($this->actions as $action) { + $supportedContexts = $action::getSupportedContexts(); + + if ($supportedContexts === [] || \in_array($context, $supportedContexts, true)) { + $actions[$action::getCode()] = $action; + } + } + + return $actions; + } +} diff --git a/Action/ActionResult.php b/Action/ActionResult.php new file mode 100644 index 0000000..95543ae --- /dev/null +++ b/Action/ActionResult.php @@ -0,0 +1,20 @@ +getParametersArray(); + $limit = (int) ($parameters['limit'] ?? self::DEFAULT_LIMIT); + $persistDays = isset($parameters['persist_days']) ? (int) $parameters['persist_days'] : null; + + if ($persistDays === null || $persistDays <= 0 || $runtimeContext->customerId === null) { + return new ActionResult( + productIds: $this->sqlBuilder->getProductIds( + $action->getConditionTreeArray(), + $runtimeContext, + $limit, + [self::CURRENT_PRODUCT_EXCLUSION] + ) + ); + } + + return $this->executeSticky($action, $runtimeContext, $limit, $persistDays); + } + + private function executeSticky( + QueryBuilderAction $action, + RuntimeContext $runtimeContext, + int $limit, + int $persistDays, + ): ActionResult { + $customerId = (int) $runtimeContext->customerId; + + //Suggestions of the current cycle, minus the products now in the cart + $productIds = array_values(array_diff( + $this->suggestionService->getActiveProductIds($customerId, (int) $action->getId()), + $runtimeContext->cartProductIds + )); + + //Active suggestions stay subject to the query scopes (visibility, price...): + //a product no longer displayable frees its slot for the backfill below + //instead of leaving a missing card until the cycle expires + if ($productIds !== []) { + $displayableIds = $this->sqlBuilder->getProductIds( + null, + $runtimeContext, + null, + [sprintf('`product`.`id` IN (%s)', implode(', ', array_map('intval', $productIds)))] + ); + //array_intersect keeps the oldest-display-first order of the suggestions + $productIds = array_values(array_intersect($productIds, $displayableIds)); + } + + $productIds = \array_slice($productIds, 0, $limit); + + $missing = $limit - \count($productIds); + + if ($missing > 0) { + $eligibleIds = array_values(array_diff( + $this->sqlBuilder->getProductIds( + $action->getConditionTreeArray(), + $runtimeContext + ), + $productIds + )); + $newProductIds = \array_slice( + $this->suggestionService->orderRefillCandidates( + $eligibleIds, + $this->suggestionService->getLastCycleEndsByProductId($customerId, (int) $action->getId()) + ), + 0, + $missing + ); + + if ($newProductIds !== []) { + $this->suggestionService->recordSuggestions( + $customerId, + $action, + $newProductIds, + $persistDays + ); + + $productIds = array_merge($productIds, $newProductIds); + } + } + + //Display-only filter: the sticky rotation (displayability, backfill, + //recording) must never react to which product page is being viewed — + //a slot silently hidden here is not a freed slot (#561) + if ($runtimeContext->productId !== null && $runtimeContext->productId > 0) { + $productIds = array_values(array_diff($productIds, [$runtimeContext->productId])); + } + + return new ActionResult(productIds: $productIds); + } +} diff --git a/Action/ExecutedAction.php b/Action/ExecutedAction.php new file mode 100644 index 0000000..fc0b6db --- /dev/null +++ b/Action/ExecutedAction.php @@ -0,0 +1,19 @@ + $context */ + public static function isFrontRead(array $context): bool + { + $groups = $context['groups'] ?? []; + + if (!\is_array($groups)) { + $groups = [$groups]; + } + + $isFrontRead = false; + + foreach ($groups as $group) { + if (!\is_string($group)) { + continue; + } + + if (str_starts_with($group, 'admin:')) { + return false; + } + + $isFrontRead = $isFrontRead || str_starts_with($group, 'front:'); + } + + return $isFrontRead; + } +} diff --git a/Api/Resource/Addon/QueryBuilderCartDiscount.php b/Api/Resource/Addon/QueryBuilderCartDiscount.php new file mode 100644 index 0000000..f53a574 --- /dev/null +++ b/Api/Resource/Addon/QueryBuilderCartDiscount.php @@ -0,0 +1,90 @@ +cartDiscountCalculator->resolve($activeRecord); + + if ($applied === null) { + return $this; + } + + $this->rate = $applied->rate; + $this->label = $applied->label; + $this->amount = $applied->amount; + $this->freeShipping = $applied->freeShipping; + + return $this; + } + + public function buildFromArray(array $data, PropelResourceInterface $abstractPropelResource): ResourceAddonInterface + { + return $this; + } + + public function doSave(ActiveRecordInterface $activeRecord, PropelResourceInterface $abstractPropelResource): void + { + } + + public function doDelete(ActiveRecordInterface $activeRecord, PropelResourceInterface $abstractPropelResource): void + { + } +} diff --git a/Api/Resource/Addon/QueryBuilderProductOffer.php b/Api/Resource/Addon/QueryBuilderProductOffer.php new file mode 100644 index 0000000..26a8c1a --- /dev/null +++ b/Api/Resource/Addon/QueryBuilderProductOffer.php @@ -0,0 +1,89 @@ +getId() === null || $runtime === null) { + return $this; + } + + $discount = $runtime->discountResolutionService->getDiscount( + $runtime->runtimeContextFactory->fromSession(), + (int) $activeRecord->getId() + ); + + if ($discount === null) { + return $this; + } + + $this->rate = $discount->rate; + $this->label = $discount->label; + $this->cumulative = $discount->cumulative; + + return $this; + } + + public function buildFromArray(array $data, PropelResourceInterface $abstractPropelResource): ResourceAddonInterface + { + return $this; + } + + public function doSave(ActiveRecordInterface $activeRecord, PropelResourceInterface $abstractPropelResource): void + { + } + + public function doDelete(ActiveRecordInterface $activeRecord, PropelResourceInterface $abstractPropelResource): void + { + } +} diff --git a/Api/Resource/QueryBuilderProducts.php b/Api/Resource/QueryBuilderProducts.php new file mode 100644 index 0000000..b302411 --- /dev/null +++ b/Api/Resource/QueryBuilderProducts.php @@ -0,0 +1,69 @@ + new Link(fromProperty: 'hookCode', identifiers: ['string']), + ], + requirements: ['hookCode' => '[a-zA-Z0-9_.\-]+'], + openapi: new Operation( + summary: 'Products selected by the QueryBuilder rules bound to a hook code', + parameters: [ + new Parameter(name: 'hookCode', in: 'path', required: true, schema: ['type' => 'string']), + new Parameter(name: 'product_id', in: 'query', required: false, schema: ['type' => 'integer'], description: 'Product being displayed (PRODUCT context rules)'), + new Parameter(name: 'category_id', in: 'query', required: false, schema: ['type' => 'integer'], description: 'Category being displayed (CATEGORY context rules)'), + new Parameter(name: 'brand_id', in: 'query', required: false, schema: ['type' => 'integer'], description: 'Brand being displayed (BRAND context rules)'), + new Parameter(name: 'order_id', in: 'query', required: false, schema: ['type' => 'integer'], description: 'Order being displayed (ORDER context rules)'), + ], + ), + provider: QueryBuilderProductsProvider::class, + ), + ], + normalizationContext: ['groups' => [self::GROUP_FRONT_READ, Product::GROUP_FRONT_READ]], +)] +class QueryBuilderProducts +{ + public const GROUP_FRONT_READ = 'front:query_builder_products:read'; + + #[ApiProperty(identifier: true)] + #[Groups([self::GROUP_FRONT_READ])] + public string $hookCode; + + /** @var int[] unique product ids selected by the display actions, in selection order */ + #[Groups([self::GROUP_FRONT_READ])] + public array $productIds = []; + + /** @var array discount offers keyed by product id */ + #[Groups([self::GROUP_FRONT_READ])] + public array $offers = []; + + /** @var array one entry per executed display action */ + #[Groups([self::GROUP_FRONT_READ])] + public array $actions = []; + + /** @var Product[] the selected products, in selection order */ + #[Groups([self::GROUP_FRONT_READ])] + public array $products = []; +} diff --git a/Api/State/QueryBuilderProductsProvider.php b/Api/State/QueryBuilderProductsProvider.php new file mode 100644 index 0000000..0d133b2 --- /dev/null +++ b/Api/State/QueryBuilderProductsProvider.php @@ -0,0 +1,110 @@ +dataDictionary->getContextForHook($hookCode) === null) { + throw new NotFoundHttpException(sprintf('Unknown QueryBuilder hook code "%s".', $hookCode)); + } + + $query = $this->requestStack->getCurrentRequest()?->query; + + $runtimeContext = $this->runtimeContextFactory->fromSession( + productId: self::optionalId($query?->get('product_id')), + orderId: self::optionalId($query?->get('order_id')), + categoryId: self::optionalId($query?->get('category_id')), + brandId: self::optionalId($query?->get('brand_id')), + ); + + $result = $this->hookResultPresenter->present($hookCode, $runtimeContext); + + $resource = new QueryBuilderProducts(); + $resource->hookCode = $hookCode; + $resource->productIds = $result['product_ids']; + $resource->offers = $result['offers']; + $resource->actions = $result['actions']; + $resource->products = $this->loadProducts($result['product_ids'], $context); + + return $resource; + } + + /** + * @param int[] $productIds + * + * @return Product[] in the given order; a product gone since the selection is dropped + */ + private function loadProducts(array $productIds, array $context): array + { + if ($productIds === []) { + return []; + } + + $modelsById = []; + foreach (ProductQuery::create()->filterById($productIds)->find() as $productModel) { + $modelsById[(int) $productModel->getId()] = $productModel; + } + + $langs = LangQuery::create()->filterByActive(1)->find(); + $productContext = array_merge($context, ['groups' => [Product::GROUP_FRONT_READ]]); + $products = []; + + foreach ($productIds as $productId) { + if (!isset($modelsById[$productId])) { + continue; + } + + $products[] = $this->apiResourcePropelTransformerService->modelToResource( + resourceClass: Product::class, + propelModel: $modelsById[$productId], + context: $productContext, + langs: $langs, + ); + } + + return $products; + } + + private static function optionalId(mixed $value): ?int + { + if ($value === null || $value === '' || !is_numeric($value)) { + return null; + } + + return (int) $value > 0 ? (int) $value : null; + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c1395e3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,47 @@ +# Changelog + +All notable changes to this module are documented here. + +The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the module adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [2.0.0] 2026-09-16 + +Port of the module to Thelia 3. The Thelia 2 line (1.0.0 to 1.2.0) lived in the project the module was written for and is not published. + +### Added +- Back-office screens on the default-twig theme (Twig, Bootstrap 5): rule list, rule in three steps, action screen, entry in the Tools menu. Screens and menu entry follow the right granted on the module itself in the administrator profiles. +- On the rule screen, an (i) mark after each action name opens a popover reading the product selection of the action, the way the action screen does above its editor, followed by its discount rate, free shipping and stackable options when set. +- Condition editor provided by `openstudio/query-builder-bundle` (`QueryBuilderType`, `native` processor, per-field operators), with a readable summary of the tree, a condition counter and the hook grid filtered by context. The editor script and stylesheet are loaded on the rule and action screens only, never on the rule list nor on the rest of the back-office. +- Front rendering through the `theme_hook()` points of Flexy (`ThemeHookInterface`) with a product list template based on the Flexy cross-selling component, overridable by the theme. +- Twig function `query_builder_products()` returning the hook result (ids, offers, actions). +- API Platform resource `GET /api/front/query_builder/products/{hookCode}` with the product resources embedded, and two front addons: `QueryBuilderCartDiscount` on the cart, `QueryBuilderProductOffer` on the products. +- Product discounts applied on the cart lines (promotion columns), with the stackable and non-stackable policies against a catalog promotion. +- Product discounts shown on the product page (`PseByProductEvent`), in the listings and on the front product resources (`ModelToResourceEvent`): the discounted product is read as a promotion at the discounted price, computed with the same policy as the cart lines. +- Discount label shown in the cart discount fragment of the checkout pages, which now lists the product discounts charged on the cart lines. +- Free shipping on `CART_SET_POSTAGE` and through the postage estimator of the core; the delivery options of the delivery step are priced at zero as well, so the option cards and the summary agree. +- Cart discount fragment shown on the checkout pages (`checkout.top`, `cart.bottom`). +- Dictionary fields for the cart products total (`:cart_total`) and the delivery country (`:delivery_country_id`); `--cart-total` and `--delivery-country` options on the debug commands. +- Dictionary fields per context: the selected product against the context product (same brand, shared category, same main category, accessory, same template, same feature value, product already in the cart), the context category and its sub-tree, the context brand; cart fields (line and item counts, product quantity, brands and categories in the cart, promotional line, same brand, shared category or accessory of a cart product) and customer fields (reseller, discount rate, seniority, orders, last order, newsletter, products, brands and categories already bought) available in every context; brand id, main category, new, promotion and stock flags on the product. +- `usage` key on the dictionary fields (`rule`, `action`): a field can be reserved to the trigger conditions of a rule or to the product selection of an action; the editors and `SqlBuilder::validateTree()` honor it. +- Field labels prefixed with their translated group (the part of the code before the first underscore) in the editors. +- `--usage` option on `querybuilder:dictionary`; `--cart`, `--order`, `--category` and `--brand` options on `querybuilder:compile`. +- Flexy hook codes declared per context in the base dictionary. +- English and French translations of every label (`querybuilder` and `querybuilder.bo.default-twig` domains). +- Unit tests (SQL compiler, dictionary, discount arithmetic), a module activation integration test and a GitHub Actions workflow running them on a fresh shop. +- `composer install` at the module root installs the dev dependencies (`thelia/core`, PHPUnit) for the unit suite and static analysis; the module `vendor/` directory and the Propel models are excluded from the service discovery so a checkout linked into a shop keeps booting it. +- Activation is refused on a Thelia 2 core (or a core whose version cannot be read) with a message naming the module version, the minimum core version and the running one; the `` bound of `module.xml` alone is not enforced by every 2.x core. + +### Changed +- Requires PHP 8.3, Thelia 3 and `openstudio/query-builder-bundle` ^1.1. +- Dictionary overrides are looked up in the directory of each module, wherever Composer installed it. +- A `datetime` field is entered as a date and compared on its date part. +- Dictionary labels, context labels and action labels are translation keys. +- `contexts` on a dictionary field defaults to `[GLOBAL]`, which means every context (an empty list had the same effect); an unknown context value is refused with an explicit message. +- Cart discount amounts are computed by a single service shared by the cart listener, the API addon and the theme hook. + +### Removed +- Smarty templates, plugin and vendored react-querybuilder build of the Thelia 2 line. +- The `/query_builder/products/{hookCode}` Symfony route, replaced by the API Platform resource. +- The `product.top` and `product.bottom` `BaseHook` front hooks, replaced by the theme hook implementation. + +[2.0.0]: https://github.com/thelia-modules/QueryBuilder/releases/tag/2.0.0 diff --git a/Command/CompileCommand.php b/Command/CompileCommand.php new file mode 100644 index 0000000..f73c331 --- /dev/null +++ b/Command/CompileCommand.php @@ -0,0 +1,102 @@ +setName('querybuilder:compile') + ->setDescription('Compile a react-querybuilder JSON condition tree into SQL') + ->addArgument('tree', InputArgument::REQUIRED, 'JSON condition tree, or @/path/to/file.json') + ->addOption('customer', null, InputOption::VALUE_REQUIRED, 'Customer id for runtime placeholders') + ->addOption('product', null, InputOption::VALUE_REQUIRED, 'Product id for runtime placeholders') + ->addOption('cart', null, InputOption::VALUE_REQUIRED, 'Cart id (:cart_id)') + ->addOption('cart-products', null, InputOption::VALUE_REQUIRED, 'Comma-separated product ids of the cart') + ->addOption('order', null, InputOption::VALUE_REQUIRED, 'Order id (:order_id)') + ->addOption('category', null, InputOption::VALUE_REQUIRED, 'Category id (:category_id)') + ->addOption('brand', null, InputOption::VALUE_REQUIRED, 'Brand id (:brand_id)') + ->addOption('locale', null, InputOption::VALUE_REQUIRED, 'Locale', 'fr_FR') + ->addOption('cart-total', null, InputOption::VALUE_REQUIRED, 'Cart products total, taxes included (:cart_total)') + ->addOption('delivery-country', null, InputOption::VALUE_REQUIRED, 'Delivery country id (:delivery_country_id)') + ->addOption('limit', null, InputOption::VALUE_REQUIRED, 'LIMIT applied to the query') + ->addOption('execute', null, InputOption::VALUE_NONE, 'Execute the query and print the product ids'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $rawTree = (string) $input->getArgument('tree'); + + if (str_starts_with($rawTree, '@')) { + $rawTree = (string) file_get_contents(substr($rawTree, 1)); + } + + $conditionTree = json_decode($rawTree, true); + + if (!\is_array($conditionTree)) { + $output->writeln('Invalid JSON condition tree.'); + + return self::FAILURE; + } + + $cartProducts = (string) $input->getOption('cart-products'); + + $runtimeContext = $this->runtimeContextFactory->withProviderParameters(new RuntimeContext( + customerId: $input->getOption('customer') !== null ? (int) $input->getOption('customer') : null, + productId: $input->getOption('product') !== null ? (int) $input->getOption('product') : null, + cartId: $input->getOption('cart') !== null ? (int) $input->getOption('cart') : null, + cartProductIds: $cartProducts !== '' + ? array_map('intval', explode(',', $cartProducts)) + : [], + orderId: $input->getOption('order') !== null ? (int) $input->getOption('order') : null, + categoryId: $input->getOption('category') !== null ? (int) $input->getOption('category') : null, + brandId: $input->getOption('brand') !== null ? (int) $input->getOption('brand') : null, + locale: (string) $input->getOption('locale'), + cartTotal: $input->getOption('cart-total') !== null ? (float) $input->getOption('cart-total') : null, + deliveryCountryId: $input->getOption('delivery-country') !== null ? (int) $input->getOption('delivery-country') : null, + )); + + $limit = $input->getOption('limit') !== null ? (int) $input->getOption('limit') : null; + + $compiledQuery = $this->sqlBuilder->compile($conditionTree, $runtimeContext, $limit); + + $output->writeln('SQL'); + $output->writeln($compiledQuery->sql); + $output->writeln(''); + $output->writeln('Parameters'); + $output->writeln((string) json_encode($compiledQuery->parameters, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_UNICODE)); + + if ($input->getOption('execute')) { + $productIds = $this->sqlBuilder->getProductIds($conditionTree, $runtimeContext, $limit); + + $output->writeln(''); + $output->writeln(sprintf('%d product(s)', \count($productIds))); + $output->writeln(implode(', ', \array_slice($productIds, 0, 50))); + } + + return self::SUCCESS; + } +} diff --git a/Command/DictionaryCommand.php b/Command/DictionaryCommand.php new file mode 100644 index 0000000..ebcd999 --- /dev/null +++ b/Command/DictionaryCommand.php @@ -0,0 +1,94 @@ +setName('querybuilder:dictionary') + ->setDescription('Dump the merged QueryBuilder data dictionary') + ->addArgument('context', InputArgument::OPTIONAL, 'Filter fields by context (PRODUCT, CART, ORDER, CATEGORY, BRAND, CUSTOMER, GLOBAL)') + ->addOption('usage', null, InputOption::VALUE_REQUIRED, 'Filter fields by editor usage (rule, action)'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $contextArgument = $input->getArgument('context'); + $context = $contextArgument !== null ? Context::from(strtoupper((string) $contextArgument)) : null; + $usage = $input->getOption('usage') !== null ? (string) $input->getOption('usage') : null; + + if ($usage !== null && !\in_array($usage, FieldDefinition::USAGES, true)) { + throw new \InvalidArgumentException(sprintf('Unknown usage "%s", expected one of %s.', $usage, implode(', ', FieldDefinition::USAGES))); + } + + $output->writeln('Joins'); + foreach ($this->dataDictionary->getJoins() as $join) { + $output->writeln(sprintf( + ' %s %s ON %s.%s = %s.%s%s', + $join->type, + $join->table, + $join->fromTable, + $join->fromColumn, + $join->table, + $join->toColumn, + $join->extraOn !== null ? ' AND ' . $join->extraOn : '' + )); + } + + $output->writeln(''); + $output->writeln(sprintf( + 'Fields%s%s', + $context !== null ? ' (' . $context->value . ')' : '', + $usage !== null ? ' [' . $usage . ']' : '' + )); + + foreach ($this->dataDictionary->getFields($context, $usage) as $field) { + $output->writeln(sprintf( + ' %-40s %-10s %-28s %-12s %s', + $field->code, + $field->type, + implode(',', array_map(static fn (Context $fieldContext): string => $fieldContext->value, $field->contexts)), + implode(',', $field->usages), + $field->column ?? ' ' . $field->expression + )); + } + + if ($context !== null) { + $output->writeln(''); + $output->writeln(sprintf('Hooks (%s)', $context->value)); + foreach ($this->dataDictionary->getHooks($context) as $hookCode => $hookLabel) { + $output->writeln(sprintf(' %-40s %s', $hookCode, $hookLabel)); + } + + $output->writeln(''); + $output->writeln('react-querybuilder fields JSON'); + $output->writeln((string) json_encode($this->fieldsBuilder->buildForContext($context, 'fr_FR', $usage), \JSON_PRETTY_PRINT | \JSON_UNESCAPED_UNICODE)); + } + + return self::SUCCESS; + } +} diff --git a/Command/RunCommand.php b/Command/RunCommand.php new file mode 100644 index 0000000..fddbe47 --- /dev/null +++ b/Command/RunCommand.php @@ -0,0 +1,86 @@ +setName('querybuilder:run') + ->setDescription('Execute the active QueryBuilder rules bound to a hook') + ->addArgument('hook', InputArgument::REQUIRED, 'Hook code (ex: product.top)') + ->addOption('customer', null, InputOption::VALUE_REQUIRED, 'Customer id') + ->addOption('product', null, InputOption::VALUE_REQUIRED, 'Product id') + ->addOption('cart-products', null, InputOption::VALUE_REQUIRED, 'Comma-separated product ids of the cart') + ->addOption('locale', null, InputOption::VALUE_REQUIRED, 'Locale', 'fr_FR') + ->addOption('cart-total', null, InputOption::VALUE_REQUIRED, 'Cart products total, taxes included (:cart_total)') + ->addOption('delivery-country', null, InputOption::VALUE_REQUIRED, 'Delivery country id (:delivery_country_id)'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $cartProducts = (string) $input->getOption('cart-products'); + + $runtimeContext = $this->runtimeContextFactory->withProviderParameters(new RuntimeContext( + customerId: $input->getOption('customer') !== null ? (int) $input->getOption('customer') : null, + productId: $input->getOption('product') !== null ? (int) $input->getOption('product') : null, + cartProductIds: $cartProducts !== '' + ? array_map('intval', explode(',', $cartProducts)) + : [], + locale: (string) $input->getOption('locale'), + cartTotal: $input->getOption('cart-total') !== null ? (float) $input->getOption('cart-total') : null, + deliveryCountryId: $input->getOption('delivery-country') !== null ? (int) $input->getOption('delivery-country') : null, + )); + + $executedActions = $this->ruleEngine->executeHook((string) $input->getArgument('hook'), $runtimeContext); + + if ($executedActions === []) { + $output->writeln('No rule matched this hook in the given context.'); + + return self::SUCCESS; + } + + foreach ($executedActions as $executedAction) { + $output->writeln(sprintf( + 'Rule "%s" → action "%s" (%s, %s)', + $executedAction->rule->getName(), + $executedAction->action->getName(), + $executedAction->action->getCode(), + $executedAction->type + )); + + if ($executedAction->result->productIds !== []) { + $output->writeln(' products: ' . implode(', ', $executedAction->result->productIds)); + } + + if ($executedAction->result->html !== null) { + $output->writeln(' html: ' . substr($executedAction->result->html, 0, 200)); + } + } + + return self::SUCCESS; + } +} diff --git a/Config/TheliaMain.sql b/Config/TheliaMain.sql new file mode 100644 index 0000000..b639c37 --- /dev/null +++ b/Config/TheliaMain.sql @@ -0,0 +1,103 @@ +# This is a fix for InnoDB in MySQL >= 4.1.x +# It "suspends judgement" for fkey relationships until are tables are set. +SET FOREIGN_KEY_CHECKS = 0; + +-- --------------------------------------------------------------------- +-- query_builder_rule +-- --------------------------------------------------------------------- + +DROP TABLE IF EXISTS `query_builder_rule`; + +CREATE TABLE `query_builder_rule` +( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(255) NOT NULL, + `description` TEXT, + `context` VARCHAR(50) NOT NULL, + `hooks` TEXT, + `condition_tree` TEXT, + `activate` TINYINT DEFAULT 0, + `position` INTEGER DEFAULT 0, + `created_at` DATETIME, + `updated_at` DATETIME, + PRIMARY KEY (`id`) +) ENGINE=InnoDB; + +-- --------------------------------------------------------------------- +-- query_builder_action +-- --------------------------------------------------------------------- + +DROP TABLE IF EXISTS `query_builder_action`; + +CREATE TABLE `query_builder_action` +( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `rule_id` INTEGER NOT NULL, + `name` VARCHAR(255) NOT NULL, + `description` TEXT, + `code` VARCHAR(100) NOT NULL, + `type` VARCHAR(20) NOT NULL, + `condition_tree` TEXT, + `parameters` TEXT, + `activate` TINYINT DEFAULT 0, + `position` INTEGER DEFAULT 0, + `created_at` DATETIME, + `updated_at` DATETIME, + PRIMARY KEY (`id`), + INDEX `fi_query_builder_action_rule_id` (`rule_id`), + CONSTRAINT `fk_query_builder_action_rule_id` + FOREIGN KEY (`rule_id`) + REFERENCES `query_builder_rule` (`id`) + ON UPDATE RESTRICT + ON DELETE CASCADE +) ENGINE=InnoDB; + +-- --------------------------------------------------------------------- +-- query_builder_suggestion +-- --------------------------------------------------------------------- + +DROP TABLE IF EXISTS `query_builder_suggestion`; + +CREATE TABLE `query_builder_suggestion` +( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `customer_id` INTEGER NOT NULL, + `product_id` INTEGER NOT NULL, + `rule_id` INTEGER NOT NULL, + `action_id` INTEGER NOT NULL, + `hook` VARCHAR(100), + `displayed_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `expires_at` TIMESTAMP NULL, + `purchased_at` TIMESTAMP NULL, + `created_at` DATETIME, + `updated_at` DATETIME, + PRIMARY KEY (`id`), + UNIQUE INDEX `query_builder_suggestion_unique` (`customer_id`, `product_id`, `action_id`), + INDEX `idx_query_builder_suggestion_active` (`customer_id`, `purchased_at`, `expires_at`), + INDEX `fi_query_builder_suggestion_product_id` (`product_id`), + INDEX `fi_query_builder_suggestion_rule_id` (`rule_id`), + INDEX `fi_query_builder_suggestion_action_id` (`action_id`), + CONSTRAINT `fk_query_builder_suggestion_customer_id` + FOREIGN KEY (`customer_id`) + REFERENCES `customer` (`id`) + ON UPDATE RESTRICT + ON DELETE CASCADE, + CONSTRAINT `fk_query_builder_suggestion_product_id` + FOREIGN KEY (`product_id`) + REFERENCES `product` (`id`) + ON UPDATE RESTRICT + ON DELETE CASCADE, + CONSTRAINT `fk_query_builder_suggestion_rule_id` + FOREIGN KEY (`rule_id`) + REFERENCES `query_builder_rule` (`id`) + ON UPDATE RESTRICT + ON DELETE CASCADE, + CONSTRAINT `fk_query_builder_suggestion_action_id` + FOREIGN KEY (`action_id`) + REFERENCES `query_builder_action` (`id`) + ON UPDATE RESTRICT + ON DELETE CASCADE +) ENGINE=InnoDB; + +# This restores the fkey checks, after having unset them earlier +SET FOREIGN_KEY_CHECKS = 1; diff --git a/Config/config.xml b/Config/config.xml new file mode 100644 index 0000000..ea7c54c --- /dev/null +++ b/Config/config.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/Config/module.xml b/Config/module.xml new file mode 100644 index 0000000..7f2894f --- /dev/null +++ b/Config/module.xml @@ -0,0 +1,30 @@ + + + QueryBuilder\QueryBuilder + + Query Builder + Generic rule engine: build condition trees in the back-office, compile them to SQL and trigger actions (product lists, processing, filters) on front-office hooks. + + + Query Builder + Moteur de règles générique : édition d'arbres de conditions en back-office, compilation en SQL et déclenchement d'actions (listes de produits, traitements, filtres) sur les hooks du front-office. + + + en_US + fr_FR + + 2.0.0 + + + Samuel Diakité + OpenStudio + sdiakite@openstudio.fr + https://github.com/openstudio-sdiakite + + + classic + 3.0.0 + beta + diff --git a/Config/query_builder.yml b/Config/query_builder.yml new file mode 100644 index 0000000..87c4c39 --- /dev/null +++ b/Config/query_builder.yml @@ -0,0 +1,396 @@ +# Base data dictionary of the QueryBuilder module (Thelia core tables only). +# Any active module can expose its own Config/query_builder.yml to add or +# override joins/fields, bind hooks to contexts, or disable base fields. +# +# Labels (fields and hooks) are translation keys of the "querybuilder" domain +# (I18n/.php of this module); a key without translation is shown as is. +# +# joins..from : source "table.column" of the join (mandatory) +# joins.
.to : destination column in
(default: id) +# joins.
.type : INNER (default) | LEFT | RIGHT +# joins.
.on : extra ON clause, may reference runtime placeholders (:locale, :customer_id...) +# joins.
.multivalued : true when the join can yield several rows per product (1-N). +# MANDATORY on such joins: comparisons on fields reached through them +# then compile to correlated EXISTS/NOT EXISTS subqueries, so that +# notIn/!=/notBetween/doesNotContain exclude the whole product instead +# of matching another joined row. Propagates to downstream joins. +# joins.
: ~ : table resolved elsewhere (ex: by a QueryScope), no dynamic join +# +# fields..label : label shown in the back-office editor (default: ). The editor +# prefixes it with the group of the field, the part of its code before +# the first underscore ("product_ref" => "product"), translated through +# its capitalized form ("Product"): pick codes whose group is meaningful. +# A placeholder without a value in the current visit (:customer_id for a visitor, :cart_id +# without a cart, :category_id outside a category page...) makes the rule or the action skip +# with a logged warning: a field reading such a placeholder never evaluates to "false", it +# is simply not evaluated. Only :product_id falls back to 0 outside a product page. +# Dynamic expressions comparing the product to the object of the context are written as +# EXISTS (...), which always yields 0/1: with :product_id bound to 0, a scalar subquery +# (SELECT brand_id FROM product WHERE id = 0) yields NULL and "= false" would let nothing +# through instead of everything; EXISTS keeps "= true" never matching and "= false" matching all. +# fields..field : "table.column" (exclusive with expression) +# fields..expression : self-contained SQL expression, may reference runtime placeholders +# (:customer_id, :cart_product_ids, :product_id, :cart_id, :order_id, +# :category_id, :brand_id, :locale, :cart_total, :delivery_country_id, +# plus those of the RuntimeParameterProviderInterface services). +# May contain the :value token: the value entered in the editor is then +# expanded inside the expression (one placeholder per item) instead of +# being compared to it, and "operators" MUST be declared among [in, notIn] +# ("in" keeps the products matching the expression, "notIn" the others) +# fields..type : text (default) | number | date | datetime | boolean +# (a datetime column is entered as a date and compared on its date part) +# fields..contexts : contexts of the rules the field is offered to, a list among +# [GLOBAL, CATEGORY, BRAND, PRODUCT, CART, ORDER, CUSTOMER] +# (QueryBuilder\Enum\Context values). GLOBAL, the default, means every +# context — a GLOBAL rule itself only sees GLOBAL fields. Restrict a field +# to the contexts where its placeholder is filled (:product_id => PRODUCT, +# :category_id => CATEGORY, :brand_id => BRAND, :order_id => ORDER); the +# cart and the customer come from the session in every context, their +# fields stay GLOBAL. +# fields..usage : editors the field is offered in, a list among [rule, action] +# (default: both). "rule" is the trigger conditions of a rule, "action" +# the product selection of an action: a field describing the selected +# product against the object of the context ("same brand as the context +# product") is always true in the rule editor of that context, declare it +# [action]; a field reading the object itself (":category_id in [...]") +# selects all or nothing in the action editor, declare it [rule]. +# fields..operators : restrict allowed operators (react-querybuilder names) +# fields..values_query : SQL listing the possible values — "value" column required, "label" +# optional, :locale allowed. The BO editor then offers a select ("list of +# values" operator) next to the free operators. Executed on the editor +# screens only; falls back to the free input beyond 300 rows or on SQL +# error. Not allowed on boolean fields. +# +# contexts.: hooks usable by rules of this context. Each entry is either a plain +# code string, or a {code, label} mapping — the functional label is what +# the back-office shows (default: the code). Merging overrides: union by +# code, the last active module wins on the label. +# The base codes are the theme_hook() points of the Flexy theme; a rule of +# the GLOBAL context can be bound to any declared hook. +# disabled: list of field codes to remove + +contexts: + GLOBAL: + - code: home.top + label: "Home page - top" + - code: home.bottom + label: "Home page - bottom" + CATEGORY: + - code: category.top + label: "Category page - top" + - code: category.bottom + label: "Category page - bottom" + BRAND: + - code: brand.top + label: "Brand page - top" + - code: brand.bottom + label: "Brand page - bottom" + PRODUCT: + - code: product.top + label: "Product page - top" + - code: product.details.bottom + label: "Product page - below the details" + - code: product.bottom + label: "Product page - bottom" + CART: + - code: cart.top + label: "Cart - top" + - code: cart.bottom + label: "Cart - bottom" + ORDER: [] + CUSTOMER: + - code: account.top + label: "Customer account - top" + - code: account.bottom + label: "Customer account - bottom" + +joins: + product_i18n: + from: product.id + to: id + on: "product_i18n.locale = :locale" + product_category: + from: product.id + to: product_id + multivalued: true + category: + from: product_category.category_id + category_i18n: + from: category.id + to: id + on: "category_i18n.locale = :locale" + product_sale_elements: + from: product.id + to: product_id + multivalued: true + product_price: + from: product_sale_elements.id + to: product_sale_elements_id + multivalued: true + brand: + from: product.brand_id + brand_i18n: + from: brand.id + to: id + on: "brand_i18n.locale = :locale" + +fields: + product_ref: + label: "Product reference" + field: product.ref + product_title: + label: "Product title" + field: product_i18n.title + product_visible: + label: "Product visible" + field: product.visible + type: boolean + product_category_id: + label: "Category (ID)" + field: product_category.category_id + type: number + product_category_title: + label: "Product category title" + field: category_i18n.title + values_query: "SELECT DISTINCT title AS value FROM category_i18n WHERE locale = :locale AND title IS NOT NULL AND title <> '' ORDER BY title" + product_brand_title: + label: "Product brand" + field: brand_i18n.title + values_query: "SELECT DISTINCT title AS value FROM brand_i18n WHERE locale = :locale AND title IS NOT NULL AND title <> '' ORDER BY title" + product_price: + label: "Product price" + field: product_price.price + type: number + product_created_at: + label: "Product creation date" + field: product.created_at + type: datetime + product_in_cart: + label: "Product in the customer cart" + expression: "product.id IN (:cart_product_ids)" + type: boolean + # Outside a product page :product_id is bound to 0 (no current product): + # "= true" never matches, "= false" lets everything through + product_is_current: + label: "Is the product of the current page" + expression: "product.id = :product_id" + type: boolean + customer_purchased_last_3_months: + label: "Product bought by the customer in the last 3 months" + expression: "EXISTS (SELECT 1 FROM `order` qb_ord JOIN `order_product` qb_ordp ON qb_ordp.order_id = qb_ord.id WHERE qb_ord.customer_id = :customer_id AND qb_ordp.product_ref = product.ref AND qb_ord.created_at >= DATE_SUB(NOW(), INTERVAL 3 MONTH))" + type: boolean + # Cart-level conditions: constant for the whole visit, so they compare the same + # for every product. The CAST keeps the comparison numeric whatever the binding. + cart_total_amount: + label: "Cart products total (taxes included)" + expression: "CAST(:cart_total AS DECIMAL(16, 6))" + type: number + delivery_country: + label: "Delivery country" + expression: ":delivery_country_id" + type: number + operators: [in, notIn, '=', '!='] + values_query: "SELECT c.id AS value, ci.title AS label FROM country c JOIN country_i18n ci ON ci.id = c.id AND ci.locale = :locale WHERE c.visible = 1 ORDER BY ci.title" + + # ---------- Product (every context) ---------- + product_brand_id: + label: "Brand (ID)" + field: product.brand_id + type: number + values_query: "SELECT b.id AS value, bi.title AS label FROM brand b JOIN brand_i18n bi ON bi.id = b.id AND bi.locale = :locale WHERE b.visible = 1 ORDER BY bi.title" + product_default_category: + label: "Main category (list)" + expression: "EXISTS (SELECT 1 FROM product_category qb_pc WHERE qb_pc.product_id = product.id AND qb_pc.default_category = 1 AND qb_pc.category_id IN (:value))" + type: number + operators: [in, notIn] + values_query: "SELECT c.id AS value, ci.title AS label FROM category c JOIN category_i18n ci ON ci.id = c.id AND ci.locale = :locale ORDER BY ci.title" + product_is_new: + label: "Product flagged as new" + expression: "EXISTS (SELECT 1 FROM product_sale_elements qb_pse WHERE qb_pse.product_id = product.id AND qb_pse.newness = 1)" + type: boolean + product_in_promo: + label: "Product on catalog promotion" + expression: "EXISTS (SELECT 1 FROM product_sale_elements qb_pse WHERE qb_pse.product_id = product.id AND qb_pse.promo = 1)" + type: boolean + product_in_stock: + label: "Product in stock" + expression: "EXISTS (SELECT 1 FROM product_sale_elements qb_pse WHERE qb_pse.product_id = product.id AND qb_pse.quantity > 0)" + type: boolean + + # ---------- Cart of the visit, every context (no cart in the visit: the rule skips, see header) ---------- + cart_lines_count: + label: "Number of lines in the cart" + expression: "(SELECT COUNT(*) FROM cart_item qb_ci WHERE qb_ci.cart_id = :cart_id)" + type: number + cart_items_count: + label: "Number of items in the cart" + expression: "(SELECT COALESCE(SUM(qb_ci.quantity), 0) FROM cart_item qb_ci WHERE qb_ci.cart_id = :cart_id)" + type: number + cart_product_quantity: + label: "Quantity of the product in the cart" + expression: "(SELECT COALESCE(SUM(qb_ci.quantity), 0) FROM cart_item qb_ci WHERE qb_ci.cart_id = :cart_id AND qb_ci.product_id = product.id)" + type: number + cart_has_brand: + label: "Cart contains a product of the brand (list)" + expression: "EXISTS (SELECT 1 FROM cart_item qb_ci JOIN product qb_cp ON qb_cp.id = qb_ci.product_id WHERE qb_ci.cart_id = :cart_id AND qb_cp.brand_id IN (:value))" + type: number + operators: [in, notIn] + values_query: "SELECT b.id AS value, bi.title AS label FROM brand b JOIN brand_i18n bi ON bi.id = b.id AND bi.locale = :locale WHERE b.visible = 1 ORDER BY bi.title" + cart_has_category: + label: "Cart contains a product of the category (list)" + expression: "EXISTS (SELECT 1 FROM cart_item qb_ci JOIN product_category qb_pc ON qb_pc.product_id = qb_ci.product_id WHERE qb_ci.cart_id = :cart_id AND qb_pc.category_id IN (:value))" + type: number + operators: [in, notIn] + values_query: "SELECT c.id AS value, ci.title AS label FROM category c JOIN category_i18n ci ON ci.id = c.id AND ci.locale = :locale ORDER BY ci.title" + cart_has_promo_item: + label: "Cart contains a promotional line" + expression: "EXISTS (SELECT 1 FROM cart_item qb_ci WHERE qb_ci.cart_id = :cart_id AND qb_ci.promo = 1)" + type: boolean + cart_shares_brand: + label: "Same brand as a product of the cart" + expression: "EXISTS (SELECT 1 FROM cart_item qb_ci JOIN product qb_cp ON qb_cp.id = qb_ci.product_id WHERE qb_ci.cart_id = :cart_id AND qb_cp.brand_id IS NOT NULL AND qb_cp.brand_id = product.brand_id)" + type: boolean + cart_shares_category: + label: "Shares a category with a product of the cart" + expression: "EXISTS (SELECT 1 FROM cart_item qb_ci JOIN product_category qb_cpc ON qb_cpc.product_id = qb_ci.product_id JOIN product_category qb_pc ON qb_pc.category_id = qb_cpc.category_id WHERE qb_ci.cart_id = :cart_id AND qb_pc.product_id = product.id)" + type: boolean + cart_accessory_of_item: + label: "Accessory of a product of the cart" + expression: "EXISTS (SELECT 1 FROM cart_item qb_ci JOIN accessory qb_acc ON qb_acc.product_id = qb_ci.product_id WHERE qb_ci.cart_id = :cart_id AND qb_acc.accessory = product.id)" + type: boolean + + # ---------- Customer of the visit, every context (anonymous visitor: the rule skips, see header) ---------- + customer_is_reseller: + label: "Customer is a reseller" + expression: "EXISTS (SELECT 1 FROM customer qb_c WHERE qb_c.id = :customer_id AND qb_c.reseller = 1)" + type: boolean + customer_discount_rate: + label: "Customer discount rate (%)" + expression: "(SELECT qb_c.discount FROM customer qb_c WHERE qb_c.id = :customer_id)" + type: number + customer_since_days: + label: "Customer seniority (days)" + expression: "(SELECT DATEDIFF(NOW(), qb_c.created_at) FROM customer qb_c WHERE qb_c.id = :customer_id)" + type: number + # Orders whose status is not "not paid", "canceled" or "refunded" (core status codes) + customer_orders_count: + label: "Number of orders of the customer" + expression: "(SELECT COUNT(*) FROM `order` qb_o JOIN order_status qb_os ON qb_os.id = qb_o.status_id WHERE qb_o.customer_id = :customer_id AND qb_os.code NOT IN ('not_paid', 'canceled', 'refunded'))" + type: number + # NULL (no comparison matches) for a customer without any order + customer_last_order_days: + label: "Days since the last order of the customer" + expression: "(SELECT DATEDIFF(NOW(), MAX(qb_o.created_at)) FROM `order` qb_o WHERE qb_o.customer_id = :customer_id)" + type: number + customer_newsletter_subscriber: + label: "Customer subscribed to the newsletter" + expression: "EXISTS (SELECT 1 FROM newsletter qb_n JOIN customer qb_c ON qb_c.email = qb_n.email WHERE qb_c.id = :customer_id AND qb_n.unsubscribed = 0)" + type: boolean + customer_purchased_ever: + label: "Product already bought by the customer" + expression: "EXISTS (SELECT 1 FROM `order` qb_o JOIN order_product qb_op ON qb_op.order_id = qb_o.id WHERE qb_o.customer_id = :customer_id AND qb_op.product_ref = product.ref)" + type: boolean + customer_purchased_brand: + label: "Brand already bought by the customer" + expression: "EXISTS (SELECT 1 FROM `order` qb_o JOIN order_product qb_op ON qb_op.order_id = qb_o.id JOIN product qb_p ON qb_p.ref = qb_op.product_ref WHERE qb_o.customer_id = :customer_id AND qb_p.brand_id IS NOT NULL AND qb_p.brand_id = product.brand_id)" + type: boolean + customer_purchased_category: + label: "Category already bought by the customer" + expression: "EXISTS (SELECT 1 FROM `order` qb_o JOIN order_product qb_op ON qb_op.order_id = qb_o.id JOIN product qb_p ON qb_p.ref = qb_op.product_ref JOIN product_category qb_ppc ON qb_ppc.product_id = qb_p.id JOIN product_category qb_pc ON qb_pc.category_id = qb_ppc.category_id WHERE qb_o.customer_id = :customer_id AND qb_pc.product_id = product.id)" + type: boolean + + # ---------- PRODUCT context: the selected product against the context product (:product_id) ---------- + context_product_same_brand: + label: "Same brand as the context product" + expression: "EXISTS (SELECT 1 FROM product qb_ctx WHERE qb_ctx.id = :product_id AND qb_ctx.brand_id IS NOT NULL AND qb_ctx.brand_id = product.brand_id)" + type: boolean + contexts: [PRODUCT] + usage: [action] + context_product_shared_category: + label: "Shares a category with the context product" + expression: "EXISTS (SELECT 1 FROM product_category qb_ctx JOIN product_category qb_pc ON qb_pc.category_id = qb_ctx.category_id WHERE qb_ctx.product_id = :product_id AND qb_pc.product_id = product.id)" + type: boolean + contexts: [PRODUCT] + usage: [action] + context_product_same_default_category: + label: "Same main category as the context product" + expression: "EXISTS (SELECT 1 FROM product_category qb_ctx JOIN product_category qb_pc ON qb_pc.category_id = qb_ctx.category_id WHERE qb_ctx.product_id = :product_id AND qb_ctx.default_category = 1 AND qb_pc.product_id = product.id AND qb_pc.default_category = 1)" + type: boolean + contexts: [PRODUCT] + usage: [action] + context_product_accessory: + label: "Accessory of the context product" + expression: "EXISTS (SELECT 1 FROM accessory qb_acc WHERE qb_acc.product_id = :product_id AND qb_acc.accessory = product.id)" + type: boolean + contexts: [PRODUCT] + usage: [action] + context_product_same_template: + label: "Same template as the context product" + expression: "EXISTS (SELECT 1 FROM product qb_ctx WHERE qb_ctx.id = :product_id AND qb_ctx.template_id IS NOT NULL AND qb_ctx.template_id = product.template_id)" + type: boolean + contexts: [PRODUCT] + usage: [action] + # "= false" removes the products already in the cart from a product list, "= true" keeps only them. + # Reads the cart of the visit (same test as product_in_cart), offered to the PRODUCT lists by design + context_product_in_cart: + label: "The product is in the cart" + expression: "product.id IN (:cart_product_ids)" + type: boolean + contexts: [PRODUCT] + usage: [action, rule] + context_product_shared_feature_value: + label: "Same value as the context product for the feature (list)" + expression: "EXISTS (SELECT 1 FROM feature_product qb_ctx JOIN feature_product qb_fp ON qb_fp.feature_id = qb_ctx.feature_id AND qb_fp.feature_av_id = qb_ctx.feature_av_id WHERE qb_ctx.product_id = :product_id AND qb_ctx.feature_av_id IS NOT NULL AND qb_fp.product_id = product.id AND qb_ctx.feature_id IN (:value))" + type: number + operators: [in, notIn] + contexts: [PRODUCT] + usage: [action] + values_query: "SELECT f.id AS value, fi.title AS label FROM feature f JOIN feature_i18n fi ON fi.id = f.id AND fi.locale = :locale ORDER BY fi.title" + + # ---------- CATEGORY context (:category_id) ---------- + context_category_id: + label: "Context category (list)" + expression: ":category_id" + type: number + operators: [in, notIn, '=', '!='] + contexts: [CATEGORY] + usage: [rule] + values_query: "SELECT c.id AS value, ci.title AS label FROM category c JOIN category_i18n ci ON ci.id = c.id AND ci.locale = :locale ORDER BY ci.title" + context_category_parent_id: + label: "Parent of the context category (list)" + expression: "(SELECT qb_c.parent FROM category qb_c WHERE qb_c.id = :category_id)" + type: number + operators: [in, notIn, '=', '!='] + contexts: [CATEGORY] + usage: [rule] + values_query: "SELECT c.id AS value, ci.title AS label FROM category c JOIN category_i18n ci ON ci.id = c.id AND ci.locale = :locale ORDER BY ci.title" + context_category_product: + label: "Product of the context category" + expression: "EXISTS (SELECT 1 FROM product_category qb_pc WHERE qb_pc.product_id = product.id AND qb_pc.category_id = :category_id)" + type: boolean + contexts: [CATEGORY] + usage: [action] + # Recursive CTE: MySQL 8.0+ / MariaDB 10.2+ + context_category_subtree_product: + label: "Product of the context category or its sub-categories" + expression: "EXISTS (WITH RECURSIVE qb_tree AS (SELECT id FROM category WHERE id = :category_id UNION ALL SELECT qb_c.id FROM category qb_c JOIN qb_tree ON qb_c.parent = qb_tree.id) SELECT 1 FROM product_category qb_pc JOIN qb_tree ON qb_tree.id = qb_pc.category_id WHERE qb_pc.product_id = product.id)" + type: boolean + contexts: [CATEGORY] + usage: [action] + + # ---------- BRAND context (:brand_id) ---------- + context_brand_id: + label: "Context brand (list)" + expression: ":brand_id" + type: number + operators: [in, notIn, '=', '!='] + contexts: [BRAND] + usage: [rule] + values_query: "SELECT b.id AS value, bi.title AS label FROM brand b JOIN brand_i18n bi ON bi.id = b.id AND bi.locale = :locale WHERE b.visible = 1 ORDER BY bi.title" + context_brand_product: + label: "Product of the context brand" + expression: "EXISTS (SELECT 1 FROM brand qb_b WHERE qb_b.id = :brand_id AND qb_b.id = product.brand_id)" + type: boolean + contexts: [BRAND] + usage: [action] +disabled: [] diff --git a/Config/schema.xml b/Config/schema.xml new file mode 100644 index 0000000..25318e5 --- /dev/null +++ b/Config/schema.xml @@ -0,0 +1,77 @@ + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/Config/update/1.1.0.sql b/Config/update/1.1.0.sql new file mode 100644 index 0000000..30446d9 --- /dev/null +++ b/Config/update/1.1.0.sql @@ -0,0 +1,7 @@ +# 1.1.0 — discounts become stateless (ApplyDiscount actions), the suggestion +# table no longer carries an offer snapshot. + +ALTER TABLE `query_builder_suggestion` + DROP COLUMN `discount_rate`, + DROP COLUMN `discount_label`, + DROP COLUMN `discount_cumulative`; diff --git a/Config/update/1.2.0.sql b/Config/update/1.2.0.sql new file mode 100644 index 0000000..0f5123f --- /dev/null +++ b/Config/update/1.2.0.sql @@ -0,0 +1,10 @@ +# 1.2.0 — #573 : les hooks orphelins (jamais consommés côté front) sont retirés +# du dictionnaire ; purge des codes fantômes encore cochés par des règles. + +UPDATE `query_builder_rule` +SET `hooks` = JSON_REMOVE(`hooks`, JSON_UNQUOTE(JSON_SEARCH(`hooks`, 'one', 'cart.addtocart.recommendations'))) +WHERE JSON_SEARCH(`hooks`, 'one', 'cart.addtocart.recommendations') IS NOT NULL; + +UPDATE `query_builder_rule` +SET `hooks` = JSON_REMOVE(`hooks`, JSON_UNQUOTE(JSON_SEARCH(`hooks`, 'one', 'customer.login.recommendations'))) +WHERE JSON_SEARCH(`hooks`, 'one', 'customer.login.recommendations') IS NOT NULL; diff --git a/Controller/Back/ActionController.php b/Controller/Back/ActionController.php new file mode 100644 index 0000000..d052e0b --- /dev/null +++ b/Controller/Back/ActionController.php @@ -0,0 +1,447 @@ + '\d+'])] +class ActionController extends BaseAdminController +{ + #[Route('/create', name: 'create', methods: 'POST')] + public function createAction( + ParserContext $parserContext, + int $ruleId, + ActionRegistry $actionRegistry, + EventDispatcherInterface $eventDispatcher, + ): RedirectResponse|Response { + if (null !== $response = $this->denyUnlessGranted(AccessManager::CREATE)) { + return $response; + } + + $form = $this->createForm(ActionForm::getName()); + + try { + $rule = $this->requireRule($ruleId); + $data = $this->validateForm($form, 'POST')->getData(); + $handler = $this->requireHandler($data['code'], $rule, $actionRegistry); + + $action = (new QueryBuilderAction()) + ->setRuleId($rule->getId()) + ->setName($data['name']) + ->setCode($data['code']) + ->setType($handler::getType()) + ->setActivate(0); + $action->save(); + $eventDispatcher->dispatch(new QueryBuilderRulesChangedEvent()); + + //URL absolue : les routes #[Route] du module ne sont pas dans "router.admin", + //seul router consulté par generateRedirectFromRoute (RouteNotFoundException sinon) + return $this->generateRedirect(URL::getInstance()->absoluteUrl( + sprintf('%s/rule/%d/action/%d', RuleController::LIST_PATH, $rule->getId(), $action->getId()) + )); + } catch (FormValidationException $exception) { + $errorMessage = $this->createStandardFormValidationErrorMessage($exception); + } catch (\InvalidArgumentException $exception) { + $errorMessage = $exception->getMessage(); + } catch (\Exception $exception) { + Tlog::getInstance()->addError(sprintf('QueryBuilder action create error: %s', $exception->getMessage())); + $errorMessage = $this->unexpectedErrorMessage(); + } + + return $this->redirectWithError($form, $parserContext, $errorMessage); + } + + #[Route('/{actionId}', name: 'edit', requirements: ['actionId' => '\d+'], methods: 'GET')] + public function editAction( + int $ruleId, + int $actionId, + ActionRegistry $actionRegistry, + FieldsBuilder $fieldsBuilder, + EditorLabels $editorLabels, + ): Response { + if (null !== $response = $this->denyUnlessGranted(AccessManager::VIEW)) { + return $response; + } + + $rule = QueryBuilderRuleQuery::create()->findOneById($ruleId); + $action = QueryBuilderActionQuery::create()->filterByRuleId($ruleId)->findOneById($actionId); + + if ($rule === null || $action === null) { + return $this->generateRedirect(URL::getInstance()->absoluteUrl(RuleController::LIST_PATH)); + } + + $context = Context::tryFrom($rule->getContext() ?? '') ?? Context::GLOBAL_SCOPE; + $parameters = $action->getParametersArray(); + + $availableActions = []; + foreach ($actionRegistry->forContext($context) as $code => $actionHandler) { + $availableActions[] = [ + 'code' => $code, + 'label' => $this->trans($actionHandler::getLabel()), + 'type' => $actionHandler::getType(), + ]; + } + + $form = $this->createForm(ActionForm::getName(), FormType::class, [ + 'name' => $action->getName(), + 'description' => $action->getDescription(), + 'code' => $action->getCode(), + 'limit' => isset($parameters['limit']) ? (int) $parameters['limit'] : null, + 'persist_days' => isset($parameters['persist_days']) ? (int) $parameters['persist_days'] : null, + 'discount_rate' => isset($parameters['discount_rate']) ? (float) $parameters['discount_rate'] : null, + 'discount_label' => $parameters['discount_label'] ?? null, + 'discount_cumulative' => (bool) ($parameters['discount_cumulative'] ?? false), + 'cart_discount_rate' => isset($parameters['cart_discount_rate']) ? (float) $parameters['cart_discount_rate'] : null, + 'cart_discount_free_shipping' => (bool) ($parameters['cart_discount_free_shipping'] ?? false), + 'condition_tree' => $action->getConditionTreeArray(), + 'activate' => (bool) $action->getActivate(), + ]); + + $contextFields = $fieldsBuilder->buildForContext($context, $this->getRequest()->getLocale(), FieldDefinition::USAGE_ACTION); + + return $this->render('action-edit', [ + 'rule' => [ + 'id' => $rule->getId(), + 'name' => $rule->getName(), + 'context' => $context->value, + 'context_label' => $this->trans($context->label()), + ], + 'action' => [ + 'id' => $action->getId(), + 'name' => $action->getName(), + 'code' => $action->getCode(), + 'type' => $action->getType(), + 'activate' => (bool) $action->getActivate(), + ], + 'form' => $form->createView()->getView(), + 'available_actions' => $availableActions, + 'discount_code' => ApplyDiscountAction::CODE, + 'cart_discount_code' => ApplyCartDiscountAction::CODE, + 'fields_by_context' => [$context->value => $contextFields], + 'context_fields' => $contextFields, + 'editor_labels' => $editorLabels->build(), + ]); + } + + #[Route('/{actionId}/save', name: 'save', requirements: ['actionId' => '\d+'], methods: 'POST')] + public function saveAction( + ParserContext $parserContext, + int $ruleId, + int $actionId, + ActionRegistry $actionRegistry, + SqlBuilder $sqlBuilder, + SuggestionService $suggestionService, + EventDispatcherInterface $eventDispatcher, + ): RedirectResponse|Response { + if (null !== $response = $this->denyUnlessGranted(AccessManager::UPDATE)) { + return $response; + } + + $form = $this->createForm(ActionForm::getName()); + + try { + $rule = $this->requireRule($ruleId); + $action = QueryBuilderActionQuery::create()->filterByRuleId($ruleId)->findOneById($actionId); + + if ($action === null) { + throw new \RuntimeException(sprintf('Action #%d not found.', $actionId)); + } + + $data = $this->validateForm($form, 'POST')->getData(); + $handler = $this->requireHandler($data['code'], $rule, $actionRegistry); + $conditionTree = $this->validateConditionTree( + $data['condition_tree'] ?? null, + $sqlBuilder, + Context::tryFrom($rule->getContext() ?? '') ?? Context::GLOBAL_SCOPE + ); + $parameters = $this->buildParameters($action->getParametersArray(), $data); + $conditionTreeChanged = $this->normalizeConditionTree($action->getConditionTreeArray()) + !== $this->normalizeConditionTree($conditionTree); + + $action + ->setName($data['name']) + ->setDescription($data['description'] ?? null) + ->setCode($data['code']) + ->setType($handler::getType()) + ->setConditionTree($conditionTree !== null ? json_encode($conditionTree, \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_UNICODE) : null) + ->setParameters($parameters !== [] ? json_encode($parameters, \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_UNICODE) : null) + ->setActivate($data['activate'] ? 1 : 0); + $action->save(); + + if ($conditionTreeChanged) { + $suggestionService->expireActiveCycles((int) $action->getId()); + } + + $eventDispatcher->dispatch(new QueryBuilderRulesChangedEvent()); + + $this->addFlash('success', $this->trans('The action has been saved.')); + + return $this->generateSuccessRedirect($form); + } catch (FormValidationException $exception) { + $errorMessage = $this->createStandardFormValidationErrorMessage($exception); + } catch (\InvalidArgumentException $exception) { + $errorMessage = $exception->getMessage(); + } catch (\Exception $exception) { + Tlog::getInstance()->addError(sprintf('QueryBuilder action save error: %s', $exception->getMessage())); + $errorMessage = $this->unexpectedErrorMessage(); + } + + return $this->redirectWithError($form, $parserContext, $errorMessage); + } + + #[Route('/{actionId}/toggle', name: 'toggle', requirements: ['actionId' => '\d+'], methods: 'POST')] + public function toggleAction( + Request $request, + TokenProvider $tokenProvider, + int $ruleId, + int $actionId, + EventDispatcherInterface $eventDispatcher, + ): Response { + if (null !== $response = $this->denyUnlessGranted(AccessManager::UPDATE)) { + return $response; + } + + $tokenProvider->checkToken((string) $request->query->get('_token')); + + $action = QueryBuilderActionQuery::create()->filterByRuleId($ruleId)->findOneById($actionId); + + if ($action !== null) { + $action->setActivate($action->getActivate() ? 0 : 1)->save(); + $eventDispatcher->dispatch(new QueryBuilderRulesChangedEvent()); + } + + return $this->generateRedirect(URL::getInstance()->absoluteUrl(RuleController::LIST_PATH . '/rule/' . $ruleId)); + } + + #[Route('/{actionId}/delete', name: 'delete', requirements: ['actionId' => '\d+'], methods: 'POST')] + public function deleteAction( + Request $request, + TokenProvider $tokenProvider, + int $ruleId, + int $actionId, + EventDispatcherInterface $eventDispatcher, + ): Response { + if (null !== $response = $this->denyUnlessGranted(AccessManager::DELETE)) { + return $response; + } + + $tokenProvider->checkToken((string) $request->query->get('_token')); + + $action = QueryBuilderActionQuery::create()->filterByRuleId($ruleId)->findOneById($actionId); + + if ($action !== null) { + $action->delete(); + $eventDispatcher->dispatch(new QueryBuilderRulesChangedEvent()); + } + + return $this->generateRedirect(URL::getInstance()->absoluteUrl(RuleController::LIST_PATH . '/rule/' . $ruleId)); + } + + private function redirectWithError(BaseForm $form, ParserContext $parserContext, string $errorMessage): RedirectResponse|Response + { + $form->setErrorMessage($errorMessage); + $parserContext->addForm($form)->setGeneralError($errorMessage); + $this->addFlash('danger', $errorMessage); + + return $this->generateErrorRedirect($form); + } + + /** + * Every screen of the module, in reading as in writing, is reserved to the + * administrators granted on the module itself: the core only checks that + * an administrator is logged in. The "admin.module" resource is not required, + * it guards the module management screens, not the screens of this module. + */ + private function denyUnlessGranted(string $access): ?Response + { + return $this->checkAuth([], QueryBuilder::getModuleCode(), $access); + } + + private function trans(string $id): string + { + return Translator::getInstance()->trans($id, [], QueryBuilder::DOMAIN_NAME); + } + + private function unexpectedErrorMessage(): string + { + return $this->trans('An unexpected error occurred, please check the logs.'); + } + + private function requireRule(int $ruleId): QueryBuilderRule + { + $rule = QueryBuilderRuleQuery::create()->findOneById($ruleId); + + if ($rule === null) { + throw new \RuntimeException(sprintf('Rule #%d not found.', $ruleId)); + } + + return $rule; + } + + private function requireHandler(string $code, QueryBuilderRule $rule, ActionRegistry $actionRegistry): ActionInterface + { + $context = Context::tryFrom($rule->getContext() ?? '') ?? Context::GLOBAL_SCOPE; + $handler = $actionRegistry->forContext($context)[$code] ?? null; + + if ($handler === null) { + throw new \InvalidArgumentException(sprintf( + 'Unknown action code "%s" for context %s.', + $code, + $context->value + )); + } + + return $handler; + } + + /** + * The form type already refused any field or operator outside the dictionary; + * the context restriction of the fields is checked here, on the stored shape. + * A group emptied of its rules is "no condition". + */ + private function validateConditionTree(?array $conditionTree, SqlBuilder $sqlBuilder, Context $context): ?array + { + if ($conditionTree === null || ($conditionTree['rules'] ?? []) === []) { + return null; + } + + $sqlBuilder->validateTree($conditionTree, $context, FieldDefinition::USAGE_ACTION); + + return $conditionTree; + } + + /** + * Strips the editor bookkeeping (node ids, default valueSource and "not" + * flags) so two trees compare on their semantics only: the editor may + * re-serialize an unchanged tree with different ids or extra defaults. + */ + private function normalizeConditionTree(?array $tree): ?array + { + if ($tree === null) { + return null; + } + + unset($tree['id']); + + if (($tree['valueSource'] ?? null) === 'value') { + unset($tree['valueSource']); + } + + if (empty($tree['not'])) { + unset($tree['not']); + } + + foreach ($tree as $key => $value) { + if (\is_array($value)) { + $tree[$key] = $this->normalizeConditionTree($value); + } + } + + return $tree; + } + + /** + * Merges the typed form fields into the stored parameters, keeping unknown + * keys untouched. Each action code owns its parameter set: display actions + * take limit/persist_days, ApplyDiscount takes the discount fields plus + * the optional limit/persist_days, ApplyCartDiscount takes the cart + * discount fields. + */ + private function buildParameters(array $existingParameters, array $data): array + { + $parameters = $existingParameters; + unset( + $parameters['limit'], + $parameters['persist_days'], + $parameters['discount_rate'], + $parameters['discount_label'], + $parameters['discount_cumulative'], + $parameters['cart_discount_rate'], + $parameters['cart_discount_free_shipping'] + ); + + if ($data['code'] === ApplyCartDiscountAction::CODE) { + $rate = $data['cart_discount_rate'] !== null ? (float) $data['cart_discount_rate'] : null; + $freeShipping = (bool) ($data['cart_discount_free_shipping'] ?? false); + + if (($rate === null || $rate <= 0) && !$freeShipping) { + throw new \InvalidArgumentException($this->trans('A cart discount needs a rate above 0 or free shipping.')); + } + + if ($rate !== null && $rate > 0) { + $parameters['cart_discount_rate'] = $rate; + } + + $parameters['cart_discount_free_shipping'] = $freeShipping; + + return $parameters; + } + + if ($data['code'] === ApplyDiscountAction::CODE) { + if ($data['discount_rate'] === null || (float) $data['discount_rate'] <= 0) { + throw new \InvalidArgumentException($this->trans('A discount action needs a discount rate above 0.')); + } + + $parameters['discount_rate'] = (float) $data['discount_rate']; + + if (($data['discount_label'] ?? '') !== '' && $data['discount_label'] !== null) { + $parameters['discount_label'] = (string) $data['discount_label']; + } + + $parameters['discount_cumulative'] = (bool) ($data['discount_cumulative'] ?? false); + + if ($data['limit'] !== null) { + $parameters['limit'] = (int) $data['limit']; + } + + if ($data['persist_days'] !== null) { + $parameters['persist_days'] = (int) $data['persist_days']; + } + + return $parameters; + } + + if ($data['limit'] !== null) { + $parameters['limit'] = (int) $data['limit']; + } + + if ($data['persist_days'] !== null) { + $parameters['persist_days'] = (int) $data['persist_days']; + } + + return $parameters; + } +} diff --git a/Controller/Back/RuleController.php b/Controller/Back/RuleController.php new file mode 100644 index 0000000..dc56a80 --- /dev/null +++ b/Controller/Back/RuleController.php @@ -0,0 +1,387 @@ +denyUnlessGranted(AccessManager::VIEW)) { + return $response; + } + + $actionCounts = []; + foreach (QueryBuilderActionQuery::create()->find() as $action) { + $actionCounts[$action->getRuleId()] = ($actionCounts[$action->getRuleId()] ?? 0) + 1; + } + + //A hook code stored by a rule but no longer declared (removed from the + //dictionary) keeps its raw code as label: visible, never hidden + $hookLabels = array_map($this->trans(...), $dataDictionary->getHooks(Context::GLOBAL_SCOPE)); + + $rules = []; + foreach (QueryBuilderRuleQuery::create()->orderByPosition()->orderById()->find() as $rule) { + $rules[] = [ + 'id' => $rule->getId(), + 'name' => $rule->getName(), + 'description' => $rule->getDescription(), + 'context' => $rule->getContext(), + 'context_label' => $this->trans(Context::tryFrom($rule->getContext() ?? '')?->label() ?? (string) $rule->getContext()), + 'hooks' => array_map( + static fn (string $hookCode): array => [ + 'code' => $hookCode, + 'label' => $hookLabels[$hookCode] ?? $hookCode, + ], + $rule->getHookCodes() + ), + 'activate' => (bool) $rule->getActivate(), + 'action_count' => $actionCounts[$rule->getId()] ?? 0, + ]; + } + + return $this->render('rule-list', [ + 'rules' => $rules, + 'contexts' => $this->contextChoices(), + 'create_form' => $this->createForm(RuleForm::getName())->createView()->getView(), + ]); + } + + #[Route('/rule/create', name: 'rule_create', methods: 'POST')] + public function createRule(ParserContext $parserContext, EventDispatcherInterface $eventDispatcher): RedirectResponse|Response + { + if (null !== $response = $this->denyUnlessGranted(AccessManager::CREATE)) { + return $response; + } + + $form = $this->createForm(RuleForm::getName()); + + try { + $data = $this->validateForm($form, 'POST')->getData(); + $context = $this->requireContext($data['context']); + + $rule = (new QueryBuilderRule()) + ->setName($data['name']) + ->setDescription($data['description'] ?? null) + ->setContext($context->value) + ->setHookCodes([]) + ->setActivate(0); + $rule->save(); + $eventDispatcher->dispatch(new QueryBuilderRulesChangedEvent()); + + //URL absolue : les routes #[Route] du module ne sont pas dans "router.admin", + //seul router consulté par generateRedirectFromRoute (RouteNotFoundException sinon) + return $this->generateRedirect(URL::getInstance()->absoluteUrl(self::LIST_PATH . '/rule/' . $rule->getId())); + } catch (FormValidationException $exception) { + $errorMessage = $this->createStandardFormValidationErrorMessage($exception); + } catch (\InvalidArgumentException $exception) { + $errorMessage = $exception->getMessage(); + } catch (\Exception $exception) { + Tlog::getInstance()->addError(sprintf('QueryBuilder rule create error: %s', $exception->getMessage())); + $errorMessage = $this->unexpectedErrorMessage(); + } + + return $this->redirectWithError($form, $parserContext, $errorMessage); + } + + #[Route('/rule/{ruleId}', name: 'rule_edit', requirements: ['ruleId' => '\d+'], methods: 'GET')] + public function editRule( + int $ruleId, + DataDictionary $dataDictionary, + FieldsBuilder $fieldsBuilder, + ActionRegistry $actionRegistry, + EditorLabels $editorLabels, + ): Response { + if (null !== $response = $this->denyUnlessGranted(AccessManager::VIEW)) { + return $response; + } + + $rule = QueryBuilderRuleQuery::create()->findOneById($ruleId); + + if ($rule === null) { + return $this->generateRedirect(URL::getInstance()->absoluteUrl(self::LIST_PATH)); + } + + $context = Context::tryFrom($rule->getContext() ?? '') ?? Context::GLOBAL_SCOPE; + + $actions = []; + foreach (QueryBuilderActionQuery::create()->filterByRuleId($ruleId)->orderByPosition()->orderById()->find() as $action) { + $actions[] = [ + 'id' => $action->getId(), + 'name' => $action->getName(), + 'code' => $action->getCode(), + 'type' => $action->getType(), + 'activate' => (bool) $action->getActivate(), + 'condition_tree' => $action->getConditionTreeArray(), + 'parameters' => $action->getParametersArray(), + ]; + } + + $availableActions = []; + foreach ($actionRegistry->forContext($context) as $code => $actionHandler) { + $availableActions[] = [ + 'code' => $code, + 'label' => $this->trans($actionHandler::getLabel()), + 'type' => $actionHandler::getType(), + ]; + } + + $hookChoices = []; + foreach (Context::cases() as $contextCase) { + foreach ($dataDictionary->getHooks($contextCase) as $hookCode => $hookLabel) { + $hookChoices[] = [ + 'context' => $contextCase->value, + 'code' => $hookCode, + 'label' => $this->trans($hookLabel), + 'checked' => $contextCase === $context && \in_array($hookCode, $rule->getHookCodes(), true), + ]; + } + } + + $form = $this->createForm(RuleForm::getName(), FormType::class, [ + 'name' => $rule->getName(), + 'description' => $rule->getDescription(), + 'context' => $context->value, + 'condition_tree' => $rule->getConditionTreeArray(), + 'activate' => (bool) $rule->getActivate(), + ]); + + $locale = $this->getRequest()->getLocale(); + $fieldsByContext = $fieldsBuilder->buildForAllContexts($locale, FieldDefinition::USAGE_RULE); + + return $this->render('rule-edit', [ + 'rule' => [ + 'id' => $rule->getId(), + 'name' => $rule->getName(), + 'context' => $context->value, + 'context_label' => $this->trans($context->label()), + 'activate' => (bool) $rule->getActivate(), + ], + 'form' => $form->createView()->getView(), + 'action_form' => $this->createForm(ActionForm::getName())->createView()->getView(), + 'actions' => $actions, + 'available_actions' => $availableActions, + 'cart_discount_code' => ApplyCartDiscountAction::CODE, + 'contexts' => $this->contextChoices(), + 'hook_choices' => $hookChoices, + 'fields_by_context' => $fieldsByContext, + 'context_fields' => $fieldsByContext[$context->value] ?? [], + //The reading of each action selection needs the fields of the action editor + 'action_fields' => $fieldsBuilder->buildForContext($context, $locale, FieldDefinition::USAGE_ACTION), + 'editor_labels' => $editorLabels->build(), + ]); + } + + #[Route('/rule/{ruleId}/save', name: 'rule_save', requirements: ['ruleId' => '\d+'], methods: 'POST')] + public function saveRule( + Request $request, + ParserContext $parserContext, + int $ruleId, + DataDictionary $dataDictionary, + SqlBuilder $sqlBuilder, + EventDispatcherInterface $eventDispatcher, + ): RedirectResponse|Response { + if (null !== $response = $this->denyUnlessGranted(AccessManager::UPDATE)) { + return $response; + } + + $form = $this->createForm(RuleForm::getName()); + + try { + $rule = QueryBuilderRuleQuery::create()->findOneById($ruleId); + + if ($rule === null) { + throw new \RuntimeException(sprintf('Rule #%d not found.', $ruleId)); + } + + $data = $this->validateForm($form, 'POST')->getData(); + $context = $this->requireContext($data['context']); + $conditionTree = $this->validateConditionTree($data['condition_tree'] ?? null, $sqlBuilder, $context); + + //Only hooks declared for the selected context are kept + $postedHooks = $request->request->all('hooks'); + $hooks = array_values(array_intersect(array_keys($dataDictionary->getHooks($context)), $postedHooks)); + + $rule + ->setName($data['name']) + ->setDescription($data['description'] ?? null) + ->setContext($context->value) + ->setHookCodes($hooks) + ->setConditionTree($conditionTree !== null ? json_encode($conditionTree, \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_UNICODE) : null) + ->setActivate($data['activate'] ? 1 : 0); + $rule->save(); + $eventDispatcher->dispatch(new QueryBuilderRulesChangedEvent()); + + $this->addFlash('success', $this->trans('The rule has been saved.')); + + return $this->generateSuccessRedirect($form); + } catch (FormValidationException $exception) { + $errorMessage = $this->createStandardFormValidationErrorMessage($exception); + } catch (\InvalidArgumentException $exception) { + $errorMessage = $exception->getMessage(); + } catch (\Exception $exception) { + Tlog::getInstance()->addError(sprintf('QueryBuilder rule save error: %s', $exception->getMessage())); + $errorMessage = $this->unexpectedErrorMessage(); + } + + return $this->redirectWithError($form, $parserContext, $errorMessage); + } + + #[Route('/rule/{ruleId}/toggle', name: 'rule_toggle', requirements: ['ruleId' => '\d+'], methods: 'POST')] + public function toggleRule( + Request $request, + TokenProvider $tokenProvider, + int $ruleId, + EventDispatcherInterface $eventDispatcher, + ): Response { + if (null !== $response = $this->denyUnlessGranted(AccessManager::UPDATE)) { + return $response; + } + + $tokenProvider->checkToken((string) $request->query->get('_token')); + + $rule = QueryBuilderRuleQuery::create()->findOneById($ruleId); + + if ($rule !== null) { + $rule->setActivate($rule->getActivate() ? 0 : 1)->save(); + $eventDispatcher->dispatch(new QueryBuilderRulesChangedEvent()); + } + + return $this->generateRedirect(URL::getInstance()->absoluteUrl(self::LIST_PATH)); + } + + #[Route('/rule/{ruleId}/delete', name: 'rule_delete', requirements: ['ruleId' => '\d+'], methods: 'POST')] + public function deleteRule( + Request $request, + TokenProvider $tokenProvider, + int $ruleId, + EventDispatcherInterface $eventDispatcher, + ): Response { + if (null !== $response = $this->denyUnlessGranted(AccessManager::DELETE)) { + return $response; + } + + $tokenProvider->checkToken((string) $request->query->get('_token')); + + $rule = QueryBuilderRuleQuery::create()->findOneById($ruleId); + + if ($rule !== null) { + $rule->delete(); + $eventDispatcher->dispatch(new QueryBuilderRulesChangedEvent()); + } + + return $this->generateRedirect(URL::getInstance()->absoluteUrl(self::LIST_PATH)); + } + + /** + * @return array + */ + private function contextChoices(): array + { + return array_map( + fn (Context $case): array => [ + 'value' => $case->value, + 'label' => $this->trans($case->label()), + 'description' => $this->trans($case->description()), + ], + Context::cases() + ); + } + + /** + * The rejected form is kept in the parser context (Thelia convention) and the + * message shown through the back-office flash block on the redirected page. + */ + private function redirectWithError(BaseForm $form, ParserContext $parserContext, string $errorMessage): RedirectResponse|Response + { + $form->setErrorMessage($errorMessage); + $parserContext->addForm($form)->setGeneralError($errorMessage); + $this->addFlash('danger', $errorMessage); + + return $this->generateErrorRedirect($form); + } + + /** + * Every screen of the module, in reading as in writing, is reserved to the + * administrators granted on the module itself: the core only checks that + * an administrator is logged in. The "admin.module" resource is not required, + * it guards the module management screens, not the screens of this module. + */ + private function denyUnlessGranted(string $access): ?Response + { + return $this->checkAuth([], QueryBuilder::getModuleCode(), $access); + } + + private function trans(string $id): string + { + return Translator::getInstance()->trans($id, [], QueryBuilder::DOMAIN_NAME); + } + + private function unexpectedErrorMessage(): string + { + return $this->trans('An unexpected error occurred, please check the logs.'); + } + + private function requireContext(?string $contextValue): Context + { + $context = Context::tryFrom((string) $contextValue); + + if ($context === null) { + throw new \InvalidArgumentException(sprintf('Unknown context "%s".', $contextValue)); + } + + return $context; + } + + /** + * The form type already refused any field or operator outside the dictionary; + * the context restriction of the fields is checked here, on the stored shape. + * A group emptied of its rules is "no condition". + */ + private function validateConditionTree(?array $conditionTree, SqlBuilder $sqlBuilder, Context $context): ?array + { + if ($conditionTree === null || ($conditionTree['rules'] ?? []) === []) { + return null; + } + + $sqlBuilder->validateTree($conditionTree, $context, FieldDefinition::USAGE_RULE); + + return $conditionTree; + } +} diff --git a/Dictionary/DictionaryOverrideLocatorInterface.php b/Dictionary/DictionaryOverrideLocatorInterface.php new file mode 100644 index 0000000..d8f558f --- /dev/null +++ b/Dictionary/DictionaryOverrideLocatorInterface.php @@ -0,0 +1,16 @@ + "product", "delivery_country" => "delivery"). + */ + public function getGroup(): string + { + return explode('_', $this->code, 2)[0]; + } + + /** + * An expression may consume the value entered in the editor through the + * :value token (expanded to one placeholder per item) instead of being + * compared to it — the in/notIn operator then only selects the polarity. + */ + public function usesValuePlaceholder(): bool + { + return $this->expression !== null && preg_match('/:value\b/', $this->expression) === 1; + } + + public function getTable(): ?string + { + if ($this->column === null || !str_contains($this->column, '.')) { + return null; + } + + return explode('.', $this->column, 2)[0]; + } + + /** A GLOBAL field is available in every context, like a GLOBAL rule listens to every hook. */ + public function isAvailableInContext(Context $context): bool + { + return \in_array(Context::GLOBAL_SCOPE, $this->contexts, true) || \in_array($context, $this->contexts, true); + } + + public function isAvailableForUsage(string $usage): bool + { + return \in_array($usage, $this->usages, true); + } +} diff --git a/Dictionary/JoinDefinition.php b/Dictionary/JoinDefinition.php new file mode 100644 index 0000000..952d1c0 --- /dev/null +++ b/Dictionary/JoinDefinition.php @@ -0,0 +1,26 @@ + [ + '=', '!=', 'contains', 'doesNotContain', 'beginsWith', 'endsWith', + 'in', 'notIn', 'null', 'notNull', + ], + FieldDefinition::TYPE_NUMBER => [ + '=', '!=', '<', '<=', '>', '>=', 'between', 'notBetween', 'in', 'notIn', 'null', 'notNull', + ], + FieldDefinition::TYPE_DATE => [ + '=', '!=', '<', '<=', '>', '>=', 'between', 'notBetween', 'null', 'notNull', + ], + FieldDefinition::TYPE_DATETIME => [ + '=', '!=', '<', '<=', '>', '>=', 'between', 'notBetween', 'null', 'notNull', + ], + FieldDefinition::TYPE_BOOLEAN => [ + '=', + ], + ]; + + + private function __construct() + { + } + + /** @return string[] */ + public static function forField(FieldDefinition $field): array + { + $allowed = self::BY_TYPE[$field->type] ?? self::BY_TYPE[FieldDefinition::TYPE_TEXT]; + + if ($field->operators === null) { + return $allowed; + } + + return array_values(array_intersect($field->operators, $allowed)); + } +} diff --git a/Discount/AppliedCartDiscount.php b/Discount/AppliedCartDiscount.php new file mode 100644 index 0000000..6760d76 --- /dev/null +++ b/Discount/AppliedCartDiscount.php @@ -0,0 +1,22 @@ += 100)) { + $rate = null; + } + + $freeShipping = (bool) ($parameters['cart_discount_free_shipping'] ?? false); + + if ($rate === null && !$freeShipping) { + return null; + } + + return new self(rate: $rate, freeShipping: $freeShipping, label: $label); + } +} diff --git a/Discount/CartLineDiscountPolicy.php b/Discount/CartLineDiscountPolicy.php new file mode 100644 index 0000000..1e9570b --- /dev/null +++ b/Discount/CartLineDiscountPolicy.php @@ -0,0 +1,46 @@ +cumulative && $catalog->inPromo() ? $catalog->promoPrice : $catalog->price; + $discountedPrice = round($base * (1 - $discount->rate / 100), 2); + + if (!$discount->cumulative && $catalog->inPromo() && $catalog->promoPrice <= $discountedPrice + self::PRICE_TOLERANCE) { + return null; + } + + return new LinePrices($catalog->price, $discountedPrice, 1); + } + + /** A promotion on the line that the catalog does not explain was written by a rule. */ + public function carriesRuleDiscount(LinePrices $line, LinePrices $catalog): bool + { + if (!$line->inPromo()) { + return false; + } + + return !$catalog->inPromo() || abs($line->promoPrice - $catalog->promoPrice) >= self::PRICE_TOLERANCE; + } + + public function sameLine(LinePrices $left, LinePrices $right): bool + { + return abs($left->price - $right->price) < self::PRICE_TOLERANCE + && abs($left->promoPrice - $right->promoPrice) < self::PRICE_TOLERANCE + && $left->promo === $right->promo; + } +} diff --git a/Discount/Discount.php b/Discount/Discount.php new file mode 100644 index 0000000..d6ced40 --- /dev/null +++ b/Discount/Discount.php @@ -0,0 +1,35 @@ +promo === 1; + } +} diff --git a/Enum/Context.php b/Enum/Context.php new file mode 100644 index 0000000..7124fdf --- /dev/null +++ b/Enum/Context.php @@ -0,0 +1,44 @@ + 'Global (all contexts)', + self::CATEGORY => 'Category', + self::BRAND => 'Brand', + self::PRODUCT => 'Product', + self::CART => 'Cart', + self::ORDER => 'Order', + self::CUSTOMER => 'Customer', + }; + } + + /** Translation key (querybuilder domain) of the help text shown in the back-office. */ + public function description(): string + { + return match ($this) { + self::GLOBAL_SCOPE => 'Rule not attached to an object: it can listen to the hooks of every context.', + self::CATEGORY => 'Rule evaluated on a catalog category.', + self::BRAND => 'Rule evaluated on a brand.', + self::PRODUCT => 'Rule evaluated on a product.', + self::CART => 'Rule evaluated on the cart of the current customer.', + self::ORDER => 'Rule evaluated on an order.', + self::CUSTOMER => 'Rule evaluated on the current (logged in) customer.', + }; + } +} diff --git a/Event/QueryBuilderRulesChangedEvent.php b/Event/QueryBuilderRulesChangedEvent.php new file mode 100644 index 0000000..0224ae3 --- /dev/null +++ b/Event/QueryBuilderRulesChangedEvent.php @@ -0,0 +1,17 @@ + ['updateCartDiscount', 1], + TheliaEvents::CART_UPDATEITEM => ['updateCartDiscount', 1], + TheliaEvents::CART_DELETEITEM => ['updateCartDiscount', 1], + TheliaEvents::CUSTOMER_LOGIN => ['updateCartDiscount', 1], + TheliaEvents::COUPON_CONSUME => ['updateCartDiscount', 1], + TheliaEvents::COUPON_CLEAR_ALL => ['updateCartDiscount', 1], + AddressEvent::POST_UPDATE => ['updateCartDiscount', 1], + TheliaEvents::CART_SET_POSTAGE => ['removeCartPostageWhenFreeShipping', 133], + TheliaEvents::ORDER_SET_POSTAGE => ['removeOrderPostageWhenFreeShipping', 133], + TheliaEvents::MODULE_DELIVERY_GET_OPTIONS => ['removeDeliveryOptionsPostageWhenFreeShipping', -128], + ]; + } + + public function updateCartDiscount(Event $event): void + { + try { + $session = $this->getStartedSession(); + + if ($session === null) { + return; + } + + //Sur les événements panier le cart est porté par l'événement ; sinon + //(login, adresse) le core vient de résoudre le panier de session à + //prio 10, le relire ici est sans risque de boucle de restauration + //Thelia 3: the session needs the dispatcher to restore or create the cart + $cart = $event instanceof CartEvent + ? $event->getCart() + : $session->getSessionCart($this->eventDispatcher); + + if (!$cart instanceof Cart) { + return; + } + + $ruleDiscountAmount = $this->cartDiscountCalculator->resolve($cart)?->amount; + + if ($ruleDiscountAmount === null || $ruleDiscountAmount <= 0) { + return; + } + + $totalDiscount = $this->cartDiscountLedger->nextTotal( + (int) $cart->getId(), + (float) $cart->getDiscount(), + $ruleDiscountAmount + ); + + //Propel decimal columns are typed string under Thelia 3 + $cart->setDiscount((string) $totalDiscount)->save(); + $session->getOrder()?->setDiscount((string) $totalDiscount); + } catch (\Throwable $throwable) { + //Ne jamais bloquer le panier pour une remise + Tlog::getInstance()->addError('QueryBuilder: cart discount not applied: ' . $throwable->getMessage()); + } + } + + /** Thelia 3 checkout: the postage is computed on the cart (Flexy, API). */ + public function removeCartPostageWhenFreeShipping(CartCheckoutEvent $event): void + { + try { + $cart = $event->getCart(); + + if (!$this->cartDiscountCalculator->resolve($cart)?->freeShipping) { + return; + } + + //Same write as Coupon::forceFreePostage: a cleared postage is a free delivery + $cart + ->setPostage(null) + ->setPostageTax(null) + ->setPostageTaxRuleTitle(null) + ->save(); + $event->stopPropagation(); + } catch (\Throwable $throwable) { + //Ne jamais bloquer la commande pour des frais de port offerts + Tlog::getInstance()->addError('QueryBuilder: free shipping rule not applied on the cart: ' . $throwable->getMessage()); + } + } + + /** Delivery step: the options a module lists are priced by the module, whatever the cart postage. */ + public function removeDeliveryOptionsPostageWhenFreeShipping(DeliveryModuleOptionEvent $event): void + { + try { + $cart = $event->getCart(); + + if (!$cart instanceof Cart || !$this->cartDiscountCalculator->resolve($cart)?->freeShipping) { + return; + } + + $this->deliveryModuleOptionPostageClearer->clear($event->getDeliveryModuleOptions()); + } catch (\Throwable $throwable) { + Tlog::getInstance()->addError('QueryBuilder: free shipping rule not applied on the delivery options: ' . $throwable->getMessage()); + } + } + + /** Legacy checkout: the postage is set on the order carried by the session. */ + public function removeOrderPostageWhenFreeShipping(OrderEvent $event): void + { + try { + $session = $this->getStartedSession(); + $cart = $session?->getSessionCart($this->eventDispatcher); + + if (!$cart instanceof Cart || !$this->cartDiscountCalculator->resolve($cart)?->freeShipping) { + return; + } + + $order = $event->getOrder(); + $order->setPostage('0'); + $event->setOrder($order); + $event->stopPropagation(); + } catch (\Throwable $throwable) { + Tlog::getInstance()->addError('QueryBuilder: free shipping rule not applied on the order: ' . $throwable->getMessage()); + } + } + + private function getStartedSession(): ?Session + { + $session = $this->requestStack->getCurrentRequest()?->getSession(); + + return $session instanceof Session && $session->isStarted() ? $session : null; + } +} diff --git a/EventListener/CartItemDiscountListener.php b/EventListener/CartItemDiscountListener.php new file mode 100644 index 0000000..8f33e4f --- /dev/null +++ b/EventListener/CartItemDiscountListener.php @@ -0,0 +1,70 @@ + ['applyProductDiscounts', self::PRIORITY], + TheliaEvents::CART_UPDATEITEM => ['applyProductDiscounts', self::PRIORITY], + TheliaEvents::CART_DELETEITEM => ['applyProductDiscounts', self::PRIORITY], + TheliaEvents::CUSTOMER_LOGIN => ['applyProductDiscounts', self::PRIORITY], + TheliaEvents::CHANGE_DEFAULT_CURRENCY => ['applyProductDiscounts', self::PRIORITY], + ]; + } + + public function applyProductDiscounts(Event $event): void + { + try { + $cart = $event instanceof CartEvent ? $event->getCart() : $this->getSessionCart(); + + if ($cart instanceof Cart) { + $this->cartItemDiscountApplier->apply($cart); + } + } catch (\Throwable $throwable) { + //Ne jamais bloquer le panier pour une remise + Tlog::getInstance()->addError('QueryBuilder: product discounts not applied on the cart lines: ' . $throwable->getMessage()); + } + } + + private function getSessionCart(): ?Cart + { + $session = $this->requestStack->getCurrentRequest()?->getSession(); + + if (!$session instanceof Session || !$session->isStarted()) { + return null; + } + + return $session->getSessionCart($this->eventDispatcher); + } +} diff --git a/EventListener/OrderListener.php b/EventListener/OrderListener.php new file mode 100644 index 0000000..4d550f9 --- /dev/null +++ b/EventListener/OrderListener.php @@ -0,0 +1,67 @@ + ['markSuggestionsPurchased', 10], + ]; + } + + public function markSuggestionsPurchased(OrderEvent $event): void + { + try { + $order = $event->getPlacedOrder() ?? $event->getOrder(); + + if (!$order instanceof Order || !$order->getCustomerId()) { + return; + } + + $productRefs = []; + foreach ($order->getOrderProducts() as $orderProduct) { + $productRefs[] = $orderProduct->getProductRef(); + } + + if ($productRefs === []) { + return; + } + + $productIds = ProductQuery::create() + ->filterByRef(array_unique($productRefs), Criteria::IN) + ->select('id') + ->find() + ->toArray(); + + $this->suggestionService->markPurchased( + (int) $order->getCustomerId(), + array_map('intval', $productIds) + ); + } catch (\Exception $exception) { + //Never break the checkout for suggestion bookkeeping + Tlog::getInstance()->addError('QueryBuilder: unable to mark purchased suggestions: ' . $exception->getMessage()); + } + } +} diff --git a/EventListener/ProductResourcePriceListener.php b/EventListener/ProductResourcePriceListener.php new file mode 100644 index 0000000..fe3925c --- /dev/null +++ b/EventListener/ProductResourcePriceListener.php @@ -0,0 +1,87 @@ + ['applyRuleDiscount', self::PRIORITY]]; + } + + public function applyRuleDiscount(ModelToResourceEvent $event): void + { + $resource = $event->getResource(); + + if (!$resource instanceof ProductSaleElementsResource || !FrontRead::isFrontRead($event->getContext())) { + return; + } + + $model = $resource->getPropelModel(); + $prices = $resource->getProductPrices(); + + if (!$model instanceof ProductSaleElementsModel || $model->getProductId() === null || $prices === []) { + return; + } + + try { + $productId = (int) $model->getProductId(); + $runtimeContext = $this->runtimeContextFactory->fromSession(); + $discounted = false; + + foreach ($prices as $price) { + if (!$price instanceof ProductPriceResource) { + continue; + } + + $discountedPrice = $this->productPriceDiscountResolver->resolve( + $runtimeContext, + $productId, + new LinePrices($price->getPrice(), $price->getPromoPrice(), $resource->getPromo() ? 1 : 0) + ); + + if ($discountedPrice === null) { + continue; + } + + $price->setPromoPrice($discountedPrice->prices->promoPrice); + $discounted = true; + } + + if ($discounted) { + $resource->setPromo(true); + } + } catch (\Throwable $throwable) { + //Never break a product read for a discount + Tlog::getInstance()->addError('QueryBuilder: product discount not shown on the product resource prices: ' . $throwable->getMessage()); + } + } +} diff --git a/EventListener/ProductSaleElementsPriceListener.php b/EventListener/ProductSaleElementsPriceListener.php new file mode 100644 index 0000000..eb33bab --- /dev/null +++ b/EventListener/ProductSaleElementsPriceListener.php @@ -0,0 +1,75 @@ + 'applyRuleDiscount']; + } + + public function applyRuleDiscount(PseByProductEvent $event): void + { + $productSaleElements = $event->getProductSaleElements(); + + //Another dispatcher of the event may not have priced the element + if (!$productSaleElements->hasVirtualColumn(self::PRICE_COLUMN) || !$productSaleElements->hasVirtualColumn(self::PROMO_PRICE_COLUMN)) { + return; + } + + try { + $productId = (int) $productSaleElements->getProductId(); + $catalog = new LinePrices( + $productSaleElements->getPrice(), + $productSaleElements->getPromoPrice(), + (int) $productSaleElements->getPromo() + ); + + $discountedPrice = $this->productPriceDiscountResolver->resolve( + $this->runtimeContextFactory->fromSession(productId: $productId), + $productId, + $catalog + ); + + if ($discountedPrice === null) { + return; + } + + $productSaleElements->setVirtualColumn(self::PROMO_PRICE_COLUMN, $discountedPrice->prices->promoPrice); + $productSaleElements->setPromo(1); + //Display only: nothing may persist the flag, and the pooled instance must not hand + //it to the next query of the request (the cart pricing reads the catalog flag) + $productSaleElements->resetModified(); + ProductSaleElementsTableMap::removeInstanceFromPool($productSaleElements); + } catch (\Throwable $throwable) { + //Never break the product page for a discount + Tlog::getInstance()->addError('QueryBuilder: product discount not shown on the product page prices: ' . $throwable->getMessage()); + } + } +} diff --git a/Form/ActionForm.php b/Form/ActionForm.php new file mode 100644 index 0000000..7935acb --- /dev/null +++ b/Form/ActionForm.php @@ -0,0 +1,93 @@ +getRequest()->getLocale(); + + $this->formBuilder + ->add('name', TextType::class, [ + 'constraints' => [new NotBlank()], + 'label' => 'Name', + ]) + ->add('description', TextareaType::class, [ + 'required' => false, + 'label' => 'Description', + ]) + ->add('code', TextType::class, [ + 'constraints' => [new NotBlank()], + 'label' => 'Action', + ]) + ->add('limit', IntegerType::class, [ + 'required' => false, + 'label' => 'Number of products', + 'constraints' => [new Range(min: 1, max: 50)], + ]) + ->add('persist_days', IntegerType::class, [ + 'required' => false, + 'label' => 'Suggestions persistence (days)', + 'constraints' => [new Range(min: 1, max: 365)], + ]) + ->add('discount_rate', NumberType::class, [ + 'required' => false, + 'label' => 'Discount rate (%)', + 'constraints' => [new Range(min: 0.01, max: 99.99)], + ]) + ->add('discount_label', TextType::class, [ + 'required' => false, + 'label' => 'Discount label', + ]) + ->add('discount_cumulative', CheckboxType::class, [ + 'required' => false, + 'label' => 'Stackable with an existing promotion', + ]) + ->add('cart_discount_rate', NumberType::class, [ + 'required' => false, + 'label' => 'Cart discount rate (%)', + 'constraints' => [new Range(min: 0.01, max: 99.99)], + ]) + ->add('cart_discount_free_shipping', CheckboxType::class, [ + 'required' => false, + 'label' => 'Free shipping', + ]) + ->add('condition_tree', QueryBuilderType::class, [ + 'required' => false, + 'label' => false, + 'processor' => QueryBuilderProcessor::Native, + //Every dictionary field, the rule context is enforced by SqlBuilder::validateTree() on save + 'fields' => $this->fieldsBuilder->buildForContext(null, $locale), + 'lang' => $locale, + ]) + ->add('activate', CheckboxType::class, [ + 'required' => false, + 'label' => 'Active', + ]); + } +} diff --git a/Form/RuleForm.php b/Form/RuleForm.php new file mode 100644 index 0000000..d422fd9 --- /dev/null +++ b/Form/RuleForm.php @@ -0,0 +1,59 @@ +getRequest()->getLocale(); + + $this->formBuilder + ->add('name', TextType::class, [ + 'constraints' => [new NotBlank()], + 'label' => 'Name', + ]) + ->add('description', TextareaType::class, [ + 'required' => false, + 'label' => 'Description', + ]) + ->add('context', TextType::class, [ + 'constraints' => [new NotBlank()], + 'label' => 'Context', + ]) + ->add('condition_tree', QueryBuilderType::class, [ + 'required' => false, + 'label' => false, + 'processor' => QueryBuilderProcessor::Native, + //Every dictionary field: the editor may switch context before submitting, the + //context restriction is enforced by SqlBuilder::validateTree() on save + 'fields' => $this->fieldsBuilder->buildForContext(null, $locale), + 'lang' => $locale, + ]) + ->add('activate', CheckboxType::class, [ + 'required' => false, + 'label' => 'Active', + ]); + } +} diff --git a/Hook/BackHook.php b/Hook/BackHook.php new file mode 100644 index 0000000..edba022 --- /dev/null +++ b/Hook/BackHook.php @@ -0,0 +1,92 @@ + [ + ['type' => 'back', 'method' => 'onMainTopMenuTools'], + ], + 'main.head-css' => [ + ['type' => 'back', 'method' => 'onMainHeadCss'], + ], + 'main.footer-js' => [ + ['type' => 'back', 'method' => 'onMainFooterJs'], + ], + ]; + } + + public function onMainTopMenuTools(HookRenderBlockEvent $event): void + { + //The controllers answer 403 to an administrator without the module right: no entry for them. + //The right is the one granted on the module itself, not the "admin.module" resource, which + //guards the module management screens and is not held by a profile limited to this module. + if (!$this->securityContext->isGranted(['ADMIN'], [], [QueryBuilder::getModuleCode()], [AccessManager::VIEW])) { + return; + } + + $event->add([ + 'id' => 'tools_menu_query_builder', + 'class' => '', + 'url' => URL::getInstance()->absoluteUrl(self::ADMIN_PATH_PREFIX), + 'title' => $this->trans('Query Builder', [], QueryBuilder::DOMAIN_NAME), + ]); + } + + public function onMainHeadCss(HookRenderEvent $event): void + { + if (!$this->isEditorPage()) { + return; + } + + $event->add($this->addCSS('assets/dist/query-builder-admin.css')); + } + + public function onMainFooterJs(HookRenderEvent $event): void + { + if (!$this->isEditorPage()) { + return; + } + + $event->add($this->addJS('assets/dist/query-builder-admin.js', ['defer' => 'defer'])); + } + + private function isEditorPage(): bool + { + return str_starts_with($this->getRequest()?->getPathInfo() ?? '', self::EDITOR_PATH_PREFIX); + } +} diff --git a/Hook/CartDiscountThemeHook.php b/Hook/CartDiscountThemeHook.php new file mode 100644 index 0000000..5e48070 --- /dev/null +++ b/Hook/CartDiscountThemeHook.php @@ -0,0 +1,126 @@ +rendered) { + return ''; + } + + $session = $this->requestStack->getCurrentRequest()?->getSession(); + + if (!$session instanceof Session || !$session->isStarted()) { + return ''; + } + + $cart = $session->getSessionCart($this->eventDispatcher); + $locale = $session->getLang()?->getLocale() ?? 'en_US'; + $applied = $this->cartDiscountCalculator->resolve($cart); + $hasCartDiscount = $applied !== null && (($applied->amount !== null && $applied->amount > 0) || $applied->freeShipping); + $lineDiscounts = $this->lineDiscounts($cart, $locale); + + if (!$hasCartDiscount && $lineDiscounts === []) { + return ''; + } + + $this->rendered = true; + + return $this->frontTemplateRenderer->render('cart-discount.html.twig', [ + 'hook' => $hookName, + 'label' => $hasCartDiscount ? $applied->label : null, + 'rate' => $hasCartDiscount ? $applied->rate : null, + 'amount' => $hasCartDiscount ? $applied->amount : null, + 'free_shipping' => $hasCartDiscount && $applied->freeShipping, + 'currency_code' => $cart->getCurrency()?->getCode(), + 'locale' => $locale, + 'line_discounts' => $lineDiscounts, + 'texts' => [ + 'discount' => $this->trans('Discount'), + 'rate_off' => $this->trans('%rate%% off the products total', ['%rate%' => (string) ($applied?->rate ?? 0)]), + 'free_shipping' => $this->trans('Free shipping'), + ], + ]); + } + + /** + * The labelled product discounts charged on the cart lines, one entry per + * label with the titles of the products it applies to. + * + * @return array + */ + private function lineDiscounts(Cart $cart, string $locale): array + { + $byLabel = []; + + foreach ($cart->getCartItems() as $cartItem) { + try { + $discount = $this->cartLineDiscountResolver->resolve($cartItem); + + if ($discount === null || $discount->label === null) { + continue; + } + + $byLabel[$discount->label] ??= ['label' => $discount->label, 'rate' => $discount->rate, 'products' => []]; + $title = (string) $cartItem->getProduct()->setLocale($locale)->getTitle(); + + if ($title !== '' && !\in_array($title, $byLabel[$discount->label]['products'], true)) { + $byLabel[$discount->label]['products'][] = $title; + } + } catch (\Throwable $throwable) { + //Never break the cart for a label + Tlog::getInstance()->addError('QueryBuilder: discount label not shown for a cart line: ' . $throwable->getMessage()); + } + } + + return array_values($byLabel); + } + + private function trans(string $id, array $parameters = []): string + { + return Translator::getInstance()->trans($id, $parameters, QueryBuilder::DOMAIN_NAME); + } +} diff --git a/Hook/FrontThemeHook.php b/Hook/FrontThemeHook.php new file mode 100644 index 0000000..f0f1292 --- /dev/null +++ b/Hook/FrontThemeHook.php @@ -0,0 +1,94 @@ +dataDictionary->getContextForHook($hookName) !== null; + } + + public function render(string $hookName, array $parameters): string + { + $runtimeContext = $this->runtimeContextFactory->fromSession( + productId: self::identifier($parameters['product'] ?? null), + categoryId: self::identifier($parameters['category'] ?? null), + brandId: self::identifier($parameters['brand'] ?? null), + ); + + //Same aggregation as the JSON endpoint and the Twig function: the presenter + //dedupes and caps the shared hook slot across rules. Offers are skipped, this + //path only renders product lists + $result = $this->hookResultPresenter->present($hookName, $runtimeContext, withOffers: false); + + $html = ''; + + foreach ($result['actions'] as $action) { + if ($action['product_ids'] === []) { + continue; + } + + $html .= $this->frontTemplateRenderer->render('product-list.html.twig', [ + 'hook' => $hookName, + 'rule_name' => (string) $action['rule'], + 'action_name' => (string) $action['action'], + 'product_ids' => array_values(array_map('intval', $action['product_ids'])), + ]); + } + + return $html; + } + + /** The theme hands over a resource array, an object or a bare id: only the id matters here. */ + private static function identifier(mixed $value): ?int + { + if (\is_int($value)) { + return $value > 0 ? $value : null; + } + + if (\is_string($value) && ctype_digit($value)) { + return self::identifier((int) $value); + } + + if (\is_array($value)) { + return self::identifier($value['id'] ?? null); + } + + if (\is_object($value)) { + if (method_exists($value, 'getId')) { + return self::identifier($value->getId()); + } + + if (property_exists($value, 'id')) { + return self::identifier($value->id); + } + } + + return null; + } +} diff --git a/I18n/backOffice/default-twig/en_US.php b/I18n/backOffice/default-twig/en_US.php new file mode 100644 index 0000000..3f0de0d --- /dev/null +++ b/I18n/backOffice/default-twig/en_US.php @@ -0,0 +1,77 @@ + 'A rule runs when one of its hooks is called, if its trigger conditions are true. It then produces its actions.', + 'Action' => 'Action', + 'Actions' => 'Actions', + 'Actions of the rule' => 'Actions of the rule', + 'Active' => 'Active', + 'Active action' => 'Active action', + 'Active rule' => 'Active rule', + 'Add a rule' => 'Add a rule', + 'Add an action' => 'Add an action', + 'An inactive rule is never evaluated.' => 'An inactive rule is never evaluated.', + 'Back to the list' => 'Back to the list', + 'Back to the rule' => 'Back to the rule', + 'Changing the context drops the conditions on fields the new context does not offer.' => 'Changing the context drops the conditions on fields the new context does not offer.', + 'Check all' => 'Check all', + 'Click to activate' => 'Click to activate', + 'Click to deactivate' => 'Click to deactivate', + 'Context' => 'Context', + 'Delete' => 'Delete', + 'Delete this action?' => 'Delete this action?', + 'Delete this rule and its actions?' => 'Delete this rule and its actions?', + 'Description' => 'Description', + 'Discount on the selected products' => 'Discount on the selected products', + 'Edit' => 'Edit', + 'Free shipping' => 'Free shipping', + 'Global cart discount' => 'Global cart discount', + 'Hooks' => 'Hooks', + 'Identification' => 'Identification', + 'Inactive' => 'Inactive', + 'Internal use: this information never appears to the customer.' => 'Internal use: this information never appears to the customer.', + 'Label shown to the customer' => 'Label shown to the customer', + 'Name' => 'Name', + 'Name of the new action' => 'Name of the new action', + 'Name of the new rule' => 'Name of the new rule', + 'No action: the rule produces nothing for now.' => 'No action: the rule produces nothing for now.', + 'No condition: add one to restrict the trigger, otherwise the rule always applies.' => 'No condition: add one to restrict the trigger, otherwise the rule always applies.', + 'No criterion: the action applies to the whole catalog.' => 'No criterion: the action applies to the whole catalog.', + 'No hook checked: the rule only runs through the API.' => 'No hook checked: the rule only runs through the API.', + 'No hook declared for this context in the dictionary.' => 'No hook declared for this context in the dictionary.', + 'No rule yet.' => 'No rule yet.', + 'Number of products' => 'Number of products', + 'Optional. Without a condition, the rule triggers on every call of its hooks.' => 'Optional. Without a condition, the rule triggers on every call of its hooks.', + 'Percentage off the cart products total (shipping excluded), as long as the rule is active, for every eligible customer. Eligibility is carried by the rule conditions.' => 'Percentage off the cart products total (shipping excluded), as long as the rule is active, for every eligible customer. Eligibility is carried by the rule conditions.', + 'Predefined action' => 'Predefined action', + 'Product selection of the action' => 'Product selection of the action', + 'Query Builder' => 'Query Builder', + 'Rate (%)' => 'Rate (%)', + 'Discount rate: %rate% %' => 'Discount rate: %rate% %', + 'Reading of the action' => 'Reading of the action', + 'Reading of the condition' => 'Reading of the condition', + 'Reading of the selection' => 'Reading of the selection', + 'Rule' => 'Rule', + 'Rule name' => 'Rule name', + 'Rules' => 'Rules', + 'Save the action' => 'Save the action', + 'Save the rule' => 'Save the rule', + 'Stackable with an existing promotion' => 'Stackable with an existing promotion', + 'Status' => 'Status', + 'Suggestions persistence (days)' => 'Suggestions persistence (days)', + 'The actions below are saved separately.' => 'The actions below are saved separately.', + 'The context sets the evaluated object, the available fields and the possible hooks.' => 'The context sets the evaluated object, the available fields and the possible hooks.', + 'The discount holds as long as the rule is active, for every eligible customer. Not stackable: it only applies when better than the current promotion.' => 'The discount holds as long as the rule is active, for every eligible customer. Not stackable: it only applies when better than the current promotion.', + 'The same products are served again for N days or until purchased.' => 'The same products are served again for N days or until purchased.', + 'Trigger conditions' => 'Trigger conditions', + 'Trigger hooks' => 'Trigger hooks', + 'Type' => 'Type', + 'Uncheck all' => 'Uncheck all', + 'What the rule produces when it triggers: each action has its own product selection and parameters.' => 'What the rule produces when it triggers: each action has its own product selection and parameters.', + 'What this rule is for, for whom, until when…' => 'What this rule is for, for whom, until when…', + 'When is the rule evaluated?' => 'When is the rule evaluated?', + 'context' => 'context', + 'empty = no persistence' => 'empty = no persistence', + 'rule' => 'rule', +]; diff --git a/I18n/backOffice/default-twig/fr_FR.php b/I18n/backOffice/default-twig/fr_FR.php new file mode 100644 index 0000000..5977ea6 --- /dev/null +++ b/I18n/backOffice/default-twig/fr_FR.php @@ -0,0 +1,77 @@ + "Une règle s'exécute quand un de ses hooks est appelé, si ses conditions de déclenchement sont vraies. Elle produit ensuite ses actions.", + 'Action' => 'Action', + 'Actions' => 'Actions', + 'Actions of the rule' => 'Actions de la règle', + 'Active' => 'Active', + 'Active action' => 'Action active', + 'Active rule' => 'Règle active', + 'Add a rule' => 'Ajouter une règle', + 'Add an action' => 'Ajouter une action', + 'An inactive rule is never evaluated.' => "Une règle inactive n'est jamais évaluée.", + 'Back to the list' => 'Retour à la liste', + 'Back to the rule' => 'Retour à la règle', + 'Changing the context drops the conditions on fields the new context does not offer.' => 'Changer de contexte supprime les conditions portant sur des champs que le nouveau contexte ne propose pas.', + 'Check all' => 'Tout cocher', + 'Click to activate' => 'Cliquer pour activer', + 'Click to deactivate' => 'Cliquer pour désactiver', + 'Context' => 'Contexte', + 'Delete' => 'Supprimer', + 'Delete this action?' => 'Supprimer cette action ?', + 'Delete this rule and its actions?' => 'Supprimer cette règle et ses actions ?', + 'Description' => 'Description', + 'Discount on the selected products' => 'Remise sur les produits sélectionnés', + 'Edit' => 'Modifier', + 'Free shipping' => 'Frais de port offerts', + 'Global cart discount' => 'Remise globale panier', + 'Hooks' => 'Hooks', + 'Identification' => 'Identification', + 'Inactive' => 'Inactive', + 'Internal use: this information never appears to the customer.' => "À usage interne : ces informations n'apparaissent jamais côté client.", + 'Label shown to the customer' => 'Libellé affiché au client', + 'Name' => 'Nom', + 'Name of the new action' => 'Nom de la nouvelle action', + 'Name of the new rule' => 'Nom de la nouvelle règle', + 'No action: the rule produces nothing for now.' => 'Aucune action : la règle ne produit rien pour le moment.', + 'No condition: add one to restrict the trigger, otherwise the rule always applies.' => "Aucune condition : ajoutez-en une pour restreindre le déclenchement, sinon la règle s'applique toujours.", + 'No criterion: the action applies to the whole catalog.' => "Aucun critère : l'action porte sur tout le catalogue.", + 'No hook checked: the rule only runs through the API.' => "Aucun hook coché : la règle ne s'exécute que via l'API.", + 'No hook declared for this context in the dictionary.' => 'Aucun hook déclaré pour ce contexte dans le dictionnaire.', + 'No rule yet.' => 'Aucune règle pour le moment.', + 'Number of products' => 'Nombre de produits', + 'Optional. Without a condition, the rule triggers on every call of its hooks.' => 'Optionnel. Sans condition, la règle se déclenche à chaque appel de ses hooks.', + 'Percentage off the cart products total (shipping excluded), as long as the rule is active, for every eligible customer. Eligibility is carried by the rule conditions.' => "Remise en pourcentage sur le total des produits du panier (port exclu), tant que la règle est active, pour tout client éligible. L'éligibilité est portée par les conditions de la règle.", + 'Predefined action' => 'Action prédéfinie', + 'Product selection of the action' => "Sélection des produits de l'action", + 'Query Builder' => 'Query Builder', + 'Rate (%)' => 'Taux (%)', + 'Discount rate: %rate% %' => 'Taux de remise : %rate% %', + 'Reading of the action' => "Lecture de l'action", + 'Reading of the condition' => 'Lecture de la condition', + 'Reading of the selection' => 'Lecture de la sélection', + 'Rule' => 'Règle', + 'Rule name' => 'Nom de la règle', + 'Rules' => 'Règles', + 'Save the action' => "Enregistrer l'action", + 'Save the rule' => 'Enregistrer la règle', + 'Stackable with an existing promotion' => 'Cumulable avec une promotion déjà appliquée', + 'Status' => 'Statut', + 'Suggestions persistence (days)' => 'Persistance des suggestions (jours)', + 'The actions below are saved separately.' => 'Les actions ci-dessous sont enregistrées séparément.', + 'The context sets the evaluated object, the available fields and the possible hooks.' => "Le contexte détermine l'objet évalué, les champs disponibles et les hooks possibles.", + 'The discount holds as long as the rule is active, for every eligible customer. Not stackable: it only applies when better than the current promotion.' => "La remise vaut tant que la règle est active, pour tout client éligible. Non cumulable : elle ne s'applique que si elle est plus avantageuse que la promotion en place.", + 'The same products are served again for N days or until purchased.' => "Les mêmes produits sont resservis N jours ou jusqu'à leur achat.", + 'Trigger conditions' => 'Conditions de déclenchement', + 'Trigger hooks' => 'Hooks déclencheurs', + 'Type' => 'Type', + 'Uncheck all' => 'Tout décocher', + 'What the rule produces when it triggers: each action has its own product selection and parameters.' => 'Ce que la règle produit quand elle se déclenche : chaque action a sa propre sélection de produits et ses paramètres.', + 'What this rule is for, for whom, until when…' => "À quoi sert cette règle, pour qui, jusqu'à quand…", + 'When is the rule evaluated?' => 'Quand la règle est-elle évaluée ?', + 'context' => 'contexte', + 'empty = no persistence' => 'vide = pas de persistance', + 'rule' => 'règle', +]; diff --git a/I18n/en_US.php b/I18n/en_US.php new file mode 100644 index 0000000..bd07164 --- /dev/null +++ b/I18n/en_US.php @@ -0,0 +1,129 @@ + '%checked% checked out of %total%', + '%count% conditions' => '%count% conditions', + '%rate%% off the products total' => '%rate%% off the products total', + '1 condition' => '1 condition', + 'A cart discount needs a rate above 0 or free shipping.' => 'A cart discount needs a rate above 0 or free shipping.', + 'A discount action needs a discount rate above 0.' => 'A discount action needs a discount rate above 0.', + 'AND' => 'AND', + 'Accessory of a product of the cart' => 'Accessory of a product of the cart', + 'Accessory of the context product' => 'Accessory of the context product', + 'All of the following conditions' => 'All of the following conditions', + 'An unexpected error occurred, please check the logs.' => 'An unexpected error occurred, please check the logs.', + 'Apply a product discount' => 'Apply a product discount', + 'At least one of the following conditions' => 'At least one of the following conditions', + 'Brand' => 'Brand', + 'Brand (ID)' => 'Brand (ID)', + 'Brand already bought by the customer' => 'Brand already bought by the customer', + 'Brand page - bottom' => 'Brand page - bottom', + 'Brand page - top' => 'Brand page - top', + 'Cart' => 'Cart', + 'Cart - bottom' => 'Cart - bottom', + 'Cart - top' => 'Cart - top', + 'Cart contains a product of the brand (list)' => 'Cart contains a product of the brand (list)', + 'Cart contains a product of the category (list)' => 'Cart contains a product of the category (list)', + 'Cart contains a promotional line' => 'Cart contains a promotional line', + 'Cart products total (taxes included)' => 'Cart products total (taxes included)', + 'Category' => 'Category', + 'Category (ID)' => 'Category (ID)', + 'Category already bought by the customer' => 'Category already bought by the customer', + 'Category page - bottom' => 'Category page - bottom', + 'Category page - top' => 'Category page - top', + 'Changing the context drops the conditions on fields the new context does not offer. Continue?' => 'Changing the context drops the conditions on fields the new context does not offer. Continue?', + 'Context' => 'Context', + 'Context brand (list)' => 'Context brand (list)', + 'Context category (list)' => 'Context category (list)', + 'Customer' => 'Customer', + 'Customer account - bottom' => 'Customer account - bottom', + 'Customer account - top' => 'Customer account - top', + 'Customer discount rate (%)' => 'Customer discount rate (%)', + 'Customer is a reseller' => 'Customer is a reseller', + 'Customer seniority (days)' => 'Customer seniority (days)', + 'Customer subscribed to the newsletter' => 'Customer subscribed to the newsletter', + 'Days since the last order of the customer' => 'Days since the last order of the customer', + 'Delivery' => 'Delivery', + 'Delivery country' => 'Delivery country', + 'Discount' => 'Discount', + 'Display a list of products' => 'Display a list of products', + 'Free shipping' => 'Free shipping', + 'Global (all contexts)' => 'Global (all contexts)', + 'Global cart discount' => 'Global cart discount', + 'Home page - bottom' => 'Home page - bottom', + 'Home page - top' => 'Home page - top', + 'Is the product of the current page' => 'Is the product of the current page', + 'Main category (list)' => 'Main category (list)', + 'None of the following conditions' => 'None of the following conditions', + 'Not "at least one" of the following conditions' => 'Not "at least one" of the following conditions', + 'Number of items in the cart' => 'Number of items in the cart', + 'Number of lines in the cart' => 'Number of lines in the cart', + 'Number of orders of the customer' => 'Number of orders of the customer', + 'OR' => 'OR', + 'Order' => 'Order', + 'Parent of the context category (list)' => 'Parent of the context category (list)', + 'Product' => 'Product', + 'Product already bought by the customer' => 'Product already bought by the customer', + 'Product bought by the customer in the last 3 months' => 'Product bought by the customer in the last 3 months', + 'Product brand' => 'Product brand', + 'Product category title' => 'Product category title', + 'Product creation date' => 'Product creation date', + 'Product flagged as new' => 'Product flagged as new', + 'Product in stock' => 'Product in stock', + 'Product in the customer cart' => 'Product in the customer cart', + 'Product of the context brand' => 'Product of the context brand', + 'Product of the context category' => 'Product of the context category', + 'Product of the context category or its sub-categories' => 'Product of the context category or its sub-categories', + 'Product on catalog promotion' => 'Product on catalog promotion', + 'Product page - below the details' => 'Product page - below the details', + 'Product page - bottom' => 'Product page - bottom', + 'Product page - top' => 'Product page - top', + 'Product price' => 'Product price', + 'Product reference' => 'Product reference', + 'Product title' => 'Product title', + 'Product visible' => 'Product visible', + 'Quantity of the product in the cart' => 'Quantity of the product in the cart', + 'Query Builder' => 'Query Builder', + 'Rule evaluated on a brand.' => 'Rule evaluated on a brand.', + 'Rule evaluated on a catalog category.' => 'Rule evaluated on a catalog category.', + 'Rule evaluated on a product.' => 'Rule evaluated on a product.', + 'Rule evaluated on an order.' => 'Rule evaluated on an order.', + 'Rule evaluated on the cart of the current customer.' => 'Rule evaluated on the cart of the current customer.', + 'Rule evaluated on the current (logged in) customer.' => 'Rule evaluated on the current (logged in) customer.', + 'Rule not attached to an object: it can listen to the hooks of every context.' => 'Rule not attached to an object: it can listen to the hooks of every context.', + 'Same brand as a product of the cart' => 'Same brand as a product of the cart', + 'Same brand as the context product' => 'Same brand as the context product', + 'Same main category as the context product' => 'Same main category as the context product', + 'Same template as the context product' => 'Same template as the context product', + 'Same value as the context product for the feature (list)' => 'Same value as the context product for the feature (list)', + 'Shares a category with a product of the cart' => 'Shares a category with a product of the cart', + 'Shares a category with the context product' => 'Shares a category with the context product', + 'The action has been saved.' => 'The action has been saved.', + 'The product is in the cart' => 'The product is in the cart', + 'The rule has been saved.' => 'The rule has been saved.', + 'and' => 'and', + 'begins with' => 'begins with', + 'contains' => 'contains', + 'does not contain' => 'does not contain', + 'ends with' => 'ends with', + 'field unavailable in this context' => 'field unavailable in this context', + 'is between' => 'is between', + 'is empty' => 'is empty', + 'is equal to' => 'is equal to', + 'is greater than' => 'is greater than', + 'is greater than or equal to' => 'is greater than or equal to', + 'is less than' => 'is less than', + 'is less than or equal to' => 'is less than or equal to', + 'is none of' => 'is none of', + 'is not between' => 'is not between', + 'is not empty' => 'is not empty', + 'is not equal to' => 'is not equal to', + 'is one of' => 'is one of', + 'no' => 'no', + 'no condition' => 'no condition', + 'or' => 'or', + 'value to fill in' => 'value to fill in', + 'yes' => 'yes', +]; diff --git a/I18n/fr_FR.php b/I18n/fr_FR.php new file mode 100644 index 0000000..ec6b706 --- /dev/null +++ b/I18n/fr_FR.php @@ -0,0 +1,129 @@ + '%checked% coché(s) sur %total%', + '%count% conditions' => '%count% conditions', + '%rate%% off the products total' => '%rate% % sur le total des produits', + '1 condition' => '1 condition', + 'A cart discount needs a rate above 0 or free shipping.' => 'Une remise panier nécessite un taux supérieur à 0 ou les frais de port offerts.', + 'A discount action needs a discount rate above 0.' => 'Une action de remise nécessite un taux de remise supérieur à 0.', + 'AND' => 'ET', + 'Accessory of a product of the cart' => 'Accessoire d’un produit du panier', + 'Accessory of the context product' => 'Accessoire du produit du contexte', + 'All of the following conditions' => 'Toutes les conditions suivantes', + 'An unexpected error occurred, please check the logs.' => 'Une erreur inattendue est survenue, consultez les journaux.', + 'Apply a product discount' => 'Appliquer une remise produit', + 'At least one of the following conditions' => 'Au moins une des conditions suivantes', + 'Brand' => 'Marque', + 'Brand (ID)' => 'Marque (ID)', + 'Brand already bought by the customer' => 'Marque déjà achetée par le client', + 'Brand page - bottom' => 'Page marque - bas', + 'Brand page - top' => 'Page marque - haut', + 'Cart' => 'Panier', + 'Cart - bottom' => 'Panier - bas', + 'Cart - top' => 'Panier - haut', + 'Cart contains a product of the brand (list)' => 'Le panier contient un produit de la marque (liste)', + 'Cart contains a product of the category (list)' => 'Le panier contient un produit de la catégorie (liste)', + 'Cart contains a promotional line' => 'Le panier contient une ligne en promotion', + 'Cart products total (taxes included)' => 'Montant des produits du panier (TTC)', + 'Category' => 'Catégorie', + 'Category (ID)' => 'Catégorie (ID)', + 'Category already bought by the customer' => 'Catégorie déjà achetée par le client', + 'Category page - bottom' => 'Page catégorie - bas', + 'Category page - top' => 'Page catégorie - haut', + 'Changing the context drops the conditions on fields the new context does not offer. Continue?' => 'Changer de contexte supprime les conditions portant sur des champs que le nouveau contexte ne propose pas. Continuer ?', + 'Context' => 'Contexte', + 'Context brand (list)' => 'Marque du contexte (liste)', + 'Context category (list)' => 'Catégorie du contexte (liste)', + 'Customer' => 'Client', + 'Customer account - bottom' => 'Espace client - bas', + 'Customer account - top' => 'Espace client - haut', + 'Customer discount rate (%)' => 'Taux de remise du client (%)', + 'Customer is a reseller' => 'Le client est un revendeur', + 'Customer seniority (days)' => 'Ancienneté du client (jours)', + 'Customer subscribed to the newsletter' => 'Le client est abonné à la newsletter', + 'Days since the last order of the customer' => 'Jours depuis la dernière commande du client', + 'Delivery' => 'Livraison', + 'Delivery country' => 'Pays de livraison', + 'Discount' => 'Remise', + 'Display a list of products' => 'Afficher une liste de produits', + 'Free shipping' => 'Frais de port offerts', + 'Global (all contexts)' => 'Global (tous les contextes)', + 'Global cart discount' => 'Remise globale panier', + 'Home page - bottom' => "Page d'accueil - bas", + 'Home page - top' => "Page d'accueil - haut", + 'Is the product of the current page' => 'Est le produit de la fiche courante', + 'Main category (list)' => 'Catégorie principale (liste)', + 'None of the following conditions' => 'Aucune des conditions suivantes', + 'Not "at least one" of the following conditions' => 'Pas « au moins une » des conditions suivantes', + 'Number of items in the cart' => 'Nombre d’articles dans le panier', + 'Number of lines in the cart' => 'Nombre de lignes dans le panier', + 'Number of orders of the customer' => 'Nombre de commandes du client', + 'OR' => 'OU', + 'Order' => 'Commande', + 'Parent of the context category (list)' => 'Parente de la catégorie du contexte (liste)', + 'Product' => 'Produit', + 'Product already bought by the customer' => 'Produit déjà acheté par le client', + 'Product bought by the customer in the last 3 months' => 'Produit acheté par le client au cours des 3 derniers mois', + 'Product brand' => 'Marque du produit', + 'Product category title' => 'Titre de la catégorie du produit', + 'Product creation date' => 'Date de création du produit', + 'Product flagged as new' => 'Produit marqué comme nouveauté', + 'Product in stock' => 'Produit en stock', + 'Product in the customer cart' => 'Produit présent dans le panier du client', + 'Product of the context brand' => 'Produit de la marque du contexte', + 'Product of the context category' => 'Produit de la catégorie du contexte', + 'Product of the context category or its sub-categories' => 'Produit de la catégorie du contexte ou de ses sous-catégories', + 'Product on catalog promotion' => 'Produit en promotion catalogue', + 'Product page - below the details' => 'Fiche produit - sous les détails', + 'Product page - bottom' => 'Fiche produit - bas', + 'Product page - top' => 'Fiche produit - haut', + 'Product price' => 'Prix du produit', + 'Product reference' => 'Référence du produit', + 'Product title' => 'Titre du produit', + 'Product visible' => 'Produit visible', + 'Quantity of the product in the cart' => 'Quantité du produit dans le panier', + 'Query Builder' => 'Query Builder', + 'Rule evaluated on a brand.' => 'Règle évaluée sur une marque.', + 'Rule evaluated on a catalog category.' => 'Règle évaluée sur une catégorie du catalogue.', + 'Rule evaluated on a product.' => 'Règle évaluée sur un produit.', + 'Rule evaluated on an order.' => 'Règle évaluée sur une commande.', + 'Rule evaluated on the cart of the current customer.' => 'Règle évaluée sur le panier du client courant.', + 'Rule evaluated on the current (logged in) customer.' => 'Règle évaluée sur le client courant (connecté).', + 'Rule not attached to an object: it can listen to the hooks of every context.' => 'Règle non rattachée à un objet : elle peut écouter les hooks de tous les contextes.', + 'Same brand as a product of the cart' => 'Même marque qu’un produit du panier', + 'Same brand as the context product' => 'Même marque que le produit du contexte', + 'Same main category as the context product' => 'Même catégorie principale que le produit du contexte', + 'Same template as the context product' => 'Même gabarit que le produit du contexte', + 'Same value as the context product for the feature (list)' => 'Même valeur que le produit du contexte pour la caractéristique (liste)', + 'Shares a category with a product of the cart' => 'Partage une catégorie avec un produit du panier', + 'Shares a category with the context product' => 'Partage une catégorie avec le produit du contexte', + 'The action has been saved.' => "L'action a été enregistrée.", + 'The product is in the cart' => 'Le produit est dans le panier', + 'The rule has been saved.' => 'La règle a été enregistrée.', + 'and' => 'et', + 'begins with' => 'commence par', + 'contains' => 'contient', + 'does not contain' => 'ne contient pas', + 'ends with' => 'finit par', + 'field unavailable in this context' => 'champ indisponible dans ce contexte', + 'is between' => 'est entre', + 'is empty' => 'est vide', + 'is equal to' => 'est égal à', + 'is greater than' => 'est supérieur à', + 'is greater than or equal to' => 'est supérieur ou égal à', + 'is less than' => 'est inférieur à', + 'is less than or equal to' => 'est inférieur ou égal à', + 'is none of' => "n'est pas parmi", + 'is not between' => "n'est pas entre", + 'is not empty' => "n'est pas vide", + 'is not equal to' => "n'est pas égal à", + 'is one of' => 'est parmi', + 'no' => 'non', + 'no condition' => 'aucune condition', + 'or' => 'ou', + 'value to fill in' => 'valeur à renseigner', + 'yes' => 'oui', +]; diff --git a/Model/JsonColumnTrait.php b/Model/JsonColumnTrait.php new file mode 100644 index 0000000..13aaf56 --- /dev/null +++ b/Model/JsonColumnTrait.php @@ -0,0 +1,32 @@ +error('QueryBuilder: invalid JSON column', [ + 'column' => $columnDescription, + 'error' => $exception->getMessage(), + ]); + + return null; + } + } +} diff --git a/Model/QueryBuilderAction.php b/Model/QueryBuilderAction.php new file mode 100644 index 0000000..0c5ac15 --- /dev/null +++ b/Model/QueryBuilderAction.php @@ -0,0 +1,26 @@ +decodeJsonColumn($this->getConditionTree(), sprintf('query_builder_action#%d.condition_tree', (int) $this->getId())); + + return \is_array($tree) ? $tree : null; + } + + public function getParametersArray(): array + { + $parameters = $this->decodeJsonColumn($this->getParameters(), sprintf('query_builder_action#%d.parameters', (int) $this->getId())); + + return \is_array($parameters) ? $parameters : []; + } +} diff --git a/Model/QueryBuilderActionQuery.php b/Model/QueryBuilderActionQuery.php new file mode 100644 index 0000000..4f7c062 --- /dev/null +++ b/Model/QueryBuilderActionQuery.php @@ -0,0 +1,11 @@ +decodeJsonColumn($this->getHooks(), sprintf('query_builder_rule#%d.hooks', (int) $this->getId())); + + return \is_array($hooks) ? $hooks : []; + } + + /** @param string[] $hookCodes */ + public function setHookCodes(array $hookCodes): static + { + return $this->setHooks(json_encode(array_values($hookCodes), \JSON_THROW_ON_ERROR)); + } + + public function getConditionTreeArray(): ?array + { + $tree = $this->decodeJsonColumn($this->getConditionTree(), sprintf('query_builder_rule#%d.condition_tree', (int) $this->getId())); + + return \is_array($tree) ? $tree : null; + } +} diff --git a/Model/QueryBuilderRuleQuery.php b/Model/QueryBuilderRuleQuery.php new file mode 100644 index 0000000..8f6783b --- /dev/null +++ b/Model/QueryBuilderRuleQuery.php @@ -0,0 +1,11 @@ + $parameters parameter name (without colon) => scalar value */ + public function __construct( + public string $sql, + public array $parameters, + ) { + } +} diff --git a/Query/QueryParts.php b/Query/QueryParts.php new file mode 100644 index 0000000..68c972c --- /dev/null +++ b/Query/QueryParts.php @@ -0,0 +1,65 @@ + JOIN clauses keyed by joined table name */ + private array $joins = []; + + /** @var string[] */ + private array $where = []; + + /** @var array parameter name (without colon) => value */ + private array $parameters = []; + + private int $parameterCounter = 0; + + public function addJoin(string $table, string $joinClause): void + { + $this->joins[$table] ??= $joinClause; + } + + public function addWhere(string $clause): void + { + $this->where[] = $clause; + } + + public function addParameter(string $name, mixed $value): void + { + $this->parameters[$name] = $value; + } + + /** Registers a value under a generated unique name and returns its placeholder (with colon). */ + public function bindValue(mixed $value): string + { + $name = 'qb_' . $this->parameterCounter++; + $this->parameters[$name] = $value; + + return ':' . $name; + } + + /** @return string[] */ + public function getJoins(): array + { + return array_values($this->joins); + } + + /** @return string[] */ + public function getWhere(): array + { + return $this->where; + } + + /** @return array */ + public function getParameters(): array + { + return $this->parameters; + } +} diff --git a/Query/QueryScopeInterface.php b/Query/QueryScopeInterface.php new file mode 100644 index 0000000..960653a --- /dev/null +++ b/Query/QueryScopeInterface.php @@ -0,0 +1,20 @@ + $parameters extra project parameters, keyed by placeholder name without colon */ + public function __construct( + public ?int $customerId = null, + public ?int $productId = null, + public ?int $cartId = null, + public array $cartProductIds = [], + public ?int $orderId = null, + public ?int $categoryId = null, + public ?int $brandId = null, + public string $locale = 'fr_FR', + //Products total of the cart, taxes included, shipping and discounts excluded + public ?float $cartTotal = null, + //Country the cart ships to (cart delivery address, else the shop default country) + public ?int $deliveryCountryId = null, + public array $parameters = [], + ) { + } + + /** @return array placeholder name (without colon) => value */ + public function getBindableParameters(): array + { + return array_merge( + [ + 'customer_id' => $this->customerId, + 'product_id' => $this->productId ?? self::NO_CURRENT_PRODUCT_ID, + 'cart_id' => $this->cartId, + 'cart_product_ids' => $this->cartProductIds, + 'order_id' => $this->orderId, + 'category_id' => $this->categoryId, + 'brand_id' => $this->brandId, + 'locale' => $this->locale, + 'cart_total' => $this->cartTotal, + 'delivery_country_id' => $this->deliveryCountryId, + ], + $this->parameters + ); + } +} diff --git a/Query/RuntimeParameterProviderInterface.php b/Query/RuntimeParameterProviderInterface.php new file mode 100644 index 0000000..d2c5108 --- /dev/null +++ b/Query/RuntimeParameterProviderInterface.php @@ -0,0 +1,21 @@ + placeholder name (without colon) => value */ + public function provide(RuntimeContext $baseContext): array; +} diff --git a/QueryBuilder.php b/QueryBuilder.php new file mode 100644 index 0000000..0db836b --- /dev/null +++ b/QueryBuilder.php @@ -0,0 +1,128 @@ + bound of module.xml is not enforced by every 2.x core: a checkout + * of this line dropped into a Thelia 2 shop must refuse to activate, with a message. + */ + public function preActivation(?ConnectionInterface $con = null): bool + { + self::assertSupportedCoreVersion(self::coreVersion()); + + return true; + } + + public function postActivation(?ConnectionInterface $con = null): void + { + if (!self::getConfigValue('is_initialized', false)) { + $database = new Database($con); + + $database->insertSql(null, [__DIR__ . '/Config/TheliaMain.sql']); + + self::setConfigValue('is_initialized', true); + } + } + + public function update($currentVersion, $newVersion, ?ConnectionInterface $con = null): void + { + $updateDir = __DIR__ . DS . 'Config' . DS . 'update'; + + if (!is_dir($updateDir)) { + return; + } + + $finder = Finder::create() + ->name('*.sql') + ->depth(0) + ->sortByName() + ->in($updateDir); + + $database = new Database($con); + + /** @var \SplFileInfo $file */ + foreach ($finder as $file) { + if (version_compare($currentVersion, $file->getBasename('.sql'), '<')) { + $database->insertSql(null, [$file->getPathname()]); + } + } + } + + public static function configureServices(ServicesConfigurator $servicesConfigurator): void + { + $servicesConfigurator->load(self::getModuleCode() . '\\', __DIR__) + ->exclude([ + __DIR__ . '/I18n/*', + __DIR__ . '/Config/**/*', + // Propel models and queries: instantiated by the ORM, never services. + __DIR__ . '/Model/*', + __DIR__ . '/templates/**/*', + __DIR__ . '/tests/*', + // Dev dependencies of a checkout linked into a shop: not module classes. + __DIR__ . '/vendor/', + __DIR__ . '/QueryBuilder.php', + ]) + ->autowire(true) + ->autoconfigure(true); + + //Thelia builds its forms from its own factory builder, fed with the types tagged + //thelia.form.type only: the bundle type, tagged form.type by Symfony, is registered + //there too so the module BaseForm subclasses can add it + $servicesConfigurator->set('querybuilder.form.type.query_builder', QueryBuilderType::class) + ->args([service(FormOptionsNormalizer::class), param('kernel.default_locale')]) + ->tag('thelia.form.type'); + } + + /** @throws \RuntimeException when the running core is older than Thelia 3 or unknown */ + public static function assertSupportedCoreVersion(?string $coreVersion): void + { + if ($coreVersion === null || version_compare($coreVersion, self::MINIMUM_CORE_VERSION, '<')) { + throw new \RuntimeException(sprintf( + 'The QueryBuilder module %s requires Thelia %s or later, this shop runs Thelia %s. The Thelia 2 line of the module is not published.', + self::currentModuleVersion(), + self::MINIMUM_CORE_VERSION, + $coreVersion ?? 'unknown' + )); + } + } + + private static function coreVersion(): ?string + { + if (\defined(TheliaKernel::class . '::THELIA_VERSION')) { + return TheliaKernel::THELIA_VERSION; + } + + //Thelia 2 exposes its version on Thelia\Core\Thelia, a class Thelia 3 removed + $legacyKernel = 'Thelia\\Core\\Thelia'; + + return \defined($legacyKernel . '::THELIA_VERSION') ? \constant($legacyKernel . '::THELIA_VERSION') : null; + } + + private static function currentModuleVersion(): string + { + $moduleXml = @simplexml_load_file(__DIR__ . '/Config/module.xml'); + + return $moduleXml instanceof \SimpleXMLElement ? (string) $moduleXml->version : 'dev'; + } +} diff --git a/README.md b/README.md index 3979690..ee2fd36 100644 --- a/README.md +++ b/README.md @@ -1 +1,206 @@ # QueryBuilder + +Rule engine for Thelia 3. A merchant composes a condition tree in the back-office, the module compiles it to parameterized SQL on the server and runs the actions bound to the rule: a product list at a theme hook point, a discount on the cart lines, a discount on the cart total or free shipping. A JSON API serves decoupled fronts. + +Only the fields of a data dictionary and a closed list of operators are accepted, and every value is bound: no SQL comes from the stored JSON. Any active module can extend the dictionary with its own fields, joins and hook points. + +## Compatibility + +- Thelia 3 (`thelia/core` ^3.0), Flexy front theme, default-twig back-office +- PHP 8.3+ +- `openstudio/query-builder-bundle` ^1.1 (the back-office editor) + +## Installation + +```bash +composer require thelia/query-builder-module --with-all-dependencies +php Thelia module:activate QueryBuilder +php bin/console cache:clear +``` + +`--with-all-dependencies` is needed on a fresh Thelia 3 shop: its lock file pins `thecodingmachine/safe` at v3, while `openstudio/query-builder-bundle` 1.1 requires `^2.5`. Composer downgrades the package (every other user of it in the shop accepts v2). + +Symfony Flex registers the bundle in `config/bundles.php`. If the condition editor stays empty in the back-office, check that the file contains `OpenStudio\QueryBuilderBundle\OpenStudioQueryBuilderBundle::class => ['all' => true]`. + +The activation creates the tables `query_builder_rule`, `query_builder_action` and `query_builder_suggestion`, and adds a "Query Builder" entry to the Tools menu. + +## Concepts + +| Concept | Where | What it is | +|---|---|---| +| Rule | table `query_builder_rule` | A context (PRODUCT, CART...), the hook codes it is bound to, an optional condition tree (empty means always), an activation flag | +| Action | table `query_builder_action` | Attached to a rule: a code mapped to a PHP class, a type (`display`, `action`, `filter`), its own tree selecting the products it works on, JSON parameters | +| Context | enum `QueryBuilder\Enum\Context` | GLOBAL, CATEGORY, BRAND, PRODUCT, CART, ORDER, CUSTOMER. Filters the hooks, fields and actions offered to a rule | +| Field, join | YAML dictionary | The fields offered in the editor and the join graph used to compile them | +| Hook | YAML dictionary | A `theme_hook()` point of the front theme, or a virtual code served through the API only | + +A rule runs when one of its hooks is called and its condition tree matches the current visit (the product being displayed, the products of the cart, the customer...). It then runs its active actions in order. + +## Back-office + +The screens live under `/admin/query_builder`: the list of rules, a rule screen in three steps (identification, context and hooks, conditions) followed by its actions, and an action screen with the parameters of the selected action code. + +The screens and the Tools menu entry follow the right granted on the module itself (Configuration > Administrators > Profiles, module rights): an administrator whose profile has no view right on QueryBuilder gets no menu entry and a 403 on every screen. Reading requires the view right, saving the create or update right, deleting the delete right. No other resource is required, in particular not the "Modules" resource, which guards the module management screens. + +The condition editor is the `QueryBuilderType` form type of the bundle, configured with the `native` processor: the stored tree is the react-querybuilder structure (`combinator`, `not`, `rules` with `field`, `operator`, `value`), without the ids the editor keeps for itself. The fields offered depend on the context of the rule and on the editor (the trigger conditions of the rule, or the product selection of an action); changing the context drops the conditions on fields the new context does not offer. Field labels are prefixed with their group (the part of the field code before the first underscore, translated), so the alphabetical list gathers the product, cart, customer and context fields. A readable summary of the tree is shown above the editor; on the rule screen the same reading of each action selection opens in a popover from the (i) mark after the action name. + +The form type checks every submitted tree against the declared fields and per-field operators; the module then checks the fields against the context of the rule and the editor they were posted from (`SqlBuilder::validateTree()`). A tree naming a field or an operator outside the dictionary is refused at save time and at run time. + +### JavaScript build + +The editor script (React, react-querybuilder, the Stimulus controller of the bundle and the three controllers of the module) is built with webpack from `Resources/` into `templates/backOffice/default-twig/assets/dist/`, which is committed and served by the Thelia asset resolver on the rule and action screens only (never on the rule list). To rebuild it: + +```bash +cd Resources +npm install +npm run build +``` + +The bundle controller is read from `vendor/openstudio/query-builder-bundle/assets`, four levels up from `Resources/` when the module sits in `local/modules/QueryBuilder`. Set `QUERY_BUILDER_BUNDLE_ASSETS` to that directory otherwise. + +## Data dictionary + +The base dictionary is `Config/query_builder.yml`; its header documents the full syntax. Every active module may ship its own `Config/query_builder.yml`, merged over the base one in module position order: joins and fields are replaced by code, hooks are merged by code (the last module wins on the label), the `disabled` list removes base fields. + +Two kinds of fields: + +- `field: table.column`: the joins from `product` to the table are added automatically from the `joins` graph. A join flagged `multivalued` (one product, several rows) compiles its comparisons to `EXISTS` or `NOT EXISTS` subqueries, so that `notIn` or `!=` exclude the whole product instead of matching another joined row. +- `expression: `: a self-contained SQL expression, subqueries allowed, which may reference runtime placeholders. + +Runtime placeholders: `:customer_id`, `:cart_id`, `:cart_product_ids`, `:product_id` (bound to 0 outside a product page, so "is the current product = false" lets everything through), `:order_id`, `:category_id`, `:brand_id`, `:locale`, `:cart_total` (products total of the cart, taxes included) and `:delivery_country_id`, plus the ones provided by the project through `RuntimeParameterProviderInterface`. A placeholder without a value in the current visit (an anonymous visitor on a rule that needs `:customer_id`, for instance) makes the rule skip with a logged warning, and the page renders anyway. + +An expression may contain the `:value` token: the value entered in the editor is then expanded inside the expression, one placeholder per item, and the field must restrict its `operators` to `in` and `notIn`, which only set the polarity. + +Field types: `text`, `number`, `date`, `datetime` (entered as a date, compared on `DATE()`), `boolean`. A field may declare `values_query` (a `value` column, an optional `label` column, `:locale` allowed): the editor then offers a "list of values" select next to the free operators. Beyond 300 rows or on SQL error the field falls back to the free input. Labels of fields and hooks are translation keys of the `querybuilder` domain. + +A field declares the contexts it is offered in with `contexts`, a list of `QueryBuilder\Enum\Context` values; `GLOBAL`, the default, means every context, and a `GLOBAL` rule itself only sees `GLOBAL` fields. Restrict a field to the contexts where its placeholder is filled: `:product_id` to `PRODUCT`, `:category_id` to `CATEGORY`, `:brand_id` to `BRAND`, `:order_id` to `ORDER`. The cart and the customer come from the session in every context, so their fields stay `GLOBAL` (a `PRODUCT` rule can select the products sharing a brand with the cart). A field also declares the editors it is offered in with `usage`, a list among `rule` (trigger conditions) and `action` (product selection), both by default: a field comparing the selected product to the object of the context is always true in the rule editor and belongs to `[action]`, a field reading the object itself (`:category_id` in a list) selects all or nothing in the action editor and belongs to `[rule]`. + +A field reading a placeholder without a value in the current visit (`:customer_id` for a visitor, `:category_id` outside a category page) is never evaluated to "false": the rule or the action skips, as described above. Dynamic expressions comparing the product to the object of the context are written as `EXISTS (...)`, which always yields 0 or 1: with `:product_id` bound to 0 outside a product page, a scalar subquery such as `(SELECT brand_id FROM product WHERE id = 0)` yields `NULL` and "= false" would let nothing through instead of everything. + +Fields shipped, by group: + +- `product`: reference, title, visibility, category, category title, brand, brand id, main category, price, creation date, in the cart, current product, flagged as new, on catalog promotion, in stock. +- `cart` (every context): number of lines and items, quantity of the product, contains a brand or a category, contains a promotional line, same brand or shared category with a cart product, accessory of a cart product, products total; `delivery`: country. +- `customer` (every context, skipped for a visitor): reseller, discount rate, seniority, number of orders, days since the last order, newsletter subscriber, product bought in the last three months or ever, brand or category already bought. +- `context`, restricted to their context and editor: same brand, shared category, same main category, accessory, same template and same feature value as the context product, product already in the cart (`PRODUCT`, action editor); context category and its parent (`CATEGORY`, rule editor), product of the context category or of its sub-tree (`CATEGORY`, action editor, recursive CTE: MySQL 8.0+ or MariaDB 10.2+); context brand (`BRAND`, rule editor), product of the context brand (`BRAND`, action editor). + +Hook points shipped, per context (the `theme_hook()` codes of Flexy): `home.top` and `home.bottom` (GLOBAL), `category.top` and `category.bottom`, `brand.top` and `brand.bottom`, `product.top`, `product.details.bottom` and `product.bottom`, `cart.top` and `cart.bottom`, `account.top` and `account.bottom` (CUSTOMER). A GLOBAL rule can be bound to any declared hook. Declare a hook only when a theme calls it, or when a front consumes it through the API. + +## Front + +The module implements `Thelia\Core\Hook\Theme\ThemeHookInterface` for every hook code of the dictionary: a `theme_hook('product.top', {product: product})` call in the theme runs the rules bound to `product.top` for the current visit and renders one product list per display action. The theme passes the resource it displays (product, category, brand); its id feeds the runtime context. + +The product list is rendered with `templates/frontOffice/default/QueryBuilder/product-list.html.twig`, which relies on the Flexy component `Layouts:CrossSelling:Base` (products fetched by the theme, picked order kept). A theme overrides it by shipping `templates/frontOffice//modules/QueryBuilder/product-list.html.twig`; the variables are `hook`, `rule_name`, `action_name` and `product_ids`. + +The cart discount granted by a rule is shown on the checkout pages by `cart-discount.html.twig` (same override path), once per request, on the `checkout.top` and `cart.bottom` points. + +A product discount shows up wherever the theme prices a product, without any template work: the product page reads its sale elements from the core access service (`PseByProductEvent`) and the listings, the cross-selling strips and the search read them from the front API (`ModelToResourceEvent`); the module answers both with a promotion at the discounted price, so the theme renders its usual struck original price and discounted price. The label of the discount is shown in the `cart-discount.html.twig` fragment above, which lists the labelled product discounts charged on the cart lines with the products they apply to; the product page shows the discounted price only. + +For a custom rendering, the Twig function returns the same structure as the API: + +```twig +{% set recommendations = query_builder_products('cart.recommendations', { product_id: product.id }) %} +{% for productId in recommendations.product_ids %}...{% endfor %} +``` + +Optional parameters: `product_id`, `order_id`, `category_id`, `brand_id`. On error the lists are empty and the error is logged. + +## API + +`GET /api/front/query_builder/products/{hookCode}` runs the display rules of a hook code for the current visit (session customer and cart) and returns: + +| Field | Content | +|---|---| +| `hookCode` | The hook code | +| `productIds` | Unique product ids selected by the display actions, in selection order | +| `offers` | Discount offers keyed by product id (`rate`, `label`, `cumulative`) | +| `actions` | One entry per executed display action (`rule`, `action`, `product_ids`, `offers`) | +| `products` | The selected products as front `Product` resources, in the same order | + +Query parameters: `product_id`, `category_id`, `brand_id`, `order_id`. A hook code the dictionary does not declare answers 404. Several rules bound to the same hook share one slot, capped by the highest action limit. + +Two addons complete the core resources on the front: `QueryBuilderCartDiscount` on the cart (`rate`, `label`, `amount`, `freeShipping`) and `QueryBuilderProductOffer` on the products (`rate`, `label`, `cumulative`). + +## Actions + +### DisplayProductsList + +Selects the products matching the action tree, `limit` at most (default 3). A product never appears in its own recommendations. + +With `persist_days`, the selection becomes sticky for an identified customer: the products shown are served again for N times 24 hours or until purchased, and only the freed slots are refilled, products never suggested first, then the oldest cycles. A product bought ends its cycle (`ORDER_PAY`). Saving an action whose tree changed expires its running cycles; saving only its parameters does not. An anonymous visitor gets the stateless selection. + +### ApplyDiscount + +Parameters: `discount_rate` (percentage, required), `discount_label`, `discount_cumulative`, and the optional `limit` and `persist_days` of a sticky selection (an anonymous visitor then gets no discount at all). + +The discount applies on the cart lines: at every cart change, customer login or currency change, the module re-reads the catalog prices of each line as the core does and writes the discounted price in the promotion columns of the line. A non-stackable rule (default) replaces a catalog promotion only when it is better; a stackable rule applies on top of it. When the rule stops applying, the line goes back to the catalog prices. The order lines inherit the cart line prices, so the amount is the same in the cart, at payment and on the order. + +The tree of a discount action is evaluated on every surface, cart events included. Conditions that describe a recommendation ("product in the cart = false") cancel the discount as soon as the product enters the cart: keep the tree of a discount on stable criteria (brand, category, visibility). + +The same discount is shown on the product page, in the listings and on the front product resources (`productSaleElements[].promo` and `promoPrice`), computed from the catalog prices of the visit (currency, customer discount) with the same policy as the cart lines, so the price announced is the price charged. The product page resolves the rules with the displayed product as current product; the listings and the API resources without one. The `QueryBuilderProductOffer` addon still exposes the rate and label for a decoupled front. The label is displayed in the cart discount fragment of the checkout pages (see Front). + +### ApplyCartDiscount + +Parameters: `cart_discount_rate` (percentage of the cart products total, taxes included, shipping excluded) and `cart_discount_free_shipping`. The tree of the action is ignored: the eligibility is the rule tree. When several rules apply, the best rate wins and the shipping is free as soon as one rule offers it. + +The amount goes through the core discount channel (`cart.discount`, then the order), without the coupon machinery: a coupon and a rule discount coexist and their amounts add up. The module listens to the cart events at priority 1, right after the core reset the column with the coupons, and keeps a per-request ledger so that nested cart events never add the amount twice. + +Free shipping follows the coupon path of the core: the cart postage is cleared on `CART_SET_POSTAGE` (priority 133) and the postage estimator of the core is told the shipping is free. The delivery options listed on the delivery step (`MODULE_DELIVERY_GET_OPTIONS`) are priced by each delivery module; once the modules have answered (priority -128) the module sets their price to zero so the option cards announce the free delivery the summary shows. The legacy `ORDER_SET_POSTAGE` point is kept for a front still going through the order session. + +## Extension points + +- `QueryBuilder\Query\QueryScopeInterface`: restricts every product query (the catalog of the logged in customer, for instance). +- `QueryBuilder\Query\RuntimeParameterProviderInterface`: exposes project placeholders to the expressions (`:customer_typology_id`...). +- `QueryBuilder\Action\ActionInterface`: a new predefined action, offered in the editor. +- `QueryBuilder\Event\QueryBuilderRulesChangedEvent`: dispatched after any back-office change of a rule or an action, for the caches that embed rule results. +- A `Config/query_builder.yml` in any active module, see the dictionary above. + +The implementations are autoconfigured through their interface. + +## Translations + +Two domains: `querybuilder` for the labels translated in PHP (contexts, actions, dictionary labels, editor wording, front fragments), in `I18n/.php`, and `querybuilder.bo.default-twig` for the back-office screens, in `I18n/backOffice/default-twig/.php`. English and French are shipped. + +## Debug commands + +```bash +php Thelia querybuilder:dictionary [CONTEXT] [--usage=rule|action] +php Thelia querybuilder:compile '{"combinator":"and","rules":[{"field":"product_ref","operator":"=","value":"ABC"}]}' --customer=42 --cart-total=120 --delivery-country=64 --execute +php Thelia querybuilder:compile '{"combinator":"and","rules":[{"field":"context_product_same_brand","operator":"=","value":true}]}' --product=123 --cart=7 --category=3 --brand=2 --order=15 --execute +php Thelia querybuilder:run product.top --customer=42 --product=123 +``` + +`querybuilder:compile` prints the SQL and its parameters; `--execute` also runs it. The tree is the stored react-querybuilder structure. + +## Tests + +From the root of a shop where the module is installed: + +```bash +php -d auto_prepend_file=./bootstrap.php vendor/bin/phpunit -c vendor/thelia/modules/QueryBuilder/phpunit.xml.dist --testsuite unit + +php bin/test-prepare +php Thelia module:activate QueryBuilder +php -d auto_prepend_file=./bootstrap.php vendor/bin/phpunit -c vendor/thelia/modules/QueryBuilder/phpunit.xml.dist --testsuite integration +``` + +`-d auto_prepend_file=./bootstrap.php` is what makes the kernel boot on the right paths. The PHPUnit entry script requires the Composer autoloader before it reads any configuration, and the autoloader pulls in `vendor/thelia/core/bootstrap.php`, which derives `THELIA_ROOT` from its own location under `vendor/`. The shop root `bootstrap.php` holds the real path constants and only wins by running first, which no PHPUnit bootstrap file can do. Without it the kernel looks for the core Propel schema under `vendor/thelia/vendor/thelia/config/` and the integration suite errors out; the test bootstrap detects the case and prints the command to use. + +The unit suite also runs from the module checkout alone, once its dev dependencies are installed (`thelia/core`, the bundle, PHPUnit): + +```bash +composer install +php vendor/bin/phpunit -c phpunit.xml.dist --testsuite unit +``` + +`THELIA_VENDOR_AUTOLOAD=/path/to/vendor/autoload.php` points the test bootstrap at another vendor. A checkout linked into a shop by symlink (Composer `path` repository) needs it set to the shop vendor for the integration suite, otherwise its own vendor is picked. When the vendor belongs to a shop, the bootstrap also loads the shop `bootstrap.php` (Thelia path constants) and `.env` before booting the kernel, and bridges the `DATABASE_*` variables from `.env.local` when nothing else defines them. + +`.github/workflows/ci.yml` installs a fresh `thelia/thelia-project` shop, requires the module from the checkout and runs both suites. + +## Author + +Samuel Diakité – OpenStudio ([openstudio-sdiakite](https://github.com/openstudio-sdiakite), ) + +## License + +GPL-3.0-or-later, see [LICENSE](LICENSE). diff --git a/Resources/assets/controllers/qb_action_form_controller.js b/Resources/assets/controllers/qb_action_form_controller.js new file mode 100644 index 0000000..aacef24 --- /dev/null +++ b/Resources/assets/controllers/qb_action_form_controller.js @@ -0,0 +1,31 @@ +import { Controller } from '@hotwired/stimulus'; + +/** + * Shows the parameters that belong to the selected predefined action: display + * actions take a limit and a persistence, ApplyDiscount the discount fields, + * ApplyCartDiscount the cart discount fields and no product selection tree. + */ +export default class extends Controller { + static targets = ['code', 'displayParams', 'discountParams', 'cartDiscountParams', 'treeGroup']; + + static values = { + discountCode: String, + cartDiscountCode: String, + }; + + connect() { + this.sync(); + } + + sync() { + const code = this.codeTarget.value; + const isDiscount = code === this.discountCodeValue; + const isCartDiscount = code === this.cartDiscountCodeValue; + + this.displayParamsTarget.hidden = isCartDiscount; + this.discountParamsTarget.hidden = !isDiscount; + this.cartDiscountParamsTarget.hidden = !isCartDiscount; + // The cart discount has no product target: its selection tree is meaningless + this.treeGroupTarget.hidden = isCartDiscount; + } +} diff --git a/Resources/assets/controllers/qb_action_summary_controller.js b/Resources/assets/controllers/qb_action_summary_controller.js new file mode 100644 index 0000000..e21c165 --- /dev/null +++ b/Resources/assets/controllers/qb_action_summary_controller.js @@ -0,0 +1,106 @@ +import { Controller } from '@hotwired/stimulus'; + +import { countRules, groupBlock, parseQuery } from '../summary.js'; + +/** + * Popover on the (i) mark next to each action of the rule screen, reading the product + * selection of the action the way the action screen does above its editor, followed by + * the parameters worth a glance (discount rate, free shipping, stackable), so the + * actions can be compared without leaving the rule. + * + * Each trigger carries the stored tree in data-tree (absent when the action has no + * product selection) and the translated detail lines in data-details. + * + * Relies on the Bootstrap 5 build the back-office theme exposes as window.bootstrap; + * without it the marks stay inert. + * + * Values: + * fields fields of the rule context, as handed to the editor + * labels translated wording (see QueryBuilder\Service\EditorLabels) + * title heading of the popover + * emptyHint wording shown when the action has no criterion + */ +export default class extends Controller { + static targets = ['trigger']; + + static values = { + fields: Array, + labels: Object, + title: String, + emptyHint: String, + }; + + connect() { + const Popover = window.bootstrap?.Popover; + this.popovers = []; + + for (const trigger of this.triggerTargets) { + const hasSelection = 'tree' in trigger.dataset; + const query = parseQuery(trigger.dataset.tree); + const details = parseDetails(trigger.dataset.details); + const isEmpty = (!hasSelection || countRules(query) === 0) && details.length === 0; + trigger.classList.toggle('qb-action-summary-trigger-empty', isEmpty); + + if (!Popover) { + continue; + } + + this.popovers.push(new Popover(trigger, { + html: true, + sanitize: false, + title: this.titleValue, + content: this.buildContent(hasSelection ? query : null, details), + trigger: 'hover focus', + placement: 'right', + container: 'body', + customClass: 'qb-action-summary-popover', + })); + } + } + + disconnect() { + this.popovers.forEach((popover) => popover.dispose()); + this.popovers = []; + } + + buildContent(query, details) { + const content = document.createElement('div'); + content.className = 'qb-action-summary'; + + if (query !== null) { + if (countRules(query) === 0) { + const hint = document.createElement('p'); + hint.className = 'qb-action-summary-empty mb-0'; + hint.textContent = this.emptyHintValue; + content.appendChild(hint); + } else { + content.appendChild(groupBlock(query, this.fieldsValue, this.labelsValue, 0)); + } + } + + if (details.length > 0) { + const list = document.createElement('ul'); + list.className = 'qb-action-summary-details'; + for (const detail of details) { + const item = document.createElement('li'); + item.textContent = detail; + list.appendChild(item); + } + content.appendChild(list); + } + + return content; + } +} + +function parseDetails(raw) { + if (!raw) { + return []; + } + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed.filter((line) => typeof line === 'string' && line !== '') : []; + } catch { + return []; + } +} diff --git a/Resources/assets/controllers/qb_editor_controller.js b/Resources/assets/controllers/qb_editor_controller.js new file mode 100644 index 0000000..44fee05 --- /dev/null +++ b/Resources/assets/controllers/qb_editor_controller.js @@ -0,0 +1,179 @@ +import { Controller } from '@hotwired/stimulus'; + +import { countRules, groupBlock, parseQuery } from '../summary.js'; + +/** + * Feedback around the query-builder-bundle widget of a rule or action form: a + * condition counter, a readable summary of the stored tree and an empty state, + * refreshed from the hidden input the bundle controller rewrites on every edit. + * + * On the rule screen it also drives the context: switching it hands the fields of + * the new context to the bundle widget (which prunes the rules they cannot hold) + * and filters the hook checkboxes. + * + * Values: + * fieldsByContext { CONTEXT: [{ name, type, label, values, operators }] } + * labels translated wording (see QueryBuilder\Service\EditorLabels) + */ +export default class extends Controller { + static targets = [ + 'context', 'contextHelp', + 'hookChoice', 'hookCount', 'hookEmpty', + 'counter', 'summary', 'summaryText', 'emptyHint', + ]; + + static values = { + fieldsByContext: Object, + labels: Object, + }; + + connect() { + this.editor = this.element.querySelector('[data-controller~="query-builder"]'); + this.input = this.element.querySelector('[data-query-builder-target="input"]'); + this.onInputChange = () => this.refreshFeedback(); + this.input?.addEventListener('change', this.onInputChange); + this.previousContext = this.hasContextTarget ? this.contextTarget.value : null; + + this.syncContextHelp(); + this.refreshHooks(); + this.refreshFeedback(); + } + + disconnect() { + this.input?.removeEventListener('change', this.onInputChange); + } + + // ------------------------------------------------------------------ context + + changeContext() { + const context = this.contextTarget.value; + const confirmMessage = this.labelsValue.confirmContextChange; + + if (countRules(this.currentQuery()) > 0 && confirmMessage && !window.confirm(confirmMessage)) { + this.contextTarget.value = this.previousContext; + return; + } + + this.previousContext = context; + + // The bundle controller rebuilds the editor on this attribute, drops the rules on fields + // the new list does not declare and rewrites the hidden input (change event: the + // feedback below is refreshed again with the pruned tree) + if (this.editor) { + this.editor.dataset.queryBuilderFieldsValue = JSON.stringify(this.currentFields()); + } + + this.syncContextHelp(); + this.refreshHooks(); + this.refreshFeedback(); + } + + syncContextHelp() { + if (!this.hasContextTarget || !this.hasContextHelpTarget) { + return; + } + const option = this.contextTarget.options[this.contextTarget.selectedIndex]; + this.contextHelpTarget.textContent = option ? option.dataset.description || '' : ''; + } + + currentContext() { + if (this.hasContextTarget) { + return this.contextTarget.value; + } + return Object.keys(this.fieldsByContextValue)[0] ?? null; + } + + currentFields() { + return this.fieldsByContextValue[this.currentContext()] || []; + } + + // -------------------------------------------------------------------- hooks + + refreshHooks() { + const context = this.currentContext(); + const visible = []; + + for (const choice of this.hookChoiceTargets) { + const matches = choice.dataset.context === context; + choice.hidden = !matches; + const checkbox = choice.querySelector('input[type="checkbox"]'); + if (!checkbox) { + continue; + } + if (!matches) { + checkbox.checked = false; + } else { + visible.push(checkbox); + } + choice.classList.toggle('qb-hook-checked', matches && checkbox.checked); + } + + if (this.hasHookCountTarget) { + const checked = visible.filter((checkbox) => checkbox.checked).length; + this.hookCountTarget.textContent = (this.labelsValue.hooks?.count || '%checked% / %total%') + .replace('%checked%', String(checked)) + .replace('%total%', String(visible.length)); + } + + if (this.hasHookEmptyTarget) { + this.hookEmptyTarget.hidden = visible.length > 0; + } + } + + checkAllHooks(event) { + event.preventDefault(); + this.setVisibleHooks(true); + } + + uncheckAllHooks(event) { + event.preventDefault(); + this.setVisibleHooks(false); + } + + setVisibleHooks(checked) { + for (const choice of this.hookChoiceTargets) { + if (choice.hidden) { + continue; + } + const checkbox = choice.querySelector('input[type="checkbox"]'); + if (checkbox) { + checkbox.checked = checked; + } + } + this.refreshHooks(); + } + + // ----------------------------------------------------------------- feedback + + currentQuery() { + return parseQuery(this.input?.value); + } + + refreshFeedback() { + const query = this.currentQuery(); + const total = countRules(query); + const labels = this.labelsValue; + + if (this.hasCounterTarget) { + const counter = labels.counter || {}; + this.counterTarget.textContent = total === 0 + ? (counter.none || '') + : total === 1 + ? (counter.one || '1') + : (counter.many || '%count%').replace('%count%', String(total)); + } + + if (this.hasEmptyHintTarget) { + this.emptyHintTarget.classList.toggle('qb-empty-hint-visible', total === 0); + } + + if (this.hasSummaryTarget) { + this.summaryTarget.classList.toggle('qb-summary-visible', total > 0); + const target = this.hasSummaryTextTarget ? this.summaryTextTarget : this.summaryTarget; + target.replaceChildren(); + if (total > 0) { + target.appendChild(groupBlock(query, this.currentFields(), labels, 0)); + } + } + } +} diff --git a/Resources/assets/query-builder-admin.css b/Resources/assets/query-builder-admin.css new file mode 100644 index 0000000..e811713 --- /dev/null +++ b/Resources/assets/query-builder-admin.css @@ -0,0 +1,375 @@ +/* Back-office skin of the QueryBuilder module screens (Bootstrap 5 back-office). + Loaded after the react-querybuilder stylesheet, whose variables it overrides. */ + +:root { + --qb-accent: var(--bs-primary, #0d6efd); + --qb-accent-soft: rgba(var(--bs-primary-rgb, 13, 110, 253), 0.08); + --qb-border: var(--bs-border-color, #dee2e6); + --qb-text-muted: var(--bs-secondary-color, #6c757d); + --qb-group-bg: var(--bs-tertiary-bg, #f8f9fa); + + /* Vendor variables: lighter frame, tighter spacing than the library default */ + --rqb-spacing: 0.45rem; + --rqb-base-color: var(--bs-primary, #0d6efd); + --rqb-background-color: transparent; + --rqb-border-color: var(--bs-border-color, #dee2e6); + --rqb-border-radius: var(--bs-border-radius, 0.375rem); + --rqb-branch-indent: 0.6rem; +} + +/* ----------------------------------------------------------------- Steps */ + +.qb-step { + flex: 0 0 auto; + width: 22px; + height: 22px; + line-height: 22px; + text-align: center; + border-radius: 50%; + background: var(--qb-accent); + color: #fff; + font-size: 12px; + font-weight: 600; +} + +/* ----------------------------------------------------------------- Hooks */ + +.qb-hooks-toolbar { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.qb-hooks-count { + font-size: 12px; + color: var(--qb-text-muted); + margin-left: auto; +} + +.qb-hooks-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: 6px; +} + +.qb-hook-label { + display: flex; + align-items: flex-start; + gap: 8px; + width: 100%; + margin: 0; + padding: 6px 10px; + border: 1px solid var(--qb-border); + border-radius: var(--bs-border-radius, 0.375rem); + font-weight: 400; + cursor: pointer; + transition: background-color 0.12s ease, border-color 0.12s ease; +} + +.qb-hook-label:hover, +.qb-hook-checked > .qb-hook-label { + border-color: var(--qb-accent); + background: var(--qb-accent-soft); +} + +.qb-hook-label > .form-check-input { + flex: 0 0 auto; + margin-top: 3px; +} + +/* min-width 0: without it the flex item refuses to shrink and the long + unbreakable hook code overflows the card */ +.qb-hook-text { + flex: 1 1 auto; + min-width: 0; +} + +.qb-hook-text code { + font-size: 12px; + word-break: break-all; +} + +/* ------------------------------------------------------------ Conditions */ + +.qb-tree-panel { + border: 1px dashed var(--qb-border); + border-radius: var(--bs-border-radius, 0.375rem); + padding: 12px; + background: var(--bs-body-bg, #fff); +} + +.qb-summary { + display: none; + margin-bottom: 12px; + padding: 10px 14px 12px; + border: 1px solid var(--qb-border); + border-left: 3px solid var(--qb-accent); + border-radius: var(--bs-border-radius, 0.375rem); + background: var(--qb-accent-soft); + font-size: 13px; + line-height: 1.5; +} + +.qb-summary.qb-summary-visible { + display: block; +} + +.qb-summary-label { + display: block; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; + color: var(--qb-accent); + margin-bottom: 2px; +} + +/* Structured recap: one line per condition, nesting shown by indented rails */ + +.qb-summary-list { + list-style: none; + margin: 0; + padding: 0; +} + +.qb-summary-heading { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 6px; +} + +.qb-summary-heading-text { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; + color: var(--qb-text-muted); +} + +.qb-summary-combinator { + display: inline-block; + min-width: 26px; + padding: 1px 6px; + border-radius: 2px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.05em; + text-align: center; + color: #fff; + background: var(--qb-text-muted); +} + +.qb-summary-combinator-and { + background: var(--qb-accent); +} + +.qb-summary-combinator-or { + background: #4a90a4; +} + +.qb-summary-rule { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 6px; + padding: 4px 0 4px 14px; + position: relative; +} + +.qb-summary-rule::before { + content: ""; + position: absolute; + left: 2px; + top: 12px; + width: 6px; + height: 1px; + background: var(--qb-border); +} + +.qb-summary-field { + font-weight: 600; +} + +.qb-summary-field-unknown { + color: var(--bs-danger, #dc3545); + text-decoration: underline dotted; +} + +.qb-summary-operator { + font-style: italic; + color: var(--qb-text-muted); +} + +.qb-summary-join { + font-size: 11px; + text-transform: uppercase; + color: var(--qb-text-muted); +} + +.qb-summary-value { + padding: 1px 7px; + border: 1px solid var(--qb-border); + border-radius: 10px; + background: var(--bs-body-bg, #fff); + font-family: var(--bs-font-monospace, monospace); + font-size: 12px; +} + +.qb-summary-missing { + padding: 1px 7px; + border-radius: 10px; + background: var(--bs-danger-bg-subtle, #f8d7da); + font-size: 12px; + font-style: italic; + color: var(--bs-danger, #dc3545); +} + +.qb-summary-nested { + margin: 6px 0 6px 14px; +} + +/* Nested groups get their own rail so the depth is obvious */ +.qb-summary-nested > .qb-summary-group { + padding: 6px 0 2px 12px; + border-left: 2px solid var(--qb-border); +} + +.qb-empty-hint { + display: none; + font-size: 13px; + color: var(--qb-text-muted); + font-style: italic; + margin-bottom: 10px; +} + +.qb-empty-hint.qb-empty-hint-visible { + display: block; +} + +/* --------------------------------------------- Action summary popovers */ + +/* Plain (i) mark after the action name; the popover itself is appended to body */ +.qb-action-summary-trigger { + padding: 0 2px; + border: 0; + background: none; + color: var(--qb-accent); + font-size: 14px; + line-height: 1; + vertical-align: -1px; + cursor: help; +} + +.qb-action-summary-trigger:hover, +.qb-action-summary-trigger:focus-visible { + color: var(--bs-link-hover-color, var(--qb-accent)); +} + +.qb-action-summary-trigger-empty { + color: var(--qb-text-muted); +} + +.qb-action-summary-popover { + max-width: 480px; + font-size: 13px; + line-height: 1.5; +} + +.qb-action-summary-popover .popover-header { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; + color: var(--qb-accent); +} + +.qb-action-summary-empty { + font-style: italic; + color: var(--qb-text-muted); +} + +/* Parameters listed under the selection; the rail separates them from the tree */ +.qb-action-summary-details { + list-style: none; + margin: 0; + padding: 0; +} + +.qb-action-summary > * + .qb-action-summary-details { + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--qb-border); +} + +.qb-action-summary-details > li { + display: flex; + align-items: baseline; + gap: 6px; + padding: 2px 0; + font-weight: 600; +} + +.qb-action-summary-details > li::before { + content: ""; + flex: 0 0 auto; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--qb-accent); + transform: translateY(-1px); +} + +/* ------------------------------------------------- react-querybuilder skin */ + +.qb-tree-panel .ruleGroup { + background: var(--qb-group-bg); + border: 1px solid var(--qb-border); + padding: 10px; +} + +/* Nested groups alternate shades so the nesting depth is readable at a glance */ +.qb-tree-panel .ruleGroup .ruleGroup { + background: var(--bs-body-bg, #fff); +} + +.qb-tree-panel .ruleGroup .ruleGroup .ruleGroup { + background: var(--qb-group-bg); +} + +.qb-tree-panel .ruleGroup-header, +.qb-tree-panel .rule { + flex-wrap: wrap; +} + +/* Field labels can be long: fix the widths so the columns stay aligned between rules */ +.qb-tree-panel select.rule-fields { + width: 320px; + font-weight: 600; +} + +.qb-tree-panel select.rule-operators { + width: 220px; +} + +.qb-tree-panel input.rule-value, +.qb-tree-panel select.rule-value, +.qb-tree-panel textarea.rule-value { + width: 240px; +} + +.qb-tree-panel input.rule-value.form-check-input { + height: 1.925em; + margin-top: -0.02em; +} + +.qb-tree-panel select.ruleGroup-combinators { + width: auto; +} + +.qb-tree-panel .form-control, +.qb-tree-panel .form-select { + display: inline-block; + max-width: 100%; +} diff --git a/Resources/assets/query-builder-admin.js b/Resources/assets/query-builder-admin.js new file mode 100644 index 0000000..59ab683 --- /dev/null +++ b/Resources/assets/query-builder-admin.js @@ -0,0 +1,19 @@ +import { Application } from '@hotwired/stimulus'; + +import 'react-querybuilder/dist/query-builder.css'; +import './query-builder-admin.css'; + +import QueryBuilderController from '@openstudio/query-builder-bundle/controllers/query_builder_controller.js'; +import EditorController from './controllers/qb_editor_controller.js'; +import ActionFormController from './controllers/qb_action_form_controller.js'; +import ActionSummaryController from './controllers/qb_action_summary_controller.js'; + +// Own Stimulus application: the back-office theme builds its controllers with Encore and +// exposes no registry, so the module registers the editor controller of the bundle itself. +// Both applications scan the same DOM and only act on the identifiers they know. +const application = Application.start(); + +application.register('query-builder', QueryBuilderController); +application.register('qb-editor', EditorController); +application.register('qb-action-form', ActionFormController); +application.register('qb-action-summary', ActionSummaryController); diff --git a/Resources/assets/summary.js b/Resources/assets/summary.js new file mode 100644 index 0000000..aef3765 --- /dev/null +++ b/Resources/assets/summary.js @@ -0,0 +1,164 @@ +/** + * Readable summary of a stored condition tree (react-querybuilder structure), shared + * by the editor feedback of the rule and action forms and by the action popovers of + * the rule screen. Builds DOM with textContent only: labels and values come from the + * shop data. + */ + +const EMPTY_QUERY = { combinator: 'and', rules: [] }; +const LIST_OPERATORS = ['in', 'notIn', 'between', 'notBetween']; +const VALUELESS_OPERATORS = ['null', 'notNull']; + +// Stored tree read from a hidden input or a data attribute: anything that is not a +// group falls back to the empty query +export function parseQuery(raw) { + if (!raw || raw === 'null') { + return EMPTY_QUERY; + } + try { + const parsed = JSON.parse(raw); + return parsed && typeof parsed === 'object' && Array.isArray(parsed.rules) ? parsed : EMPTY_QUERY; + } catch { + return EMPTY_QUERY; + } +} + +export function countRules(group) { + if (!group || !Array.isArray(group.rules)) { + return 0; + } + return group.rules.reduce( + (total, rule) => total + (rule && Array.isArray(rule.rules) ? countRules(rule) : (rule && rule.field ? 1 : 0)), + 0, + ); +} + +function element(tag, className, text) { + const node = document.createElement(tag); + if (className) { + node.className = className; + } + // textContent only: field labels and values come from the shop data + if (text !== undefined) { + node.textContent = text; + } + return node; +} + +// A stored list is a comma-separated string (in/notIn textareas, between pairs); arrays are +// accepted for trees written by hand +function valueItems(rule) { + const value = rule.value; + if (Array.isArray(value)) { + return value; + } + if (LIST_OPERATORS.includes(rule.operator) && typeof value === 'string') { + return value.split(',').map((item) => item.trim()).filter((item) => item !== ''); + } + return value === undefined || value === null ? [] : [value]; +} + +function valueLabels(field, rule, labels) { + const options = Array.isArray(field?.values) ? field.values : []; + const words = labels.values || {}; + + return valueItems(rule).map((single) => { + if (single === true || single === 'true') { + return words.true || 'true'; + } + if (single === false || single === 'false') { + return words.false || 'false'; + } + const known = options.find((candidate) => String(candidate.value ?? candidate.name) === String(single)); + return known ? (known.label ?? known.name) : String(single); + }); +} + +function ruleLine(rule, fields, labels) { + const field = fields.find((candidate) => candidate.name === rule.field); + const operatorLabel = (labels.operators || {})[rule.operator] || rule.operator; + const words = labels.values || {}; + + const line = element('li', 'qb-summary-rule'); + // An unknown field means the tree was built for another context: make it visible + const fieldNode = element( + 'span', + field ? 'qb-summary-field' : 'qb-summary-field qb-summary-field-unknown', + field ? field.label : rule.field, + ); + if (!field) { + fieldNode.title = words.unknownField || ''; + } + line.appendChild(fieldNode); + line.appendChild(element('span', 'qb-summary-operator', operatorLabel)); + + if (VALUELESS_OPERATORS.includes(rule.operator)) { + return line; + } + + const items = valueLabels(field, rule, labels).filter((label) => label !== ''); + + if (items.length === 0) { + line.appendChild(element('span', 'qb-summary-missing', words.missing || '')); + return line; + } + + const isRange = rule.operator === 'between' || rule.operator === 'notBetween'; + const join = labels.join || {}; + + items.forEach((label, index) => { + if (index > 0) { + line.appendChild(element('span', 'qb-summary-join', isRange ? (join.and || 'and') : (join.or || 'or'))); + } + line.appendChild(element('span', 'qb-summary-value', label)); + }); + + return line; +} + +// Structured recap of the tree: nesting, fields and values must stay readable +// even with a dozen rules, which a single flat sentence does not allow +export function groupBlock(group, fields, labels, depth) { + const rules = Array.isArray(group.rules) ? group.rules : []; + const combinator = group.combinator === 'or' ? 'or' : 'and'; + const block = element('div', `qb-summary-group qb-summary-depth-${Math.min(depth, 3)}`); + const headings = labels.groups || {}; + const combinators = labels.combinators || {}; + + if (rules.length > 1 || depth > 0 || group.not) { + const heading = element('div', 'qb-summary-heading'); + heading.appendChild(element( + 'span', + `qb-summary-combinator qb-summary-combinator-${combinator}`, + combinators[combinator] || combinator.toUpperCase(), + )); + heading.appendChild(element( + 'span', + 'qb-summary-heading-text', + headings[group.not ? `${combinator}Not` : combinator] || '', + )); + block.appendChild(heading); + } + + const list = element('ul', 'qb-summary-list'); + + for (const rule of rules) { + if (!rule || typeof rule !== 'object') { + continue; + } + if (Array.isArray(rule.rules)) { + const nested = element('li', 'qb-summary-nested'); + nested.appendChild(groupBlock(rule, fields, labels, depth + 1)); + list.appendChild(nested); + continue; + } + if (!rule.field) { + continue; + } + list.appendChild(ruleLine(rule, fields, labels)); + } + + block.appendChild(list); + + return block; +} diff --git a/Resources/package-lock.json b/Resources/package-lock.json new file mode 100644 index 0000000..e24deb4 --- /dev/null +++ b/Resources/package-lock.json @@ -0,0 +1,2166 @@ +{ + "name": "thelia-query-builder-module", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "thelia-query-builder-module", + "version": "2.0.0", + "dependencies": { + "@hotwired/stimulus": "^3.2.2", + "@react-querybuilder/bootstrap": "^8.23.1", + "@react-querybuilder/dnd": "^8.23.1", + "react": "^19.2.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", + "react-dnd-touch-backend": "^16.0.1", + "react-dom": "^19.2.0", + "react-querybuilder": "^8.23.1", + "redux": "^5.0.1" + }, + "devDependencies": { + "css-loader": "^7.1.2", + "mini-css-extract-plugin": "^2.9.0", + "terser-webpack-plugin": "^5.6.1", + "webpack": "^5.94.0", + "webpack-cli": "^5.1.4" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@hotwired/stimulus": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.2.2.tgz", + "integrity": "sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==", + "license": "MIT" + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@react-dnd/asap": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==", + "license": "MIT" + }, + "node_modules/@react-dnd/invariant": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==", + "license": "MIT" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==", + "license": "MIT" + }, + "node_modules/@react-querybuilder/bootstrap": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/@react-querybuilder/bootstrap/-/bootstrap-8.23.1.tgz", + "integrity": "sha512-kMk/wqh4tDzzvjcO/BJE1sdBYQf2ApWaOzb4mO04BfsvSoGuUzb7gNrEOL0gTDSZzWMNA/Nj7cvdDZwsNKDnrA==", + "license": "MIT", + "peerDependencies": { + "bootstrap": ">=5", + "bootstrap-icons": ">=1.7.0", + "react": ">=18", + "react-querybuilder": "8.23.1" + } + }, + "node_modules/@react-querybuilder/core": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/@react-querybuilder/core/-/core-8.23.1.tgz", + "integrity": "sha512-cj9QC/xaxw/unNpGZ6dOuiUNUBU9SzccIbMAZTBF2Sb64OERA70HzmBEmn8e4+RwPe9U0zEiTz1mkhDLDmFnNw==", + "license": "MIT", + "dependencies": { + "@ts-jison/lexer": "0.4.1-alpha.1", + "@ts-jison/parser": "0.4.1-alpha.1", + "@types/json-logic-js": ">=2", + "immer": "^11.1.16", + "numeric-quantity": "^3.2.2" + }, + "peerDependencies": { + "@tanstack/db": ">=0.6.9", + "@traqula/parser-sparql-1-2": ">=0.1.0", + "chevrotain": ">=11", + "drizzle-orm": ">=0.38.0", + "json-logic-js": ">=2", + "jsonata": ">=2", + "sequelize": ">=6", + "spel2js": ">=0.2.0" + }, + "peerDependenciesMeta": { + "@tanstack/db": { + "optional": true + }, + "@traqula/parser-sparql-1-2": { + "optional": true + }, + "chevrotain": { + "optional": true + }, + "drizzle-orm": { + "optional": true + }, + "json-logic-js": { + "optional": true + }, + "jsonata": { + "optional": true + }, + "sequelize": { + "optional": true + }, + "spel2js": { + "optional": true + } + } + }, + "node_modules/@react-querybuilder/dnd": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/@react-querybuilder/dnd/-/dnd-8.23.1.tgz", + "integrity": "sha512-4VCQ3KUc/dYp/zMbK5D9OSOVNrvdyEZdpisyk6+IKBvWZMEdsSJ5om9X6FNVIbvUvk07ykdtQK/9hdQcc7EYbg==", + "license": "MIT", + "peerDependencies": { + "@atlaskit/pragmatic-drag-and-drop": ">=1.0.0", + "@dnd-kit/core": ">=6.0.0", + "react": ">=18", + "react-dnd": ">=14.0.0", + "react-dnd-html5-backend": ">=14.0.0", + "react-dnd-touch-backend": ">=14.0.0", + "react-querybuilder": "8.23.1" + }, + "peerDependenciesMeta": { + "@atlaskit/pragmatic-drag-and-drop": { + "optional": true + }, + "@dnd-kit/core": { + "optional": true + }, + "react-dnd": { + "optional": true + }, + "react-dnd-html5-backend": { + "optional": true + }, + "react-dnd-touch-backend": { + "optional": true + } + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@ts-jison/common": { + "version": "0.4.1-alpha.4", + "resolved": "https://registry.npmjs.org/@ts-jison/common/-/common-0.4.1-alpha.4.tgz", + "integrity": "sha512-3P2x2i/sIh8mVeAY+lu9/NqpuLm4V+McGPce3k/1Zf5pw6fQPa/ideqNu5A5331UEEdMYlQFpP8fJDdAytY/9g==", + "license": "MIT" + }, + "node_modules/@ts-jison/lexer": { + "version": "0.4.1-alpha.1", + "resolved": "https://registry.npmjs.org/@ts-jison/lexer/-/lexer-0.4.1-alpha.1.tgz", + "integrity": "sha512-5C1Wr+wixAzn2MOFtgy7KbT6N6j9mhmbjAtyvOqZKsikKtNOQj22MM5HxT+ooRexG2NbtxnDSXYdhHR1Lg58ow==", + "license": "MIT", + "dependencies": { + "@ts-jison/common": "^0.4.1-alpha.1" + } + }, + "node_modules/@ts-jison/parser": { + "version": "0.4.1-alpha.1", + "resolved": "https://registry.npmjs.org/@ts-jison/parser/-/parser-0.4.1-alpha.1.tgz", + "integrity": "sha512-xNj+qOez/7dju44LlYiTlCjxMzW5oek9EckUAElfln/GBK9vgMSk0swWcnacMr0TYbGjUQuXvL2wEgmDf5WajQ==", + "license": "MIT", + "dependencies": { + "@ts-jison/common": "^0.4.1-alpha.1", + "@ts-jison/lexer": "^0.4.1-alpha.1" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-logic-js": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@types/json-logic-js/-/json-logic-js-2.0.8.tgz", + "integrity": "sha512-WgNsDPuTPKYXl0Jh0IfoCoJoAGGYZt5qzpmjuLSEg7r0cKp/kWtWp0HAsVepyPSPyXiHo6uXp/B/kW/2J1fa2Q==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.4.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.1.tgz", + "integrity": "sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", + "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bootstrap": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", + "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peer": true, + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/bootstrap-icons": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz", + "integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/browserslist": { + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.5.tgz", + "integrity": "sha512-Q7iAfQkU2twNBryKX/vGAlE+GAmkF7quhSzAGNK8fBimxk3+tqg245rH52UPb9dioBolBc8rP0ihmHBaDTJQBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.40", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.6.3" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/dnd-core": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", + "license": "MIT", + "dependencies": { + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" + } + }, + "node_modules/dnd-core/node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.422", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.422.tgz", + "integrity": "sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/immer": { + "version": "11.1.18", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz", + "integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.2.tgz", + "integrity": "sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimizer-webpack-plugin": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.10.0.tgz", + "integrity": "sha512-2//60T4S0X1Ug/dqLDOgDaGlZsN1Lv8hf8oB0NOV/KIHSaXlPwXBBIbim79dE2Z7NEs52ES8YHoSv6Lmsk+XNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.3", + "terser": "^5.51.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@napi-rs/image": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "imagemin": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "sharp": { + "optional": true + }, + "svgo": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/numeric-quantity": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/numeric-quantity/-/numeric-quantity-3.3.1.tgz", + "integrity": "sha512-rVWQd+eDc3LDDq7L/T//BL1eZ/LLBAnp0CY5qampKWlHdkz/SXyzQqq+cC8763w1UqZgNxRQycKB+7gd5sNumw==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.6.tgz", + "integrity": "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dnd": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", + "license": "MIT", + "dependencies": { + "@react-dnd/invariant": "^4.0.1", + "@react-dnd/shallowequal": "^4.0.1", + "dnd-core": "^16.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", + "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", + "license": "MIT", + "dependencies": { + "dnd-core": "^16.0.1" + } + }, + "node_modules/react-dnd-touch-backend": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-16.0.1.tgz", + "integrity": "sha512-NonoCABzzjyWGZuDxSG77dbgMZ2Wad7eQiCd/ECtsR2/NBLTjGksPUx9UPezZ1nQ/L7iD130Tz3RUshL/ClKLA==", + "license": "MIT", + "dependencies": { + "@react-dnd/invariant": "^4.0.1", + "dnd-core": "^16.0.1" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-querybuilder": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/react-querybuilder/-/react-querybuilder-8.23.1.tgz", + "integrity": "sha512-QrzjpB9/jc8Ak3ghH6gbyc4Qe0hKFQEUD3+7rmSEYhP9XnssOgO8hMBhZPzsz80cetCXvSIBIZX8Ik+Ywg2YHA==", + "license": "MIT", + "dependencies": { + "@react-querybuilder/core": "^8.23.1", + "@reduxjs/toolkit": "^2.12.0", + "react-redux": "^9.3.0" + }, + "peerDependencies": { + "react": ">=18" + } + }, + "node_modules/react-querybuilder/node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/react-querybuilder/node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-querybuilder/node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/reselect": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.3.0.tgz", + "integrity": "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.51.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz", + "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.6.1.tgz", + "integrity": "sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@minify-html/node": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "@swc/css": { + "optional": true + }, + "@swc/html": { + "optional": true + }, + "clean-css": { + "optional": true + }, + "cssnano": { + "optional": true + }, + "csso": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "html-minifier-terser": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "postcss": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/watchpack": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", + "integrity": "sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.110.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.110.3.tgz", + "integrity": "sha512-GuizBzRvo9YPpyoNMf3ag7AzxbaW85qrRSqTha345KyJbAFPt3/cMzBM0h+RWg7SK/7DdzRLINP3LvQ0hvr4hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.24.4", + "es-module-lexer": "^2.1.0", + "events": "^3.2.0", + "graceful-fs": "^4.2.11", + "mime-db": "^1.54.0", + "minimizer-webpack-plugin": "^5.7.0", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "watchpack": "^2.5.2", + "webpack-sources": "^3.5.1" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.5.1.tgz", + "integrity": "sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/Resources/package.json b/Resources/package.json new file mode 100644 index 0000000..b7ad9bc --- /dev/null +++ b/Resources/package.json @@ -0,0 +1,31 @@ +{ + "name": "thelia-query-builder-module", + "version": "2.0.0", + "private": true, + "author": "Samuel Diakité – OpenStudio (https://github.com/openstudio-sdiakite)", + "description": "Back-office editor bundle of the Thelia QueryBuilder module (react-querybuilder through openstudio/query-builder-bundle, Stimulus controllers of the rule and action screens).", + "scripts": { + "build": "webpack --mode production", + "watch": "webpack --mode development --watch", + "dev": "webpack --mode development" + }, + "dependencies": { + "@hotwired/stimulus": "^3.2.2", + "@react-querybuilder/bootstrap": "^8.23.1", + "@react-querybuilder/dnd": "^8.23.1", + "react": "^19.2.0", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", + "react-dnd-touch-backend": "^16.0.1", + "react-dom": "^19.2.0", + "react-querybuilder": "^8.23.1", + "redux": "^5.0.1" + }, + "devDependencies": { + "css-loader": "^7.1.2", + "mini-css-extract-plugin": "^2.9.0", + "terser-webpack-plugin": "^5.6.1", + "webpack": "^5.94.0", + "webpack-cli": "^5.1.4" + } +} diff --git a/Resources/webpack.config.js b/Resources/webpack.config.js new file mode 100644 index 0000000..b4c912d --- /dev/null +++ b/Resources/webpack.config.js @@ -0,0 +1,64 @@ +const path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const TerserPlugin = require('terser-webpack-plugin'); + +// The Stimulus controller of the editor is shipped by openstudio/query-builder-bundle in its +// assets/ directory. From Resources/ of a module installed in local/modules/QueryBuilder that +// is the project vendor/ four levels up; override with QUERY_BUILDER_BUNDLE_ASSETS otherwise. +const bundleAssets = process.env.QUERY_BUILDER_BUNDLE_ASSETS + || path.resolve(__dirname, '../../../../vendor/openstudio/query-builder-bundle/assets'); + +module.exports = (env, argv) => { + const isProduction = argv.mode === 'production'; + + return { + entry: { + 'query-builder-admin': './assets/query-builder-admin.js', + }, + output: { + // Served by the Thelia asset resolver (BaseHook::addJS/addCSS), no public path needed + path: path.resolve(__dirname, '../templates/backOffice/default-twig/assets/dist'), + filename: '[name].js', + clean: true, + }, + resolve: { + alias: { + '@openstudio/query-builder-bundle': bundleAssets, + }, + // The bundle controller lives outside this tree: its bare imports (react, stimulus...) + // must resolve from the node_modules of this build + modules: [path.resolve(__dirname, 'node_modules'), 'node_modules'], + }, + module: { + parser: { + javascript: { + // The bundle controller imports its stylesheet with import(): inline it, the + // asset resolver only copies the files the hooks reference + dynamicImportMode: 'eager', + }, + }, + rules: [ + { + test: /\.css$/i, + use: [MiniCssExtractPlugin.loader, 'css-loader'], + }, + ], + }, + plugins: [ + new MiniCssExtractPlugin({ + filename: '[name].css', + }), + ], + optimization: { + minimize: isProduction, + // Third-party licence notices stay inside the bundle instead of a side file + minimizer: [new TerserPlugin({ extractComments: false })], + splitChunks: false, + runtimeChunk: false, + }, + devtool: isProduction ? false : 'source-map', + performance: { + hints: false, + }, + }; +}; diff --git a/Service/ActiveModulesDictionaryLocator.php b/Service/ActiveModulesDictionaryLocator.php new file mode 100644 index 0000000..4d6917e --- /dev/null +++ b/Service/ActiveModulesDictionaryLocator.php @@ -0,0 +1,40 @@ +filterByActivate(BaseModule::IS_ACTIVATED) + ->filterByCode(QueryBuilder::getModuleCode(), Criteria::NOT_EQUAL) + ->orderByPosition() + ->find(); + + foreach ($modules as $module) { + //A Thelia 3 module lives either in local/modules or in vendor/thelia/modules (Composer) + $file = $module->getAbsoluteBaseDir() . DS . 'Config' . DS . DataDictionary::DICTIONARY_FILENAME; + + if (is_file($file)) { + $files[] = $file; + } + } + + return $files; + } +} diff --git a/Service/AddonRuntime.php b/Service/AddonRuntime.php new file mode 100644 index 0000000..54a9fe3 --- /dev/null +++ b/Service/AddonRuntime.php @@ -0,0 +1,45 @@ + ['onKernelRequest', 4096]]; + } + + public function onKernelRequest(): void + { + //Being instantiated is the whole point: the constructor published the instance + } +} diff --git a/Service/CartDiscountCalculator.php b/Service/CartDiscountCalculator.php new file mode 100644 index 0000000..4b1c9e2 --- /dev/null +++ b/Service/CartDiscountCalculator.php @@ -0,0 +1,49 @@ +cartDiscountResolutionService->getCartDiscount( + $this->runtimeContextFactory->forCart($cart) + ); + + if ($cartDiscount === null) { + return null; + } + + $amount = null; + + if ($cartDiscount->rate !== null) { + $productsTotal = $cart->getTaxedAmount($this->deliveryCountryResolver->resolve($cart), false); + $amount = round($productsTotal * $cartDiscount->rate / 100, 2); + } + + return new AppliedCartDiscount( + rate: $cartDiscount->rate, + label: $cartDiscount->label, + freeShipping: $cartDiscount->freeShipping, + amount: $amount, + currencyCode: $cart->getCurrency()?->getCode(), + ); + } +} diff --git a/Service/CartDiscountLedger.php b/Service/CartDiscountLedger.php new file mode 100644 index 0000000..6db97c4 --- /dev/null +++ b/Service/CartDiscountLedger.php @@ -0,0 +1,45 @@ + rule discount amount applied by this request, by cart id */ + private array $appliedAmountByCartId = []; + + /** @var array total discount written by this request, by cart id */ + private array $writtenTotalByCartId = []; + + /** The total to write in cart.discount, given the current column value and the rule amount. */ + public function nextTotal(int $cartId, float $currentDiscount, float $ruleAmount): float + { + $baseDiscount = $currentDiscount; + + if (isset($this->writtenTotalByCartId[$cartId]) + && abs($currentDiscount - $this->writtenTotalByCartId[$cartId]) < self::TOLERANCE + ) { + $baseDiscount = max(0.0, $currentDiscount - $this->appliedAmountByCartId[$cartId]); + } + + $totalDiscount = round($baseDiscount + $ruleAmount, 2); + + $this->appliedAmountByCartId[$cartId] = $ruleAmount; + $this->writtenTotalByCartId[$cartId] = $totalDiscount; + + return $totalDiscount; + } +} diff --git a/Service/CartDiscountResolutionService.php b/Service/CartDiscountResolutionService.php new file mode 100644 index 0000000..b4ed564 --- /dev/null +++ b/Service/CartDiscountResolutionService.php @@ -0,0 +1,117 @@ + */ + private array $memoizedDiscounts = []; + + public function __construct( + private readonly RuleEngine $ruleEngine, + ) { + } + + public function getCartDiscount(RuntimeContext $runtimeContext): ?CartDiscount + { + $memoKey = md5(serialize($runtimeContext)); + + if (!\array_key_exists($memoKey, $this->memoizedDiscounts)) { + $this->memoizedDiscounts[$memoKey] = $this->resolve($runtimeContext); + } + + return $this->memoizedDiscounts[$memoKey]; + } + + private function resolve(RuntimeContext $runtimeContext): ?CartDiscount + { + $actionsByRuleId = []; + + $actions = QueryBuilderActionQuery::create() + ->filterByCode(ApplyCartDiscountAction::CODE) + ->filterByActivate(1) + ->orderByPosition() + ->find(); + + foreach ($actions as $action) { + $actionsByRuleId[(int) $action->getRuleId()][] = $action; + } + + if ($actionsByRuleId === []) { + return null; + } + + $rules = QueryBuilderRuleQuery::create() + ->filterById(array_keys($actionsByRuleId), Criteria::IN) + ->filterByActivate(1) + ->orderByPosition() + ->find(); + + $bestRate = null; + $bestLabel = null; + $freeShipping = false; + + foreach ($rules as $rule) { + try { + if (!$this->ruleEngine->ruleMatches($rule, $runtimeContext)) { + continue; + } + } catch (\InvalidArgumentException $exception) { + //Placeholder runtime absent du contexte courant (ex: visiteur anonyme) + Tlog::getInstance()->addWarning(sprintf( + 'QueryBuilder: cart discount rule "%s" skipped: %s', + $rule->getName(), + $exception->getMessage() + )); + + continue; + } + + foreach ($actionsByRuleId[(int) $rule->getId()] as $action) { + $cartDiscount = CartDiscount::fromActionParameters($action->getParametersArray(), $action->getName()); + + if ($cartDiscount === null) { + Tlog::getInstance()->addWarning(sprintf( + 'QueryBuilder: ApplyCartDiscount action #%d of rule "%s" has no effect (no rate, no free shipping), skipped.', + $action->getId(), + $rule->getName() + )); + + continue; + } + + if ($cartDiscount->rate !== null && ($bestRate === null || $cartDiscount->rate > $bestRate)) { + $bestRate = $cartDiscount->rate; + $bestLabel = $cartDiscount->label; + } + + $freeShipping = $freeShipping || $cartDiscount->freeShipping; + } + } + + if ($bestRate === null && !$freeShipping) { + return null; + } + + return new CartDiscount(rate: $bestRate, freeShipping: $freeShipping, label: $bestLabel); + } +} diff --git a/Service/CartItemDiscountApplier.php b/Service/CartItemDiscountApplier.php new file mode 100644 index 0000000..4e9e532 --- /dev/null +++ b/Service/CartItemDiscountApplier.php @@ -0,0 +1,90 @@ +getCartItems(); + + if (\count($cartItems) === 0) { + return; + } + + $productIds = []; + foreach ($cartItems as $cartItem) { + $productIds[] = (int) $cartItem->getProductId(); + } + + $discounts = $this->discountResolutionService->getDiscounts( + $this->runtimeContextFactory->forCart($cart), + array_values(array_unique($productIds)) + ); + + foreach ($cartItems as $cartItem) { + $productSaleElements = $cartItem->getProductSaleElements(); + + if ($productSaleElements === null) { + continue; + } + + $catalog = $this->catalogPriceReader->forCart($productSaleElements, $cart); + $line = new LinePrices((float) $cartItem->getPrice(), (float) $cartItem->getPromoPrice(), (int) $cartItem->getPromo()); + + $discount = $discounts[(int) $cartItem->getProductId()] ?? null; + $target = $discount instanceof Discount + ? $this->cartLineDiscountPolicy->discountedLine($discount, $catalog) + : null; + + //No rule (or the catalog promotion wins): a line written by a rule goes back to the catalog + if ($target === null && $this->cartLineDiscountPolicy->carriesRuleDiscount($line, $catalog)) { + $target = $catalog; + } + + if ($target === null || $this->cartLineDiscountPolicy->sameLine($line, $target)) { + continue; + } + + $this->writeLine($cartItem, $target); + } + } + + private function writeLine(CartItem $cartItem, LinePrices $target): void + { + //Propel decimal columns are typed string, tinyint columns int, under Thelia 3 + $cartItem + ->setPrice((string) $target->price) + ->setPromoPrice((string) $target->promoPrice) + ->setPromo($target->promo) + ->save(); + } +} diff --git a/Service/CartLineDiscountResolver.php b/Service/CartLineDiscountResolver.php new file mode 100644 index 0000000..027132f --- /dev/null +++ b/Service/CartLineDiscountResolver.php @@ -0,0 +1,51 @@ +getCart(); + $productSaleElements = $cartItem->getProductSaleElements(); + + if ($cart === null || $productSaleElements === null) { + return null; + } + + $discountedPrice = $this->productPriceDiscountResolver->resolve( + $this->runtimeContextFactory->forCart($cart), + (int) $cartItem->getProductId(), + $this->catalogPriceReader->forCart($productSaleElements, $cart) + ); + + if ($discountedPrice === null) { + return null; + } + + $line = new LinePrices((float) $cartItem->getPrice(), (float) $cartItem->getPromoPrice(), (int) $cartItem->getPromo()); + + return $this->cartLineDiscountPolicy->sameLine($line, $discountedPrice->prices) ? $discountedPrice->discount : null; + } +} diff --git a/Service/CatalogPriceReader.php b/Service/CatalogPriceReader.php new file mode 100644 index 0000000..d490148 --- /dev/null +++ b/Service/CatalogPriceReader.php @@ -0,0 +1,68 @@ +read( + $productSaleElements, + $cart->getCurrency() ?? Currency::getDefaultCurrency(), + self::customerDiscount($cart->getCustomer()) + ); + } + + /** Prices as the visitor sees them: the session currency, the discount of the logged-in customer. */ + public function forSession(ProductSaleElements $productSaleElements): LinePrices + { + $session = $this->requestStack->getCurrentRequest()?->getSession(); + $customer = $session instanceof Session ? $session->getCustomerUser() : null; + + return $this->read( + $productSaleElements, + $session instanceof Session ? $session->getCurrency() : Currency::getDefaultCurrency(), + self::customerDiscount($customer instanceof Customer ? $customer : null) + ); + } + + private function read(ProductSaleElements $productSaleElements, Currency $currency, float $customerDiscount): LinePrices + { + $prices = $productSaleElements->getPricesByCurrency($currency, $customerDiscount); + + return new LinePrices( + (float) $prices->getPrice(), + (float) $prices->getPromoPrice(), + (int) $productSaleElements->getPromo() + ); + } + + private static function customerDiscount(?Customer $customer): float + { + //Same reading as the core: the DECIMAL column comes back as a string + return $customer !== null && (float) $customer->getDiscount() > 0 ? (float) $customer->getDiscount() : 0.0; + } +} diff --git a/Service/DataDictionary.php b/Service/DataDictionary.php new file mode 100644 index 0000000..dd8745f --- /dev/null +++ b/Service/DataDictionary.php @@ -0,0 +1,340 @@ + keyed by joined table name */ + public function getJoins(): array + { + return $this->load()['joins']; + } + + public function getJoin(string $table): ?JoinDefinition + { + return $this->load()['joins'][$table] ?? null; + } + + /** + * Fields available in the given context (every context when null) and for + * the given editor usage (FieldDefinition::USAGE_*, every usage when null). + * + * @return array keyed by field code + */ + public function getFields(?Context $context = null, ?string $usage = null): array + { + return array_filter( + $this->load()['fields'], + static fn (FieldDefinition $field): bool => ($context === null || $field->isAvailableInContext($context)) + && ($usage === null || $field->isAvailableForUsage($usage)) + ); + } + + public function getField(string $code): ?FieldDefinition + { + return $this->load()['fields'][$code] ?? null; + } + + /** @return array hook code => functional label, for the given context */ + public function getHooks(Context $context): array + { + //A GLOBAL rule can be bound to any hook: the same rule serves + //identical recommendations on every hook it is checked on + if ($context === Context::GLOBAL_SCOPE) { + $allHooks = []; + foreach ($this->load()['contexts'] as $hooks) { + $allHooks = array_replace($allHooks, $hooks); + } + + return $allHooks; + } + + return $this->load()['contexts'][$context->value] ?? []; + } + + public function getContextForHook(string $hookCode): ?Context + { + foreach ($this->load()['contexts'] as $contextValue => $hooks) { + if (\array_key_exists($hookCode, $hooks)) { + return Context::from($contextValue); + } + } + + return null; + } + + private function load(): array + { + if ($this->dictionary !== null) { + return $this->dictionary; + } + + $raw = $this->loadFile($this->baseFile); + + foreach ($this->overrideLocator->locate() as $file) { + $override = $this->loadFile($file); + + $raw['joins'] = array_replace($raw['joins'], $override['joins']); + $raw['fields'] = array_replace($raw['fields'], $override['fields']); + $raw['disabled'] = array_merge($raw['disabled'], $override['disabled']); + + //Union by hook code, the last module wins on the label (a project + //override can refine the label of a base hook) + foreach ($override['contexts'] as $contextValue => $hooks) { + $raw['contexts'][$contextValue] = array_replace($raw['contexts'][$contextValue] ?? [], $hooks); + } + } + + foreach ($raw['disabled'] as $disabledFieldCode) { + unset($raw['fields'][$disabledFieldCode]); + } + + return $this->dictionary = [ + 'joins' => $this->buildJoins($raw['joins']), + 'fields' => $this->buildFields($raw['fields']), + 'contexts' => $raw['contexts'], + ]; + } + + private function loadFile(string $path): array + { + $content = Yaml::parseFile($path) ?? []; + + $contexts = []; + foreach ($content['contexts'] ?? [] as $contextValue => $hooks) { + $contexts[$contextValue] = $this->normalizeHooks($hooks ?? [], (string) $contextValue); + } + + return [ + 'joins' => $content['joins'] ?? [], + 'fields' => $content['fields'] ?? [], + 'contexts' => $contexts, + 'disabled' => $content['disabled'] ?? [], + ]; + } + + /** + * A hook entry is either a plain code string or a {code, label} mapping — + * the label defaults to the code, like the label of a field definition. + * + * @return array hook code => label + */ + private function normalizeHooks(array $entries, string $contextValue): array + { + $hooks = []; + + foreach ($entries as $entry) { + if (\is_string($entry)) { + $hooks[$entry] = $entry; + continue; + } + + if (\is_array($entry) && isset($entry['code'])) { + $hooks[(string) $entry['code']] = (string) ($entry['label'] ?? $entry['code']); + continue; + } + + throw new \InvalidArgumentException(sprintf( + 'QueryBuilder dictionary: invalid hook entry in context "%s" — expected a code string or a {code, label} mapping.', + $contextValue + )); + } + + return $hooks; + } + + /** @return array */ + private function buildJoins(array $rawJoins): array + { + $joins = []; + + foreach ($rawJoins as $table => $definition) { + //"~" marks a table resolved elsewhere (ex: added by a QueryScope), no dynamic join to build + if ($definition === null) { + continue; + } + + $from = (string) ($definition['from'] ?? ''); + + if (!str_contains($from, '.')) { + throw new \InvalidArgumentException(sprintf( + 'QueryBuilder dictionary: join "%s" requires a "from" in "table.column" form.', + $table + )); + } + + [$fromTable, $fromColumn] = explode('.', $from, 2); + + $joins[$table] = new JoinDefinition( + table: (string) $table, + fromTable: $fromTable, + fromColumn: $fromColumn, + toColumn: (string) ($definition['to'] ?? 'id'), + type: strtoupper((string) ($definition['type'] ?? JoinDefinition::TYPE_INNER)), + extraOn: isset($definition['on']) ? (string) $definition['on'] : null, + multivalued: (bool) ($definition['multivalued'] ?? false), + ); + } + + return $joins; + } + + /** @return array */ + private function buildFields(array $rawFields): array + { + $fields = []; + + foreach ($rawFields as $code => $definition) { + $column = isset($definition['field']) ? (string) $definition['field'] : null; + $expression = isset($definition['expression']) ? (string) $definition['expression'] : null; + + if (($column === null) === ($expression === null)) { + throw new \InvalidArgumentException(sprintf( + 'QueryBuilder dictionary: field "%s" requires exactly one of "field" or "expression".', + $code + )); + } + + $type = (string) ($definition['type'] ?? FieldDefinition::TYPE_TEXT); + + if (!\in_array($type, FieldDefinition::TYPES, true)) { + throw new \InvalidArgumentException(sprintf( + 'QueryBuilder dictionary: field "%s" has unknown type "%s".', + $code, + $type + )); + } + + $valuesQuery = isset($definition['values_query']) ? (string) $definition['values_query'] : null; + + if ($valuesQuery !== null && $type === FieldDefinition::TYPE_BOOLEAN) { + throw new \InvalidArgumentException(sprintf( + 'QueryBuilder dictionary: field "%s" is boolean, "values_query" does not apply.', + $code + )); + } + + $field = new FieldDefinition( + code: (string) $code, + label: (string) ($definition['label'] ?? $code), + type: $type, + column: $column, + expression: $expression, + contexts: $this->buildContexts((string) $code, $definition['contexts'] ?? null), + operators: $definition['operators'] ?? null, + valuesQuery: $valuesQuery, + usages: $this->buildUsages((string) $code, $definition['usage'] ?? null), + ); + + //A :value expression consumes the entered value itself: the field must + //restrict its operators to in/notIn so the comparison path never applies + if ($field->usesValuePlaceholder() + && ($field->operators === null || $field->operators === [] || array_diff($field->operators, ['in', 'notIn']) !== []) + ) { + throw new \InvalidArgumentException(sprintf( + 'QueryBuilder dictionary: field "%s" uses the :value placeholder, its "operators" must be declared among [in, notIn].', + $code + )); + } + + $fields[$code] = $field; + } + + return $fields; + } + + /** + * Contexts of a field: a list of Context values, GLOBAL (every context) when + * absent or empty. + * + * @return Context[] + */ + private function buildContexts(string $code, mixed $rawContexts): array + { + if ($rawContexts === null || $rawContexts === []) { + return [Context::GLOBAL_SCOPE]; + } + + if (!\is_array($rawContexts)) { + throw new \InvalidArgumentException(sprintf( + 'QueryBuilder dictionary: field "%s" expects a list of contexts in "contexts".', + $code + )); + } + + $contexts = []; + + foreach ($rawContexts as $contextValue) { + $context = \is_string($contextValue) ? Context::tryFrom($contextValue) : null; + + if ($context === null) { + throw new \InvalidArgumentException(sprintf( + 'QueryBuilder dictionary: field "%s" has unknown context "%s" (expected one of %s).', + $code, + \is_scalar($contextValue) ? (string) $contextValue : get_debug_type($contextValue), + implode(', ', array_map(static fn (Context $case): string => $case->value, Context::cases())) + )); + } + + $contexts[] = $context; + } + + return array_values(array_unique($contexts, \SORT_REGULAR)); + } + + /** + * Editors a field is offered in: a list of FieldDefinition::USAGE_* values, + * both when absent or empty. + * + * @return string[] + */ + private function buildUsages(string $code, mixed $rawUsages): array + { + if ($rawUsages === null || $rawUsages === []) { + return FieldDefinition::USAGES; + } + + if (!\is_array($rawUsages)) { + throw new \InvalidArgumentException(sprintf( + 'QueryBuilder dictionary: field "%s" expects a list in "usage" (values: %s).', + $code, + implode(', ', FieldDefinition::USAGES) + )); + } + + foreach ($rawUsages as $usage) { + if (!\is_string($usage) || !\in_array($usage, FieldDefinition::USAGES, true)) { + throw new \InvalidArgumentException(sprintf( + 'QueryBuilder dictionary: field "%s" has unknown usage "%s" (expected one of %s).', + $code, + \is_scalar($usage) ? (string) $usage : get_debug_type($usage), + implode(', ', FieldDefinition::USAGES) + )); + } + } + + return array_values(array_unique($rawUsages)); + } +} diff --git a/Service/DeliveryCountryResolver.php b/Service/DeliveryCountryResolver.php new file mode 100644 index 0000000..50401a1 --- /dev/null +++ b/Service/DeliveryCountryResolver.php @@ -0,0 +1,48 @@ +getCartAddressRelatedByAddressDeliveryId()?->getCountry(); + + if ($country !== null) { + return $country; + } + + $session = $this->requestStack->getCurrentRequest()?->getSession(); + $deliveryAddressId = $session instanceof Session && $session->isStarted() + ? $session->getOrder()?->getChoosenDeliveryAddress() + : null; + + if ($deliveryAddressId) { + $country = AddressQuery::create()->findPk($deliveryAddressId)?->getCountry(); + + if ($country !== null) { + return $country; + } + } + + return Country::getDefaultCountry(); + } +} diff --git a/Service/DeliveryModuleOptionPostageClearer.php b/Service/DeliveryModuleOptionPostageClearer.php new file mode 100644 index 0000000..6d59d21 --- /dev/null +++ b/Service/DeliveryModuleOptionPostageClearer.php @@ -0,0 +1,36 @@ +setPostage(0.0) + ->setPostageTax(0.0) + ->setPostageUntaxed(0.0); + } + } +} diff --git a/Service/DiscountResolutionService.php b/Service/DiscountResolutionService.php new file mode 100644 index 0000000..b50e627 --- /dev/null +++ b/Service/DiscountResolutionService.php @@ -0,0 +1,358 @@ +> */ + private array $memoizedDiscounts = []; + + /** @var array */ + private array $memoizedDiscountedProductIds = []; + + /** @var array */ + private array $memoizedActionProductIds = []; + + /** @var array> */ + private array $memoizedEligibleRules = []; + + public function __construct( + private readonly RuleEngine $ruleEngine, + private readonly SqlBuilder $sqlBuilder, + private readonly SuggestionService $suggestionService, + ) { + } + + /** @return array keyed by product id */ + public function getDiscounts(RuntimeContext $runtimeContext, array $productIds): array + { + $productIds = array_values(array_unique(array_map('intval', $productIds))); + + if ($productIds === []) { + return []; + } + + $memoKey = md5(serialize([$runtimeContext, $productIds])); + + return $this->memoizedDiscounts[$memoKey] ??= $this->resolve($runtimeContext, $productIds); + } + + public function getDiscount(RuntimeContext $runtimeContext, int $productId): ?Discount + { + return $this->getDiscounts($runtimeContext, [$productId])[$productId] ?? null; + } + + /** + * Every product matched by an active ApplyDiscount rule for this context + * (scopes applied, no other restriction). Lets the project treat rule + * discounted products as promotions in its own SQL (filters, sorting). + * + * @return int[] + */ + public function getDiscountedProductIds(RuntimeContext $runtimeContext): array + { + $memoKey = md5(serialize($runtimeContext)); + + return $this->memoizedDiscountedProductIds[$memoKey] ??= $this->resolveDiscountedProductIds($runtimeContext); + } + + /** @return int[] */ + private function resolveDiscountedProductIds(RuntimeContext $runtimeContext): array + { + $productIdsByAction = []; + + foreach ($this->getDiscountActionsByEligibleRule($runtimeContext) as [$rule, $actions]) { + foreach ($actions as $action) { + if (Discount::fromActionParameters($action->getParametersArray()) === null) { + continue; + } + + $actionProductIds = $this->getActionProductIds($rule, $action, $runtimeContext, null); + + if ($actionProductIds !== null) { + $productIdsByAction[] = $actionProductIds; + } + } + } + + return $productIdsByAction !== [] + ? array_values(array_unique(array_merge(...$productIdsByAction))) + : []; + } + + /** @return array */ + private function resolve(RuntimeContext $runtimeContext, array $productIds): array + { + $discounts = []; + + foreach ($this->getDiscountActionsByEligibleRule($runtimeContext) as [$rule, $actions]) { + foreach ($actions as $action) { + $discount = Discount::fromActionParameters($action->getParametersArray()); + + if ($discount === null) { + Tlog::getInstance()->addWarning(sprintf( + 'QueryBuilder: ApplyDiscount action #%d of rule "%s" has no valid discount_rate, skipped.', + $action->getId(), + $rule->getName() + )); + + continue; + } + + $matchedProductIds = $this->getActionProductIds($rule, $action, $runtimeContext, $productIds); + + if ($matchedProductIds === null) { + continue; + } + + foreach ($matchedProductIds as $matchedProductId) { + $currentDiscount = $discounts[$matchedProductId] ?? null; + + if ($currentDiscount === null || $discount->rate > $currentDiscount->rate) { + $discounts[$matchedProductId] = $discount; + } + } + } + } + + return $discounts; + } + + /** + * Products currently discounted by this action, restricted to the + * requested ids when given. A limited or persisted action resolves its + * product set against the full eligible universe (memoized per request, + * never restricted to the requested ids: a product must not steal a slot + * just because its price is being displayed), then intersects. A plain + * action keeps the historical single-query path. + * + * @param int[]|null $restrictToIds + * + * @return int[]|null null when the action must be skipped + */ + private function getActionProductIds( + QueryBuilderRule $rule, + QueryBuilderAction $action, + RuntimeContext $runtimeContext, + ?array $restrictToIds, + ): ?array { + $parameters = $action->getParametersArray(); + $persistDays = (int) ($parameters['persist_days'] ?? 0); + $limit = (int) ($parameters['limit'] ?? ($persistDays > 0 ? self::DEFAULT_STICKY_LIMIT : 0)); + + if ($persistDays <= 0 && $limit <= 0) { + return $this->queryActionProductIds($rule, $action, $runtimeContext, $restrictToIds, null); + } + + if ($persistDays > 0 && $runtimeContext->customerId === null) { + return null; + } + + $memoKey = md5(serialize($runtimeContext)) . '#' . (int) $action->getId(); + + if (!\array_key_exists($memoKey, $this->memoizedActionProductIds)) { + $this->memoizedActionProductIds[$memoKey] = $persistDays > 0 + ? $this->resolveStickyProductIds($rule, $action, $runtimeContext, $limit, $persistDays) + : $this->queryActionProductIds($rule, $action, $runtimeContext, null, $limit); + } + + $actionProductIds = $this->memoizedActionProductIds[$memoKey]; + + if ($actionProductIds === null || $restrictToIds === null) { + return $actionProductIds; + } + + return array_values(array_intersect($actionProductIds, $restrictToIds)); + } + + /** + * The persisted selection of the action: still-active cycles first (kept + * as long as the product stays sellable per the query scopes), completed + * up to the limit by rotating refill, cart cycles extended. + * + * @return int[]|null null when the action must be skipped + */ + private function resolveStickyProductIds( + QueryBuilderRule $rule, + QueryBuilderAction $action, + RuntimeContext $runtimeContext, + int $limit, + int $persistDays, + ): ?array { + $customerId = (int) $runtimeContext->customerId; + $actionId = (int) $action->getId(); + + $activeIds = $this->suggestionService->getActiveProductIds($customerId, $actionId); + + if ($activeIds !== []) { + try { + $sellableIds = $this->sqlBuilder->getProductIds( + null, + $runtimeContext, + null, + [sprintf('`product`.`id` IN (%s)', implode(', ', $activeIds))] + ); + } catch (\InvalidArgumentException $exception) { + $this->logSkippedAction($rule, $action, $exception); + + return null; + } + + $activeIds = array_values(array_intersect($activeIds, $sellableIds)); + } + + $stickyIds = \array_slice($activeIds, 0, $limit); + $missing = $limit - \count($stickyIds); + + if ($missing > 0) { + $eligibleIds = $this->queryActionProductIds($rule, $action, $runtimeContext, null, null); + + if ($eligibleIds !== null) { + $newProductIds = \array_slice( + $this->suggestionService->orderRefillCandidates( + array_values(array_diff($eligibleIds, $stickyIds)), + $this->suggestionService->getLastCycleEndsByProductId($customerId, $actionId) + ), + 0, + $missing + ); + + if ($newProductIds !== []) { + $this->suggestionService->recordSuggestions($customerId, $action, $newProductIds, $persistDays); + $stickyIds = array_merge($stickyIds, $newProductIds); + } + } + } + + $cartProductIds = array_values(array_intersect($stickyIds, $runtimeContext->cartProductIds)); + + if ($cartProductIds !== []) { + $this->suggestionService->extendCycles($customerId, $actionId, $cartProductIds, $persistDays); + } + + return $stickyIds; + } + + /** + * @param int[]|null $restrictToIds + * + * @return int[]|null null when the condition cannot be compiled for this context + */ + private function queryActionProductIds( + QueryBuilderRule $rule, + QueryBuilderAction $action, + RuntimeContext $runtimeContext, + ?array $restrictToIds, + ?int $limit, + ): ?array { + $extraWhere = $restrictToIds !== null + ? [sprintf('`product`.`id` IN (%s)', implode(', ', $restrictToIds))] + : []; + + try { + return $this->sqlBuilder->getProductIds( + $action->getConditionTreeArray(), + $runtimeContext, + $limit, + $extraWhere + ); + } catch (\InvalidArgumentException $exception) { + $this->logSkippedAction($rule, $action, $exception); + + return null; + } + } + + private function logSkippedAction(QueryBuilderRule $rule, QueryBuilderAction $action, \InvalidArgumentException $exception): void + { + Tlog::getInstance()->addWarning(sprintf( + 'QueryBuilder: ApplyDiscount action #%d of rule "%s" skipped: %s', + $action->getId(), + $rule->getName(), + $exception->getMessage() + )); + } + + /** + * Active rules carrying at least one active ApplyDiscount action, filtered + * on their eligibility (rule condition tree). Rule hooks are irrelevant + * here: a discount is resolved wherever a price is needed, not on a hook. + * Memoized per context: a listing resolves one product at a time. + * + * @return array + */ + private function getDiscountActionsByEligibleRule(RuntimeContext $runtimeContext): array + { + $memoKey = md5(serialize($runtimeContext)); + + return $this->memoizedEligibleRules[$memoKey] ??= $this->resolveDiscountActionsByEligibleRule($runtimeContext); + } + + /** @return array */ + private function resolveDiscountActionsByEligibleRule(RuntimeContext $runtimeContext): array + { + $actionsByRuleId = []; + + $actions = QueryBuilderActionQuery::create() + ->filterByCode(ApplyDiscountAction::CODE) + ->filterByActivate(1) + ->orderByPosition() + ->find(); + + foreach ($actions as $action) { + $actionsByRuleId[(int) $action->getRuleId()][] = $action; + } + + if ($actionsByRuleId === []) { + return []; + } + + $eligible = []; + + $rules = QueryBuilderRuleQuery::create() + ->filterById(array_keys($actionsByRuleId), Criteria::IN) + ->filterByActivate(1) + ->orderByPosition() + ->find(); + + foreach ($rules as $rule) { + if ($this->ruleEngine->ruleMatches($rule, $runtimeContext)) { + $eligible[] = [$rule, $actionsByRuleId[(int) $rule->getId()]]; + } + } + + return $eligible; + } +} diff --git a/Service/EditorLabels.php b/Service/EditorLabels.php new file mode 100644 index 0000000..6857d16 --- /dev/null +++ b/Service/EditorLabels.php @@ -0,0 +1,74 @@ + */ + public function build(): array + { + return [ + 'counter' => [ + 'none' => $this->trans('no condition'), + 'one' => $this->trans('1 condition'), + 'many' => $this->trans('%count% conditions'), + ], + 'hooks' => [ + 'count' => $this->trans('%checked% checked out of %total%'), + ], + 'confirmContextChange' => $this->trans('Changing the context drops the conditions on fields the new context does not offer. Continue?'), + 'groups' => [ + 'and' => $this->trans('All of the following conditions'), + 'or' => $this->trans('At least one of the following conditions'), + 'andNot' => $this->trans('None of the following conditions'), + 'orNot' => $this->trans('Not "at least one" of the following conditions'), + ], + 'combinators' => [ + 'and' => $this->trans('AND'), + 'or' => $this->trans('OR'), + ], + 'join' => [ + 'and' => $this->trans('and'), + 'or' => $this->trans('or'), + ], + 'values' => [ + 'true' => $this->trans('yes'), + 'false' => $this->trans('no'), + 'missing' => $this->trans('value to fill in'), + 'unknownField' => $this->trans('field unavailable in this context'), + ], + 'operators' => [ + '=' => $this->trans('is equal to'), + '!=' => $this->trans('is not equal to'), + '<' => $this->trans('is less than'), + '<=' => $this->trans('is less than or equal to'), + '>' => $this->trans('is greater than'), + '>=' => $this->trans('is greater than or equal to'), + 'contains' => $this->trans('contains'), + 'doesNotContain' => $this->trans('does not contain'), + 'beginsWith' => $this->trans('begins with'), + 'endsWith' => $this->trans('ends with'), + 'between' => $this->trans('is between'), + 'notBetween' => $this->trans('is not between'), + 'in' => $this->trans('is one of'), + 'notIn' => $this->trans('is none of'), + 'null' => $this->trans('is empty'), + 'notNull' => $this->trans('is not empty'), + ], + ]; + } + + private function trans(string $id): string + { + return Translator::getInstance()->trans($id, [], QueryBuilder::DOMAIN_NAME); + } +} diff --git a/Service/FieldsBuilder.php b/Service/FieldsBuilder.php new file mode 100644 index 0000000..a1056ad --- /dev/null +++ b/Service/FieldsBuilder.php @@ -0,0 +1,165 @@ + 'text', + FieldDefinition::TYPE_NUMBER => 'number', + FieldDefinition::TYPE_DATE => 'date', + FieldDefinition::TYPE_DATETIME => 'date', + FieldDefinition::TYPE_BOOLEAN => 'boolean', + ]; + + public function __construct( + private DataDictionary $dataDictionary, + ) { + } + + //Between the translated group of a field and its label + private const GROUP_SEPARATOR = ' / '; + + /** + * Fields of the given context and editor usage (FieldDefinition::USAGE_*), + * every field when null (the form option must accept every field: the + * context and usage restrictions are enforced by SqlBuilder::validateTree()). + * Labels are prefixed with the translated group of the field (the part of + * its code before the first underscore), so the alphabetical order of the + * editor gathers the fields of a same group. + * + * @return list, operators: list}> + */ + public function buildForContext(?Context $context, string $locale = 'fr_FR', ?string $usage = null): array + { + $fields = []; + + foreach ($this->dataDictionary->getFields($context, $usage) as $field) { + $fields[] = $this->buildField($field, $locale); + } + + usort($fields, static fn (array $a, array $b): int => strcasecmp($a['label'], $b['label'])); + + return $fields; + } + + /** @return array> keyed by context value */ + public function buildForAllContexts(string $locale = 'fr_FR', ?string $usage = null): array + { + $fieldsByContext = []; + + foreach (Context::cases() as $context) { + $fieldsByContext[$context->value] = $this->buildForContext($context, $locale, $usage); + } + + return $fieldsByContext; + } + + private function buildField(FieldDefinition $field, string $locale): array + { + $operators = Operators::forField($field); + $values = $field->valuesQuery !== null ? $this->fetchValues($field, $locale) : null; + + if ($values !== null) { + //A field with a list of values gets the select editor first, the free operators after + array_unshift($operators, self::VALUES_LIST_OPERATOR); + } + + return [ + 'name' => $field->code, + 'type' => self::VALUE_TYPES[$field->type] ?? 'text', + 'label' => $this->translateGroup($field) . self::GROUP_SEPARATOR . Translator::getInstance()->trans($field->label, [], QueryBuilder::DOMAIN_NAME), + 'labelInformation' => null, + 'values' => $values, + 'operators' => $operators, + ]; + } + + /** The group is translated through its capitalized code ("cart" => "Cart"), shown as is without translation. */ + private function translateGroup(FieldDefinition $field): string + { + return Translator::getInstance()->trans(ucfirst($field->getGroup()), [], QueryBuilder::DOMAIN_NAME); + } + + /** + * Runs the values_query of the field ("value" + optional "label" columns; a + * "group" column is accepted but flattened, the editor has no option groups). + * Returns null (free input fallback) on SQL error, oversized or empty result: + * the editor must stay usable even when a referential misbehaves. + * + * @return list|null + */ + private function fetchValues(FieldDefinition $field, string $locale): ?array + { + try { + $statement = Propel::getConnection()->prepare((string) $field->valuesQuery); + + if (str_contains((string) $field->valuesQuery, ':locale')) { + $statement->bindValue(':locale', $locale); + } + + $statement->execute(); + $rows = $statement->fetchAll(\PDO::FETCH_ASSOC); + } catch (\Exception $exception) { + Tlog::getInstance()->error('QueryBuilder: values_query failed, falling back to free input', [ + 'field' => $field->code, + 'error' => $exception->getMessage(), + ]); + + return null; + } + + if ($rows === []) { + return null; + } + + if (\count($rows) > self::MAX_VALUES) { + Tlog::getInstance()->addWarning(sprintf( + 'QueryBuilder: values_query of field "%s" returned %d rows (max %d), falling back to free input.', + $field->code, + \count($rows), + self::MAX_VALUES + )); + + return null; + } + + $values = []; + + foreach ($rows as $row) { + if (!\array_key_exists('value', $row)) { + Tlog::getInstance()->error('QueryBuilder: values_query must expose a "value" column', [ + 'field' => $field->code, + ]); + + return null; + } + + $value = (string) $row['value']; + $values[] = ['name' => $value, 'label' => (string) ($row['label'] ?? $value)]; + } + + return $values; + } +} diff --git a/Service/FrontTemplateRenderer.php b/Service/FrontTemplateRenderer.php new file mode 100644 index 0000000..b3b31fa --- /dev/null +++ b/Service/FrontTemplateRenderer.php @@ -0,0 +1,46 @@ +/modules//... + private const THEME_OVERRIDE_DIRECTORY = 'frontOffice/%s/modules/QueryBuilder/'; + + public function __construct( + private Environment $twig, + //No interface alias in the core container: the helper is only known by its service id + #[Autowire(service: 'thelia.template_helper')] + private TemplateHelperInterface $templateHelper, + ) { + } + + /** @param array $variables */ + public function render(string $templateName, array $variables): string + { + return $this->twig->render($this->resolveTemplate($templateName), $variables); + } + + private function resolveTemplate(string $templateName): string + { + $themeOverride = sprintf(self::THEME_OVERRIDE_DIRECTORY, $this->templateHelper->getActiveFrontTemplate()->getName()) . $templateName; + + return $this->twig->getLoader()->exists($themeOverride) + ? $themeOverride + : self::MODULE_TEMPLATE_DIRECTORY . $templateName; + } +} diff --git a/Service/HookResultPresenter.php b/Service/HookResultPresenter.php new file mode 100644 index 0000000..66e0807 --- /dev/null +++ b/Service/HookResultPresenter.php @@ -0,0 +1,115 @@ +, + * actions: array + * } + */ + public function present(string $hookCode, RuntimeContext $runtimeContext, bool $withOffers = true): array + { + $actions = []; + $displayLimits = []; + + foreach ($this->ruleEngine->executeHook($hookCode, $runtimeContext) as $executedAction) { + if ($executedAction->type !== ActionInterface::TYPE_DISPLAY) { + continue; + } + + $displayLimits[] = (int) ($executedAction->action->getParametersArray()['limit'] + ?? DisplayProductsListAction::DEFAULT_LIMIT); + + $actions[] = [ + 'rule' => $executedAction->rule->getName(), + 'action' => $executedAction->action->getName(), + 'product_ids' => $executedAction->result->productIds, + ]; + } + + //The hook slot is shared: several rules on the same hook fill a single + //budget (the highest action limit) in rule order, instead of stacking + //their own limits (#562). Presentation only — sticky cycles already + //recorded by the actions are never affected. + //Floor at 1 like SqlBuilder::compile() does on the per-action LIMIT, + //so a hand-written "limit: 0" never blanks the whole hook slot + $hookLimit = $displayLimits === [] ? 0 : max(1, max($displayLimits)); + $productIds = []; + + foreach ($actions as $index => $action) { + $keptIds = \array_slice( + array_values(array_diff($action['product_ids'], $productIds)), + 0, + max(0, $hookLimit - \count($productIds)) + ); + + $actions[$index]['product_ids'] = $keptIds; + $productIds = array_merge($productIds, $keptIds); + } + + //The native hook path (FrontHook) only renders product ids: skip the + //discount resolution instead of throwing its result away + if (!$withOffers) { + foreach ($actions as $index => $action) { + $actions[$index]['offers'] = []; + } + + return [ + 'hook' => $hookCode, + 'product_ids' => $productIds, + 'offers' => [], + 'actions' => $actions, + ]; + } + + //The current product carries its own offer (badge « Pour vous aujourd'hui ») + //even though it never appears in its own recommendations (#561) + $offerProductIds = $runtimeContext->productId !== null && $runtimeContext->productId > 0 + ? array_values(array_unique([...$productIds, $runtimeContext->productId])) + : $productIds; + + $offers = []; + foreach ($this->discountResolutionService->getDiscounts($runtimeContext, $offerProductIds) as $productId => $discount) { + $offers[$productId] = [ + 'rate' => $discount->rate, + 'label' => $discount->label, + 'cumulative' => $discount->cumulative, + ]; + } + + foreach ($actions as $index => $action) { + $actions[$index]['offers'] = array_intersect_key($offers, array_flip($action['product_ids'])); + } + + return [ + 'hook' => $hookCode, + 'product_ids' => $productIds, + 'offers' => $offers, + 'actions' => $actions, + ]; + } +} diff --git a/Service/ProductPriceDiscountResolver.php b/Service/ProductPriceDiscountResolver.php new file mode 100644 index 0000000..a53bf21 --- /dev/null +++ b/Service/ProductPriceDiscountResolver.php @@ -0,0 +1,38 @@ +discountResolutionService->getDiscount($runtimeContext, $productId); + + if ($discount === null) { + return null; + } + + $prices = $this->cartLineDiscountPolicy->discountedLine($discount, $catalog); + + return $prices === null ? null : new DiscountedPrice($discount, $prices); + } +} diff --git a/Service/RuleEngine.php b/Service/RuleEngine.php new file mode 100644 index 0000000..4c521cf --- /dev/null +++ b/Service/RuleEngine.php @@ -0,0 +1,162 @@ +getActiveRulesForHook($hookCode), + fn (QueryBuilderRule $rule): bool => $this->ruleMatches($rule, $runtimeContext) + )); + + if ($matchedRules === []) { + return []; + } + + //Toutes les actions actives des règles matchées en une seule requête + $actionsByRuleId = []; + $actions = QueryBuilderActionQuery::create() + ->filterByRuleId( + array_map(static fn (QueryBuilderRule $rule): int => (int) $rule->getId(), $matchedRules), + Criteria::IN + ) + ->filterByActivate(1) + ->orderByPosition() + ->orderById() + ->find(); + + foreach ($actions as $action) { + $actionsByRuleId[(int) $action->getRuleId()][] = $action; + } + + $executedActions = []; + + foreach ($matchedRules as $rule) { + foreach ($actionsByRuleId[(int) $rule->getId()] ?? [] as $action) { + $handler = $this->actionRegistry->get($action->getCode()); + + if ($handler === null) { + Tlog::getInstance()->addWarning(sprintf( + 'QueryBuilder: no ActionInterface implementation found for code "%s" (action #%d).', + $action->getCode(), + $action->getId() + )); + + continue; + } + + try { + $executedActions[] = new ExecutedAction( + $rule, + $action, + $handler::getType(), + $handler->execute($action, $runtimeContext) + ); + } catch (\InvalidArgumentException $exception) { + //A rule may reference a runtime placeholder missing from the current + //context (ex: anonymous visitor): skip the action instead of breaking the page + Tlog::getInstance()->addWarning(sprintf( + 'QueryBuilder: action #%d of rule "%s" skipped: %s', + $action->getId(), + $rule->getName(), + $exception->getMessage() + )); + } + } + } + + return $executedActions; + } + + /** @return QueryBuilderRule[] */ + private function getActiveRulesForHook(string $hookCode): array + { + $rules = QueryBuilderRuleQuery::create() + ->filterByActivate(1) + ->orderByPosition() + ->orderById() + ->find(); + + return array_values(array_filter( + iterator_to_array($rules), + static fn (QueryBuilderRule $rule): bool => \in_array($hookCode, $rule->getHookCodes(), true) + )); + } + + /** + * A rule matches when its condition tree, restricted to the entity of the + * current context (product being viewed, products of the cart...), selects + * at least one product. A rule without condition tree always matches. + * Public: also used by DiscountResolutionService for rule eligibility. + */ + public function ruleMatches(QueryBuilderRule $rule, RuntimeContext $runtimeContext): bool + { + $conditionTree = $rule->getConditionTreeArray(); + + if ($conditionTree === null) { + return true; + } + + try { + return $this->sqlBuilder->exists( + $conditionTree, + $runtimeContext, + $this->getContextRestrictions($rule, $runtimeContext) + ); + } catch (\InvalidArgumentException $exception) { + Tlog::getInstance()->addWarning(sprintf( + 'QueryBuilder: rule "%s" skipped: %s', + $rule->getName(), + $exception->getMessage() + )); + + return false; + } + } + + /** @return string[] */ + private function getContextRestrictions(QueryBuilderRule $rule, RuntimeContext $runtimeContext): array + { + $context = Context::tryFrom($rule->getContext() ?? ''); + + return match ($context) { + Context::PRODUCT => $runtimeContext->productId !== null + ? ['`product`.`id` = :product_id'] + : [], + Context::CART => ['`product`.`id` IN (:cart_product_ids)'], + Context::CATEGORY => $runtimeContext->categoryId !== null + ? ['`product`.`id` IN (SELECT product_id FROM product_category WHERE category_id = :category_id)'] + : [], + Context::BRAND => $runtimeContext->brandId !== null + ? ['`product`.`brand_id` = :brand_id'] + : [], + default => [], + }; + } +} diff --git a/Service/RuleFreeShippingEvaluator.php b/Service/RuleFreeShippingEvaluator.php new file mode 100644 index 0000000..747bd26 --- /dev/null +++ b/Service/RuleFreeShippingEvaluator.php @@ -0,0 +1,56 @@ +inner->isCouponRemovingPostage($countryId, $deliveryModuleId)) { + return true; + } + + try { + $session = $this->requestStack->getCurrentRequest()?->getSession(); + + if (!$session instanceof Session || !$session->isStarted()) { + return false; + } + + return (bool) $this->cartDiscountCalculator->resolve($session->getSessionCart($this->eventDispatcher))?->freeShipping; + } catch (\Throwable $throwable) { + Tlog::getInstance()->addError('QueryBuilder: free shipping evaluation failed: ' . $throwable->getMessage()); + + return false; + } + } +} diff --git a/Service/RuntimeContextFactory.php b/Service/RuntimeContextFactory.php new file mode 100644 index 0000000..0bfd17a --- /dev/null +++ b/Service/RuntimeContextFactory.php @@ -0,0 +1,115 @@ + $parameterProviders */ + public function __construct( + private RequestStack $requestStack, + private EventDispatcherInterface $eventDispatcher, + private DeliveryCountryResolver $deliveryCountryResolver, + #[AutowireIterator(RuntimeParameterProviderInterface::TAG)] + private iterable $parameterProviders = [], + ) { + } + + public function fromSession( + ?int $productId = null, + ?int $orderId = null, + ?int $categoryId = null, + ?int $brandId = null, + ): RuntimeContext { + $session = $this->requestStack->getCurrentRequest()?->getSession(); + $customer = $session instanceof Session ? $session->getCustomerUser() : null; + $cart = $session instanceof Session ? $session->getSessionCart($this->eventDispatcher) : null; + $country = $this->deliveryCountryResolver->resolve($cart); + + return $this->withProviderParameters(new RuntimeContext( + customerId: $customer?->getId(), + productId: $productId, + cartId: $cart?->getId(), + cartProductIds: $cart !== null ? $this->cartProductIds($cart) : [], + orderId: $orderId, + categoryId: $categoryId, + brandId: $brandId, + locale: $session instanceof Session ? ($session->getLang()?->getLocale() ?? 'fr_FR') : 'fr_FR', + cartTotal: $cart?->getTaxedAmount($country, false), + deliveryCountryId: (int) $country->getId(), + )); + } + + /** + * Context of a given cart (cart events, API cart reads): the customer is the + * cart owner, not the session user, so a listener stays right when the two differ. + */ + public function forCart(Cart $cart): RuntimeContext + { + $session = $this->requestStack->getCurrentRequest()?->getSession(); + $locale = $session instanceof Session ? $session->getLang()?->getLocale() : null; + $country = $this->deliveryCountryResolver->resolve($cart); + + return $this->withProviderParameters(new RuntimeContext( + customerId: $cart->getCustomerId() !== null ? (int) $cart->getCustomerId() : null, + cartId: $cart->getId() !== null ? (int) $cart->getId() : null, + cartProductIds: $this->cartProductIds($cart), + locale: $locale ?? Lang::getDefaultLanguage()->getLocale(), + cartTotal: $cart->getTaxedAmount($country, false), + deliveryCountryId: (int) $country->getId(), + )); + } + + /** Applies the registered project providers to an already built context. */ + public function withProviderParameters(RuntimeContext $baseContext): RuntimeContext + { + $parameters = []; + foreach ($this->parameterProviders as $parameterProvider) { + $parameters = array_merge($parameters, $parameterProvider->provide($baseContext)); + } + + if ($parameters === []) { + return $baseContext; + } + + return new RuntimeContext( + customerId: $baseContext->customerId, + productId: $baseContext->productId, + cartId: $baseContext->cartId, + cartProductIds: $baseContext->cartProductIds, + orderId: $baseContext->orderId, + categoryId: $baseContext->categoryId, + brandId: $baseContext->brandId, + locale: $baseContext->locale, + cartTotal: $baseContext->cartTotal, + deliveryCountryId: $baseContext->deliveryCountryId, + parameters: array_merge($parameters, $baseContext->parameters), + ); + } + + /** @return int[] */ + private function cartProductIds(Cart $cart): array + { + $cartProductIds = []; + foreach ($cart->getCartItems() as $cartItem) { + $cartProductIds[] = (int) $cartItem->getProductId(); + } + + return array_values(array_unique($cartProductIds)); + } +} diff --git a/Service/SqlBuilder.php b/Service/SqlBuilder.php new file mode 100644 index 0000000..204581b --- /dev/null +++ b/Service/SqlBuilder.php @@ -0,0 +1,588 @@ + $queryScopes */ + public function __construct( + private DataDictionary $dataDictionary, + #[AutowireIterator(QueryScopeInterface::TAG)] + private iterable $queryScopes = [], + ) { + } + + /** @param string[] $extraWhere trusted SQL clauses (never user input), may reference runtime placeholders */ + public function compile(?array $conditionTree, RuntimeContext $runtimeContext, ?int $limit = null, array $extraWhere = []): CompiledQuery + { + $queryParts = new QueryParts(); + + foreach ($this->queryScopes as $queryScope) { + $queryScope->apply($queryParts, $runtimeContext); + } + + if ($conditionTree !== null) { + $queryParts->addWhere($this->compileGroup($conditionTree, $queryParts)); + } + + foreach ($extraWhere as $clause) { + $queryParts->addWhere($clause); + } + + $sql = 'SELECT DISTINCT `product`.`id` FROM `product`'; + + foreach ($queryParts->getJoins() as $joinClause) { + $sql .= "\n" . $joinClause; + } + + if ($queryParts->getWhere() !== []) { + $sql .= "\nWHERE " . implode("\n AND ", $queryParts->getWhere()); + } + + if ($limit !== null) { + $sql .= "\nLIMIT " . max(1, $limit); + } + + return $this->bindRuntimeParameters($sql, $queryParts, $runtimeContext); + } + + /** + * @param string[] $extraWhere trusted SQL clauses (never user input) + * @return int[] + */ + public function getProductIds(?array $conditionTree, RuntimeContext $runtimeContext, ?int $limit = null, array $extraWhere = []): array + { + $compiledQuery = $this->compile($conditionTree, $runtimeContext, $limit, $extraWhere); + + $connection = Propel::getConnection(); + $statement = $connection->prepare($compiledQuery->sql); + + foreach ($compiledQuery->parameters as $name => $value) { + $statement->bindValue( + ':' . $name, + $value, + \is_int($value) ? \PDO::PARAM_INT : \PDO::PARAM_STR + ); + } + + $statement->execute(); + + return array_map('intval', $statement->fetchAll(\PDO::FETCH_COLUMN)); + } + + /** @param string[] $extraWhere trusted SQL clauses (never user input) */ + public function exists(?array $conditionTree, RuntimeContext $runtimeContext, array $extraWhere = []): bool + { + return $this->getProductIds($conditionTree, $runtimeContext, 1, $extraWhere) !== []; + } + + /** + * Structural validation of a condition tree (known fields, allowed + * operators, resolvable joins) without binding runtime values. When a + * context is given, fields restricted to other contexts are rejected; when + * a usage (FieldDefinition::USAGE_*) is given, fields reserved to the other + * editor are rejected — the editor field list is filtered on both, but the + * posted JSON must not be trusted to honor that filter. + * + * @throws \InvalidArgumentException when the tree is invalid + */ + public function validateTree(?array $conditionTree, ?Context $context = null, ?string $usage = null): void + { + if ($conditionTree === null) { + return; + } + + $this->compileGroup($conditionTree, new QueryParts(), $context, $usage); + } + + private function compileGroup(array $group, QueryParts $queryParts, ?Context $context = null, ?string $usage = null): string + { + $combinator = strtoupper((string) ($group['combinator'] ?? 'AND')); + + if (!\in_array($combinator, ['AND', 'OR'], true)) { + throw new \InvalidArgumentException(sprintf('QueryBuilder: unknown combinator "%s".', $combinator)); + } + + $clauses = []; + + foreach ($group['rules'] ?? [] as $rule) { + if (!\is_array($rule)) { + continue; + } + + $clauses[] = isset($rule['rules']) + ? $this->compileGroup($rule, $queryParts, $context, $usage) + : $this->compileRule($rule, $queryParts, $context, $usage); + } + + if ($clauses === []) { + return '1=1'; + } + + $sql = '(' . implode(' ' . $combinator . ' ', $clauses) . ')'; + + if (!empty($group['not'])) { + $sql = 'NOT ' . $sql; + } + + return $sql; + } + + private function compileRule(array $rule, QueryParts $queryParts, ?Context $context = null, ?string $usage = null): string + { + $fieldCode = (string) ($rule['field'] ?? ''); + $field = $this->dataDictionary->getField($fieldCode); + + if ($field === null) { + throw new \InvalidArgumentException(sprintf('QueryBuilder: unknown field "%s".', $fieldCode)); + } + + if ($context !== null && !$field->isAvailableInContext($context)) { + throw new \InvalidArgumentException(sprintf( + 'QueryBuilder: field "%s" is not available in context %s.', + $fieldCode, + $context->value + )); + } + + if ($usage !== null && !$field->isAvailableForUsage($usage)) { + throw new \InvalidArgumentException(sprintf( + 'QueryBuilder: field "%s" is not available in the %s editor.', + $fieldCode, + $usage + )); + } + + $operator = (string) ($rule['operator'] ?? ''); + + if (!\in_array($operator, Operators::forField($field), true)) { + throw new \InvalidArgumentException(sprintf( + 'QueryBuilder: operator "%s" is not allowed for field "%s".', + $operator, + $fieldCode + )); + } + + if ($field->column !== null && $this->fieldJoinChain($field) !== []) { + return $this->compileMultivaluedComparison($field, $operator, $rule['value'] ?? null, $queryParts); + } + + if ($field->usesValuePlaceholder()) { + return $this->compileValueInExpression($field, $operator, $rule['value'] ?? null, $queryParts); + } + + return $this->compileComparison( + $this->compileLeftHandSide($field, $queryParts), + $operator, + $rule['value'] ?? null, + $field, + $queryParts + ); + } + + /** + * Ordered join chain of a column field when it crosses a multivalued (1-N) + * join, empty otherwise. Multivaluedness propagates transitively: any table + * reached through a multivalued hop yields several rows per product. + * + * @return JoinDefinition[] + */ + private function fieldJoinChain(FieldDefinition $field): array + { + [$table] = explode('.', (string) $field->column, 2); + + if ($table === self::BASE_TABLE) { + return []; + } + + $chain = $this->resolveJoinChain($table, []); + + foreach ($chain as $join) { + if ($join->multivalued) { + return $chain; + } + } + + return []; + } + + /** + * A comparison on a field reached through a 1-N join must be evaluated per + * product, not per joined row: "category notIn [X]" would otherwise match + * any OTHER category attached to the product. Negative operators compile + * to NOT EXISTS around the positive comparison; null/notNull keep their + * positive form ("has a row with a NULL/non-NULL value" — ambiguous on + * multivalued fields, out of scope here). + */ + private function compileMultivaluedComparison( + FieldDefinition $field, + string $operator, + mixed $value, + QueryParts $queryParts, + ): string { + $negatedOperators = ['!=' => '=', 'notIn' => 'in', 'notBetween' => 'between', 'doesNotContain' => 'contains']; + + [$table, $column] = explode('.', (string) $field->column, 2); + + if (!preg_match(self::IDENTIFIER_PATTERN, $table) || !preg_match(self::IDENTIFIER_PATTERN, $column)) { + throw new \InvalidArgumentException(sprintf( + 'QueryBuilder dictionary: invalid identifier in field "%s".', + $field->code + )); + } + + $chain = $this->fieldJoinChain($field); + $comparison = $this->compileComparison( + $this->dateOnly($field, sprintf('`%s`.`%s`', $table, $column)), + $negatedOperators[$operator] ?? $operator, + $value, + $field, + $queryParts + ); + + $anchor = $chain[0]; + $subquery = sprintf('SELECT 1 FROM `%s`', $anchor->table); + + foreach (\array_slice($chain, 1) as $join) { + $subquery .= ' ' . $this->buildJoinClause($join); + } + + $subquery .= sprintf( + ' WHERE `%s`.`%s` = `%s`.`%s`%s AND %s', + $anchor->table, + $anchor->toColumn, + self::BASE_TABLE, + $anchor->fromColumn, + $anchor->extraOn !== null ? ' AND ' . $anchor->extraOn : '', + $comparison + ); + + return sprintf( + '%s (%s)', + isset($negatedOperators[$operator]) ? 'NOT EXISTS' : 'EXISTS', + $subquery + ); + } + + /** + * A :value expression consumes the entered list itself (expanded to one + * bound placeholder per item), the operator only selecting the polarity: + * "in" keeps the products matching the expression, "notIn" the others. + */ + private function compileValueInExpression( + FieldDefinition $field, + string $operator, + mixed $value, + QueryParts $queryParts, + ): string { + $placeholders = array_map( + fn (mixed $item): string => $queryParts->bindValue($this->normalizeValue($item, $field)), + $this->normalizeList($value) + ); + + if ($placeholders === []) { + return $operator === 'in' ? '1=0' : '1=1'; + } + + $expression = (string) preg_replace( + '/:value\b/', + implode(', ', $placeholders), + (string) $field->expression + ); + + return ($operator === 'in' ? '' : 'NOT ') . '(' . $expression . ')'; + } + + private function compileLeftHandSide(FieldDefinition $field, QueryParts $queryParts): string + { + if ($field->expression !== null) { + return $this->dateOnly($field, '(' . $field->expression . ')'); + } + + [$table, $column] = explode('.', (string) $field->column, 2); + + if (!preg_match(self::IDENTIFIER_PATTERN, $table) || !preg_match(self::IDENTIFIER_PATTERN, $column)) { + throw new \InvalidArgumentException(sprintf( + 'QueryBuilder dictionary: invalid identifier in field "%s".', + $field->code + )); + } + + $this->collectJoinChain($table, $queryParts, []); + + return $this->dateOnly($field, sprintf('`%s`.`%s`', $table, $column)); + } + + /** + * The back-office editor has no datetime type: a datetime field is entered as a + * plain date, so its column is compared on its date part. + */ + private function dateOnly(FieldDefinition $field, string $leftHandSide): string + { + return $field->type === FieldDefinition::TYPE_DATETIME ? 'DATE(' . $leftHandSide . ')' : $leftHandSide; + } + + /** @param string[] $visiting */ + private function collectJoinChain(string $table, QueryParts $queryParts, array $visiting): void + { + foreach ($this->resolveJoinChain($table, $visiting) as $join) { + $queryParts->addJoin($join->table, $this->buildJoinClause($join)); + } + } + + /** + * Ordered chain of joins linking "product" to the given table. + * + * @param string[] $visiting + * @return JoinDefinition[] + */ + private function resolveJoinChain(string $table, array $visiting): array + { + if ($table === self::BASE_TABLE) { + return []; + } + + if (\in_array($table, $visiting, true)) { + throw new \LogicException(sprintf('QueryBuilder dictionary: circular join path around "%s".', $table)); + } + + $join = $this->dataDictionary->getJoin($table); + + if ($join === null) { + throw new \InvalidArgumentException(sprintf( + 'QueryBuilder dictionary: no join path from "product" to table "%s".', + $table + )); + } + + //The source table of the join must be resolved first + return [...$this->resolveJoinChain($join->fromTable, [...$visiting, $table]), $join]; + } + + private function buildJoinClause(JoinDefinition $join): string + { + foreach ([$join->table, $join->fromTable, $join->fromColumn, $join->toColumn] as $identifier) { + if (!preg_match(self::IDENTIFIER_PATTERN, $identifier)) { + throw new \InvalidArgumentException(sprintf( + 'QueryBuilder dictionary: invalid identifier "%s" in join "%s".', + $identifier, + $join->table + )); + } + } + + if (!\in_array($join->type, [JoinDefinition::TYPE_INNER, JoinDefinition::TYPE_LEFT, JoinDefinition::TYPE_RIGHT], true)) { + throw new \InvalidArgumentException(sprintf( + 'QueryBuilder dictionary: invalid join type "%s" for table "%s".', + $join->type, + $join->table + )); + } + + return sprintf( + '%s JOIN `%s` ON `%s`.`%s` = `%s`.`%s`%s', + $join->type, + $join->table, + $join->fromTable, + $join->fromColumn, + $join->table, + $join->toColumn, + $join->extraOn !== null ? ' AND ' . $join->extraOn : '' + ); + } + + private function compileComparison( + string $leftHandSide, + string $operator, + mixed $value, + FieldDefinition $field, + QueryParts $queryParts, + ): string { + switch ($operator) { + case 'null': + return $leftHandSide . ' IS NULL'; + case 'notNull': + return $leftHandSide . ' IS NOT NULL'; + case '=': + case '!=': + case '<': + case '<=': + case '>': + case '>=': + return sprintf( + '%s %s %s', + $leftHandSide, + $operator === '!=' ? '<>' : $operator, + $queryParts->bindValue($this->normalizeValue($value, $field)) + ); + case 'contains': + case 'doesNotContain': + return sprintf( + '%s %s %s', + $leftHandSide, + $operator === 'contains' ? 'LIKE' : 'NOT LIKE', + $queryParts->bindValue('%' . $this->escapeLikeValue($value) . '%') + ); + case 'beginsWith': + return $leftHandSide . ' LIKE ' . $queryParts->bindValue($this->escapeLikeValue($value) . '%'); + case 'endsWith': + return $leftHandSide . ' LIKE ' . $queryParts->bindValue('%' . $this->escapeLikeValue($value)); + case 'between': + case 'notBetween': + [$lower, $upper] = $this->normalizeRange($value, $field); + + return sprintf( + '%s %s %s AND %s', + $leftHandSide, + $operator === 'between' ? 'BETWEEN' : 'NOT BETWEEN', + $queryParts->bindValue($lower), + $queryParts->bindValue($upper) + ); + case 'in': + case 'notIn': + $placeholders = array_map( + fn (mixed $item): string => $queryParts->bindValue($this->normalizeValue($item, $field)), + $this->normalizeList($value) + ); + + if ($placeholders === []) { + return $operator === 'in' ? '1=0' : '1=1'; + } + + return sprintf( + '%s %s (%s)', + $leftHandSide, + $operator === 'in' ? 'IN' : 'NOT IN', + implode(', ', $placeholders) + ); + default: + throw new \InvalidArgumentException(sprintf('QueryBuilder: unsupported operator "%s".', $operator)); + } + } + + private function normalizeValue(mixed $value, FieldDefinition $field): int|float|string + { + return match ($field->type) { + FieldDefinition::TYPE_BOOLEAN => (int) filter_var($value, \FILTER_VALIDATE_BOOLEAN), + FieldDefinition::TYPE_NUMBER => \is_string($value) && str_contains($value, '.') + ? (float) $value + : (int) $value, + default => (string) $value, + }; + } + + /** @return array{0: int|float|string, 1: int|float|string} */ + private function normalizeRange(mixed $value, FieldDefinition $field): array + { + $items = $this->normalizeList($value); + + if (\count($items) !== 2) { + throw new \InvalidArgumentException('QueryBuilder: a between operator requires exactly two values.'); + } + + return [ + $this->normalizeValue($items[0], $field), + $this->normalizeValue($items[1], $field), + ]; + } + + /** @return list */ + private function normalizeList(mixed $value): array + { + if (\is_array($value)) { + return array_values($value); + } + + if (\is_string($value)) { + return array_values(array_filter(array_map('trim', explode(',', $value)), static fn (string $item): bool => $item !== '')); + } + + return $value === null ? [] : [$value]; + } + + private function escapeLikeValue(mixed $value): string + { + return addcslashes((string) $value, '%_\\'); + } + + /** + * Replaces the runtime placeholders (:customer_id, :locale, :cart_product_ids...) + * used by dictionary expressions, join clauses and scopes with bound values. + * List values are expanded into one placeholder per item. + */ + private function bindRuntimeParameters(string $sql, QueryParts $queryParts, RuntimeContext $runtimeContext): CompiledQuery + { + $parameters = $queryParts->getParameters(); + $bindable = $runtimeContext->getBindableParameters(); + + preg_match_all('/:([a-z][a-z0-9_]*)/i', $sql, $matches); + + foreach (array_unique($matches[1]) as $placeholder) { + if (isset($parameters[$placeholder])) { + continue; + } + + if (!\array_key_exists($placeholder, $bindable)) { + throw new \InvalidArgumentException(sprintf( + 'QueryBuilder: unknown runtime placeholder ":%s" — not provided by the runtime context.', + $placeholder + )); + } + + $value = $bindable[$placeholder]; + + if (\is_array($value)) { + $values = $value === [] ? [self::EMPTY_LIST_SENTINEL] : array_values($value); + $expandedPlaceholders = []; + + foreach ($values as $index => $item) { + $expandedName = $placeholder . '_' . $index; + $parameters[$expandedName] = \is_int($item) || \is_float($item) ? $item : (string) $item; + $expandedPlaceholders[] = ':' . $expandedName; + } + + $sql = preg_replace( + '/:' . preg_quote($placeholder, '/') . '\b/', + implode(', ', $expandedPlaceholders), + $sql + ); + + continue; + } + + if ($value === null) { + throw new \InvalidArgumentException(sprintf( + 'QueryBuilder: runtime placeholder ":%s" is required but has no value in the current context.', + $placeholder + )); + } + + $parameters[$placeholder] = $value; + } + + return new CompiledQuery($sql, $parameters); + } +} diff --git a/Service/SuggestionService.php b/Service/SuggestionService.php new file mode 100644 index 0000000..7b0a2fc --- /dev/null +++ b/Service/SuggestionService.php @@ -0,0 +1,193 @@ +createActiveQuery($customerId) + ->filterByActionId($actionId) + ->orderByDisplayedAt() + ->find(); + + foreach ($suggestions as $suggestion) { + $productIds[] = (int) $suggestion->getProductId(); + } + + return $productIds; + } + + /** + * Registers a new display cycle for the given products. Existing rows are + * refreshed (a product re-selected after expiry or purchase starts a new + * cycle), new ones are created. + * + * @param int[] $productIds + */ + public function recordSuggestions( + int $customerId, + QueryBuilderAction $action, + array $productIds, + int $persistDays, + ?string $hook = null, + ): void { + if ($productIds === []) { + return; + } + + $now = new \DateTime(); + $expiresAt = (new \DateTime())->modify(sprintf('+%d day', max(1, $persistDays))); + + //Les cycles existants sont chargés en une seule requête (pas de findOne par produit) + $existingSuggestions = []; + $suggestions = QueryBuilderSuggestionQuery::create() + ->filterByCustomerId($customerId) + ->filterByActionId($action->getId()) + ->filterByProductId($productIds, Criteria::IN) + ->find(); + + foreach ($suggestions as $suggestion) { + $existingSuggestions[(int) $suggestion->getProductId()] = $suggestion; + } + + foreach ($productIds as $productId) { + $suggestion = $existingSuggestions[(int) $productId] + ?? (new QueryBuilderSuggestion()) + ->setCustomerId($customerId) + ->setProductId($productId) + ->setActionId($action->getId()); + + $suggestion + ->setRuleId($action->getRuleId()) + ->setHook($hook) + ->setDisplayedAt($now) + ->setExpiresAt($expiresAt) + ->setPurchasedAt(null) + ->save(); + } + } + + /** Pushes back the expiry of still-active cycles (a discounted product in the cart keeps its price). */ + public function extendCycles(int $customerId, int $actionId, array $productIds, int $persistDays): void + { + if ($productIds === []) { + return; + } + + $this->createActiveQuery($customerId) + ->filterByActionId($actionId) + ->filterByProductId($productIds, Criteria::IN) + ->update([ + 'ExpiresAt' => (new \DateTime())->modify(sprintf('+%d day', max(1, $persistDays))), + 'UpdatedAt' => new \DateTime(), + ]); + } + + /** + * Ends the active cycles of the action for every customer. Called when the + * action condition tree changes: the sticky path never re-checks the tree, + * so a still-running cycle would keep serving products the new conditions + * exclude until its natural expiry. + */ + public function expireActiveCycles(int $actionId): void + { + $now = new \DateTime(); + + QueryBuilderSuggestionQuery::create() + ->filterByActionId($actionId) + ->filterByPurchasedAt(null, Criteria::ISNULL) + ->filterByExpiresAt($now, Criteria::GREATER_THAN) + ->update(['ExpiresAt' => $now, 'UpdatedAt' => $now]); + } + + /** + * End of the latest cycle of every product ever selected for this action, + * active cycles included (their end is in the future). + * + * @return array product id => cycle end (sortable datetime string) + */ + public function getLastCycleEndsByProductId(int $customerId, int $actionId): array + { + $cycleEnds = []; + + $suggestions = QueryBuilderSuggestionQuery::create() + ->filterByCustomerId($customerId) + ->filterByActionId($actionId) + ->find(); + + foreach ($suggestions as $suggestion) { + $cycleEnd = $suggestion->getPurchasedAt() ?? $suggestion->getExpiresAt(); + $cycleEnds[(int) $suggestion->getProductId()] = $cycleEnd?->format('Y-m-d H:i:s') ?? ''; + } + + return $cycleEnds; + } + + /** + * Rotation order for the refill: products never selected first (keeping + * the eligibility order), then already-cycled products from the oldest + * cycle end — an expired product goes to the back of the queue instead of + * being immediately re-selected. + * + * @param int[] $eligibleProductIds + * @param array $lastCycleEnds + * + * @return int[] + */ + public function orderRefillCandidates(array $eligibleProductIds, array $lastCycleEnds): array + { + $fresh = []; + $cycled = []; + + foreach ($eligibleProductIds as $productId) { + if (isset($lastCycleEnds[$productId])) { + $cycled[$productId] = $lastCycleEnds[$productId]; + } else { + $fresh[] = $productId; + } + } + + asort($cycled); + + return array_merge($fresh, array_keys($cycled)); + } + + /** Ends the display cycle of the purchased products (the suggestion dies with the purchase). */ + public function markPurchased(int $customerId, array $productIds): void + { + if ($productIds === []) { + return; + } + + $now = new \DateTime(); + + //Mass update: single SQL query, no per-row hydration/save + $this->createActiveQuery($customerId) + ->filterByProductId($productIds, Criteria::IN) + ->update(['PurchasedAt' => $now, 'UpdatedAt' => $now]); + } + + private function createActiveQuery(int $customerId): QueryBuilderSuggestionQuery + { + return QueryBuilderSuggestionQuery::create() + ->filterByCustomerId($customerId) + ->filterByPurchasedAt(null, Criteria::ISNULL) + ->filterByExpiresAt(new \DateTime(), Criteria::GREATER_THAN); + } +} diff --git a/Twig/QueryBuilderExtension.php b/Twig/QueryBuilderExtension.php new file mode 100644 index 0000000..a786420 --- /dev/null +++ b/Twig/QueryBuilderExtension.php @@ -0,0 +1,73 @@ +products(...)), + ]; + } + + /** + * @param array{product_id?: int|string|null, order_id?: int|string|null, category_id?: int|string|null, brand_id?: int|string|null} $parameters + * + * @return array{hook: string, product_ids: int[], offers: array, actions: array} + */ + public function products(string $hookCode, array $parameters = []): array + { + try { + if ($hookCode === '') { + throw new \InvalidArgumentException('query_builder_products: the hook code is required.'); + } + + $runtimeContext = $this->runtimeContextFactory->fromSession( + productId: self::optionalId($parameters['product_id'] ?? null), + orderId: self::optionalId($parameters['order_id'] ?? null), + categoryId: self::optionalId($parameters['category_id'] ?? null), + brandId: self::optionalId($parameters['brand_id'] ?? null), + ); + + return $this->hookResultPresenter->present($hookCode, $runtimeContext); + } catch (\Throwable $throwable) { + Tlog::getInstance()->error('QueryBuilder: query_builder_products failed', [ + 'hook' => $hookCode, + 'error' => $throwable->getMessage(), + ]); + + return ['hook' => $hookCode, 'product_ids' => [], 'offers' => [], 'actions' => []]; + } + } + + private static function optionalId(mixed $value): ?int + { + return $value === null || $value === '' ? null : (int) $value; + } +} diff --git a/composer.json b/composer.json index 3e5e030..acdd3e3 100644 --- a/composer.json +++ b/composer.json @@ -1,17 +1,49 @@ { "name": "thelia/query-builder-module", - "description": "QueryBuilder module for Thelia — Generic rule engine: build condition trees in the back-office, compile them to SQL and trigger actions (product lists, processing, filters) on front-office hooks.", + "description": "QueryBuilder module for Thelia 3 — Generic rule engine: build condition trees in the back-office, compile them to SQL and trigger actions (product lists, cart and product discounts) on front-office hooks and through the API.", "license": "GPL-3.0-or-later", "type": "thelia-module", + "authors": [ + { + "name": "Samuel Diakité – OpenStudio", + "email": "sdiakite@openstudio.fr", + "homepage": "https://github.com/openstudio-sdiakite", + "role": "Developer" + } + ], + "support": { + "issues": "https://github.com/thelia-modules/QueryBuilder/issues", + "source": "https://github.com/thelia-modules/QueryBuilder", + "docs": "https://github.com/thelia-modules/QueryBuilder#readme" + }, "require": { - "thelia/installer": "~1.1" + "php": ">=8.3", + "thelia/installer": "~1.1", + "openstudio/query-builder-bundle": "^1.1" + }, + "require-dev": { + "thelia/core": "^3.0", + "phpunit/phpunit": "^11.5" + }, + "autoload": { + "psr-4": { + "QueryBuilder\\": "" + } + }, + "autoload-dev": { + "psr-4": { + "QueryBuilder\\Tests\\": "tests/" + } }, "extra": { "installer-name": "QueryBuilder" }, "config": { "allow-plugins": { - "thelia/installer": true + "composer/installers": false, + "symfony/flex": false, + "symfony/runtime": false, + "thelia/installer": false } } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..f76b61c --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + tests/Unit + + + tests/Integration + + + + + + . + + + Resources + templates + tests + + + diff --git a/templates/backOffice/default-twig/QueryBuilder/_condition-editor.html.twig b/templates/backOffice/default-twig/QueryBuilder/_condition-editor.html.twig new file mode 100644 index 0000000..66675ba --- /dev/null +++ b/templates/backOffice/default-twig/QueryBuilder/_condition-editor.html.twig @@ -0,0 +1,19 @@ +{# Condition tree editor: the query-builder-bundle widget plus the module feedback + (condition counter, readable summary, empty state), driven by the qb-editor + Stimulus controller declared on the enclosing form. + + Variables: field (the QueryBuilderType form view), fields (the fields of the + current context, rendered in the widget), summary_label, empty_hint. #} +
+
+ {{ summary_label }} +
+
+ +

{{ empty_hint }}

+ +
+ {{ form_errors(field) }} + {{ form_widget(field, { fields: fields }) }} +
+
diff --git a/templates/backOffice/default-twig/QueryBuilder/_layout.html.twig b/templates/backOffice/default-twig/QueryBuilder/_layout.html.twig new file mode 100644 index 0000000..6222f0c --- /dev/null +++ b/templates/backOffice/default-twig/QueryBuilder/_layout.html.twig @@ -0,0 +1,23 @@ +{# Back-office layout of the module pages. + + The module controllers extend BaseAdminController, so their views are rendered by + Thelia's TwigParser, not by the Twig environment the theme's own controllers use. + That parser assigns its own `app` variable - a plain object carrying environment, + request, session and debug - which shadows the Symfony AppVariable global for the + whole render. `app.flashes`, which the theme base reads, resolves to nothing there, + so every message added through addFlash() was dropped on the way to the page. + The session the parser does expose carries the flash bag, so the messages are read + off it instead. The markup stays the one of @BackOfficeDefaultTwig/base.html.twig. #} +{% extends '@BackOfficeDefaultTwig/base.html.twig' %} + +{% block flash %} + {% set flashes = app.session is not null ? app.session.flashBag.all : {} %} + {% for type, messages in flashes %} + {% for message in messages %} + + {% endfor %} + {% endfor %} +{% endblock %} diff --git a/templates/backOffice/default-twig/QueryBuilder/action-edit.html.twig b/templates/backOffice/default-twig/QueryBuilder/action-edit.html.twig new file mode 100644 index 0000000..4d6cb64 --- /dev/null +++ b/templates/backOffice/default-twig/QueryBuilder/action-edit.html.twig @@ -0,0 +1,135 @@ +{% extends 'QueryBuilder/_layout.html.twig' %} + +{% set d = 'querybuilder.bo.default-twig' %} +{% form_theme form with bo_form_themes|merge(['@OpenStudioQueryBuilder/form/query_builder.html.twig']) only %} + +{% block title %}{{ 'Action'|trans({}, d) }} {{ action.name }}{% endblock %} +{% block body_testid %}query-builder-action-edit{% endblock %} + +{% block breadcrumb %} + +{% endblock %} + +{% block content %} + {% set edit_url = path('admin_query_builder_action_edit', { ruleId: rule.id, actionId: action.id }) %} + {% set rule_url = path('admin_query_builder_rule_edit', { ruleId: rule.id }) %} + +
+ {% embed '@BackOfficeDefaultTwig/_page_header.html.twig' with { title: 'Action'|trans({}, d) ~ ' « ' ~ action.name ~ ' »' } %} + {% block title %} + {{ title }} + {{ 'rule'|trans({}, d) }} « {{ rule.name }} », {{ 'context'|trans({}, d) }} {{ rule.context_label }} + {% endblock %} + {% block actions %} + + {{ 'Back to the rule'|trans({}, d) }} + + {% endblock %} + {% endembed %} + +
+ {{ form_widget(form._token) }} + + + +
+
+
+
+ {{ form_row(form.name, { label: 'Name'|trans({}, d) }) }} + {{ form_row(form.description, { label: 'Description'|trans({}, d), attr: { rows: 2 } }) }} + {{ form_row(form.activate, { label: 'Active action'|trans({}, d) }) }} +
+
+
+ + + {% do form.code.setRendered %} +
+ +
+
+ {{ form_row(form.limit, { label: 'Number of products'|trans({}, d), attr: { min: 1, max: 50, placeholder: 3 } }) }} +
+
+ {{ form_row(form.persist_days, { label: 'Suggestions persistence (days)'|trans({}, d), attr: { min: 1, max: 365, placeholder: 'empty = no persistence'|trans({}, d) }, help: 'The same products are served again for N days or until purchased.'|trans({}, d) }) }} +
+
+ +
+ {{ 'Discount on the selected products'|trans({}, d) }} +
+
+ {{ form_row(form.discount_rate, { label: 'Rate (%)'|trans({}, d), attr: { min: 0.01, max: 99.99, step: 0.01 } }) }} +
+
+ {{ form_row(form.discount_label, { label: 'Label shown to the customer'|trans({}, d) }) }} +
+
+ {{ form_row(form.discount_cumulative, { label: 'Stackable with an existing promotion'|trans({}, d), help: 'The discount holds as long as the rule is active, for every eligible customer. Not stackable: it only applies when better than the current promotion.'|trans({}, d) }) }} +
+ +
+ {{ 'Global cart discount'|trans({}, d) }} +
+
+ {{ form_row(form.cart_discount_rate, { label: 'Rate (%)'|trans({}, d), attr: { min: 0.01, max: 99.99, step: 0.01 } }) }} +
+
+ {{ form_row(form.cart_discount_free_shipping, { label: 'Free shipping'|trans({}, d) }) }} +
+
+
{{ 'Percentage off the cart products total (shipping excluded), as long as the rule is active, for every eligible customer. Eligibility is carried by the rule conditions.'|trans({}, d) }}
+
+
+
+
+
+ +
+
+
+

{{ 'Product selection of the action'|trans({}, d) }}

+
+ +
+
+ {% include 'QueryBuilder/_condition-editor.html.twig' with { + field: form.condition_tree, + fields: context_fields, + summary_label: 'Reading of the selection'|trans({}, d), + empty_hint: 'No criterion: the action applies to the whole catalog.'|trans({}, d), + } only %} +
+
+ +
+ {% if is_granted('UPDATE', { module: 'QueryBuilder' }) %} + + {% endif %} + {{ 'Back to the rule'|trans({}, d) }} +
+
+
+{% endblock %} diff --git a/templates/backOffice/default-twig/QueryBuilder/rule-edit.html.twig b/templates/backOffice/default-twig/QueryBuilder/rule-edit.html.twig new file mode 100644 index 0000000..f8a7595 --- /dev/null +++ b/templates/backOffice/default-twig/QueryBuilder/rule-edit.html.twig @@ -0,0 +1,296 @@ +{% extends 'QueryBuilder/_layout.html.twig' %} + +{% set d = 'querybuilder.bo.default-twig' %} +{% form_theme form with bo_form_themes|merge(['@OpenStudioQueryBuilder/form/query_builder.html.twig']) only %} +{% form_theme action_form with bo_form_themes only %} + +{% block title %}{{ 'Rule'|trans({}, d) }} {{ rule.name }}{% endblock %} +{% block body_testid %}query-builder-rule-edit{% endblock %} + +{% block breadcrumb %} + +{% endblock %} + +{% block content %} + {% set edit_url = path('admin_query_builder_rule_edit', { ruleId: rule.id }) %} + +
+ {% embed '@BackOfficeDefaultTwig/_page_header.html.twig' with { title: 'Rule'|trans({}, d) ~ ' « ' ~ rule.name ~ ' »' } %} + {% block title %} + {{ title }} + {% if rule.activate %} + {{ 'Active'|trans({}, d) }} + {% else %} + {{ 'Inactive'|trans({}, d) }} + {% endif %} + {{ rule.context_label }} + {% endblock %} + {% block actions %} + + {{ 'Back to the list'|trans({}, d) }} + + {% endblock %} + {% endembed %} + +

{{ 'A rule runs when one of its hooks is called, if its trigger conditions are true. It then produces its actions.'|trans({}, d) }}

+ +
+ {{ form_widget(form._token) }} + + + + {# ------------------------------------------------------------ 1. Identification #} +
+
+ 1 +
+

{{ 'Identification'|trans({}, d) }}

+
{{ 'Internal use: this information never appears to the customer.'|trans({}, d) }}
+
+
+
+
+
+ {{ form_row(form.name, { label: 'Rule name'|trans({}, d) }) }} + {{ form_row(form.activate, { label: 'Active rule'|trans({}, d), help: 'An inactive rule is never evaluated.'|trans({}, d) }) }} +
+
+ {{ form_row(form.description, { label: 'Description'|trans({}, d), attr: { rows: 4, placeholder: 'What this rule is for, for whom, until when…'|trans({}, d) } }) }} +
+
+
+
+ + {# ------------------------------------------------------------------ 2. Trigger #} +
+
+ 2 +
+

{{ 'When is the rule evaluated?'|trans({}, d) }}

+
{{ 'The context sets the evaluated object, the available fields and the possible hooks.'|trans({}, d) }}
+
+
+
+
+
+
+ + + {% do form.context.setRendered %} +
+
+ + {{ 'Changing the context drops the conditions on fields the new context does not offer.'|trans({}, d) }} +
+
+
+
+
+ + +
+ + + +
+ +
+ {% for hook in hook_choices %} + + {% endfor %} +
+ + +
{{ 'No hook checked: the rule only runs through the API.'|trans({}, d) }}
+
+
+
+
+
+ + {# --------------------------------------------------------------- 3. Conditions #} +
+
+ 3 +
+

{{ 'Trigger conditions'|trans({}, d) }}

+
{{ 'Optional. Without a condition, the rule triggers on every call of its hooks.'|trans({}, d) }}
+
+ +
+
+ {% include 'QueryBuilder/_condition-editor.html.twig' with { + field: form.condition_tree, + fields: context_fields, + summary_label: 'Reading of the condition'|trans({}, d), + empty_hint: 'No condition: add one to restrict the trigger, otherwise the rule always applies.'|trans({}, d), + } only %} +
+
+ +
+ {% if is_granted('UPDATE', { module: 'QueryBuilder' }) %} + + {% endif %} + {{ 'Back to the list'|trans({}, d) }} + {{ 'The actions below are saved separately.'|trans({}, d) }} +
+
+ + {# ------------------------------------------------------------------ 4. Actions #} +
+
+

{{ 'Actions of the rule'|trans({}, d) }}

+ {{ actions|length }} +
+
+

{{ 'What the rule produces when it triggers: each action has its own product selection and parameters.'|trans({}, d) }}

+ + {% if is_granted('CREATE', { module: 'QueryBuilder' }) %} +
+ {{ form_widget(action_form._token) }} + +
+ + +
+
+ + +
+
+ +
+
+ {% endif %} + + {% if actions is empty %} +
{{ 'No action: the rule produces nothing for now.'|trans({}, d) }}
+ {% else %} +
+ + + + + + + + + + + + {% for action in actions %} + + + + + + + + {% endfor %} + +
{{ 'Name'|trans({}, d) }}{{ 'Action'|trans({}, d) }}{{ 'Type'|trans({}, d) }}{{ 'Status'|trans({}, d) }}
+ {% if is_granted('VIEW', { module: 'QueryBuilder' }) %} + {{ action.name }} + {% else %} + {{ action.name }} + {% endif %} + {# Parameters worth reading from here: the discount rate, and the options only when on #} + {% set details = [] %} + {% set rate = action.parameters.discount_rate ?? action.parameters.cart_discount_rate ?? null %} + {% if rate %} + {% set details = details|merge(['Discount rate: %rate% %'|trans({ '%rate%': rate }, d)]) %} + {% endif %} + {% if action.parameters.cart_discount_free_shipping ?? false %} + {% set details = details|merge(['Free shipping'|trans({}, d)]) %} + {% endif %} + {% if action.parameters.discount_cumulative ?? false %} + {% set details = details|merge(['Stackable with an existing promotion'|trans({}, d)]) %} + {% endif %} + + {{ action.code }}{{ action.type }} + {% if is_granted('UPDATE', { module: 'QueryBuilder' }) %} +
+ {% if action.activate %} + + {% else %} + + {% endif %} +
+ {% else %} + {% if action.activate %} + {{ 'Active'|trans({}, d) }} + {% else %} + {{ 'Inactive'|trans({}, d) }} + {% endif %} + {% endif %} +
+ {% if is_granted('VIEW', { module: 'QueryBuilder' }) %} + + + + {% endif %} + {% if is_granted('DELETE', { module: 'QueryBuilder' }) %} +
+ +
+ {% endif %} +
+
+ {% endif %} +
+
+
+{% endblock %} diff --git a/templates/backOffice/default-twig/QueryBuilder/rule-list.html.twig b/templates/backOffice/default-twig/QueryBuilder/rule-list.html.twig new file mode 100644 index 0000000..781f0c1 --- /dev/null +++ b/templates/backOffice/default-twig/QueryBuilder/rule-list.html.twig @@ -0,0 +1,122 @@ +{% extends 'QueryBuilder/_layout.html.twig' %} + +{% set d = 'querybuilder.bo.default-twig' %} +{% form_theme create_form with bo_form_themes only %} + +{% block title %}{{ 'Query Builder'|trans({}, d) }}{% endblock %} +{% block body_testid %}query-builder-rules{% endblock %} + +{% block breadcrumb %} + +{% endblock %} + +{% block content %} +
+ {% embed '@BackOfficeDefaultTwig/_page_header.html.twig' with { title: 'Rules'|trans({}, d) } %}{% endembed %} + + {% if is_granted('CREATE', { module: 'QueryBuilder' }) %} +
+
+
+ {{ form_widget(create_form._token) }} + +
+ + +
+
+ + +
+
+ +
+
+
+
+ {% endif %} + + {% if rules is empty %} +
{{ 'No rule yet.'|trans({}, d) }}
+ {% else %} +
+
+ + + + + + + + + + + + + {% for rule in rules %} + + + + + + + + + {% endfor %} + +
{{ 'Name'|trans({}, d) }}{{ 'Context'|trans({}, d) }}{{ 'Hooks'|trans({}, d) }}{{ 'Actions'|trans({}, d) }}{{ 'Status'|trans({}, d) }}
+ {{ rule.name }} + {% if rule.description %}
{{ rule.description|length > 80 ? rule.description|slice(0, 80) ~ '…' : rule.description }}
{% endif %} +
{{ rule.context_label }} + {% for hook in rule.hooks %} + {{ hook.label }} + {% else %} + – + {% endfor %} + {{ rule.action_count }} + {% if is_granted('UPDATE', { module: 'QueryBuilder' }) %} +
+ {% if rule.activate %} + + {% else %} + + {% endif %} +
+ {% else %} + {% if rule.activate %} + {{ 'Active'|trans({}, d) }} + {% else %} + {{ 'Inactive'|trans({}, d) }} + {% endif %} + {% endif %} +
+ {% if is_granted('VIEW', { module: 'QueryBuilder' }) %} + + + + {% endif %} + {% if is_granted('DELETE', { module: 'QueryBuilder' }) %} +
+ +
+ {% endif %} +
+
+
+ {% endif %} +
+{% endblock %} diff --git a/templates/backOffice/default-twig/assets/dist/query-builder-admin.css b/templates/backOffice/default-twig/assets/dist/query-builder-admin.css new file mode 100644 index 0000000..ea4edbf --- /dev/null +++ b/templates/backOffice/default-twig/assets/dist/query-builder-admin.css @@ -0,0 +1,378 @@ +:root{--rqb-spacing: 0.5rem;--rqb-border-width: 1px;--rqb-branch-indent: var(--rqb-spacing);--rqb-branch-width: var(--rqb-border-width);--rqb-base-color: #004bb8;--rqb-background-color: color-mix(in srgb, transparent, #004bb8 20%);--rqb-border-color: #8081a2;--rqb-border-style: solid;--rqb-border-radius: 0.25rem;--rqb-dnd-drop-indicator-color: rebeccapurple;--rqb-dnd-drop-indicator-style: dashed;--rqb-dnd-drop-indicator-width: 2px;--rqb-dnd-drop-indicator-copy-color: #693;--rqb-dnd-drop-indicator-copy-style: dashed;--rqb-dnd-drop-indicator-copy-width: 2px;--rqb-dnd-drop-indicator-group-color: #369;--rqb-dnd-drop-indicator-group-style: dashed;--rqb-dnd-drop-indicator-group-width: 2px;--rqb-dnd-drop-not-allowed-cursor: not-allowed;--rqb-branch-color: var(--rqb-border-color);--rqb-branch-radius: var(--rqb-border-radius);--rqb-branch-style: var(--rqb-border-style)}.ruleGroup{display:flex;flex-direction:column;gap:var(--rqb-spacing);padding:var(--rqb-spacing);border-width:var(--rqb-border-width)}.ruleGroup .ruleGroup-body{display:flex;flex-direction:column;gap:var(--rqb-spacing)}.ruleGroup .ruleGroup-body:empty{display:none}.ruleGroup .ruleGroup-header,.ruleGroup .rule,.ruleGroup .undoRedoActions{display:flex;gap:var(--rqb-spacing);align-items:center}.ruleGroup .rule .rule-value:has(.rule-value-list-item){display:flex;gap:var(--rqb-spacing);align-items:baseline}.ruleGroup .rule.rule-hasSubQuery{flex-wrap:wrap;padding:var(--rqb-spacing);border-width:var(--rqb-border-width)}.ruleGroup .rule.rule-hasSubQuery .ruleGroup-body{flex-basis:100%}.ruleGroup .betweenRules{display:flex}.ruleGroup .shiftActions{display:flex;flex-direction:column}.ruleGroup .shiftActions>*{padding:0}.dndOver.rule,.dndOver.ruleGroup-header{padding-bottom:var(--rqb-spacing)}.dndOver.betweenRules{padding-top:var(--rqb-spacing)}.dndGroup.rule.dndOver,.dndGroup.ruleGroup:has(.ruleGroup-header.dndOver){padding:var(--rqb-spacing)}.queryBuilder-branches:dir(ltr) .ruleGroup-body{margin-left:calc(2*var(--rqb-branch-indent))}.queryBuilder-branches:dir(ltr) .rule,.queryBuilder-branches:dir(ltr) .ruleGroup .ruleGroup{position:relative}.queryBuilder-branches:dir(ltr) .rule::before,.queryBuilder-branches:dir(ltr) .rule::after,.queryBuilder-branches:dir(ltr) .ruleGroup .ruleGroup::before,.queryBuilder-branches:dir(ltr) .ruleGroup .ruleGroup::after{content:"";width:var(--rqb-branch-indent);left:calc(-1*(var(--rqb-branch-indent) + var(--rqb-branch-width)));border-radius:0;position:absolute}.queryBuilder-branches:dir(ltr) .rule::before,.queryBuilder-branches:dir(ltr) .ruleGroup .ruleGroup::before{top:calc(-1*var(--rqb-spacing));height:calc(50% + var(--rqb-spacing));border-width:0 0 var(--rqb-branch-width) var(--rqb-branch-width)}.queryBuilder-branches:dir(ltr) .rule::after,.queryBuilder-branches:dir(ltr) .ruleGroup .ruleGroup::after{top:50%;height:50%;border-width:0 0 0 var(--rqb-branch-width)}.queryBuilder-branches:dir(ltr) .rule:last-child::after,.queryBuilder-branches:dir(ltr) .ruleGroup .ruleGroup:last-child::after{display:none}.queryBuilder-branches:dir(ltr) .rule-hasSubQuery::before,.queryBuilder-branches:dir(ltr) .rule-hasSubQuery::after,.queryBuilder-branches:dir(ltr) .ruleGroup .ruleGroup::before,.queryBuilder-branches:dir(ltr) .ruleGroup .ruleGroup::after{left:calc(-1*(var(--rqb-branch-indent) + var(--rqb-branch-width) + var(--rqb-border-width)))}.queryBuilder-branches:dir(ltr) .rule-hasSubQuery::before,.queryBuilder-branches:dir(ltr) .ruleGroup .ruleGroup::before{top:calc(-1*(var(--rqb-spacing) + var(--rqb-border-width)));height:calc(50% + var(--rqb-spacing) + var(--rqb-border-width))}.queryBuilder-branches:dir(ltr) .rule-hasSubQuery::after,.queryBuilder-branches:dir(ltr) .ruleGroup .ruleGroup::after{height:calc(50% + var(--rqb-border-width))}.queryBuilder-branches:dir(ltr) .betweenRules{position:relative}.queryBuilder-branches:dir(ltr) .betweenRules::before{content:"";width:var(--rqb-branch-indent);left:calc(-1*(var(--rqb-branch-indent) + var(--rqb-branch-width)));border-radius:0;position:absolute;top:calc(-1*var(--rqb-spacing));height:calc(100% + var(--rqb-spacing));border-width:0 0 0 var(--rqb-branch-width)}.queryBuilder-branches:dir(rtl) .ruleGroup-body{margin-right:calc(2*var(--rqb-branch-indent))}.queryBuilder-branches:dir(rtl) .rule,.queryBuilder-branches:dir(rtl) .ruleGroup .ruleGroup{position:relative}.queryBuilder-branches:dir(rtl) .rule::before,.queryBuilder-branches:dir(rtl) .rule::after,.queryBuilder-branches:dir(rtl) .ruleGroup .ruleGroup::before,.queryBuilder-branches:dir(rtl) .ruleGroup .ruleGroup::after{content:"";width:var(--rqb-branch-indent);right:calc(-1*(var(--rqb-branch-indent) + var(--rqb-branch-width)));border-radius:0;position:absolute}.queryBuilder-branches:dir(rtl) .rule::after,.queryBuilder-branches:dir(rtl) .ruleGroup .ruleGroup::after{top:calc(-1*var(--rqb-spacing));height:calc(50% + var(--rqb-spacing));border-width:0 var(--rqb-branch-width) var(--rqb-branch-width) 0}.queryBuilder-branches:dir(rtl) .rule::before,.queryBuilder-branches:dir(rtl) .ruleGroup .ruleGroup::before{top:50%;height:50%;border-width:0 var(--rqb-branch-width) 0 0}.queryBuilder-branches:dir(rtl) .rule:last-child::before,.queryBuilder-branches:dir(rtl) .ruleGroup .ruleGroup:last-child::before{display:none}.queryBuilder-branches:dir(rtl) .rule-hasSubQuery::before,.queryBuilder-branches:dir(rtl) .rule-hasSubQuery::after,.queryBuilder-branches:dir(rtl) .ruleGroup .ruleGroup::before,.queryBuilder-branches:dir(rtl) .ruleGroup .ruleGroup::after{right:calc(-1*(var(--rqb-branch-indent) + var(--rqb-branch-width) + var(--rqb-border-width)))}.queryBuilder-branches:dir(rtl) .rule-hasSubQuery::after,.queryBuilder-branches:dir(rtl) .ruleGroup .ruleGroup::after{top:calc(-1*(var(--rqb-spacing) + var(--rqb-border-width)));height:calc(50% + var(--rqb-spacing) + var(--rqb-border-width))}.queryBuilder-branches:dir(rtl) .rule-hasSubQuery::before,.queryBuilder-branches:dir(rtl) .ruleGroup .ruleGroup::before{height:calc(50% + var(--rqb-border-width))}.queryBuilder-branches:dir(rtl) .betweenRules{position:relative}.queryBuilder-branches:dir(rtl) .betweenRules::after{content:"";width:var(--rqb-branch-indent);right:calc(-1*(var(--rqb-branch-indent) + var(--rqb-branch-width)));border-radius:0;position:absolute;top:calc(-1*var(--rqb-spacing));height:calc(100% + var(--rqb-spacing));border-width:0 var(--rqb-branch-width) 0 0}.queryBuilder-justified:dir(ltr) .ruleGroup-combinators+.ruleGroup-addRule,.queryBuilder-justified:dir(ltr) .ruleGroup-combinators+.ruleGroup-addGroup,.queryBuilder-justified:dir(ltr) .ruleGroup-combinators+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(ltr) .ruleGroup-combinators+.ruleGroup-lock,.queryBuilder-justified:dir(ltr) .ruleGroup-combinators+.ruleGroup-remove,.queryBuilder-justified:dir(ltr) .ruleGroup-combinators+.undoRedoActions,.queryBuilder-justified:dir(ltr) .ruleGroup-combinators+.rule-cloneRule,.queryBuilder-justified:dir(ltr) .ruleGroup-combinators+.rule-lock,.queryBuilder-justified:dir(ltr) .ruleGroup-combinators+.rule-remove,.queryBuilder-justified:dir(ltr) .ruleGroup-notToggle+.ruleGroup-addRule,.queryBuilder-justified:dir(ltr) .ruleGroup-notToggle+.ruleGroup-addGroup,.queryBuilder-justified:dir(ltr) .ruleGroup-notToggle+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(ltr) .ruleGroup-notToggle+.ruleGroup-lock,.queryBuilder-justified:dir(ltr) .ruleGroup-notToggle+.ruleGroup-remove,.queryBuilder-justified:dir(ltr) .ruleGroup-notToggle+.undoRedoActions,.queryBuilder-justified:dir(ltr) .ruleGroup-notToggle+.rule-cloneRule,.queryBuilder-justified:dir(ltr) .ruleGroup-notToggle+.rule-lock,.queryBuilder-justified:dir(ltr) .ruleGroup-notToggle+.rule-remove,.queryBuilder-justified:dir(ltr) .shiftActions+.ruleGroup-addRule,.queryBuilder-justified:dir(ltr) .shiftActions+.ruleGroup-addGroup,.queryBuilder-justified:dir(ltr) .shiftActions+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(ltr) .shiftActions+.ruleGroup-lock,.queryBuilder-justified:dir(ltr) .shiftActions+.ruleGroup-remove,.queryBuilder-justified:dir(ltr) .shiftActions+.undoRedoActions,.queryBuilder-justified:dir(ltr) .shiftActions+.rule-cloneRule,.queryBuilder-justified:dir(ltr) .shiftActions+.rule-lock,.queryBuilder-justified:dir(ltr) .shiftActions+.rule-remove,.queryBuilder-justified:dir(ltr) .queryBuilder-dragHandle+.ruleGroup-addRule,.queryBuilder-justified:dir(ltr) .queryBuilder-dragHandle+.ruleGroup-addGroup,.queryBuilder-justified:dir(ltr) .queryBuilder-dragHandle+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(ltr) .queryBuilder-dragHandle+.ruleGroup-lock,.queryBuilder-justified:dir(ltr) .queryBuilder-dragHandle+.ruleGroup-remove,.queryBuilder-justified:dir(ltr) .queryBuilder-dragHandle+.undoRedoActions,.queryBuilder-justified:dir(ltr) .queryBuilder-dragHandle+.rule-cloneRule,.queryBuilder-justified:dir(ltr) .queryBuilder-dragHandle+.rule-lock,.queryBuilder-justified:dir(ltr) .queryBuilder-dragHandle+.rule-remove,.queryBuilder-justified:dir(ltr) .ruleGroup-notToggle+.ruleGroup-addRule,.queryBuilder-justified:dir(ltr) .ruleGroup-notToggle+.ruleGroup-addGroup,.queryBuilder-justified:dir(ltr) .ruleGroup-notToggle+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(ltr) .ruleGroup-notToggle+.ruleGroup-lock,.queryBuilder-justified:dir(ltr) .ruleGroup-notToggle+.ruleGroup-remove,.queryBuilder-justified:dir(ltr) .ruleGroup-notToggle+.undoRedoActions,.queryBuilder-justified:dir(ltr) .ruleGroup-notToggle+.rule-cloneRule,.queryBuilder-justified:dir(ltr) .ruleGroup-notToggle+.rule-lock,.queryBuilder-justified:dir(ltr) .ruleGroup-notToggle+.rule-remove,.queryBuilder-justified:dir(ltr) .ruleGroup-header+.ruleGroup-addRule,.queryBuilder-justified:dir(ltr) .ruleGroup-header+.ruleGroup-addGroup,.queryBuilder-justified:dir(ltr) .ruleGroup-header+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(ltr) .ruleGroup-header+.ruleGroup-lock,.queryBuilder-justified:dir(ltr) .ruleGroup-header+.ruleGroup-remove,.queryBuilder-justified:dir(ltr) .ruleGroup-header+.undoRedoActions,.queryBuilder-justified:dir(ltr) .ruleGroup-header+.rule-cloneRule,.queryBuilder-justified:dir(ltr) .ruleGroup-header+.rule-lock,.queryBuilder-justified:dir(ltr) .ruleGroup-header+.rule-remove,.queryBuilder-justified:dir(ltr) .rule-operators+.ruleGroup-addRule,.queryBuilder-justified:dir(ltr) .rule-operators+.ruleGroup-addGroup,.queryBuilder-justified:dir(ltr) .rule-operators+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(ltr) .rule-operators+.ruleGroup-lock,.queryBuilder-justified:dir(ltr) .rule-operators+.ruleGroup-remove,.queryBuilder-justified:dir(ltr) .rule-operators+.undoRedoActions,.queryBuilder-justified:dir(ltr) .rule-operators+.rule-cloneRule,.queryBuilder-justified:dir(ltr) .rule-operators+.rule-lock,.queryBuilder-justified:dir(ltr) .rule-operators+.rule-remove,.queryBuilder-justified:dir(ltr) .rule-value+.ruleGroup-addRule,.queryBuilder-justified:dir(ltr) .rule-value+.ruleGroup-addGroup,.queryBuilder-justified:dir(ltr) .rule-value+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(ltr) .rule-value+.ruleGroup-lock,.queryBuilder-justified:dir(ltr) .rule-value+.ruleGroup-remove,.queryBuilder-justified:dir(ltr) .rule-value+.undoRedoActions,.queryBuilder-justified:dir(ltr) .rule-value+.rule-cloneRule,.queryBuilder-justified:dir(ltr) .rule-value+.rule-lock,.queryBuilder-justified:dir(ltr) .rule-value+.rule-remove,.queryBuilder-justified:dir(ltr) .control+.ruleGroup-addRule,.queryBuilder-justified:dir(ltr) .control+.ruleGroup-addGroup,.queryBuilder-justified:dir(ltr) .control+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(ltr) .control+.ruleGroup-lock,.queryBuilder-justified:dir(ltr) .control+.ruleGroup-remove,.queryBuilder-justified:dir(ltr) .control+.undoRedoActions,.queryBuilder-justified:dir(ltr) .control+.rule-cloneRule,.queryBuilder-justified:dir(ltr) .control+.rule-lock,.queryBuilder-justified:dir(ltr) .control+.rule-remove,.queryBuilder-justified:dir(ltr) .chakra-select__wrapper+.ruleGroup-addRule,.queryBuilder-justified:dir(ltr) .chakra-select__wrapper+.ruleGroup-addGroup,.queryBuilder-justified:dir(ltr) .chakra-select__wrapper+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(ltr) .chakra-select__wrapper+.ruleGroup-lock,.queryBuilder-justified:dir(ltr) .chakra-select__wrapper+.ruleGroup-remove,.queryBuilder-justified:dir(ltr) .chakra-select__wrapper+.undoRedoActions,.queryBuilder-justified:dir(ltr) .chakra-select__wrapper+.rule-cloneRule,.queryBuilder-justified:dir(ltr) .chakra-select__wrapper+.rule-lock,.queryBuilder-justified:dir(ltr) .chakra-select__wrapper+.rule-remove,.queryBuilder-justified:dir(ltr) input[type=hidden]+.ruleGroup-addRule,.queryBuilder-justified:dir(ltr) input[type=hidden]+.ruleGroup-addGroup,.queryBuilder-justified:dir(ltr) input[type=hidden]+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(ltr) input[type=hidden]+.ruleGroup-lock,.queryBuilder-justified:dir(ltr) input[type=hidden]+.ruleGroup-remove,.queryBuilder-justified:dir(ltr) input[type=hidden]+.undoRedoActions,.queryBuilder-justified:dir(ltr) input[type=hidden]+.rule-cloneRule,.queryBuilder-justified:dir(ltr) input[type=hidden]+.rule-lock,.queryBuilder-justified:dir(ltr) input[type=hidden]+.rule-remove{margin-left:auto !important}.queryBuilder-justified:dir(ltr) .ruleGroup-addRule:first-child,.queryBuilder-justified:dir(ltr) .ruleGroup-addGroup:first-child,.queryBuilder-justified:dir(ltr) .undoRedoActions:first-child{margin-left:auto !important}.queryBuilder-justified:dir(rtl) .ruleGroup-combinators+.ruleGroup-addRule,.queryBuilder-justified:dir(rtl) .ruleGroup-combinators+.ruleGroup-addGroup,.queryBuilder-justified:dir(rtl) .ruleGroup-combinators+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(rtl) .ruleGroup-combinators+.ruleGroup-lock,.queryBuilder-justified:dir(rtl) .ruleGroup-combinators+.ruleGroup-remove,.queryBuilder-justified:dir(rtl) .ruleGroup-combinators+.undoRedoActions,.queryBuilder-justified:dir(rtl) .ruleGroup-combinators+.rule-cloneRule,.queryBuilder-justified:dir(rtl) .ruleGroup-combinators+.rule-lock,.queryBuilder-justified:dir(rtl) .ruleGroup-combinators+.rule-remove,.queryBuilder-justified:dir(rtl) .ruleGroup-notToggle+.ruleGroup-addRule,.queryBuilder-justified:dir(rtl) .ruleGroup-notToggle+.ruleGroup-addGroup,.queryBuilder-justified:dir(rtl) .ruleGroup-notToggle+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(rtl) .ruleGroup-notToggle+.ruleGroup-lock,.queryBuilder-justified:dir(rtl) .ruleGroup-notToggle+.ruleGroup-remove,.queryBuilder-justified:dir(rtl) .ruleGroup-notToggle+.undoRedoActions,.queryBuilder-justified:dir(rtl) .ruleGroup-notToggle+.rule-cloneRule,.queryBuilder-justified:dir(rtl) .ruleGroup-notToggle+.rule-lock,.queryBuilder-justified:dir(rtl) .ruleGroup-notToggle+.rule-remove,.queryBuilder-justified:dir(rtl) .shiftActions+.ruleGroup-addRule,.queryBuilder-justified:dir(rtl) .shiftActions+.ruleGroup-addGroup,.queryBuilder-justified:dir(rtl) .shiftActions+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(rtl) .shiftActions+.ruleGroup-lock,.queryBuilder-justified:dir(rtl) .shiftActions+.ruleGroup-remove,.queryBuilder-justified:dir(rtl) .shiftActions+.undoRedoActions,.queryBuilder-justified:dir(rtl) .shiftActions+.rule-cloneRule,.queryBuilder-justified:dir(rtl) .shiftActions+.rule-lock,.queryBuilder-justified:dir(rtl) .shiftActions+.rule-remove,.queryBuilder-justified:dir(rtl) .queryBuilder-dragHandle+.ruleGroup-addRule,.queryBuilder-justified:dir(rtl) .queryBuilder-dragHandle+.ruleGroup-addGroup,.queryBuilder-justified:dir(rtl) .queryBuilder-dragHandle+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(rtl) .queryBuilder-dragHandle+.ruleGroup-lock,.queryBuilder-justified:dir(rtl) .queryBuilder-dragHandle+.ruleGroup-remove,.queryBuilder-justified:dir(rtl) .queryBuilder-dragHandle+.undoRedoActions,.queryBuilder-justified:dir(rtl) .queryBuilder-dragHandle+.rule-cloneRule,.queryBuilder-justified:dir(rtl) .queryBuilder-dragHandle+.rule-lock,.queryBuilder-justified:dir(rtl) .queryBuilder-dragHandle+.rule-remove,.queryBuilder-justified:dir(rtl) .ruleGroup-notToggle+.ruleGroup-addRule,.queryBuilder-justified:dir(rtl) .ruleGroup-notToggle+.ruleGroup-addGroup,.queryBuilder-justified:dir(rtl) .ruleGroup-notToggle+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(rtl) .ruleGroup-notToggle+.ruleGroup-lock,.queryBuilder-justified:dir(rtl) .ruleGroup-notToggle+.ruleGroup-remove,.queryBuilder-justified:dir(rtl) .ruleGroup-notToggle+.undoRedoActions,.queryBuilder-justified:dir(rtl) .ruleGroup-notToggle+.rule-cloneRule,.queryBuilder-justified:dir(rtl) .ruleGroup-notToggle+.rule-lock,.queryBuilder-justified:dir(rtl) .ruleGroup-notToggle+.rule-remove,.queryBuilder-justified:dir(rtl) .ruleGroup-header+.ruleGroup-addRule,.queryBuilder-justified:dir(rtl) .ruleGroup-header+.ruleGroup-addGroup,.queryBuilder-justified:dir(rtl) .ruleGroup-header+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(rtl) .ruleGroup-header+.ruleGroup-lock,.queryBuilder-justified:dir(rtl) .ruleGroup-header+.ruleGroup-remove,.queryBuilder-justified:dir(rtl) .ruleGroup-header+.undoRedoActions,.queryBuilder-justified:dir(rtl) .ruleGroup-header+.rule-cloneRule,.queryBuilder-justified:dir(rtl) .ruleGroup-header+.rule-lock,.queryBuilder-justified:dir(rtl) .ruleGroup-header+.rule-remove,.queryBuilder-justified:dir(rtl) .rule-operators+.ruleGroup-addRule,.queryBuilder-justified:dir(rtl) .rule-operators+.ruleGroup-addGroup,.queryBuilder-justified:dir(rtl) .rule-operators+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(rtl) .rule-operators+.ruleGroup-lock,.queryBuilder-justified:dir(rtl) .rule-operators+.ruleGroup-remove,.queryBuilder-justified:dir(rtl) .rule-operators+.undoRedoActions,.queryBuilder-justified:dir(rtl) .rule-operators+.rule-cloneRule,.queryBuilder-justified:dir(rtl) .rule-operators+.rule-lock,.queryBuilder-justified:dir(rtl) .rule-operators+.rule-remove,.queryBuilder-justified:dir(rtl) .rule-value+.ruleGroup-addRule,.queryBuilder-justified:dir(rtl) .rule-value+.ruleGroup-addGroup,.queryBuilder-justified:dir(rtl) .rule-value+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(rtl) .rule-value+.ruleGroup-lock,.queryBuilder-justified:dir(rtl) .rule-value+.ruleGroup-remove,.queryBuilder-justified:dir(rtl) .rule-value+.undoRedoActions,.queryBuilder-justified:dir(rtl) .rule-value+.rule-cloneRule,.queryBuilder-justified:dir(rtl) .rule-value+.rule-lock,.queryBuilder-justified:dir(rtl) .rule-value+.rule-remove,.queryBuilder-justified:dir(rtl) .control+.ruleGroup-addRule,.queryBuilder-justified:dir(rtl) .control+.ruleGroup-addGroup,.queryBuilder-justified:dir(rtl) .control+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(rtl) .control+.ruleGroup-lock,.queryBuilder-justified:dir(rtl) .control+.ruleGroup-remove,.queryBuilder-justified:dir(rtl) .control+.undoRedoActions,.queryBuilder-justified:dir(rtl) .control+.rule-cloneRule,.queryBuilder-justified:dir(rtl) .control+.rule-lock,.queryBuilder-justified:dir(rtl) .control+.rule-remove,.queryBuilder-justified:dir(rtl) .chakra-select__wrapper+.ruleGroup-addRule,.queryBuilder-justified:dir(rtl) .chakra-select__wrapper+.ruleGroup-addGroup,.queryBuilder-justified:dir(rtl) .chakra-select__wrapper+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(rtl) .chakra-select__wrapper+.ruleGroup-lock,.queryBuilder-justified:dir(rtl) .chakra-select__wrapper+.ruleGroup-remove,.queryBuilder-justified:dir(rtl) .chakra-select__wrapper+.undoRedoActions,.queryBuilder-justified:dir(rtl) .chakra-select__wrapper+.rule-cloneRule,.queryBuilder-justified:dir(rtl) .chakra-select__wrapper+.rule-lock,.queryBuilder-justified:dir(rtl) .chakra-select__wrapper+.rule-remove,.queryBuilder-justified:dir(rtl) input[type=hidden]+.ruleGroup-addRule,.queryBuilder-justified:dir(rtl) input[type=hidden]+.ruleGroup-addGroup,.queryBuilder-justified:dir(rtl) input[type=hidden]+.ruleGroup-cloneGroup,.queryBuilder-justified:dir(rtl) input[type=hidden]+.ruleGroup-lock,.queryBuilder-justified:dir(rtl) input[type=hidden]+.ruleGroup-remove,.queryBuilder-justified:dir(rtl) input[type=hidden]+.undoRedoActions,.queryBuilder-justified:dir(rtl) input[type=hidden]+.rule-cloneRule,.queryBuilder-justified:dir(rtl) input[type=hidden]+.rule-lock,.queryBuilder-justified:dir(rtl) input[type=hidden]+.rule-remove{margin-right:auto !important}.queryBuilder-justified:dir(rtl) .ruleGroup-addRule:first-child,.queryBuilder-justified:dir(rtl) .ruleGroup-addGroup:first-child,.queryBuilder-justified:dir(rtl) .undoRedoActions:first-child{margin-right:auto !important}.queryBuilder-responsive .rule,.queryBuilder-responsive .ruleGroup-header{flex-wrap:wrap}.queryBuilder-responsive .rule:not(.rule-hasSubQuery),.queryBuilder-responsive .ruleGroup-header{padding-inline-start:var(--rqb-spacing)}.queryBuilder-responsive .rule:not(.rule-hasSubQuery)>:first-child,.queryBuilder-responsive .ruleGroup-header>:first-child{margin-inline-start:calc(-1*var(--rqb-spacing))}.ruleGroup,.rule-hasSubQuery{border-color:var(--rqb-border-color);border-style:var(--rqb-border-style);border-radius:var(--rqb-border-radius);background:var(--rqb-background-color)}.ruleGroup .shiftActions>*,.rule-hasSubQuery .shiftActions>*{background-color:rgba(0,0,0,0);border:none;cursor:pointer}.dndOver.rule,.dndOver.ruleGroup-header:not(.dndGroup .ruleGroup-header){border-bottom-width:var(--rqb-dnd-drop-indicator-width);border-bottom-style:var(--rqb-dnd-drop-indicator-style);border-bottom-color:var(--rqb-dnd-drop-indicator-color)}.dndOver.rule.dndCopy,.dndOver.ruleGroup-header:not(.dndGroup .ruleGroup-header).dndCopy{border-bottom-width:var(--rqb-dnd-drop-indicator-copy-width);border-bottom-style:var(--rqb-dnd-drop-indicator-copy-style);border-bottom-color:var(--rqb-dnd-drop-indicator-copy-color)}.dndOver.betweenRules{border-top-width:var(--rqb-dnd-drop-indicator-width);border-top-style:var(--rqb-dnd-drop-indicator-style);border-top-color:var(--rqb-dnd-drop-indicator-color)}.dndOver.betweenRules.dndCopy{border-top-width:var(--rqb-dnd-drop-indicator-copy-width);border-top-style:var(--rqb-dnd-drop-indicator-copy-style);border-top-color:var(--rqb-dnd-drop-indicator-copy-color)}.dndDropNotAllowed{cursor:var(--rqb-dnd-drop-not-allowed-cursor)}.dndGroup.rule.dndOver,.dndGroup.ruleGroup:has(>.ruleGroup-header.dndOver){border-width:var(--rqb-dnd-drop-indicator-group-width);border-style:var(--rqb-dnd-drop-indicator-group-style);border-color:var(--rqb-dnd-drop-indicator-group-color);border-radius:var(--rqb-border-radius)}.dndGroup.rule.dndOver.dndCopy,.dndGroup.ruleGroup:has(>.ruleGroup-header.dndOver).dndCopy{border-color:var(--rqb-dnd-drop-indicator-copy-color);border-width:var(--rqb-dnd-drop-indicator-copy-width);border-style:var(--rqb-dnd-drop-indicator-copy-style)}.ruleGroup.dndDragging,.rule.dndDragging{opacity:.5}.ruleGroup.dndHidden,.rule.dndHidden{display:none}.ruleGroup .queryBuilder-dragHandle,.rule .queryBuilder-dragHandle{cursor:move}.ruleGroup.queryBuilder-disabled .queryBuilder-dragHandle,.rule.queryBuilder-disabled .queryBuilder-dragHandle{cursor:not-allowed;opacity:.5}.queryBuilder-branches:dir(ltr) .rule::before,.queryBuilder-branches:dir(ltr) .rule::after,.queryBuilder-branches:dir(ltr) .ruleGroup .ruleGroup::before,.queryBuilder-branches:dir(ltr) .ruleGroup .ruleGroup::after{border-color:var(--rqb-branch-color);border-style:var(--rqb-branch-style)}.queryBuilder-branches:dir(ltr) .rule:last-child::before,.queryBuilder-branches:dir(ltr) .ruleGroup .ruleGroup:last-child::before{border-bottom-left-radius:var(--rqb-branch-radius)}.queryBuilder-branches:dir(ltr) .betweenRules{position:relative}.queryBuilder-branches:dir(ltr) .betweenRules::before{border-color:var(--rqb-branch-color);border-style:var(--rqb-branch-style)}.queryBuilder-branches:dir(rtl) .rule::before,.queryBuilder-branches:dir(rtl) .rule::after,.queryBuilder-branches:dir(rtl) .ruleGroup .ruleGroup::before,.queryBuilder-branches:dir(rtl) .ruleGroup .ruleGroup::after{border-color:var(--rqb-branch-color);border-style:var(--rqb-branch-style)}.queryBuilder-branches:dir(rtl) .rule:last-child::after,.queryBuilder-branches:dir(rtl) .ruleGroup .ruleGroup:last-child::after{border-bottom-right-radius:var(--rqb-branch-radius)}.queryBuilder-branches:dir(rtl) .betweenRules{position:relative}.queryBuilder-branches:dir(rtl) .betweenRules::after{border-color:var(--rqb-branch-color);border-style:var(--rqb-branch-style)} + +/* Back-office skin of the QueryBuilder module screens (Bootstrap 5 back-office). + Loaded after the react-querybuilder stylesheet, whose variables it overrides. */ + +:root { + --qb-accent: var(--bs-primary, #0d6efd); + --qb-accent-soft: rgba(var(--bs-primary-rgb, 13, 110, 253), 0.08); + --qb-border: var(--bs-border-color, #dee2e6); + --qb-text-muted: var(--bs-secondary-color, #6c757d); + --qb-group-bg: var(--bs-tertiary-bg, #f8f9fa); + + /* Vendor variables: lighter frame, tighter spacing than the library default */ + --rqb-spacing: 0.45rem; + --rqb-base-color: var(--bs-primary, #0d6efd); + --rqb-background-color: transparent; + --rqb-border-color: var(--bs-border-color, #dee2e6); + --rqb-border-radius: var(--bs-border-radius, 0.375rem); + --rqb-branch-indent: 0.6rem; +} + +/* ----------------------------------------------------------------- Steps */ + +.qb-step { + flex: 0 0 auto; + width: 22px; + height: 22px; + line-height: 22px; + text-align: center; + border-radius: 50%; + background: var(--qb-accent); + color: #fff; + font-size: 12px; + font-weight: 600; +} + +/* ----------------------------------------------------------------- Hooks */ + +.qb-hooks-toolbar { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.qb-hooks-count { + font-size: 12px; + color: var(--qb-text-muted); + margin-left: auto; +} + +.qb-hooks-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: 6px; +} + +.qb-hook-label { + display: flex; + align-items: flex-start; + gap: 8px; + width: 100%; + margin: 0; + padding: 6px 10px; + border: 1px solid var(--qb-border); + border-radius: var(--bs-border-radius, 0.375rem); + font-weight: 400; + cursor: pointer; + transition: background-color 0.12s ease, border-color 0.12s ease; +} + +.qb-hook-label:hover, +.qb-hook-checked > .qb-hook-label { + border-color: var(--qb-accent); + background: var(--qb-accent-soft); +} + +.qb-hook-label > .form-check-input { + flex: 0 0 auto; + margin-top: 3px; +} + +/* min-width 0: without it the flex item refuses to shrink and the long + unbreakable hook code overflows the card */ +.qb-hook-text { + flex: 1 1 auto; + min-width: 0; +} + +.qb-hook-text code { + font-size: 12px; + word-break: break-all; +} + +/* ------------------------------------------------------------ Conditions */ + +.qb-tree-panel { + border: 1px dashed var(--qb-border); + border-radius: var(--bs-border-radius, 0.375rem); + padding: 12px; + background: var(--bs-body-bg, #fff); +} + +.qb-summary { + display: none; + margin-bottom: 12px; + padding: 10px 14px 12px; + border: 1px solid var(--qb-border); + border-left: 3px solid var(--qb-accent); + border-radius: var(--bs-border-radius, 0.375rem); + background: var(--qb-accent-soft); + font-size: 13px; + line-height: 1.5; +} + +.qb-summary.qb-summary-visible { + display: block; +} + +.qb-summary-label { + display: block; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; + color: var(--qb-accent); + margin-bottom: 2px; +} + +/* Structured recap: one line per condition, nesting shown by indented rails */ + +.qb-summary-list { + list-style: none; + margin: 0; + padding: 0; +} + +.qb-summary-heading { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 6px; +} + +.qb-summary-heading-text { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; + color: var(--qb-text-muted); +} + +.qb-summary-combinator { + display: inline-block; + min-width: 26px; + padding: 1px 6px; + border-radius: 2px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.05em; + text-align: center; + color: #fff; + background: var(--qb-text-muted); +} + +.qb-summary-combinator-and { + background: var(--qb-accent); +} + +.qb-summary-combinator-or { + background: #4a90a4; +} + +.qb-summary-rule { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 6px; + padding: 4px 0 4px 14px; + position: relative; +} + +.qb-summary-rule::before { + content: ""; + position: absolute; + left: 2px; + top: 12px; + width: 6px; + height: 1px; + background: var(--qb-border); +} + +.qb-summary-field { + font-weight: 600; +} + +.qb-summary-field-unknown { + color: var(--bs-danger, #dc3545); + text-decoration: underline dotted; +} + +.qb-summary-operator { + font-style: italic; + color: var(--qb-text-muted); +} + +.qb-summary-join { + font-size: 11px; + text-transform: uppercase; + color: var(--qb-text-muted); +} + +.qb-summary-value { + padding: 1px 7px; + border: 1px solid var(--qb-border); + border-radius: 10px; + background: var(--bs-body-bg, #fff); + font-family: var(--bs-font-monospace, monospace); + font-size: 12px; +} + +.qb-summary-missing { + padding: 1px 7px; + border-radius: 10px; + background: var(--bs-danger-bg-subtle, #f8d7da); + font-size: 12px; + font-style: italic; + color: var(--bs-danger, #dc3545); +} + +.qb-summary-nested { + margin: 6px 0 6px 14px; +} + +/* Nested groups get their own rail so the depth is obvious */ +.qb-summary-nested > .qb-summary-group { + padding: 6px 0 2px 12px; + border-left: 2px solid var(--qb-border); +} + +.qb-empty-hint { + display: none; + font-size: 13px; + color: var(--qb-text-muted); + font-style: italic; + margin-bottom: 10px; +} + +.qb-empty-hint.qb-empty-hint-visible { + display: block; +} + +/* --------------------------------------------- Action summary popovers */ + +/* Plain (i) mark after the action name; the popover itself is appended to body */ +.qb-action-summary-trigger { + padding: 0 2px; + border: 0; + background: none; + color: var(--qb-accent); + font-size: 14px; + line-height: 1; + vertical-align: -1px; + cursor: help; +} + +.qb-action-summary-trigger:hover, +.qb-action-summary-trigger:focus-visible { + color: var(--bs-link-hover-color, var(--qb-accent)); +} + +.qb-action-summary-trigger-empty { + color: var(--qb-text-muted); +} + +.qb-action-summary-popover { + max-width: 480px; + font-size: 13px; + line-height: 1.5; +} + +.qb-action-summary-popover .popover-header { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; + color: var(--qb-accent); +} + +.qb-action-summary-empty { + font-style: italic; + color: var(--qb-text-muted); +} + +/* Parameters listed under the selection; the rail separates them from the tree */ +.qb-action-summary-details { + list-style: none; + margin: 0; + padding: 0; +} + +.qb-action-summary > * + .qb-action-summary-details { + margin-top: 8px; + padding-top: 8px; + border-top: 1px solid var(--qb-border); +} + +.qb-action-summary-details > li { + display: flex; + align-items: baseline; + gap: 6px; + padding: 2px 0; + font-weight: 600; +} + +.qb-action-summary-details > li::before { + content: ""; + flex: 0 0 auto; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--qb-accent); + transform: translateY(-1px); +} + +/* ------------------------------------------------- react-querybuilder skin */ + +.qb-tree-panel .ruleGroup { + background: var(--qb-group-bg); + border: 1px solid var(--qb-border); + padding: 10px; +} + +/* Nested groups alternate shades so the nesting depth is readable at a glance */ +.qb-tree-panel .ruleGroup .ruleGroup { + background: var(--bs-body-bg, #fff); +} + +.qb-tree-panel .ruleGroup .ruleGroup .ruleGroup { + background: var(--qb-group-bg); +} + +.qb-tree-panel .ruleGroup-header, +.qb-tree-panel .rule { + flex-wrap: wrap; +} + +/* Field labels can be long: fix the widths so the columns stay aligned between rules */ +.qb-tree-panel select.rule-fields { + width: 320px; + font-weight: 600; +} + +.qb-tree-panel select.rule-operators { + width: 220px; +} + +.qb-tree-panel input.rule-value, +.qb-tree-panel select.rule-value, +.qb-tree-panel textarea.rule-value { + width: 240px; +} + +.qb-tree-panel input.rule-value.form-check-input { + height: 1.925em; + margin-top: -0.02em; +} + +.qb-tree-panel select.ruleGroup-combinators { + width: auto; +} + +.qb-tree-panel .form-control, +.qb-tree-panel .form-select { + display: inline-block; + max-width: 100%; +} + diff --git a/templates/backOffice/default-twig/assets/dist/query-builder-admin.js b/templates/backOffice/default-twig/assets/dist/query-builder-admin.js new file mode 100644 index 0000000..2e02d84 --- /dev/null +++ b/templates/backOffice/default-twig/assets/dist/query-builder-admin.js @@ -0,0 +1,93 @@ +(()=>{var e={290(e){function t(e){return Promise.resolve().then(()=>{const t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t})}t.keys=()=>[],t.resolve=t,t.id=290,e.exports=t},236(e,t,n){"use strict";n.r(t)}};const t={};function n(r){const o=t[r];if(void 0!==o)return o.exports;const a=t[r]={exports:{}};return e[r](a,a.exports,n),a.exports}n.cw=e=>{var t;return()=>{if(e){var n=e;e=0,t={exports:{}},n.call(t.exports,t,t.exports)}return t.exports}},n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),n.r=e=>{Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.dn=e=>{var t=Object.getOwnPropertyDescriptor(e,"name");(!t||!t.writable&&t.configurable)&&Object.defineProperty(e,"name",{value:"default",configurable:!0})},(()=>{"use strict";var e=n.cw(function(e,t){e.exports=function e(t,n){if(t===n)return!0;if(t&&n&&"object"==typeof t&&"object"==typeof n){if(t.constructor!==n.constructor)return!1;var r,o,a;if(Array.isArray(t)){if((r=t.length)!=n.length)return!1;for(o=r;0!==o--;)if(!e(t[o],n[o]))return!1;return!0}if(t.constructor===RegExp)return t.source===n.source&&t.flags===n.flags;if(t.valueOf!==Object.prototype.valueOf)return t.valueOf()===n.valueOf();if(t.toString!==Object.prototype.toString)return t.toString()===n.toString();if((r=(a=Object.keys(t)).length)!==Object.keys(n).length)return!1;for(o=r;0!==o--;)if(!Object.prototype.hasOwnProperty.call(n,a[o]))return!1;for(o=r;0!==o--;){var i=a[o];if(!e(t[i],n[i]))return!1}return!0}return t!=t&&n!=n}}),t=n.cw(function(e,t){var n=d(),r=s(),o=a(); +/** + * @license React + * react-dom-client.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */function i(e){var t="https://react.dev/errors/"+e;if(1z||(e.current=F[z],F[z]=null,z--)}function B(e,t){z++,F[z]=e.current,e.current=t}var V,H,U=j(null),W=j(null),Q=j(null),K=j(null);function X(e,t){switch(B(Q,t),B(W,e),B(U,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?yd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)e=wd(t=yd(t),e);else switch(e){case"svg":e=1;break;case"math":e=2;break;default:e=0}}q(U),B(U,e)}function Y(){q(U),q(W),q(Q)}function J(e){null!==e.memoizedState&&B(K,e);var t=U.current,n=wd(t,e.type);t!==n&&(B(W,e),B(U,n))}function Z(e){W.current===e&&(q(U),q(W)),K.current===e&&(q(K),pf._currentValue=G)}function ee(e){if(void 0===V)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);V=t&&t[1]||"",H=-1)":-1--o||s[r]!==u[o]){var c="\n"+s[r].replace(" at new "," at ");return e.displayName&&c.includes("")&&(c=c.replace("",e.displayName)),c}}while(1<=r&&0<=o);break}}}finally{te=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:"")?ee(n):""}function re(e,t){switch(e.tag){case 26:case 27:case 5:return ee(e.type);case 16:return ee("Lazy");case 13:return e.child!==t&&null!==t?ee("Suspense Fallback"):ee("Suspense");case 19:return ee("SuspenseList");case 0:case 15:return ne(e.type,!1);case 11:return ne(e.type.render,!1);case 1:return ne(e.type,!0);case 31:return ee("Activity");default:return""}}function oe(e){try{var t="",n=null;do{t+=re(e,n),n=e,e=e.return}while(e);return t}catch(e){return"\nError generating stack: "+e.message+"\n"+e.stack}}var ae=Object.prototype.hasOwnProperty,ie=n.unstable_scheduleCallback,le=n.unstable_cancelCallback,se=n.unstable_shouldYield,ue=n.unstable_requestPaint,ce=n.unstable_now,de=n.unstable_getCurrentPriorityLevel,fe=n.unstable_ImmediatePriority,pe=n.unstable_UserBlockingPriority,he=n.unstable_NormalPriority,me=n.unstable_LowPriority,ge=n.unstable_IdlePriority,ve=n.log,be=n.unstable_setDisableYieldValue,ye=null,we=null;function Se(e){if("function"==typeof ve&&be(e),we&&"function"==typeof we.setStrictMode)try{we.setStrictMode(ye,e)}catch(e){}}var ke=Math.clz32?Math.clz32:function(e){return 0===(e>>>=0)?32:31-(Ee(e)/Oe|0)|0},Ee=Math.log,Oe=Math.LN2;var Ne=256,Ce=262144,De=4194304;function Ae(e){var t=42&e;if(0!==t)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return 261888&e;case 262144:case 524288:case 1048576:case 2097152:return 3932160&e;case 4194304:case 8388608:case 16777216:case 33554432:return 62914560&e;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function Te(e,t,n){var r=e.pendingLanes;if(0===r)return 0;var o=0,a=e.suspendedLanes,i=e.pingedLanes;e=e.warmLanes;var l=134217727&r;return 0!==l?0!==(r=l&~a)?o=Ae(r):0!==(i&=l)?o=Ae(i):n||0!==(n=l&~e)&&(o=Ae(n)):0!==(l=r&~a)?o=Ae(l):0!==i?o=Ae(i):n||0!==(n=r&~e)&&(o=Ae(n)),0===o?0:0!==t&&t!==o&&0===(t&a)&&((a=o&-o)>=(n=t&-t)||32===a&&4194048&n)?t:o}function $e(e,t){return 0===(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)}function Me(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;default:return-1}}function Re(){var e=De;return!(62914560&(De<<=1))&&(De=4194304),e}function xe(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function Ie(e,t){e.pendingLanes|=t,268435456!==t&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function Pe(e,t,n){e.pendingLanes|=t,e.suspendedLanes&=~t;var r=31-ke(t);e.entangledLanes|=t,e.entanglements[r]=1073741824|e.entanglements[r]|261930&n}function _e(e,t){var n=e.entangledLanes|=t;for(e=e.entanglements;n;){var r=31-ke(n),o=1<=An),Mn=String.fromCharCode(32),Rn=!1;function xn(e,t){switch(e){case"keyup":return-1!==Cn.indexOf(t.keyCode);case"keydown":return 229!==t.keyCode;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function In(e){return"object"==typeof(e=e.detail)&&"data"in e?e.data:null}var Pn=!1;var _n={color:!0,date:!0,datetime:!0,"datetime-local":!0,email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0};function Ln(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return"input"===t?!!_n[e.type]:"textarea"===t}function Gn(e,t,n,r){Lt?Gt?Gt.push(r):Gt=[r]:Lt=r,0<(t=ad(t,"onChange")).length&&(n=new on("onChange","change",null,n,r),e.push({event:n,listeners:t}))}var Fn=null,zn=null;function jn(e){Yc(e,0)}function qn(e){if(gt(et(e)))return e}function Bn(e,t){if("change"===e)return t}var Vn=!1;if(Bt){var Hn;if(Bt){var Un="oninput"in document;if(!Un){var Wn=document.createElement("div");Wn.setAttribute("oninput","return;"),Un="function"==typeof Wn.oninput}Hn=Un}else Hn=!1;Vn=Hn&&(!document.documentMode||9=t)return{node:r,offset:t-e};e=n}e:{for(;r;){if(r.nextSibling){r=r.nextSibling;break e}r=r.parentNode}r=void 0}r=nr(r)}}function or(e,t){return!(!e||!t)&&(e===t||(!e||3!==e.nodeType)&&(t&&3===t.nodeType?or(e,t.parentNode):"contains"in e?e.contains(t):!!e.compareDocumentPosition&&!!(16&e.compareDocumentPosition(t))))}function ar(e){for(var t=vt((e=null!=e&&null!=e.ownerDocument&&null!=e.ownerDocument.defaultView?e.ownerDocument.defaultView:window).document);t instanceof e.HTMLIFrameElement;){try{var n="string"==typeof t.contentWindow.location.href}catch(e){n=!1}if(!n)break;t=vt((e=t.contentWindow).document)}return t}function ir(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&("input"===t&&("text"===e.type||"search"===e.type||"tel"===e.type||"url"===e.type||"password"===e.type)||"textarea"===t||"true"===e.contentEditable)}var lr=Bt&&"documentMode"in document&&11>=document.documentMode,sr=null,ur=null,cr=null,dr=!1;function fr(e,t,n){var r=n.window===n?n.document:9===n.nodeType?n:n.ownerDocument;dr||null==sr||sr!==vt(r)||("selectionStart"in(r=sr)&&ir(r)?r={start:r.selectionStart,end:r.selectionEnd}:r={anchorNode:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection()).anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset},cr&&tr(cr,r)||(cr=r,0<(r=ad(ur,"onSelect")).length&&(t=new on("onSelect","select",null,t,n),e.push({event:t,listeners:r}),t.target=sr)))}function pr(e,t){var n={};return n[e.toLowerCase()]=t.toLowerCase(),n["Webkit"+e]="webkit"+t,n["Moz"+e]="moz"+t,n}var hr={animationend:pr("Animation","AnimationEnd"),animationiteration:pr("Animation","AnimationIteration"),animationstart:pr("Animation","AnimationStart"),transitionrun:pr("Transition","TransitionRun"),transitionstart:pr("Transition","TransitionStart"),transitioncancel:pr("Transition","TransitionCancel"),transitionend:pr("Transition","TransitionEnd")},mr={},gr={};function vr(e){if(mr[e])return mr[e];if(!hr[e])return e;var t,n=hr[e];for(t in n)if(n.hasOwnProperty(t)&&t in gr)return mr[e]=n[t];return e}Bt&&(gr=document.createElement("div").style,"AnimationEvent"in window||(delete hr.animationend.animation,delete hr.animationiteration.animation,delete hr.animationstart.animation),"TransitionEvent"in window||delete hr.transitionend.transition);var br=vr("animationend"),yr=vr("animationiteration"),wr=vr("animationstart"),Sr=vr("transitionrun"),kr=vr("transitionstart"),Er=vr("transitioncancel"),Or=vr("transitionend"),Nr=new Map,Cr="abort auxClick beforeToggle cancel canPlay canPlayThrough click close contextMenu copy cut drag dragEnd dragEnter dragExit dragLeave dragOver dragStart drop durationChange emptied encrypted ended error gotPointerCapture input invalid keyDown keyPress keyUp load loadedData loadedMetadata loadStart lostPointerCapture mouseDown mouseMove mouseOut mouseOver mouseUp paste pause play playing pointerCancel pointerDown pointerMove pointerOut pointerOver pointerUp progress rateChange reset resize seeked seeking stalled submit suspend timeUpdate touchCancel touchEnd touchStart volumeChange scroll toggle touchMove waiting wheel".split(" ");function Dr(e,t){Nr.set(e,t),at(t,[e])}Cr.push("scrollEnd");var Ar="function"==typeof reportError?reportError:function(e){if("object"==typeof window&&"function"==typeof window.ErrorEvent){var t=new window.ErrorEvent("error",{bubbles:!0,cancelable:!0,message:"object"==typeof e&&null!==e&&"string"==typeof e.message?String(e.message):String(e),error:e});if(!window.dispatchEvent(t))return}else if("object"==typeof process&&"function"==typeof process.emit)return void process.emit("uncaughtException",e);console.error(e)},Tr=[],$r=0,Mr=0;function Rr(){for(var e=$r,t=Mr=$r=0;t>=i,o-=i,oo=1<<32-ke(t)+o|n<m?(g=d,d=null):g=d.sibling;var v=p(o,d,l[m],s);if(null===v){null===d&&(d=g);break}e&&d&&null===v.alternate&&t(o,d),i=a(v,i,m),null===c?u=v:c.sibling=v,c=v,d=g}if(m===l.length)return n(o,d),ho&&io(o,m),u;if(null===d){for(;mg?(v=m,m=null):v=m.sibling;var y=p(o,m,b.value,u);if(null===y){null===m&&(m=v);break}e&&m&&null===y.alternate&&t(o,m),l=a(y,l,g),null===d?c=y:d.sibling=y,d=y,m=v}if(b.done)return n(o,m),ho&&io(o,g),c;if(null===m){for(;!b.done;g++,b=s.next())null!==(b=f(o,b.value,u))&&(l=a(b,l,g),null===d?c=b:d.sibling=b,d=b);return ho&&io(o,g),c}for(m=r(m);!b.done;g++,b=s.next())null!==(b=h(m,o,g,b.value,u))&&(e&&null!==b.alternate&&m.delete(null===b.key?g:b.key),l=a(b,l,g),null===d?c=b:d.sibling=b,d=b);return e&&m.forEach(function(e){return t(o,e)}),ho&&io(o,g),c}(s,u,c=g.call(c),d)}if("function"==typeof c.then)return m(s,u,fa(c),d);if(c.$$typeof===E)return m(s,u,_o(s,c),d);ha(s,c)}return"string"==typeof c&&""!==c||"number"==typeof c||"bigint"==typeof c?(c=""+c,null!==u&&6===u.tag?(n(s,u.sibling),(d=o(u,c)).return=s,s=d):(n(s,u),(d=Ur(c,s.mode,d)).return=s,s=d),l(s)):n(s,u)}return function(e,t,n,r){try{da=0;var o=m(e,t,n,r);return ca=null,o}catch(t){if(t===ea||t===na)throw t;var a=zr(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var ga=ma(!0),va=ma(!1),ba=!1;function ya(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function wa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Sa(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function ka(e,t,n){var r=e.updateQueue;if(null===r)return null;if(r=r.shared,2&mu){var o=r.pending;return null===o?t.next=t:(t.next=o.next,o.next=t),r.pending=t,t=Lr(e),_r(e,null,n),t}return xr(e,r,t,n),Lr(e)}function Ea(e,t,n){if(null!==(t=t.updateQueue)&&(t=t.shared,4194048&n)){var r=t.lanes;n|=r&=e.pendingLanes,t.lanes=n,_e(e,n)}}function Oa(e,t){var n=e.updateQueue,r=e.alternate;if(null!==r&&n===(r=r.updateQueue)){var o=null,a=null;if(null!==(n=n.firstBaseUpdate)){do{var i={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};null===a?o=a=i:a=a.next=i,n=n.next}while(null!==n);null===a?o=a=t:a=a.next=t}else o=a=t;return n={baseState:r.baseState,firstBaseUpdate:o,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},void(e.updateQueue=n)}null===(e=n.lastBaseUpdate)?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Na=!1;function Ca(){if(Na){if(null!==Wo)throw Wo}}function Da(e,t,n,r){Na=!1;var o=e.updateQueue;ba=!1;var a=o.firstBaseUpdate,i=o.lastBaseUpdate,l=o.shared.pending;if(null!==l){o.shared.pending=null;var s=l,u=s.next;s.next=null,null===i?a=u:i.next=u,i=s;var c=e.alternate;null!==c&&((l=(c=c.updateQueue).lastBaseUpdate)!==i&&(null===l?c.firstBaseUpdate=u:l.next=u,c.lastBaseUpdate=s))}if(null!==a){var d=o.baseState;for(i=0,c=u=s=null,l=a;;){var f=-536870913&l.lane,p=f!==l.lane;if(p?(bu&f)===f:(r&f)===f){0!==f&&f===Uo&&(Na=!0),null!==c&&(c=c.next={lane:0,tag:l.tag,payload:l.payload,callback:null,next:null});e:{var h=e,g=l;f=t;var v=n;switch(g.tag){case 1:if("function"==typeof(h=g.payload)){d=h.call(v,d,f);break e}d=h;break e;case 3:h.flags=-65537&h.flags|128;case 0:if(null==(f="function"==typeof(h=g.payload)?h.call(v,d,f):h))break e;d=m({},d,f);break e;case 2:ba=!0}}null!==(f=l.callback)&&(e.flags|=64,p&&(e.flags|=8192),null===(p=o.callbacks)?o.callbacks=[f]:p.push(f))}else p={lane:f,tag:l.tag,payload:l.payload,callback:l.callback,next:null},null===c?(u=c=p,s=d):c=c.next=p,i|=f;if(null===(l=l.next)){if(null===(l=o.shared.pending))break;l=(p=l).next,p.next=null,o.lastBaseUpdate=p,o.shared.pending=null}}null===c&&(s=d),o.baseState=s,o.firstBaseUpdate=u,o.lastBaseUpdate=c,null===a&&(o.shared.lanes=0),Cu|=i,e.lanes=i,e.memoizedState=d}}function Aa(e,t){if("function"!=typeof e)throw Error(i(191,e));e.call(t)}function Ta(e,t){var n=e.callbacks;if(null!==n)for(e.callbacks=null,e=0;ea?a:8;var i,l,s,u=_.T,c={};_.T=c,pl(e,!1,t,n);try{var d=o(),f=_.S;if(null!==f&&f(c,d),null!==d&&"object"==typeof d&&"function"==typeof d.then)fl(e,t,(i=r,l=[],s={status:"pending",value:null,reason:null,then:function(e){l.push(e)}},d.then(function(){s.status="fulfilled",s.value=i;for(var e=0;e<\/script>",a=a.removeChild(a.firstChild);break;case"select":a="string"==typeof r.is?l.createElement("select",{is:r.is}):l.createElement("select"),r.multiple?a.multiple=!0:r.size&&(a.size=r.size);break;default:a="string"==typeof r.is?l.createElement(o,{is:r.is}):l.createElement(o)}}a[Be]=t,a[Ve]=r;e:for(l=t.child;null!==l;){if(5===l.tag||6===l.tag)a.appendChild(l.stateNode);else if(4!==l.tag&&27!==l.tag&&null!==l.child){l.child.return=l,l=l.child;continue}if(l===t)break e;for(;null===l.sibling;){if(null===l.return||l.return===t)break e;l=l.return}l.sibling.return=l.return,l=l.sibling}t.stateNode=a;e:switch(hd(a,o,r),o){case"button":case"input":case"select":case"textarea":r=!!r.autoFocus;break e;case"img":r=!0;break e;default:r=!1}r&&us(t)}}return hs(t),cs(t,t.type,null===e||e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&null!=t.stateNode)e.memoizedProps!==r&&us(t);else{if("string"!=typeof r&&null===t.stateNode)throw Error(i(166));if(e=Q.current,So(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,null!==(o=fo))switch(o.tag){case 27:case 5:r=o.memoizedProps}e[Be]=t,(e=!!(e.nodeValue===n||null!==r&&!0===r.suppressHydrationWarning||dd(e.nodeValue,n)))||bo(t,!0)}else(e=bd(e).createTextNode(r))[Be]=t,t.stateNode=e}return hs(t),null;case 31:if(n=t.memoizedState,null===e||null!==e.memoizedState){if(r=So(t),null!==n){if(null===e){if(!r)throw Error(i(318));if(!(e=null!==(e=t.memoizedState)?e.dehydrated:null))throw Error(i(557));e[Be]=t}else ko(),!(128&t.flags)&&(t.memoizedState=null),t.flags|=4;hs(t),e=!1}else n=Eo(),null!==e&&null!==e.memoizedState&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return 256&t.flags?(ja(t),t):(ja(t),null);if(128&t.flags)throw Error(i(558))}return hs(t),null;case 13:if(r=t.memoizedState,null===e||null!==e.memoizedState&&null!==e.memoizedState.dehydrated){if(o=So(t),null!==r&&null!==r.dehydrated){if(null===e){if(!o)throw Error(i(318));if(!(o=null!==(o=t.memoizedState)?o.dehydrated:null))throw Error(i(317));o[Be]=t}else ko(),!(128&t.flags)&&(t.memoizedState=null),t.flags|=4;hs(t),o=!1}else o=Eo(),null!==e&&null!==e.memoizedState&&(e.memoizedState.hydrationErrors=o),o=!0;if(!o)return 256&t.flags?(ja(t),t):(ja(t),null)}return ja(t),128&t.flags?(t.lanes=n,t):(n=null!==r,e=null!==e&&null!==e.memoizedState,n&&(o=null,null!==(r=t.child).alternate&&null!==r.alternate.memoizedState&&null!==r.alternate.memoizedState.cachePool&&(o=r.alternate.memoizedState.cachePool.pool),a=null,null!==r.memoizedState&&null!==r.memoizedState.cachePool&&(a=r.memoizedState.cachePool.pool),a!==o&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),fs(t,t.updateQueue),hs(t),null);case 4:return Y(),null===e&&td(t.stateNode.containerInfo),hs(t),null;case 10:return To(t.type),hs(t),null;case 19:if(q(qa),null===(r=t.memoizedState))return hs(t),null;if(o=!!(128&t.flags),null===(a=r.rendering))if(o)ps(r,!1);else{if(0!==Nu||null!==e&&128&e.flags)for(e=t.child;null!==e;){if(null!==(a=Ba(e))){for(t.flags|=128,ps(r,!1),e=a.updateQueue,t.updateQueue=e,fs(t,e),t.subtreeFlags=0,e=n,n=t.child;null!==n;)Br(n,e),n=n.sibling;return B(qa,1&qa.current|2),ho&&io(t,r.treeForkCount),t.child}e=e.sibling}null!==r.tail&&ce()>_u&&(t.flags|=128,o=!0,ps(r,!1),t.lanes=4194304)}else{if(!o)if(null!==(e=Ba(a))){if(t.flags|=128,o=!0,e=e.updateQueue,t.updateQueue=e,fs(t,e),ps(r,!0),null===r.tail&&"hidden"===r.tailMode&&!a.alternate&&!ho)return hs(t),null}else 2*ce()-r.renderingStartTime>_u&&536870912!==n&&(t.flags|=128,o=!0,ps(r,!1),t.lanes=4194304);r.isBackwards?(a.sibling=t.child,t.child=a):(null!==(e=r.last)?e.sibling=a:t.child=a,r.last=a)}return null!==r.tail?(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=ce(),e.sibling=null,n=qa.current,B(qa,o?1&n|2:1&n),ho&&io(t,r.treeForkCount),e):(hs(t),null);case 22:case 23:return ja(t),Ia(),r=null!==t.memoizedState,null!==e?null!==e.memoizedState!==r&&(t.flags|=8192):r&&(t.flags|=8192),r?!!(536870912&n)&&!(128&t.flags)&&(hs(t),6&t.subtreeFlags&&(t.flags|=8192)):hs(t),null!==(n=t.updateQueue)&&fs(t,n.retryQueue),n=null,null!==e&&null!==e.memoizedState&&null!==e.memoizedState.cachePool&&(n=e.memoizedState.cachePool.pool),r=null,null!==t.memoizedState&&null!==t.memoizedState.cachePool&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),null!==e&&q(Xo),null;case 24:return n=null,null!==e&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),To(jo),hs(t),null;case 25:case 30:return null}throw Error(i(156,t.tag))}function gs(e,t){switch(uo(t),t.tag){case 1:return 65536&(e=t.flags)?(t.flags=-65537&e|128,t):null;case 3:return To(jo),Y(),65536&(e=t.flags)&&!(128&e)?(t.flags=-65537&e|128,t):null;case 26:case 27:case 5:return Z(t),null;case 31:if(null!==t.memoizedState){if(ja(t),null===t.alternate)throw Error(i(340));ko()}return 65536&(e=t.flags)?(t.flags=-65537&e|128,t):null;case 13:if(ja(t),null!==(e=t.memoizedState)&&null!==e.dehydrated){if(null===t.alternate)throw Error(i(340));ko()}return 65536&(e=t.flags)?(t.flags=-65537&e|128,t):null;case 19:return q(qa),null;case 4:return Y(),null;case 10:return To(t.type),null;case 22:case 23:return ja(t),Ia(),null!==e&&q(Xo),65536&(e=t.flags)?(t.flags=-65537&e|128,t):null;case 24:return To(jo),null;default:return null}}function vs(e,t){switch(uo(t),t.tag){case 3:To(jo),Y();break;case 26:case 27:case 5:Z(t);break;case 4:Y();break;case 31:null!==t.memoizedState&&ja(t);break;case 13:ja(t);break;case 19:q(qa);break;case 10:To(t.type);break;case 22:case 23:ja(t),Ia(),null!==e&&q(Xo);break;case 24:To(jo)}}function bs(e,t){try{var n=t.updateQueue,r=null!==n?n.lastEffect:null;if(null!==r){var o=r.next;n=o;do{if((n.tag&e)===e){r=void 0;var a=n.create,i=n.inst;r=a(),i.destroy=r}n=n.next}while(n!==o)}}catch(e){Nc(t,t.return,e)}}function ys(e,t,n){try{var r=t.updateQueue,o=null!==r?r.lastEffect:null;if(null!==o){var a=o.next;r=a;do{if((r.tag&e)===e){var i=r.inst,l=i.destroy;if(void 0!==l){i.destroy=void 0,o=t;var s=n,u=l;try{u()}catch(e){Nc(o,s,e)}}}r=r.next}while(r!==a)}}catch(e){Nc(t,t.return,e)}}function ws(e){var t=e.updateQueue;if(null!==t){var n=e.stateNode;try{Ta(t,n)}catch(t){Nc(e,e.return,t)}}}function Ss(e,t,n){n.props=Nl(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Nc(e,t,n)}}function ks(e,t){try{var n=e.ref;if(null!==n){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;default:r=e.stateNode}"function"==typeof n?e.refCleanup=n(r):n.current=r}}catch(n){Nc(e,t,n)}}function Es(e,t){var n=e.ref,r=e.refCleanup;if(null!==n)if("function"==typeof r)try{r()}catch(n){Nc(e,t,n)}finally{e.refCleanup=null,null!=(e=e.alternate)&&(e.refCleanup=null)}else if("function"==typeof n)try{n(null)}catch(n){Nc(e,t,n)}else n.current=null}function Os(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{e:switch(t){case"button":case"input":case"select":case"textarea":n.autoFocus&&r.focus();break e;case"img":n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Nc(e,e.return,t)}}function Ns(e,t,n){try{var r=e.stateNode;!function(e,t,n,r){switch(t){case"div":case"span":case"svg":case"path":case"a":case"g":case"p":case"li":break;case"input":var o=null,a=null,l=null,s=null,u=null,c=null,d=null;for(h in n){var f=n[h];if(n.hasOwnProperty(h)&&null!=f)switch(h){case"checked":case"value":break;case"defaultValue":u=f;default:r.hasOwnProperty(h)||fd(e,t,h,null,r,f)}}for(var p in r){var h=r[p];if(f=n[p],r.hasOwnProperty(p)&&(null!=h||null!=f))switch(p){case"type":a=h;break;case"name":o=h;break;case"checked":c=h;break;case"defaultChecked":d=h;break;case"value":l=h;break;case"defaultValue":s=h;break;case"children":case"dangerouslySetInnerHTML":if(null!=h)throw Error(i(137,t));break;default:h!==f&&fd(e,t,p,h,r,f)}}return void wt(e,l,s,u,c,d,a,o);case"select":for(a in h=l=s=p=null,n)if(u=n[a],n.hasOwnProperty(a)&&null!=u)switch(a){case"value":break;case"multiple":h=u;default:r.hasOwnProperty(a)||fd(e,t,a,null,r,u)}for(o in r)if(a=r[o],u=n[o],r.hasOwnProperty(o)&&(null!=a||null!=u))switch(o){case"value":p=a;break;case"defaultValue":s=a;break;case"multiple":l=a;default:a!==u&&fd(e,t,o,a,r,u)}return t=s,n=l,r=h,void(null!=p?Et(e,!!n,p,!1):!!r!=!!n&&(null!=t?Et(e,!!n,t,!0):Et(e,!!n,n?[]:"",!1)));case"textarea":for(s in h=p=null,n)if(o=n[s],n.hasOwnProperty(s)&&null!=o&&!r.hasOwnProperty(s))switch(s){case"value":case"children":break;default:fd(e,t,s,null,r,o)}for(l in r)if(o=r[l],a=n[l],r.hasOwnProperty(l)&&(null!=o||null!=a))switch(l){case"value":p=o;break;case"defaultValue":h=o;break;case"children":break;case"dangerouslySetInnerHTML":if(null!=o)throw Error(i(91));break;default:o!==a&&fd(e,t,l,o,r,a)}return void Ot(e,p,h);case"option":for(var m in n)if(p=n[m],n.hasOwnProperty(m)&&null!=p&&!r.hasOwnProperty(m))if("selected"===m)e.selected=!1;else fd(e,t,m,null,r,p);for(u in r)if(p=r[u],h=n[u],r.hasOwnProperty(u)&&p!==h&&(null!=p||null!=h))if("selected"===u)e.selected=p&&"function"!=typeof p&&"symbol"!=typeof p;else fd(e,t,u,p,r,h);return;case"img":case"link":case"area":case"base":case"br":case"col":case"embed":case"hr":case"keygen":case"meta":case"param":case"source":case"track":case"wbr":case"menuitem":for(var g in n)p=n[g],n.hasOwnProperty(g)&&null!=p&&!r.hasOwnProperty(g)&&fd(e,t,g,null,r,p);for(c in r)if(p=r[c],h=n[c],r.hasOwnProperty(c)&&p!==h&&(null!=p||null!=h))switch(c){case"children":case"dangerouslySetInnerHTML":if(null!=p)throw Error(i(137,t));break;default:fd(e,t,c,p,r,h)}return;default:if($t(t)){for(var v in n)p=n[v],n.hasOwnProperty(v)&&void 0!==p&&!r.hasOwnProperty(v)&&pd(e,t,v,void 0,r,p);for(d in r)p=r[d],h=n[d],!r.hasOwnProperty(d)||p===h||void 0===p&&void 0===h||pd(e,t,d,p,r,h);return}}for(var b in n)p=n[b],n.hasOwnProperty(b)&&null!=p&&!r.hasOwnProperty(b)&&fd(e,t,b,null,r,p);for(f in r)p=r[f],h=n[f],!r.hasOwnProperty(f)||p===h||null==p&&null==h||fd(e,t,f,p,r,h)}(r,e.type,n,t),r[Ve]=t}catch(t){Nc(e,e.return,t)}}function Cs(e){return 5===e.tag||3===e.tag||26===e.tag||27===e.tag&&Ad(e.type)||4===e.tag}function Ds(e){e:for(;;){for(;null===e.sibling;){if(null===e.return||Cs(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;5!==e.tag&&6!==e.tag&&18!==e.tag;){if(27===e.tag&&Ad(e.type))continue e;if(2&e.flags)continue e;if(null===e.child||4===e.tag)continue e;e.child.return=e,e=e.child}if(!(2&e.flags))return e.stateNode}}function As(e,t,n){var r=e.tag;if(5===r||6===r)e=e.stateNode,t?(9===n.nodeType?n.body:"HTML"===n.nodeName?n.ownerDocument.body:n).insertBefore(e,t):((t=9===n.nodeType?n.body:"HTML"===n.nodeName?n.ownerDocument.body:n).appendChild(e),null!=(n=n._reactRootContainer)||null!==t.onclick||(t.onclick=It));else if(4!==r&&(27===r&&Ad(e.type)&&(n=e.stateNode,t=null),null!==(e=e.child)))for(As(e,t,n),e=e.sibling;null!==e;)As(e,t,n),e=e.sibling}function Ts(e,t,n){var r=e.tag;if(5===r||6===r)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(4!==r&&(27===r&&Ad(e.type)&&(n=e.stateNode),null!==(e=e.child)))for(Ts(e,t,n),e=e.sibling;null!==e;)Ts(e,t,n),e=e.sibling}function $s(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,o=t.attributes;o.length;)t.removeAttributeNode(o[0]);hd(t,r,n),t[Be]=e,t[Ve]=n}catch(t){Nc(e,e.return,t)}}var Ms=!1,Rs=!1,xs=!1,Is="function"==typeof WeakSet?WeakSet:Set,Ps=null;function _s(e,t,n){var r=n.flags;switch(n.tag){case 0:case 11:case 15:Xs(e,n),4&r&&bs(5,n);break;case 1:if(Xs(e,n),4&r)if(e=n.stateNode,null===t)try{e.componentDidMount()}catch(e){Nc(n,n.return,e)}else{var o=Nl(n.type,t.memoizedProps);t=t.memoizedState;try{e.componentDidUpdate(o,t,e.__reactInternalSnapshotBeforeUpdate)}catch(e){Nc(n,n.return,e)}}64&r&&ws(n),512&r&&ks(n,n.return);break;case 3:if(Xs(e,n),64&r&&null!==(e=n.updateQueue)){if(t=null,null!==n.child)switch(n.child.tag){case 27:case 5:case 1:t=n.child.stateNode}try{Ta(e,t)}catch(e){Nc(n,n.return,e)}}break;case 27:null===t&&4&r&&$s(n);case 26:case 5:Xs(e,n),null===t&&4&r&&Os(n),512&r&&ks(n,n.return);break;case 12:Xs(e,n);break;case 31:Xs(e,n),4&r&&qs(e,n);break;case 13:Xs(e,n),4&r&&Bs(e,n),64&r&&(null!==(e=n.memoizedState)&&(null!==(e=e.dehydrated)&&function(e,t){var n=e.ownerDocument;if("$~"===e.data)e._reactRetry=t;else if("$?"!==e.data||"loading"!==n.readyState)t();else{var r=function(){t(),n.removeEventListener("DOMContentLoaded",r)};n.addEventListener("DOMContentLoaded",r),e._reactRetry=r}}(e,n=Tc.bind(null,n))));break;case 22:if(!(r=null!==n.memoizedState||Ms)){t=null!==t&&null!==t.memoizedState||Rs,o=Ms;var a=Rs;Ms=r,(Rs=t)&&!a?Js(e,n,!!(8772&n.subtreeFlags)):Xs(e,n),Ms=o,Rs=a}break;case 30:break;default:Xs(e,n)}}function Ls(e){var t=e.alternate;null!==t&&(e.alternate=null,Ls(t)),e.child=null,e.deletions=null,e.sibling=null,5===e.tag&&(null!==(t=e.stateNode)&&Ye(t)),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}var Gs=null,Fs=!1;function zs(e,t,n){for(n=n.child;null!==n;)js(e,t,n),n=n.sibling}function js(e,t,n){if(we&&"function"==typeof we.onCommitFiberUnmount)try{we.onCommitFiberUnmount(ye,n)}catch(e){}switch(n.tag){case 26:Rs||Es(n,t),zs(e,t,n),n.memoizedState?n.memoizedState.count--:n.stateNode&&(n=n.stateNode).parentNode.removeChild(n);break;case 27:Rs||Es(n,t);var r=Gs,o=Fs;Ad(n.type)&&(Gs=n.stateNode,Fs=!1),zs(e,t,n),zd(n.stateNode),Gs=r,Fs=o;break;case 5:Rs||Es(n,t);case 6:if(r=Gs,o=Fs,Gs=null,zs(e,t,n),Fs=o,null!==(Gs=r))if(Fs)try{(9===Gs.nodeType?Gs.body:"HTML"===Gs.nodeName?Gs.ownerDocument.body:Gs).removeChild(n.stateNode)}catch(e){Nc(n,t,e)}else try{Gs.removeChild(n.stateNode)}catch(e){Nc(n,t,e)}break;case 18:null!==Gs&&(Fs?(Td(9===(e=Gs).nodeType?e.body:"HTML"===e.nodeName?e.ownerDocument.body:e,n.stateNode),Wf(e)):Td(Gs,n.stateNode));break;case 4:r=Gs,o=Fs,Gs=n.stateNode.containerInfo,Fs=!0,zs(e,t,n),Gs=r,Fs=o;break;case 0:case 11:case 14:case 15:ys(2,n,t),Rs||ys(4,n,t),zs(e,t,n);break;case 1:Rs||(Es(n,t),"function"==typeof(r=n.stateNode).componentWillUnmount&&Ss(n,t,r)),zs(e,t,n);break;case 21:zs(e,t,n);break;case 22:Rs=(r=Rs)||null!==n.memoizedState,zs(e,t,n),Rs=r;break;default:zs(e,t,n)}}function qs(e,t){if(null===t.memoizedState&&(null!==(e=t.alternate)&&null!==(e=e.memoizedState))){e=e.dehydrated;try{Wf(e)}catch(e){Nc(t,t.return,e)}}}function Bs(e,t){if(null===t.memoizedState&&(null!==(e=t.alternate)&&(null!==(e=e.memoizedState)&&null!==(e=e.dehydrated))))try{Wf(e)}catch(e){Nc(t,t.return,e)}}function Vs(e,t){var n=function(e){switch(e.tag){case 31:case 13:case 19:var t=e.stateNode;return null===t&&(t=e.stateNode=new Is),t;case 22:return null===(t=(e=e.stateNode)._retryCache)&&(t=e._retryCache=new Is),t;default:throw Error(i(435,e.tag))}}(e);t.forEach(function(t){if(!n.has(t)){n.add(t);var r=$c.bind(null,e,t);t.then(r,r)}})}function Hs(e,t){var n=t.deletions;if(null!==n)for(var r=0;r title"))),hd(a,r,n),a[Be]=e,nt(a),r=a;break e;case"link":var l=of("link","href",o).get(r+(n.href||""));if(l)for(var s=0;sl)break;var c=s.transferSize,d=s.initiatorType;c&&md(d)&&(i+=c*((s=s.responseEnd)sf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(o)}}:null}(d,h)))return qu=a,e.cancelPendingCommit=h(vc.bind(null,e,t,a,n,r,o,i,l,s,c,d,null,f,p)),void ec(e,a,i,!u)}vc(e,t,a,n,r,o,i,l,s)}function Zu(e){for(var t=e;;){var n=t.tag;if((0===n||11===n||15===n)&&16384&t.flags&&(null!==(n=t.updateQueue)&&null!==(n=n.stores)))for(var r=0;rg&&(i=g,g=m,m=i);var v=rr(l,m),b=rr(l,g);if(v&&b&&(1!==p.rangeCount||p.anchorNode!==v.node||p.anchorOffset!==v.offset||p.focusNode!==b.node||p.focusOffset!==b.offset)){var y=d.createRange();y.setStart(v.node,v.offset),p.removeAllRanges(),m>g?(p.addRange(y),p.extend(b.node,b.offset)):(y.setEnd(b.node,b.offset),p.addRange(y))}}}}for(d=[],p=l;p=p.parentNode;)1===p.nodeType&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for("function"==typeof l.focus&&l.focus(),l=0;ln?32:n,_.T=null,n=Vu,Vu=null;var a=zu,l=qu;if(Fu=0,ju=zu=null,qu=0,6&mu)throw Error(i(331));var s=mu;if(mu|=4,cu(a.current),nu(a,a.current,l,n),mu=s,Gc(0,!1),we&&"function"==typeof we.onPostCommitFiberRoot)try{we.onPostCommitFiberRoot(ye,a)}catch(e){}return!0}finally{L.p=o,_.T=r,Sc(e,t)}}function Oc(e,t,n){t=Xr(n,t),null!==(e=ka(e,t=Ml(e.stateNode,t,2),2))&&(Ie(e,2),Lc(e))}function Nc(e,t,n){if(3===e.tag)Oc(e,e,n);else for(;null!==t;){if(3===t.tag){Oc(t,e,n);break}if(1===t.tag){var r=t.stateNode;if("function"==typeof t.type.getDerivedStateFromError||"function"==typeof r.componentDidCatch&&(null===Gu||!Gu.has(r))){e=Xr(n,e),null!==(r=ka(t,n=Rl(2),2))&&(xl(n,r,t,e),Ie(r,2),Lc(r));break}}t=t.return}}function Cc(e,t,n){var r=e.pingCache;if(null===r){r=e.pingCache=new hu;var o=new Set;r.set(t,o)}else void 0===(o=r.get(t))&&(o=new Set,r.set(t,o));o.has(n)||(Eu=!0,o.add(n),e=Dc.bind(null,e,t,n),t.then(e,e))}function Dc(e,t,n){var r=e.pingCache;null!==r&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,gu===e&&(bu&n)===n&&(4===Nu||3===Nu&&(62914560&bu)===bu&&300>ce()-Iu?!(2&mu)&&rc(e,0):Au|=n,$u===bu&&($u=0)),Lc(e)}function Ac(e,t){0===t&&(t=Re()),null!==(e=Pr(e,t))&&(Ie(e,t),Lc(e))}function Tc(e){var t=e.memoizedState,n=0;null!==t&&(n=t.retryLane),Ac(e,n)}function $c(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,o=e.memoizedState;null!==o&&(n=o.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}null!==r&&r.delete(t),Ac(e,n)}var Mc=null,Rc=null,xc=!1,Ic=!1,Pc=!1,_c=0;function Lc(e){e!==Rc&&null===e.next&&(null===Rc?Mc=Rc=e:Rc=Rc.next=e),Ic=!0,xc||(xc=!0,Cd(function(){6&mu?ie(fe,Fc):zc()}))}function Gc(e,t){if(!Pc&&Ic){Pc=!0;do{for(var n=!1,r=Mc;null!==r;){if(!t)if(0!==e){var o=r.pendingLanes;if(0===o)var a=0;else{var i=r.suspendedLanes,l=r.pingedLanes;a=(1<<31-ke(42|e)+1)-1,a=201326741&(a&=o&~(i&~l))?201326741&a|1:a?2|a:0}0!==a&&(n=!0,Bc(r,a))}else a=bu,!(3&(a=Te(r,r===gu?a:0,null!==r.cancelPendingCommit||-1!==r.timeoutHandle)))||$e(r,a)||(n=!0,Bc(r,a));r=r.next}}while(n);Pc=!1}}function Fc(){zc()}function zc(){Ic=xc=!1;var e=0;0!==_c&&function(){var e=window.event;if(e&&"popstate"===e.type)return e!==kd&&(kd=e,!0);return kd=null,!1}()&&(e=_c);for(var t=ce(),n=null,r=Mc;null!==r;){var o=r.next,a=jc(r,t);0===a?(r.next=null,null===n?Mc=o:n.next=o,null===o&&(Rc=n)):(n=r,(0!==e||3&a)&&(Ic=!0)),r=o}0!==Fu&&5!==Fu||Gc(e,!1),0!==_c&&(_c=0)}function jc(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,o=e.expirationTimes,a=-62914561&e.pendingLanes;0 title"):null)}function lf(e){return!!("stylesheet"!==e.type||3&e.state.loading)}var sf=0;function uf(){if(this.count--,0===this.count&&(0===this.imgCount||!this.waitingForImages))if(this.stylesheets)df(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}var cf=null;function df(e,t){e.stylesheets=null,null!==e.unsuspend&&(e.count++,cf=new Map,t.forEach(ff,e),cf=null,uf.call(e))}function ff(e,t){if(!(4&t.state.loading)){var n=cf.get(e);if(n)var r=n.get(null);else{n=new Map,cf.set(e,n);for(var o=e.querySelectorAll("link[data-precedence],style[data-precedence]"),a=0;a>>1,o=e[r];if(!(0>>1;ra(s,n))ua(c,s)?(e[r]=c,e[u]=n,r=u):(e[r]=s,e[l]=n,r=l);else{if(!(ua(c,n)))break e;e[r]=c,e[u]=n,r=u}}}return t}function a(e,t){var n=e.sortIndex-t.sortIndex;return 0!==n?n:e.id-t.id}if(t.unstable_now=void 0,"object"==typeof performance&&"function"==typeof performance.now){var i=performance;t.unstable_now=function(){return i.now()}}else{var l=Date,s=l.now();t.unstable_now=function(){return l.now()-s}}var u=[],c=[],d=1,f=null,p=3,h=!1,m=!1,g=!1,v=!1,b="function"==typeof setTimeout?setTimeout:null,y="function"==typeof clearTimeout?clearTimeout:null,w="undefined"!=typeof setImmediate?setImmediate:null;function S(e){for(var t=r(c);null!==t;){if(null===t.callback)o(c);else{if(!(t.startTime<=e))break;o(c),t.sortIndex=t.expirationTime,n(u,t)}t=r(c)}}function k(e){if(g=!1,S(e),!m)if(null!==r(u))m=!0,O||(O=!0,E());else{var t=r(c);null!==t&&R(k,t.startTime-e)}}var E,O=!1,N=-1,C=5,D=-1;function A(){return!!v||!(t.unstable_now()-De&&A());){var i=f.callback;if("function"==typeof i){f.callback=null,p=f.priorityLevel;var l=i(f.expirationTime<=e);if(e=t.unstable_now(),"function"==typeof l){f.callback=l,S(e),n=!0;break t}f===r(u)&&o(u),S(e)}else o(u);f=r(u)}if(null!==f)n=!0;else{var s=r(c);null!==s&&R(k,s.startTime-e),n=!1}}break e}finally{f=null,p=a,h=!1}n=void 0}}finally{n?E():O=!1}}}if("function"==typeof w)E=function(){w(T)};else if("undefined"!=typeof MessageChannel){var $=new MessageChannel,M=$.port2;$.port1.onmessage=T,E=function(){M.postMessage(null)}}else E=function(){b(T,0)};function R(e,n){N=b(function(){e(t.unstable_now())},n)}t.unstable_IdlePriority=5,t.unstable_ImmediatePriority=1,t.unstable_LowPriority=4,t.unstable_NormalPriority=3,t.unstable_Profiling=null,t.unstable_UserBlockingPriority=2,t.unstable_cancelCallback=function(e){e.callback=null},t.unstable_forceFrameRate=function(e){0>e||125i?(e.sortIndex=a,n(c,e),null===r(u)&&e===r(c)&&(g?(y(N),N=-1):g=!0,R(k,a-i))):(e.sortIndex=l,n(u,e),m||h||(m=!0,O||(O=!0,E()))),e},t.unstable_shouldYield=A,t.unstable_wrapCallback=function(e){var t=p;return function(){var n=p;p=t;try{return e.apply(this,arguments)}finally{p=n}}}}),d=n.cw(function(e,t){e.exports=c()}),f=n.cw(function(e,t){ +/** + * @license React + * use-sync-external-store-with-selector.production.js + * + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +var n=s();var r="function"==typeof Object.is?Object.is:function(e,t){return e===t&&(0!==e||1/e==1/t)||e!=e&&t!=t},o=n.useSyncExternalStore,a=n.useRef,i=n.useEffect,l=n.useMemo,u=n.useDebugValue;t.useSyncExternalStoreWithSelector=function(e,t,n,s,c){var d=a(null);if(null===d.current){var f={hasValue:!1,value:null};d.current=f}else f=d.current;d=l(function(){function e(e){if(!i){if(i=!0,o=e,e=s(e),void 0!==c&&f.hasValue){var t=f.value;if(c(t,e))return a=t}return a=e}if(t=a,r(o,e))return t;var n=s(e);return void 0!==c&&c(t,n)?(o=e,t):(o=e,a=n)}var o,a,i=!1,l=void 0===n?null:n;return[function(){return e(t())},null===l?void 0:function(){return e(l())}]},[t,n,s,c]);var p=o(e,d[0],d[1]);return i(function(){f.hasValue=!0,f.value=p},[p]),u(p),p}}),p=n.cw(function(e,t){e.exports=f()}),h={};n.r(h),n.d(h,{DndContext:()=>uc,DndProvider:()=>md,DragPreviewImage:()=>vd,useDrag:()=>_d,useDragDropManager:()=>$d,useDragLayer:()=>Ld,useDrop:()=>Vd});var m={};n.r(m),n.d(m,{FILE:()=>Qd,HTML:()=>Yd,TEXT:()=>Xd,URL:()=>Kd});var g={};n.r(g),n.d(g,{FILE:()=>Qd,HTML:()=>Yd,TEXT:()=>Xd,URL:()=>Kd});var v={};n.r(v),n.d(v,{HTML5Backend:()=>hf,NativeTypes:()=>m,getEmptyImage:()=>pf});var b={};n.r(b),n.d(b,{ListenerType:()=>mf,TouchBackend:()=>Nf,TouchBackendImpl:()=>Of});class y{constructor(e,t,n){this.eventTarget=e,this.eventName=t,this.eventOptions=n,this.unorderedBindings=new Set}connect(){this.eventTarget.addEventListener(this.eventName,this,this.eventOptions)}disconnect(){this.eventTarget.removeEventListener(this.eventName,this,this.eventOptions)}bindingConnected(e){this.unorderedBindings.add(e)}bindingDisconnected(e){this.unorderedBindings.delete(e)}handleEvent(e){const t=function(e){if("immediatePropagationStopped"in e)return e;{const{stopImmediatePropagation:t}=e;return Object.assign(e,{immediatePropagationStopped:!1,stopImmediatePropagation(){this.immediatePropagationStopped=!0,t.call(this)}})}}(e);for(const e of this.bindings){if(t.immediatePropagationStopped)break;e.handleEvent(t)}}hasBindings(){return this.unorderedBindings.size>0}get bindings(){return Array.from(this.unorderedBindings).sort((e,t)=>{const n=e.index,r=t.index;return nr?1:0})}}class w{constructor(e){this.application=e,this.eventListenerMaps=new Map,this.started=!1}start(){this.started||(this.started=!0,this.eventListeners.forEach(e=>e.connect()))}stop(){this.started&&(this.started=!1,this.eventListeners.forEach(e=>e.disconnect()))}get eventListeners(){return Array.from(this.eventListenerMaps.values()).reduce((e,t)=>e.concat(Array.from(t.values())),[])}bindingConnected(e){this.fetchEventListenerForBinding(e).bindingConnected(e)}bindingDisconnected(e,t=!1){this.fetchEventListenerForBinding(e).bindingDisconnected(e),t&&this.clearEventListenersForBinding(e)}handleError(e,t,n={}){this.application.handleError(e,`Error ${t}`,n)}clearEventListenersForBinding(e){const t=this.fetchEventListenerForBinding(e);t.hasBindings()||(t.disconnect(),this.removeMappedEventListenerFor(e))}removeMappedEventListenerFor(e){const{eventTarget:t,eventName:n,eventOptions:r}=e,o=this.fetchEventListenerMapForEventTarget(t),a=this.cacheKey(n,r);o.delete(a),0==o.size&&this.eventListenerMaps.delete(t)}fetchEventListenerForBinding(e){const{eventTarget:t,eventName:n,eventOptions:r}=e;return this.fetchEventListener(t,n,r)}fetchEventListener(e,t,n){const r=this.fetchEventListenerMapForEventTarget(e),o=this.cacheKey(t,n);let a=r.get(o);return a||(a=this.createEventListener(e,t,n),r.set(o,a)),a}createEventListener(e,t,n){const r=new y(e,t,n);return this.started&&r.connect(),r}fetchEventListenerMapForEventTarget(e){let t=this.eventListenerMaps.get(e);return t||(t=new Map,this.eventListenerMaps.set(e,t)),t}cacheKey(e,t){const n=[e];return Object.keys(t).sort().forEach(e=>{n.push(`${t[e]?"":"!"}${e}`)}),n.join(":")}}const S={stop:({event:e,value:t})=>(t&&e.stopPropagation(),!0),prevent:({event:e,value:t})=>(t&&e.preventDefault(),!0),self:({event:e,value:t,element:n})=>!t||n===e.target},k=/^(?:(?:([^.]+?)\+)?(.+?)(?:\.(.+?))?(?:@(window|document))?->)?(.+?)(?:#([^:]+?))(?::(.+))?$/;function E(e){return"window"==e?window:"document"==e?document:void 0}function O(e){return e.replace(/(?:[_-])([a-z0-9])/g,(e,t)=>t.toUpperCase())}function N(e){return O(e.replace(/--/g,"-").replace(/__/g,"_"))}function C(e){return e.charAt(0).toUpperCase()+e.slice(1)}function D(e){return e.replace(/([A-Z])/g,(e,t)=>`-${t.toLowerCase()}`)}function A(e){return null!=e}function T(e,t){return Object.prototype.hasOwnProperty.call(e,t)}const $=["meta","ctrl","alt","shift"];class M{constructor(e,t,n,r){this.element=e,this.index=t,this.eventTarget=n.eventTarget||e,this.eventName=n.eventName||function(e){const t=e.tagName.toLowerCase();if(t in R)return R[t](e)}(e)||x("missing event name"),this.eventOptions=n.eventOptions||{},this.identifier=n.identifier||x("missing identifier"),this.methodName=n.methodName||x("missing method name"),this.keyFilter=n.keyFilter||"",this.schema=r}static forToken(e,t){return new this(e.element,e.index,function(e){const t=e.trim().match(k)||[];let n=t[2],r=t[3];return r&&!["keydown","keyup","keypress"].includes(n)&&(n+=`.${r}`,r=""),{eventTarget:E(t[4]),eventName:n,eventOptions:t[7]?(o=t[7],o.split(":").reduce((e,t)=>Object.assign(e,{[t.replace(/^!/,"")]:!/^!/.test(t)}),{})):{},identifier:t[5],methodName:t[6],keyFilter:t[1]||r};var o}(e.content),t)}toString(){const e=this.keyFilter?`.${this.keyFilter}`:"",t=this.eventTargetName?`@${this.eventTargetName}`:"";return`${this.eventName}${e}${t}->${this.identifier}#${this.methodName}`}shouldIgnoreKeyboardEvent(e){if(!this.keyFilter)return!1;const t=this.keyFilter.split("+");if(this.keyFilterDissatisfied(e,t))return!0;const n=t.filter(e=>!$.includes(e))[0];return!!n&&(T(this.keyMappings,n)||x(`contains unknown key filter: ${this.keyFilter}`),this.keyMappings[n].toLowerCase()!==e.key.toLowerCase())}shouldIgnoreMouseEvent(e){if(!this.keyFilter)return!1;const t=[this.keyFilter];return!!this.keyFilterDissatisfied(e,t)}get params(){const e={},t=new RegExp(`^data-${this.identifier}-(.+)-param$`,"i");for(const{name:n,value:r}of Array.from(this.element.attributes)){const o=n.match(t),a=o&&o[1];a&&(e[O(a)]=I(r))}return e}get eventTargetName(){return(e=this.eventTarget)==window?"window":e==document?"document":void 0;var e}get keyMappings(){return this.schema.keyMappings}keyFilterDissatisfied(e,t){const[n,r,o,a]=$.map(e=>t.includes(e));return e.metaKey!==n||e.ctrlKey!==r||e.altKey!==o||e.shiftKey!==a}}const R={a:()=>"click",button:()=>"click",form:()=>"submit",details:()=>"toggle",input:e=>"submit"==e.getAttribute("type")?"click":"input",select:()=>"change",textarea:()=>"input"};function x(e){throw new Error(e)}function I(e){try{return JSON.parse(e)}catch(t){return e}}class P{constructor(e,t){this.context=e,this.action=t}get index(){return this.action.index}get eventTarget(){return this.action.eventTarget}get eventOptions(){return this.action.eventOptions}get identifier(){return this.context.identifier}handleEvent(e){const t=this.prepareActionEvent(e);this.willBeInvokedByEvent(e)&&this.applyEventModifiers(t)&&this.invokeWithEvent(t)}get eventName(){return this.action.eventName}get method(){const e=this.controller[this.methodName];if("function"==typeof e)return e;throw new Error(`Action "${this.action}" references undefined method "${this.methodName}"`)}applyEventModifiers(e){const{element:t}=this.action,{actionDescriptorFilters:n}=this.context.application,{controller:r}=this.context;let o=!0;for(const[a,i]of Object.entries(this.eventOptions))if(a in n){const l=n[a];o=o&&l({name:a,value:i,event:e,element:t,controller:r})}return o}prepareActionEvent(e){return Object.assign(e,{params:this.action.params})}invokeWithEvent(e){const{target:t,currentTarget:n}=e;try{this.method.call(this.controller,e),this.context.logDebugActivity(this.methodName,{event:e,target:t,currentTarget:n,action:this.methodName})}catch(t){const{identifier:n,controller:r,element:o,index:a}=this,i={identifier:n,controller:r,element:o,index:a,event:e};this.context.handleError(t,`invoking action "${this.action}"`,i)}}willBeInvokedByEvent(e){const t=e.target;return!(e instanceof KeyboardEvent&&this.action.shouldIgnoreKeyboardEvent(e))&&(!(e instanceof MouseEvent&&this.action.shouldIgnoreMouseEvent(e))&&(this.element===t||(t instanceof Element&&this.element.contains(t)?this.scope.containsElement(t):this.scope.containsElement(this.action.element))))}get controller(){return this.context.controller}get methodName(){return this.action.methodName}get element(){return this.scope.element}get scope(){return this.context.scope}}class _{constructor(e,t){this.mutationObserverInit={attributes:!0,childList:!0,subtree:!0},this.element=e,this.started=!1,this.delegate=t,this.elements=new Set,this.mutationObserver=new MutationObserver(e=>this.processMutations(e))}start(){this.started||(this.started=!0,this.mutationObserver.observe(this.element,this.mutationObserverInit),this.refresh())}pause(e){this.started&&(this.mutationObserver.disconnect(),this.started=!1),e(),this.started||(this.mutationObserver.observe(this.element,this.mutationObserverInit),this.started=!0)}stop(){this.started&&(this.mutationObserver.takeRecords(),this.mutationObserver.disconnect(),this.started=!1)}refresh(){if(this.started){const e=new Set(this.matchElementsInTree());for(const t of Array.from(this.elements))e.has(t)||this.removeElement(t);for(const t of Array.from(e))this.addElement(t)}}processMutations(e){if(this.started)for(const t of e)this.processMutation(t)}processMutation(e){"attributes"==e.type?this.processAttributeChange(e.target,e.attributeName):"childList"==e.type&&(this.processRemovedNodes(e.removedNodes),this.processAddedNodes(e.addedNodes))}processAttributeChange(e,t){this.elements.has(e)?this.delegate.elementAttributeChanged&&this.matchElement(e)?this.delegate.elementAttributeChanged(e,t):this.removeElement(e):this.matchElement(e)&&this.addElement(e)}processRemovedNodes(e){for(const t of Array.from(e)){const e=this.elementFromNode(t);e&&this.processTree(e,this.removeElement)}}processAddedNodes(e){for(const t of Array.from(e)){const e=this.elementFromNode(t);e&&this.elementIsActive(e)&&this.processTree(e,this.addElement)}}matchElement(e){return this.delegate.matchElement(e)}matchElementsInTree(e=this.element){return this.delegate.matchElementsInTree(e)}processTree(e,t){for(const n of this.matchElementsInTree(e))t.call(this,n)}elementFromNode(e){if(e.nodeType==Node.ELEMENT_NODE)return e}elementIsActive(e){return e.isConnected==this.element.isConnected&&this.element.contains(e)}addElement(e){this.elements.has(e)||this.elementIsActive(e)&&(this.elements.add(e),this.delegate.elementMatched&&this.delegate.elementMatched(e))}removeElement(e){this.elements.has(e)&&(this.elements.delete(e),this.delegate.elementUnmatched&&this.delegate.elementUnmatched(e))}}class L{constructor(e,t,n){this.attributeName=t,this.delegate=n,this.elementObserver=new _(e,this)}get element(){return this.elementObserver.element}get selector(){return`[${this.attributeName}]`}start(){this.elementObserver.start()}pause(e){this.elementObserver.pause(e)}stop(){this.elementObserver.stop()}refresh(){this.elementObserver.refresh()}get started(){return this.elementObserver.started}matchElement(e){return e.hasAttribute(this.attributeName)}matchElementsInTree(e){const t=this.matchElement(e)?[e]:[],n=Array.from(e.querySelectorAll(this.selector));return t.concat(n)}elementMatched(e){this.delegate.elementMatchedAttribute&&this.delegate.elementMatchedAttribute(e,this.attributeName)}elementUnmatched(e){this.delegate.elementUnmatchedAttribute&&this.delegate.elementUnmatchedAttribute(e,this.attributeName)}elementAttributeChanged(e,t){this.delegate.elementAttributeValueChanged&&this.attributeName==t&&this.delegate.elementAttributeValueChanged(e,t)}}function G(e,t,n){z(e,t).add(n)}function F(e,t,n){z(e,t).delete(n),function(e,t){const n=e.get(t);null!=n&&0==n.size&&e.delete(t)}(e,t)}function z(e,t){let n=e.get(t);return n||(n=new Set,e.set(t,n)),n}class j{constructor(){this.valuesByKey=new Map}get keys(){return Array.from(this.valuesByKey.keys())}get values(){return Array.from(this.valuesByKey.values()).reduce((e,t)=>e.concat(Array.from(t)),[])}get size(){return Array.from(this.valuesByKey.values()).reduce((e,t)=>e+t.size,0)}add(e,t){G(this.valuesByKey,e,t)}delete(e,t){F(this.valuesByKey,e,t)}has(e,t){const n=this.valuesByKey.get(e);return null!=n&&n.has(t)}hasKey(e){return this.valuesByKey.has(e)}hasValue(e){return Array.from(this.valuesByKey.values()).some(t=>t.has(e))}getValuesForKey(e){const t=this.valuesByKey.get(e);return t?Array.from(t):[]}getKeysForValue(e){return Array.from(this.valuesByKey).filter(([t,n])=>n.has(e)).map(([e,t])=>e)}}class q{constructor(e,t,n,r){this._selector=t,this.details=r,this.elementObserver=new _(e,this),this.delegate=n,this.matchesByElement=new j}get started(){return this.elementObserver.started}get selector(){return this._selector}set selector(e){this._selector=e,this.refresh()}start(){this.elementObserver.start()}pause(e){this.elementObserver.pause(e)}stop(){this.elementObserver.stop()}refresh(){this.elementObserver.refresh()}get element(){return this.elementObserver.element}matchElement(e){const{selector:t}=this;if(t){const n=e.matches(t);return this.delegate.selectorMatchElement?n&&this.delegate.selectorMatchElement(e,this.details):n}return!1}matchElementsInTree(e){const{selector:t}=this;if(t){const n=this.matchElement(e)?[e]:[],r=Array.from(e.querySelectorAll(t)).filter(e=>this.matchElement(e));return n.concat(r)}return[]}elementMatched(e){const{selector:t}=this;t&&this.selectorMatched(e,t)}elementUnmatched(e){const t=this.matchesByElement.getKeysForValue(e);for(const n of t)this.selectorUnmatched(e,n)}elementAttributeChanged(e,t){const{selector:n}=this;if(n){const t=this.matchElement(e),r=this.matchesByElement.has(n,e);t&&!r?this.selectorMatched(e,n):!t&&r&&this.selectorUnmatched(e,n)}}selectorMatched(e,t){this.delegate.selectorMatched(e,t,this.details),this.matchesByElement.add(t,e)}selectorUnmatched(e,t){this.delegate.selectorUnmatched(e,t,this.details),this.matchesByElement.delete(t,e)}}class B{constructor(e,t){this.element=e,this.delegate=t,this.started=!1,this.stringMap=new Map,this.mutationObserver=new MutationObserver(e=>this.processMutations(e))}start(){this.started||(this.started=!0,this.mutationObserver.observe(this.element,{attributes:!0,attributeOldValue:!0}),this.refresh())}stop(){this.started&&(this.mutationObserver.takeRecords(),this.mutationObserver.disconnect(),this.started=!1)}refresh(){if(this.started)for(const e of this.knownAttributeNames)this.refreshAttribute(e,null)}processMutations(e){if(this.started)for(const t of e)this.processMutation(t)}processMutation(e){const t=e.attributeName;t&&this.refreshAttribute(t,e.oldValue)}refreshAttribute(e,t){const n=this.delegate.getStringMapKeyForAttribute(e);if(null!=n){this.stringMap.has(e)||this.stringMapKeyAdded(n,e);const r=this.element.getAttribute(e);if(this.stringMap.get(e)!=r&&this.stringMapValueChanged(r,n,t),null==r){const t=this.stringMap.get(e);this.stringMap.delete(e),t&&this.stringMapKeyRemoved(n,e,t)}else this.stringMap.set(e,r)}}stringMapKeyAdded(e,t){this.delegate.stringMapKeyAdded&&this.delegate.stringMapKeyAdded(e,t)}stringMapValueChanged(e,t,n){this.delegate.stringMapValueChanged&&this.delegate.stringMapValueChanged(e,t,n)}stringMapKeyRemoved(e,t,n){this.delegate.stringMapKeyRemoved&&this.delegate.stringMapKeyRemoved(e,t,n)}get knownAttributeNames(){return Array.from(new Set(this.currentAttributeNames.concat(this.recordedAttributeNames)))}get currentAttributeNames(){return Array.from(this.element.attributes).map(e=>e.name)}get recordedAttributeNames(){return Array.from(this.stringMap.keys())}}class V{constructor(e,t,n){this.attributeObserver=new L(e,t,this),this.delegate=n,this.tokensByElement=new j}get started(){return this.attributeObserver.started}start(){this.attributeObserver.start()}pause(e){this.attributeObserver.pause(e)}stop(){this.attributeObserver.stop()}refresh(){this.attributeObserver.refresh()}get element(){return this.attributeObserver.element}get attributeName(){return this.attributeObserver.attributeName}elementMatchedAttribute(e){this.tokensMatched(this.readTokensForElement(e))}elementAttributeValueChanged(e){const[t,n]=this.refreshTokensForElement(e);this.tokensUnmatched(t),this.tokensMatched(n)}elementUnmatchedAttribute(e){this.tokensUnmatched(this.tokensByElement.getValuesForKey(e))}tokensMatched(e){e.forEach(e=>this.tokenMatched(e))}tokensUnmatched(e){e.forEach(e=>this.tokenUnmatched(e))}tokenMatched(e){this.delegate.tokenMatched(e),this.tokensByElement.add(e.element,e)}tokenUnmatched(e){this.delegate.tokenUnmatched(e),this.tokensByElement.delete(e.element,e)}refreshTokensForElement(e){const t=this.tokensByElement.getValuesForKey(e),n=this.readTokensForElement(e),r=function(e,t){const n=Math.max(e.length,t.length);return Array.from({length:n},(n,r)=>[e[r],t[r]])}(t,n).findIndex(([e,t])=>{return r=t,!((n=e)&&r&&n.index==r.index&&n.content==r.content);var n,r});return-1==r?[[],[]]:[t.slice(r),n.slice(r)]}readTokensForElement(e){const t=this.attributeName;return function(e,t,n){return e.trim().split(/\s+/).filter(e=>e.length).map((e,r)=>({element:t,attributeName:n,content:e,index:r}))}(e.getAttribute(t)||"",e,t)}}class H{constructor(e,t,n){this.tokenListObserver=new V(e,t,this),this.delegate=n,this.parseResultsByToken=new WeakMap,this.valuesByTokenByElement=new WeakMap}get started(){return this.tokenListObserver.started}start(){this.tokenListObserver.start()}stop(){this.tokenListObserver.stop()}refresh(){this.tokenListObserver.refresh()}get element(){return this.tokenListObserver.element}get attributeName(){return this.tokenListObserver.attributeName}tokenMatched(e){const{element:t}=e,{value:n}=this.fetchParseResultForToken(e);n&&(this.fetchValuesByTokenForElement(t).set(e,n),this.delegate.elementMatchedValue(t,n))}tokenUnmatched(e){const{element:t}=e,{value:n}=this.fetchParseResultForToken(e);n&&(this.fetchValuesByTokenForElement(t).delete(e),this.delegate.elementUnmatchedValue(t,n))}fetchParseResultForToken(e){let t=this.parseResultsByToken.get(e);return t||(t=this.parseToken(e),this.parseResultsByToken.set(e,t)),t}fetchValuesByTokenForElement(e){let t=this.valuesByTokenByElement.get(e);return t||(t=new Map,this.valuesByTokenByElement.set(e,t)),t}parseToken(e){try{return{value:this.delegate.parseValueForToken(e)}}catch(e){return{error:e}}}}class U{constructor(e,t){this.context=e,this.delegate=t,this.bindingsByAction=new Map}start(){this.valueListObserver||(this.valueListObserver=new H(this.element,this.actionAttribute,this),this.valueListObserver.start())}stop(){this.valueListObserver&&(this.valueListObserver.stop(),delete this.valueListObserver,this.disconnectAllActions())}get element(){return this.context.element}get identifier(){return this.context.identifier}get actionAttribute(){return this.schema.actionAttribute}get schema(){return this.context.schema}get bindings(){return Array.from(this.bindingsByAction.values())}connectAction(e){const t=new P(this.context,e);this.bindingsByAction.set(e,t),this.delegate.bindingConnected(t)}disconnectAction(e){const t=this.bindingsByAction.get(e);t&&(this.bindingsByAction.delete(e),this.delegate.bindingDisconnected(t))}disconnectAllActions(){this.bindings.forEach(e=>this.delegate.bindingDisconnected(e,!0)),this.bindingsByAction.clear()}parseValueForToken(e){const t=M.forToken(e,this.schema);if(t.identifier==this.identifier)return t}elementMatchedValue(e,t){this.connectAction(t)}elementUnmatchedValue(e,t){this.disconnectAction(t)}}class W{constructor(e,t){this.context=e,this.receiver=t,this.stringMapObserver=new B(this.element,this),this.valueDescriptorMap=this.controller.valueDescriptorMap}start(){this.stringMapObserver.start(),this.invokeChangedCallbacksForDefaultValues()}stop(){this.stringMapObserver.stop()}get element(){return this.context.element}get controller(){return this.context.controller}getStringMapKeyForAttribute(e){if(e in this.valueDescriptorMap)return this.valueDescriptorMap[e].name}stringMapKeyAdded(e,t){const n=this.valueDescriptorMap[t];this.hasValue(e)||this.invokeChangedCallback(e,n.writer(this.receiver[e]),n.writer(n.defaultValue))}stringMapValueChanged(e,t,n){const r=this.valueDescriptorNameMap[t];null!==e&&(null===n&&(n=r.writer(r.defaultValue)),this.invokeChangedCallback(t,e,n))}stringMapKeyRemoved(e,t,n){const r=this.valueDescriptorNameMap[e];this.hasValue(e)?this.invokeChangedCallback(e,r.writer(this.receiver[e]),n):this.invokeChangedCallback(e,r.writer(r.defaultValue),n)}invokeChangedCallbacksForDefaultValues(){for(const{key:e,name:t,defaultValue:n,writer:r}of this.valueDescriptors)null==n||this.controller.data.has(e)||this.invokeChangedCallback(t,r(n),void 0)}invokeChangedCallback(e,t,n){const r=`${e}Changed`,o=this.receiver[r];if("function"==typeof o){const r=this.valueDescriptorNameMap[e];try{const e=r.reader(t);let a=n;n&&(a=r.reader(n)),o.call(this.receiver,e,a)}catch(e){throw e instanceof TypeError&&(e.message=`Stimulus Value "${this.context.identifier}.${r.name}" - ${e.message}`),e}}}get valueDescriptors(){const{valueDescriptorMap:e}=this;return Object.keys(e).map(t=>e[t])}get valueDescriptorNameMap(){const e={};return Object.keys(this.valueDescriptorMap).forEach(t=>{const n=this.valueDescriptorMap[t];e[n.name]=n}),e}hasValue(e){const t=`has${C(this.valueDescriptorNameMap[e].name)}`;return this.receiver[t]}}class Q{constructor(e,t){this.context=e,this.delegate=t,this.targetsByName=new j}start(){this.tokenListObserver||(this.tokenListObserver=new V(this.element,this.attributeName,this),this.tokenListObserver.start())}stop(){this.tokenListObserver&&(this.disconnectAllTargets(),this.tokenListObserver.stop(),delete this.tokenListObserver)}tokenMatched({element:e,content:t}){this.scope.containsElement(e)&&this.connectTarget(e,t)}tokenUnmatched({element:e,content:t}){this.disconnectTarget(e,t)}connectTarget(e,t){var n;this.targetsByName.has(t,e)||(this.targetsByName.add(t,e),null===(n=this.tokenListObserver)||void 0===n||n.pause(()=>this.delegate.targetConnected(e,t)))}disconnectTarget(e,t){var n;this.targetsByName.has(t,e)&&(this.targetsByName.delete(t,e),null===(n=this.tokenListObserver)||void 0===n||n.pause(()=>this.delegate.targetDisconnected(e,t)))}disconnectAllTargets(){for(const e of this.targetsByName.keys)for(const t of this.targetsByName.getValuesForKey(e))this.disconnectTarget(t,e)}get attributeName(){return`data-${this.context.identifier}-target`}get element(){return this.context.element}get scope(){return this.context.scope}}function K(e,t){const n=Y(e);return Array.from(n.reduce((e,n)=>(function(e,t){const n=e[t];return Array.isArray(n)?n:[]}(n,t).forEach(t=>e.add(t)),e),new Set))}function X(e,t){return Y(e).reduce((e,n)=>(e.push(...function(e,t){const n=e[t];return n?Object.keys(n).map(e=>[e,n[e]]):[]}(n,t)),e),[])}function Y(e){const t=[];for(;e;)t.push(e),e=Object.getPrototypeOf(e);return t.reverse()}class J{constructor(e,t){this.started=!1,this.context=e,this.delegate=t,this.outletsByName=new j,this.outletElementsByName=new j,this.selectorObserverMap=new Map,this.attributeObserverMap=new Map}start(){this.started||(this.outletDefinitions.forEach(e=>{this.setupSelectorObserverForOutlet(e),this.setupAttributeObserverForOutlet(e)}),this.started=!0,this.dependentContexts.forEach(e=>e.refresh()))}refresh(){this.selectorObserverMap.forEach(e=>e.refresh()),this.attributeObserverMap.forEach(e=>e.refresh())}stop(){this.started&&(this.started=!1,this.disconnectAllOutlets(),this.stopSelectorObservers(),this.stopAttributeObservers())}stopSelectorObservers(){this.selectorObserverMap.size>0&&(this.selectorObserverMap.forEach(e=>e.stop()),this.selectorObserverMap.clear())}stopAttributeObservers(){this.attributeObserverMap.size>0&&(this.attributeObserverMap.forEach(e=>e.stop()),this.attributeObserverMap.clear())}selectorMatched(e,t,{outletName:n}){const r=this.getOutlet(e,n);r&&this.connectOutlet(r,e,n)}selectorUnmatched(e,t,{outletName:n}){const r=this.getOutletFromMap(e,n);r&&this.disconnectOutlet(r,e,n)}selectorMatchElement(e,{outletName:t}){const n=this.selector(t),r=this.hasOutlet(e,t),o=e.matches(`[${this.schema.controllerAttribute}~=${t}]`);return!!n&&(r&&o&&e.matches(n))}elementMatchedAttribute(e,t){const n=this.getOutletNameFromOutletAttributeName(t);n&&this.updateSelectorObserverForOutlet(n)}elementAttributeValueChanged(e,t){const n=this.getOutletNameFromOutletAttributeName(t);n&&this.updateSelectorObserverForOutlet(n)}elementUnmatchedAttribute(e,t){const n=this.getOutletNameFromOutletAttributeName(t);n&&this.updateSelectorObserverForOutlet(n)}connectOutlet(e,t,n){var r;this.outletElementsByName.has(n,t)||(this.outletsByName.add(n,e),this.outletElementsByName.add(n,t),null===(r=this.selectorObserverMap.get(n))||void 0===r||r.pause(()=>this.delegate.outletConnected(e,t,n)))}disconnectOutlet(e,t,n){var r;this.outletElementsByName.has(n,t)&&(this.outletsByName.delete(n,e),this.outletElementsByName.delete(n,t),null===(r=this.selectorObserverMap.get(n))||void 0===r||r.pause(()=>this.delegate.outletDisconnected(e,t,n)))}disconnectAllOutlets(){for(const e of this.outletElementsByName.keys)for(const t of this.outletElementsByName.getValuesForKey(e))for(const n of this.outletsByName.getValuesForKey(e))this.disconnectOutlet(n,t,e)}updateSelectorObserverForOutlet(e){const t=this.selectorObserverMap.get(e);t&&(t.selector=this.selector(e))}setupSelectorObserverForOutlet(e){const t=this.selector(e),n=new q(document.body,t,this,{outletName:e});this.selectorObserverMap.set(e,n),n.start()}setupAttributeObserverForOutlet(e){const t=this.attributeNameForOutletName(e),n=new L(this.scope.element,t,this);this.attributeObserverMap.set(e,n),n.start()}selector(e){return this.scope.outlets.getSelectorForOutletName(e)}attributeNameForOutletName(e){return this.scope.schema.outletAttributeForScope(this.identifier,e)}getOutletNameFromOutletAttributeName(e){return this.outletDefinitions.find(t=>this.attributeNameForOutletName(t)===e)}get outletDependencies(){const e=new j;return this.router.modules.forEach(t=>{K(t.definition.controllerConstructor,"outlets").forEach(n=>e.add(n,t.identifier))}),e}get outletDefinitions(){return this.outletDependencies.getKeysForValue(this.identifier)}get dependentControllerIdentifiers(){return this.outletDependencies.getValuesForKey(this.identifier)}get dependentContexts(){const e=this.dependentControllerIdentifiers;return this.router.contexts.filter(t=>e.includes(t.identifier))}hasOutlet(e,t){return!!this.getOutlet(e,t)||!!this.getOutletFromMap(e,t)}getOutlet(e,t){return this.application.getControllerForElementAndIdentifier(e,t)}getOutletFromMap(e,t){return this.outletsByName.getValuesForKey(t).find(t=>t.element===e)}get scope(){return this.context.scope}get schema(){return this.context.schema}get identifier(){return this.context.identifier}get application(){return this.context.application}get router(){return this.application.router}}class Z{constructor(e,t){this.logDebugActivity=(e,t={})=>{const{identifier:n,controller:r,element:o}=this;t=Object.assign({identifier:n,controller:r,element:o},t),this.application.logDebugActivity(this.identifier,e,t)},this.module=e,this.scope=t,this.controller=new e.controllerConstructor(this),this.bindingObserver=new U(this,this.dispatcher),this.valueObserver=new W(this,this.controller),this.targetObserver=new Q(this,this),this.outletObserver=new J(this,this);try{this.controller.initialize(),this.logDebugActivity("initialize")}catch(e){this.handleError(e,"initializing controller")}}connect(){this.bindingObserver.start(),this.valueObserver.start(),this.targetObserver.start(),this.outletObserver.start();try{this.controller.connect(),this.logDebugActivity("connect")}catch(e){this.handleError(e,"connecting controller")}}refresh(){this.outletObserver.refresh()}disconnect(){try{this.controller.disconnect(),this.logDebugActivity("disconnect")}catch(e){this.handleError(e,"disconnecting controller")}this.outletObserver.stop(),this.targetObserver.stop(),this.valueObserver.stop(),this.bindingObserver.stop()}get application(){return this.module.application}get identifier(){return this.module.identifier}get schema(){return this.application.schema}get dispatcher(){return this.application.dispatcher}get element(){return this.scope.element}get parentElement(){return this.element.parentElement}handleError(e,t,n={}){const{identifier:r,controller:o,element:a}=this;n=Object.assign({identifier:r,controller:o,element:a},n),this.application.handleError(e,`Error ${t}`,n)}targetConnected(e,t){this.invokeControllerMethod(`${t}TargetConnected`,e)}targetDisconnected(e,t){this.invokeControllerMethod(`${t}TargetDisconnected`,e)}outletConnected(e,t,n){this.invokeControllerMethod(`${N(n)}OutletConnected`,e,t)}outletDisconnected(e,t,n){this.invokeControllerMethod(`${N(n)}OutletDisconnected`,e,t)}invokeControllerMethod(e,...t){const n=this.controller;"function"==typeof n[e]&&n[e](...t)}}function ee(e){return function(e,t){const n=ne(e),r=function(e,t){return te(t).reduce((n,r)=>{const o=function(e,t,n){const r=Object.getOwnPropertyDescriptor(e,n);if(!r||!("value"in r)){const e=Object.getOwnPropertyDescriptor(t,n).value;return r&&(e.get=r.get||e.get,e.set=r.set||e.set),e}}(e,t,r);return o&&Object.assign(n,{[r]:o}),n},{})}(e.prototype,t);return Object.defineProperties(n.prototype,r),n}(e,function(e){const t=K(e,"blessings");return t.reduce((t,n)=>{const r=n(e);for(const e in r){const n=t[e]||{};t[e]=Object.assign(n,r[e])}return t},{})}(e))}const te="function"==typeof Object.getOwnPropertySymbols?e=>[...Object.getOwnPropertyNames(e),...Object.getOwnPropertySymbols(e)]:Object.getOwnPropertyNames,ne=(()=>{function e(e){function t(){return Reflect.construct(e,arguments,new.target)}return t.prototype=Object.create(e.prototype,{constructor:{value:t}}),Reflect.setPrototypeOf(t,e),t}try{return function(){const t=e(function(){this.a.call(this)});t.prototype.a=function(){},new t}(),e}catch(e){return e=>class extends e{}}})();class re{constructor(e,t){this.application=e,this.definition=function(e){return{identifier:e.identifier,controllerConstructor:ee(e.controllerConstructor)}}(t),this.contextsByScope=new WeakMap,this.connectedContexts=new Set}get identifier(){return this.definition.identifier}get controllerConstructor(){return this.definition.controllerConstructor}get contexts(){return Array.from(this.connectedContexts)}connectContextForScope(e){const t=this.fetchContextForScope(e);this.connectedContexts.add(t),t.connect()}disconnectContextForScope(e){const t=this.contextsByScope.get(e);t&&(this.connectedContexts.delete(t),t.disconnect())}fetchContextForScope(e){let t=this.contextsByScope.get(e);return t||(t=new Z(this,e),this.contextsByScope.set(e,t)),t}}class oe{constructor(e){this.scope=e}has(e){return this.data.has(this.getDataKey(e))}get(e){return this.getAll(e)[0]}getAll(e){const t=this.data.get(this.getDataKey(e))||"";return t.match(/[^\s]+/g)||[]}getAttributeName(e){return this.data.getAttributeNameForKey(this.getDataKey(e))}getDataKey(e){return`${e}-class`}get data(){return this.scope.data}}class ae{constructor(e){this.scope=e}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get(e){const t=this.getAttributeNameForKey(e);return this.element.getAttribute(t)}set(e,t){const n=this.getAttributeNameForKey(e);return this.element.setAttribute(n,t),this.get(e)}has(e){const t=this.getAttributeNameForKey(e);return this.element.hasAttribute(t)}delete(e){if(this.has(e)){const t=this.getAttributeNameForKey(e);return this.element.removeAttribute(t),!0}return!1}getAttributeNameForKey(e){return`data-${this.identifier}-${D(e)}`}}class ie{constructor(e){this.warnedKeysByObject=new WeakMap,this.logger=e}warn(e,t,n){let r=this.warnedKeysByObject.get(e);r||(r=new Set,this.warnedKeysByObject.set(e,r)),r.has(t)||(r.add(t),this.logger.warn(n,e))}}function le(e,t){return`[${e}~="${t}"]`}class se{constructor(e){this.scope=e}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get schema(){return this.scope.schema}has(e){return null!=this.find(e)}find(...e){return e.reduce((e,t)=>e||this.findTarget(t)||this.findLegacyTarget(t),void 0)}findAll(...e){return e.reduce((e,t)=>[...e,...this.findAllTargets(t),...this.findAllLegacyTargets(t)],[])}findTarget(e){const t=this.getSelectorForTargetName(e);return this.scope.findElement(t)}findAllTargets(e){const t=this.getSelectorForTargetName(e);return this.scope.findAllElements(t)}getSelectorForTargetName(e){return le(this.schema.targetAttributeForScope(this.identifier),e)}findLegacyTarget(e){const t=this.getLegacySelectorForTargetName(e);return this.deprecate(this.scope.findElement(t),e)}findAllLegacyTargets(e){const t=this.getLegacySelectorForTargetName(e);return this.scope.findAllElements(t).map(t=>this.deprecate(t,e))}getLegacySelectorForTargetName(e){const t=`${this.identifier}.${e}`;return le(this.schema.targetAttribute,t)}deprecate(e,t){if(e){const{identifier:n}=this,r=this.schema.targetAttribute,o=this.schema.targetAttributeForScope(n);this.guide.warn(e,`target:${t}`,`Please replace ${r}="${n}.${t}" with ${o}="${t}". The ${r} attribute is deprecated and will be removed in a future version of Stimulus.`)}return e}get guide(){return this.scope.guide}}class ue{constructor(e,t){this.scope=e,this.controllerElement=t}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get schema(){return this.scope.schema}has(e){return null!=this.find(e)}find(...e){return e.reduce((e,t)=>e||this.findOutlet(t),void 0)}findAll(...e){return e.reduce((e,t)=>[...e,...this.findAllOutlets(t)],[])}getSelectorForOutletName(e){const t=this.schema.outletAttributeForScope(this.identifier,e);return this.controllerElement.getAttribute(t)}findOutlet(e){const t=this.getSelectorForOutletName(e);if(t)return this.findElement(t,e)}findAllOutlets(e){const t=this.getSelectorForOutletName(e);return t?this.findAllElements(t,e):[]}findElement(e,t){return this.scope.queryElements(e).filter(n=>this.matchesElement(n,e,t))[0]}findAllElements(e,t){return this.scope.queryElements(e).filter(n=>this.matchesElement(n,e,t))}matchesElement(e,t,n){const r=e.getAttribute(this.scope.schema.controllerAttribute)||"";return e.matches(t)&&r.split(" ").includes(n)}}class ce{constructor(e,t,n,r){this.targets=new se(this),this.classes=new oe(this),this.data=new ae(this),this.containsElement=e=>e.closest(this.controllerSelector)===this.element,this.schema=e,this.element=t,this.identifier=n,this.guide=new ie(r),this.outlets=new ue(this.documentScope,t)}findElement(e){return this.element.matches(e)?this.element:this.queryElements(e).find(this.containsElement)}findAllElements(e){return[...this.element.matches(e)?[this.element]:[],...this.queryElements(e).filter(this.containsElement)]}queryElements(e){return Array.from(this.element.querySelectorAll(e))}get controllerSelector(){return le(this.schema.controllerAttribute,this.identifier)}get isDocumentScope(){return this.element===document.documentElement}get documentScope(){return this.isDocumentScope?this:new ce(this.schema,document.documentElement,this.identifier,this.guide.logger)}}class de{constructor(e,t,n){this.element=e,this.schema=t,this.delegate=n,this.valueListObserver=new H(this.element,this.controllerAttribute,this),this.scopesByIdentifierByElement=new WeakMap,this.scopeReferenceCounts=new WeakMap}start(){this.valueListObserver.start()}stop(){this.valueListObserver.stop()}get controllerAttribute(){return this.schema.controllerAttribute}parseValueForToken(e){const{element:t,content:n}=e;return this.parseValueForElementAndIdentifier(t,n)}parseValueForElementAndIdentifier(e,t){const n=this.fetchScopesByIdentifierForElement(e);let r=n.get(t);return r||(r=this.delegate.createScopeForElementAndIdentifier(e,t),n.set(t,r)),r}elementMatchedValue(e,t){const n=(this.scopeReferenceCounts.get(t)||0)+1;this.scopeReferenceCounts.set(t,n),1==n&&this.delegate.scopeConnected(t)}elementUnmatchedValue(e,t){const n=this.scopeReferenceCounts.get(t);n&&(this.scopeReferenceCounts.set(t,n-1),1==n&&this.delegate.scopeDisconnected(t))}fetchScopesByIdentifierForElement(e){let t=this.scopesByIdentifierByElement.get(e);return t||(t=new Map,this.scopesByIdentifierByElement.set(e,t)),t}}class fe{constructor(e){this.application=e,this.scopeObserver=new de(this.element,this.schema,this),this.scopesByIdentifier=new j,this.modulesByIdentifier=new Map}get element(){return this.application.element}get schema(){return this.application.schema}get logger(){return this.application.logger}get controllerAttribute(){return this.schema.controllerAttribute}get modules(){return Array.from(this.modulesByIdentifier.values())}get contexts(){return this.modules.reduce((e,t)=>e.concat(t.contexts),[])}start(){this.scopeObserver.start()}stop(){this.scopeObserver.stop()}loadDefinition(e){this.unloadIdentifier(e.identifier);const t=new re(this.application,e);this.connectModule(t);const n=e.controllerConstructor.afterLoad;n&&n.call(e.controllerConstructor,e.identifier,this.application)}unloadIdentifier(e){const t=this.modulesByIdentifier.get(e);t&&this.disconnectModule(t)}getContextForElementAndIdentifier(e,t){const n=this.modulesByIdentifier.get(t);if(n)return n.contexts.find(t=>t.element==e)}proposeToConnectScopeForElementAndIdentifier(e,t){const n=this.scopeObserver.parseValueForElementAndIdentifier(e,t);n?this.scopeObserver.elementMatchedValue(n.element,n):console.error(`Couldn't find or create scope for identifier: "${t}" and element:`,e)}handleError(e,t,n){this.application.handleError(e,t,n)}createScopeForElementAndIdentifier(e,t){return new ce(this.schema,e,t,this.logger)}scopeConnected(e){this.scopesByIdentifier.add(e.identifier,e);const t=this.modulesByIdentifier.get(e.identifier);t&&t.connectContextForScope(e)}scopeDisconnected(e){this.scopesByIdentifier.delete(e.identifier,e);const t=this.modulesByIdentifier.get(e.identifier);t&&t.disconnectContextForScope(e)}connectModule(e){this.modulesByIdentifier.set(e.identifier,e);this.scopesByIdentifier.getValuesForKey(e.identifier).forEach(t=>e.connectContextForScope(t))}disconnectModule(e){this.modulesByIdentifier.delete(e.identifier);this.scopesByIdentifier.getValuesForKey(e.identifier).forEach(t=>e.disconnectContextForScope(t))}}const pe={controllerAttribute:"data-controller",actionAttribute:"data-action",targetAttribute:"data-target",targetAttributeForScope:e=>`data-${e}-target`,outletAttributeForScope:(e,t)=>`data-${e}-${t}-outlet`,keyMappings:Object.assign(Object.assign({enter:"Enter",tab:"Tab",esc:"Escape",space:" ",up:"ArrowUp",down:"ArrowDown",left:"ArrowLeft",right:"ArrowRight",home:"Home",end:"End",page_up:"PageUp",page_down:"PageDown"},he("abcdefghijklmnopqrstuvwxyz".split("").map(e=>[e,e]))),he("0123456789".split("").map(e=>[e,e])))};function he(e){return e.reduce((e,[t,n])=>Object.assign(Object.assign({},e),{[t]:n}),{})}function me(e,t,n){return e.application.getControllerForElementAndIdentifier(t,n)}function ge(e,t,n){let r=me(e,t,n);return r||(e.application.router.proposeToConnectScopeForElementAndIdentifier(t,n),r=me(e,t,n),r||void 0)}function ve([e,t],n){return function(e){const{token:t,typeDefinition:n}=e,r=`${D(t)}-value`,o=function(e){const{controller:t,token:n,typeDefinition:r}=e,o={controller:t,token:n,typeObject:r},a=function(e){const{controller:t,token:n,typeObject:r}=e,o=A(r.type),a=A(r.default),i=o&&a,l=o&&!a,s=!o&&a,u=be(r.type),c=ye(e.typeObject.default);if(l)return u;if(s)return c;if(u!==c){throw new Error(`The specified default value for the Stimulus Value "${t?`${t}.${n}`:n}" must match the defined type "${u}". The provided default value of "${r.default}" is of type "${c}".`)}if(i)return u}(o),i=ye(r),l=be(r),s=a||i||l;if(s)return s;const u=t?`${t}.${r}`:n;throw new Error(`Unknown value type "${u}" for "${n}" value`)}(e);return{type:o,key:r,name:O(r),get defaultValue(){return function(e){const t=be(e);if(t)return we[t];const n=T(e,"default"),r=T(e,"type"),o=e;if(n)return o.default;if(r){const{type:e}=o,t=be(e);if(t)return we[t]}return e}(n)},get hasCustomDefaultValue(){return void 0!==ye(n)},reader:Se[o],writer:ke[o]||ke.default}}({controller:n,token:e,typeDefinition:t})}function be(e){switch(e){case Array:return"array";case Boolean:return"boolean";case Number:return"number";case Object:return"object";case String:return"string"}}function ye(e){switch(typeof e){case"boolean":return"boolean";case"number":return"number";case"string":return"string"}return Array.isArray(e)?"array":"[object Object]"===Object.prototype.toString.call(e)?"object":void 0}const we={get array(){return[]},boolean:!1,number:0,get object(){return{}},string:""},Se={array(e){const t=JSON.parse(e);if(!Array.isArray(t))throw new TypeError(`expected value of type "array" but instead got value "${e}" of type "${ye(t)}"`);return t},boolean:e=>!("0"==e||"false"==String(e).toLowerCase()),number:e=>Number(e.replace(/_/g,"")),object(e){const t=JSON.parse(e);if(null===t||"object"!=typeof t||Array.isArray(t))throw new TypeError(`expected value of type "object" but instead got value "${e}" of type "${ye(t)}"`);return t},string:e=>e},ke={default:function(e){return`${e}`},array:Ee,object:Ee};function Ee(e){return JSON.stringify(e)}class Oe{constructor(e){this.context=e}static get shouldLoad(){return!0}static afterLoad(e,t){}get application(){return this.context.application}get scope(){return this.context.scope}get element(){return this.scope.element}get identifier(){return this.scope.identifier}get targets(){return this.scope.targets}get outlets(){return this.scope.outlets}get classes(){return this.scope.classes}get data(){return this.scope.data}initialize(){}connect(){}disconnect(){}dispatch(e,{target:t=this.element,detail:n={},prefix:r=this.identifier,bubbles:o=!0,cancelable:a=!0}={}){const i=new CustomEvent(r?`${r}:${e}`:e,{detail:n,bubbles:o,cancelable:a});return t.dispatchEvent(i),i}}Oe.blessings=[function(e){return K(e,"classes").reduce((e,t)=>{return Object.assign(e,(n=t,{[`${n}Class`]:{get(){const{classes:e}=this;if(e.has(n))return e.get(n);{const t=e.getAttributeName(n);throw new Error(`Missing attribute "${t}"`)}}},[`${n}Classes`]:{get(){return this.classes.getAll(n)}},[`has${C(n)}Class`]:{get(){return this.classes.has(n)}}}));var n},{})},function(e){return K(e,"targets").reduce((e,t)=>{return Object.assign(e,(n=t,{[`${n}Target`]:{get(){const e=this.targets.find(n);if(e)return e;throw new Error(`Missing target element "${n}" for "${this.identifier}" controller`)}},[`${n}Targets`]:{get(){return this.targets.findAll(n)}},[`has${C(n)}Target`]:{get(){return this.targets.has(n)}}}));var n},{})},function(e){const t=X(e,"values"),n={valueDescriptorMap:{get(){return t.reduce((e,t)=>{const n=ve(t,this.identifier),r=this.data.getAttributeNameForKey(n.key);return Object.assign(e,{[r]:n})},{})}}};return t.reduce((e,t)=>Object.assign(e,function(e,t){const n=ve(e,t),{key:r,name:o,reader:a,writer:i}=n;return{[o]:{get(){const e=this.data.get(r);return null!==e?a(e):n.defaultValue},set(e){void 0===e?this.data.delete(r):this.data.set(r,i(e))}},[`has${C(o)}`]:{get(){return this.data.has(r)||n.hasCustomDefaultValue}}}}(t)),n)},function(e){return K(e,"outlets").reduce((e,t)=>Object.assign(e,function(e){const t=N(e);return{[`${t}Outlet`]:{get(){const t=this.outlets.find(e),n=this.outlets.getSelectorForOutletName(e);if(t){const n=ge(this,t,e);if(n)return n;throw new Error(`The provided outlet element is missing an outlet controller "${e}" instance for host controller "${this.identifier}"`)}throw new Error(`Missing outlet element "${e}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${n}".`)}},[`${t}Outlets`]:{get(){const t=this.outlets.findAll(e);return t.length>0?t.map(t=>{const n=ge(this,t,e);if(n)return n;console.warn(`The provided outlet element is missing an outlet controller "${e}" instance for host controller "${this.identifier}"`,t)}).filter(e=>e):[]}},[`${t}OutletElement`]:{get(){const t=this.outlets.find(e),n=this.outlets.getSelectorForOutletName(e);if(t)return t;throw new Error(`Missing outlet element "${e}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${n}".`)}},[`${t}OutletElements`]:{get(){return this.outlets.findAll(e)}},[`has${C(t)}Outlet`]:{get(){return this.outlets.has(e)}}}}(t)),{})}],Oe.targets=[],Oe.outlets=[],Oe.values={};n(236);s(),o();const Ne=()=>{},Ce=(0,l().createContext)({dragPreviewState:null,updatePreviewPosition:Ne,commitDrag:Ne,cancelDrag:Ne}),De=[48,1632,1776,1984,2406,2534,2662,2790,2918,3046,3174,3302,3430,3558,3664,3792,3872,4160,4240,6112,6160,6470,6608,6784,6800,6992,7088,7232,7248,42528,43216,43264,43472,43504,43600,44016,65296,66720,68912,68928,69734,69872,69942,70096,70384,70736,70864,71248,71360,71376,71386,71472,71904,72016,72688,72784,73040,73120,73184,73552,90416,92768,92864,93008,93552,118e3,120782,120792,120802,120812,120822,123200,123632,124144,124401,125264,130032],Ae={"⁰":"0","¹":"1","²":"2","³":"3","⁴":"4","⁵":"5","⁶":"6","⁷":"7","⁸":"8","⁹":"9","₀":"0","₁":"1","₂":"2","₃":"3","₄":"4","₅":"5","₆":"6","₇":"7","₈":"8","₉":"9"},Te=/[⁰¹²³⁴⁵⁶⁷⁸⁹₀₁₂₃₄₅₆₇₈₉]/g,$e={"¼":"1/4","½":"1/2","¾":"3/4","⅐":"1/7","⅑":"1/9","⅒":"1/10","⅓":"1/3","⅔":"2/3","⅕":"1/5","⅖":"2/5","⅗":"3/5","⅘":"4/5","⅙":"1/6","⅚":"5/6","⅛":"1/8","⅜":"3/8","⅝":"5/8","⅞":"7/8","⅟":"1/"},Me=/^(?=[-+]?\s*\.\d|[-+]?\s*\d)([-+])?\s*((?:\d(?:[,_]\d|\d)*)*)(([eE][+-]?\d(?:[,_]\d|\d)*)?|\.\d(?:[,_]\d|\d)*([eE][+-]?\d(?:[,_]\d|\d)*)?|(\s+\d(?:[,_]\d|\d)*\s*)?\s*\/\s*\d(?:[,_]\d|\d)*)?$/,Re=/^(?=[-+]?\s*\.\d|[-+]?\s*\d)([-+])?\s*((?:\d(?:[,_]\d|\d)*)*)(([eE][+-]?\d(?:[,_]\d|\d)*)?|\.\d(?:[,_]\d|\d)*([eE][+-]?\d(?:[,_]\d|\d)*)?|(\s+\d(?:[,_]\d|\d)*\s*)?\s*\/\s*\d(?:[,_]\d|\d)*)?(\s*[^.\d/].*)?/,xe=/([¼½¾⅐⅑⅒⅓⅔⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞⅟])/g,Ie={MMM:3e3,MM:2e3,M:1e3,CM:900,DCCC:800,DCC:700,DC:600,D:500,CD:400,CCC:300,CC:200,C:100,XC:90,LXXX:80,LXX:70,LX:60,L:50,XL:40,XXX:30,XX:20,XII:12,XI:11,X:10,IX:9,VIII:8,VII:7,VI:6,V:5,IV:4,III:3,II:2,I:1},Pe={Ⅰ:"I",Ⅱ:"II",Ⅲ:"III",Ⅳ:"IV",Ⅴ:"V",Ⅵ:"VI",Ⅶ:"VII",Ⅷ:"VIII",Ⅸ:"IX",Ⅹ:"X",Ⅺ:"XI",Ⅻ:"XII",Ⅼ:"L",Ⅽ:"C",Ⅾ:"D",Ⅿ:"M",ⅰ:"I",ⅱ:"II",ⅲ:"III",ⅳ:"IV",ⅴ:"V",ⅵ:"VI",ⅶ:"VII",ⅷ:"VIII",ⅸ:"IX",ⅹ:"X",ⅺ:"XI",ⅻ:"XII",ⅼ:"L",ⅽ:"C",ⅾ:"D",ⅿ:"M"},_e=/([ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫⅬⅭⅮⅯⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹⅺⅻⅼⅽⅾⅿ])/gi,Le=/^(?=[MDCLXVI])(M{0,3})(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$/i,Ge={round:3,allowTrailingInvalid:!1,romanNumerals:!1,bigIntOnOverflow:!1,decimalSeparator:".",allowCurrency:!1,percentage:!1,verbose:!1},Fe=/^\s*\//,ze=/^([-+]?)\s*(\p{Sc}+)\s*/u,je=/\s*(\p{Sc}+)\s*$/u,qe=/\s*%$/,Be=BigInt(Number.MAX_SAFE_INTEGER),Ve=e=>10n**BigInt(e);function He(e,t=Ge){var n;const r={...Ge,...t};let o,a,i,l,s,u,c,d,f;if("string"==typeof e)o=e;else try{o=String(e)}catch{o=""}const p=e=>r.verbose?(e=>{const t={value:e,input:o};return a&&(t.currencyPrefix=a),i&&(t.currencySuffix=i),l&&(t.percentageSuffix=l),s&&(t.trailingInvalid=s),u&&(t.sign=u),void 0!==c&&(t.whole=c),void 0!==d&&(t.numerator=d),void 0!==f&&(t.denominator=f),t})(e):e,h=e=>l&&"number"!==r.percentage?e/100:e;if("symbol"==typeof e)return p(NaN);if("number"==typeof e||"bigint"==typeof e)return p(e);let m=NaN,g=o,v=!0;for(;v;){if(v=!1,r.allowCurrency){const e=ze.exec(g);(null==e?void 0:e[2])&&(a=(a??"")+e[2],g=(e[1]||"")+g.slice(e[0].length),v=!0);const t=je.exec(g);t&&(i=t[1]+(i??""),g=g.slice(0,-t[0].length),v=!0)}if(!l&&r.percentage){const e=qe.exec(g);e&&(l=!0,g=g.slice(0,-e[0].length),v=!0)}}const b=g.replace(xe,(e,t)=>` ${$e[t]}`).replace(Te,e=>Ae[e]).replace("⁄","/").trim().replace(/\p{Nd}/gu,e=>{const t=e.codePointAt(0);if(t<=57)return e;let n=0,r=De.length-1;for(;n>>1;De[e]<=t?n=e:r=e-1}return String(t-De[n])});if(0===b.length)return p(NaN);let y=b;if(","===r.decimalSeparator){const e=(b.match(/,/g)||[]).length;if(1===e)y=b.replaceAll(".","_").replace(",",".");else if(e>1){const e=b.indexOf(","),t=b.indexOf(",",e+1);y=b.substring(0,t).replaceAll(".","_").replace(",",".")}else y=b.replaceAll(".","_")}const w=Re.exec(y);if(!w)return r.romanNumerals?p(h((e=>{const t=e.replace(_e,(e,t)=>Pe[t]).toUpperCase(),n=Le.exec(t);if(!n)return NaN;const[,r,o,a,i]=n;return(Ie[r]??0)+(Ie[o]??0)+(Ie[a]??0)+(Ie[i]??0)})(b))):p(NaN);const S=w[0].length-((null===(n=w[7])||void 0===n?void 0:n.length)??0),k=b.slice(S).trim();if(k&&(s=k,!r.allowTrailingInvalid))return p(NaN);const[,E,O,N]=w;"-"!==E&&"+"!==E||(u=E);const C=O.replaceAll(",","").replaceAll("_",""),D=null==N?void 0:N.replaceAll(",","").replaceAll("_","");if(r.bigIntOnOverflow){const e=((e,t,n)=>{let r,o=1n;if(t)if(t.startsWith(".")||t.startsWith("e")||t.startsWith("E")){const n=t.search(/[eE]/),a=-1===n?0:parseInt(t.slice(n+1));if(!Number.isFinite(a)||Math.abs(a)>1e4)return;const i=(-1===n?t:t.slice(0,n)).slice(1);r=BigInt(`${e}${i}`),i&&(o=Ve(i.length)),a>0?r*=Ve(a):a<0&&(o*=Ve(-a))}else if(Fe.test(t))r=BigInt(e),o=BigInt(t.replace("/","").trim());else{const[n,a]=t.split("/");o=BigInt(a.trim()),r=BigInt(e)*o+BigInt(n.trim())}else r=BigInt(e);if(n&&(o*=100n),0n===o)return;const a=(2n*r+o)/(2n*o);return a>Be?a:void 0})(C,D,!!l&&"number"!==r.percentage);if(void 0!==e)return p("-"===E?-e:e)}if(m=!C&&D&&D.startsWith(".")?0:parseInt(C),!D)return m="-"===E?-1*m:m,p(h(m));const A=!("number"!=typeof r.round||!Number.isFinite(r.round))&&Math.floor(Math.max(0,r.round)),T=!1===A?NaN:10**A,$=Number.isFinite(T)?T:NaN,M=(e,t)=>{const n=Math.round(t)/$;return Number.isFinite(n)||!Number.isFinite(e)?n:e};if(D.startsWith(".")||D.startsWith("e")||D.startsWith("E")){const e=parseFloat(`${m}${D}`);m=isNaN($)?e:M(e,e*$)}else if(Fe.test(D)){const e=parseInt(C),t=parseInt(D.replace("/",""));d=e,f=t,m=isNaN($)?e/t:M(e/t,e*$/t)}else{const[e,t]=D.split("/").map(e=>parseInt(e));c=m,d=e,f=t,m+=isNaN($)?e/t:M(e/t,e*$/t)}return m="-"===E?-1*m:m,p(h(m))}var Ue=Symbol.for("immer-nothing"),We=Symbol.for("immer-draftable"),Qe=Symbol.for("immer-state");function Ke(e,...t){throw new Error(`[Immer] minified error nr: ${e}. Full error at: https://bit.ly/3cXEKWf`)}var Xe=Object,Ye=Xe.getPrototypeOf,Je="constructor",Ze="prototype",et="configurable",tt="enumerable",nt="writable",rt="value",ot=e=>!!e&&!!e[Qe];function at(e){return!!e&&(st(e)||ht(e)||!!e[We]||!!e[Je]?.[We]||mt(e)||gt(e))}var it=Xe[Ze][Je].toString(),lt=new WeakMap;function st(e){if(!e||!vt(e))return!1;const t=Ye(e);if(null===t||t===Xe[Ze])return!0;const n=Xe.hasOwnProperty.call(t,Je)&&t[Je];if(n===Object)return!0;if(!bt(n))return!1;let r=lt.get(n);return void 0===r&&(r=Function.toString.call(n),lt.set(n,r)),r===it}function ut(e,t,n=!0){if(0===ct(e)){(n?Reflect.ownKeys(e):Xe.keys(e)).forEach(n=>{t(n,e[n],e)})}else e.forEach((n,r)=>t(r,n,e))}function ct(e){const t=e[Qe];return t?t.type_:ht(e)?1:mt(e)?2:gt(e)?3:0}var dt=(e,t,n=ct(e))=>2===n?e.has(t):Xe[Ze].hasOwnProperty.call(e,t),ft=(e,t,n=ct(e))=>2===n?e.get(t):e[t],pt=(e,t,n,r=ct(e))=>{2===r?e.set(t,n):3===r?e.add(n):e[t]=n};var ht=Array.isArray,mt=e=>e instanceof Map,gt=e=>e instanceof Set,vt=e=>"object"==typeof e,bt=e=>"function"==typeof e,yt=e=>"boolean"==typeof e;var wt=e=>e.copy_||e.base_,St=e=>e.modified_?e.copy_:e.base_;function kt(e,t){if(mt(e))return new Map(e);if(gt(e))return new Set(e);if(ht(e))return Array[Ze].slice.call(e);const n=st(e);if(!0===t||"class_only"===t&&!n){const t=Xe.getOwnPropertyDescriptors(e);delete t[Qe];let n=Reflect.ownKeys(t);for(let r=0;r1&&Xe.defineProperties(e,{set:Ot,add:Ot,clear:Ot,delete:Ot}),Xe.freeze(e),t&&ut(e,(e,t)=>{Et(t,!0)},!1)),e}var Ot={[rt]:function(){Ke(2)}};function Nt(e){return null===e||!vt(e)||Xe.isFrozen(e)}var Ct="MapSet",Dt="Patches",At="ArrayMethods",Tt={};function $t(e){const t=Tt[e];return t||Ke(0),t}var Mt,Rt=e=>!!Tt[e];var xt=()=>Mt;function It(e,t){t&&(e.patchPlugin_=$t(Dt),e.patches_=[],e.inversePatches_=[],e.patchListener_=t)}function Pt(e){_t(e),e.drafts_.forEach(Gt),e.drafts_=null}function _t(e){e===Mt&&(Mt=e.parent_)}var Lt=e=>Mt={drafts_:[],parent_:Mt,immer_:e,canAutoFreeze_:!0,unfinalizedDrafts_:0,handledSet_:new Set,processedForPatches_:new Set,mapSetPlugin_:Rt(Ct)?$t(Ct):void 0,arrayMethodsPlugin_:Rt(At)?$t(At):void 0};function Gt(e){const t=e[Qe];0===t.type_||1===t.type_?t.revoke_():t.revoked_=!0}function Ft(e,t){t.unfinalizedDrafts_=t.drafts_.length;const n=t.drafts_[0];if(void 0!==e&&e!==n){n[Qe].modified_&&(Pt(t),Ke(4)),at(e)&&(e=zt(t,e));const{patchPlugin_:r}=t;r&&r.generateReplacementPatches_(n[Qe].base_,e,t)}else e=zt(t,n);return function(e,t,n=!1){!e.parent_&&e.immer_.autoFreeze_&&e.canAutoFreeze_&&Et(t,n)}(t,e,!0),Pt(t),t.patches_&&t.patchListener_(t.patches_,t.inversePatches_),e!==Ue?e:void 0}function zt(e,t){if(Nt(t))return t;const n=t[Qe];if(!n){return Wt(t,e.handledSet_,e)}if(!qt(n,e))return t;if(!n.modified_)return n.base_;if(!n.finalized_){const{callbacks_:t}=n;if(t)for(;t.length>0;){t.pop()(e)}Ht(n,e)}return n.copy_}function jt(e){e.finalized_=!0,e.scope_.unfinalizedDrafts_--}var qt=(e,t)=>e.scope_===t,Bt=[];function Vt(e,t,n,r){const o=wt(e),a=e.type_;if(void 0!==r){if(ft(o,r,a)===t)return void pt(o,r,n,a)}if(!e.draftLocations_){const t=e.draftLocations_=new Map;ut(o,(e,n)=>{if(ot(n)){const r=t.get(n)||[];r.push(e),t.set(n,r)}})}const i=e.draftLocations_.get(t)??Bt;for(const e of i)pt(o,e,n,a)}function Ht(e,t){if(e.modified_&&!e.finalized_&&(3===e.type_||1===e.type_&&e.allIndicesReassigned_||(e.assigned_?.size??0)>0)){const{patchPlugin_:n}=t;if(n){const r=n.getPath(e);r&&n.generatePatches_(e,r,t)}jt(e)}}function Ut(e,t,n){const{scope_:r}=e;if(ot(n)){const o=n[Qe];qt(o,r)&&o.callbacks_.push(function(){Zt(e);const r=St(o);Vt(e,n,r,t)})}else at(n)&&e.callbacks_.push(function(){const o=wt(e);3===e.type_?o.has(n)&&Wt(n,r.handledSet_,r):ft(o,t,e.type_)===n&&r.drafts_.length>1&&!0===(e.assigned_.get(t)??!1)&&e.copy_&&Wt(ft(e.copy_,t,e.type_),r.handledSet_,r)})}function Wt(e,t,n){return!n.immer_.autoFreeze_&&n.unfinalizedDrafts_<1||ot(e)||t.has(e)||!at(e)||Nt(e)||(t.add(e),ut(e,(r,o)=>{if(ot(o)){const t=o[Qe];if(qt(t,n)){const n=St(t);pt(e,r,n,e.type_),jt(t)}}else at(o)&&Wt(o,t,n)})),e}var Qt={get(e,t){if(t===Qe)return e;let n=e.scope_.arrayMethodsPlugin_;const r=1===e.type_&&"string"==typeof t;if(r&&n?.isArrayOperationMethod(t))return n.createMethodInterceptor(e,t);const o=wt(e);if(!dt(o,t,e.type_))return function(e,t,n){const r=Yt(t,n);return r?rt in r?r[rt]:r.get?.call(e.draft_):void 0}(e,o,t);const a=o[t];if(e.finalized_||!at(a))return a;if(r&&e.operationMethod&&n?.isMutatingArrayMethod(e.operationMethod)&&function(e){const t=+e;return Number.isInteger(t)&&String(t)===e}(t))return a;if(a===Xt(e.base_,t)||function(e,t,n){if(1!==e.type_||!e.allIndicesReassigned_||e.assigned_?.get(t)||!at(n)||n[Qe])return!1;return e.baseRefs_.has(n)}(e,t,a)){Zt(e);const n=1===e.type_?+t:t,r=tn(e.scope_,a,e,n);return e.copy_[n]=r}return a},has:(e,t)=>t in wt(e),ownKeys:e=>Reflect.ownKeys(wt(e)),set(e,t,n){const r=Yt(wt(e),t);if(r?.set)return r.set.call(e.draft_,n),!0;if(!e.modified_){const r=Xt(wt(e),t),i=r?.[Qe];if(i&&i.base_===n)return e.copy_[t]=n,e.assigned_.set(t,!1),!0;if(((o=n)===(a=r)?0!==o||1/o==1/a:o!=o&&a!=a)&&(void 0!==n||dt(e.base_,t,e.type_)))return!0;Zt(e),Jt(e)}var o,a;return e.copy_[t]===n&&(void 0!==n||dt(e.copy_,t,e.type_))||Number.isNaN(n)&&Number.isNaN(e.copy_[t])||(e.copy_[t]=n,e.assigned_.set(t,!0),Ut(e,t,n)),!0},deleteProperty:(e,t)=>(Zt(e),void 0!==Xt(e.base_,t)||t in e.base_?(e.assigned_.set(t,!1),Jt(e)):e.assigned_.delete(t),e.copy_&&delete e.copy_[t],!0),getOwnPropertyDescriptor(e,t){const n=wt(e),r=Reflect.getOwnPropertyDescriptor(n,t);return r?{[nt]:!0,[et]:1!==e.type_||"length"!==t,[tt]:r[tt],[rt]:n[t]}:r},defineProperty(){Ke(11)},getPrototypeOf:e=>Ye(e.base_),setPrototypeOf(){Ke(12)}},Kt={};for(let e in Qt){let t=Qt[e];Kt[e]=function(){const e=arguments;return e[0]=e[0][0],t.apply(this,e)}}function Xt(e,t){const n=e[Qe];return(n?wt(n):e)[t]}function Yt(e,t){if(!(t in e))return;let n=Ye(e);for(;n;){const e=Object.getOwnPropertyDescriptor(n,t);if(e)return e;n=Ye(n)}}function Jt(e){e.modified_||(e.modified_=!0,e.parent_&&Jt(e.parent_))}function Zt(e){e.copy_||(e.assigned_=new Map,e.copy_=kt(e.base_,e.scope_.immer_.useStrictShallowCopy_))}Kt.deleteProperty=function(e,t){return Kt.set.call(this,e,t,void 0)},Kt.set=function(e,t,n){return Qt.set.call(this,e[0],t,n,e[0])};var en=class{constructor(e){this.autoFreeze_=!0,this.useStrictShallowCopy_=!1,this.useStrictIteration_=!1,this.produce=(e,t,n)=>{if(bt(e)&&!bt(t)){const n=t;t=e;const r=this;return function(e=n,...o){return r.produce(e,e=>t.call(this,e,...o))}}let r;if(bt(t)||Ke(6),void 0===n||bt(n)||Ke(7),at(e)){const o=Lt(this),a=tn(o,e,void 0);let i=!0;try{r=t(a),i=!1}finally{i?Pt(o):_t(o)}return It(o,n),Ft(r,o)}if(!e||!vt(e)){if(r=t(e),void 0===r&&(r=e),r===Ue&&(r=void 0),this.autoFreeze_&&Et(r,!0),n){const t=[],o=[];$t(Dt).generateReplacementPatches_(e,r,{patches_:t,inversePatches_:o}),n(t,o)}return r}Ke(1)},this.produceWithPatches=(e,t)=>{if(bt(e))return(t,...n)=>this.produceWithPatches(t,t=>e(t,...n));let n,r;return[this.produce(e,t,(e,t)=>{n=e,r=t}),n,r]},yt(e?.autoFreeze)&&this.setAutoFreeze(e.autoFreeze),yt(e?.useStrictShallowCopy)&&this.setUseStrictShallowCopy(e.useStrictShallowCopy),yt(e?.useStrictIteration)&&this.setUseStrictIteration(e.useStrictIteration)}createDraft(e){at(e)||Ke(8),ot(e)&&(e=nn(e));const t=Lt(this),n=tn(t,e,void 0);return n[Qe].isManual_=!0,_t(t),n}finishDraft(e,t){const n=e&&e[Qe];n&&n.isManual_||Ke(9);const{scope_:r}=n;return It(r,t),Ft(void 0,r)}setAutoFreeze(e){this.autoFreeze_=e}setUseStrictShallowCopy(e){this.useStrictShallowCopy_=e}setUseStrictIteration(e){this.useStrictIteration_=e}shouldUseStrictIteration(){return this.useStrictIteration_}applyPatches(e,t){let n;for(n=t.length-1;n>=0;n--){const r=t[n];if(0===r.path.length&&"replace"===r.op){e=r.value;break}}n>-1&&(t=t.slice(n+1));const r=$t(Dt).applyPatches_;return ot(e)?r(e,t):this.produce(e,e=>r(e,t))}};function tn(e,t,n,r){const[o,a]=mt(t)?$t(Ct).proxyMap_(t,n):gt(t)?$t(Ct).proxySet_(t,n):function(e,t){const n=ht(e),r={type_:n?1:0,scope_:t?t.scope_:xt(),modified_:!1,finalized_:!1,assigned_:void 0,parent_:t,base_:e,draft_:null,copy_:null,revoke_:null,isManual_:!1,callbacks_:void 0};let o=r,a=Qt;n&&(o=[r],a=Kt);const{revoke:i,proxy:l}=Proxy.revocable(o,a);return r.draft_=l,r.revoke_=i,[l,r]}(t,n);return(n?.scope_??xt()).drafts_.push(o),a.callbacks_=n?.callbacks_??[],a.key_=r,n&&void 0!==r?function(e,t,n){e.callbacks_.push(function(r){const o=t;if(!o||!qt(o,r))return;r.mapSetPlugin_?.fixSetContents(o);const a=St(o);Vt(e,o.draft_??o,a,n),Ht(o,r)})}(n,a,r):a.callbacks_.push(function(e){e.mapSetPlugin_?.fixSetContents(a);const{patchPlugin_:t}=e;a.modified_&&t&&t.generatePatches_(a,[],e)}),o}function nn(e){return ot(e)||Ke(10),rn(e)}function rn(e){if(!at(e)||Nt(e))return e;const t=e[Qe];let n,r=!0;if(t){if(!t.modified_)return t.base_;t.finalized_=!0,n=kt(e,t.scope_.immer_.useStrictShallowCopy_),r=t.scope_.immer_.shouldUseStrictIteration()}else n=kt(e,!0);return ut(n,(e,t)=>{pt(n,e,rn(t))},r),t&&(t.finalized_=!1),n}globalThis.Iterator;var on=new en,an=on.produce;const ln="------",sn={fields:{title:"Field",placeholderName:"~",placeholderLabel:ln,placeholderGroupLabel:ln},operators:{title:"Operator",placeholderName:"~",placeholderLabel:ln,placeholderGroupLabel:ln},values:{title:"Values",placeholderName:"~",placeholderLabel:ln,placeholderGroupLabel:ln},matchMode:{title:"Match mode"},matchThreshold:{title:"Match threshold",placeholderName:"#"},value:{title:"Value"},removeRule:{label:"⨯",title:"Remove rule"},removeGroup:{label:"⨯",title:"Remove group"},addRule:{label:"+ Rule",title:"Add rule"},addGroup:{label:"+ Group",title:"Add group"},combinators:{title:"Combinator"},notToggle:{label:"Not",title:"Invert this group"},cloneRule:{label:"⧉",title:"Clone rule"},cloneRuleGroup:{label:"⧉",title:"Clone group"},shiftActionUp:{label:"˄",title:"Shift up"},shiftActionDown:{label:"˅",title:"Shift down"},undo:{label:"↶",title:"Undo"},redo:{label:"↷",title:"Redo"},dragHandle:{label:"⁞⁞",title:"Drag handle"},lockRule:{label:"🔓",title:"Lock rule"},lockGroup:{label:"🔓",title:"Lock group"},lockRuleDisabled:{label:"🔒",title:"Unlock rule"},lockGroupDisabled:{label:"🔒",title:"Unlock group"},muteRule:{label:"🔊",title:"Mute rule"},muteGroup:{label:"🔊",title:"Mute group"},unmuteRule:{label:"🔇",title:"Unmute rule"},unmuteGroup:{label:"🔇",title:"Unmute group"},valueSourceSelector:{title:"Value source"}},un={"=":"=","!=":"!=","<":"<",">":">","<=":"<=",">=":">=",contains:"contains",beginsWith:"begins with",endsWith:"ends with",doesNotContain:"does not contain",doesNotBeginWith:"does not begin with",doesNotEndWith:"does not end with",null:"is null",notNull:"is not null",in:"in",notIn:"not in",between:"between",notBetween:"not between"},cn={and:"AND",or:"OR",xor:"XOR"},dn=[{name:"=",value:"=",label:"="},{name:"!=",value:"!=",label:"!="},{name:"<",value:"<",label:"<"},{name:">",value:">",label:">"},{name:"<=",value:"<=",label:"<="},{name:">=",value:">=",label:">="},{name:"contains",value:"contains",label:"contains"},{name:"beginsWith",value:"beginsWith",label:"begins with"},{name:"endsWith",value:"endsWith",label:"ends with"},{name:"doesNotContain",value:"doesNotContain",label:"does not contain"},{name:"doesNotBeginWith",value:"doesNotBeginWith",label:"does not begin with"},{name:"doesNotEndWith",value:"doesNotEndWith",label:"does not end with"},{name:"null",value:"null",label:"is null"},{name:"notNull",value:"notNull",label:"is not null"},{name:"in",value:"in",label:"in"},{name:"notIn",value:"notIn",label:"not in"},{name:"between",value:"between",label:"between"},{name:"notBetween",value:"notBetween",label:"not between"}],fn=[{name:"and",value:"and",label:"AND"},{name:"or",value:"or",label:"OR"}],pn=[{name:"all",value:"all",label:"all"},{name:"some",value:"some",label:"some"},{name:"none",value:"none",label:"none"},{name:"atLeast",value:"atLeast",label:"at least"},{name:"atMost",value:"atMost",label:"at most"},{name:"exactly",value:"exactly",label:"exactly"}],hn={queryBuilder:"queryBuilder",ruleGroup:"ruleGroup",header:"ruleGroup-header",body:"ruleGroup-body",combinators:"ruleGroup-combinators",addRule:"ruleGroup-addRule",addGroup:"ruleGroup-addGroup",cloneRule:"rule-cloneRule",cloneGroup:"ruleGroup-cloneGroup",removeGroup:"ruleGroup-remove",notToggle:"ruleGroup-notToggle",rule:"rule",fields:"rule-fields",matchMode:"rule-matchMode",matchThreshold:"rule-matchThreshold",operators:"rule-operators",value:"rule-value",removeRule:"rule-remove",betweenRules:"betweenRules",valid:"queryBuilder-valid",invalid:"queryBuilder-invalid",shiftActions:"shiftActions",undoRedoActions:"undoRedoActions",undoAction:"undoRedoActions-undo",redoAction:"undoRedoActions-redo",dndDragging:"dndDragging",dndOver:"dndOver",dndCopy:"dndCopy",dndGroup:"dndGroup",dndDropNotAllowed:"dndDropNotAllowed",dndPreviewPosition:"dndPreviewPosition",dndHidden:"dndHidden",dragHandle:"queryBuilder-dragHandle",disabled:"queryBuilder-disabled",muted:"queryBuilder-muted",lockRule:"rule-lock",lockGroup:"ruleGroup-lock",muteRule:"rule-mute",muteGroup:"ruleGroup-mute",valueSource:"rule-valueSource",valueListItem:"rule-value-list-item",branches:"queryBuilder-branches",justified:"queryBuilder-justified",responsive:"queryBuilder-responsive",hasSubQuery:"rule-hasSubQuery",loading:"queryBuilder-loading",valueDateTimeRelative:"rule-value-dateTimeRelative"},mn="empty",gn="invalid combinator",vn="invalid independent combinators",bn="rule",yn="rule-group",wn="inline-combinator",Sn="add-group",kn="remove-group",En="clone-group",On="clone-rule",Nn="add-rule",Cn="remove-rule",Dn="combinators",An="fields",Tn="operators",$n="value-editor",Mn="not-toggle",Rn="shift-actions",xn="undo-redo-actions",In="drag-handle",Pn="lock-rule",_n="lock-group",Ln="mute-rule",Gn="mute-group",Fn="value-source-selector",zn="match-mode-editor",jn="action aborted: parent path disabled",qn="action aborted: path is disabled",Bn="onAddRule callback returned false",Vn="onAddGroup callback returned false",Hn="onGroupRule callback returned false",Un="onGroupGroup callback returned false",Wn="onMoveRule callback returned false",Qn="onMoveGroup callback returned false",Kn="onRemove callback returned false",Xn="rule or group added",Yn="rule or group removed",Jn="rule or group updated",Zn="rule or group moved",er="rule or group grouped with another",tr=[],nr={addRuleToNewGroups:!1,autoSelectField:!0,autoSelectOperator:!0,autoSelectValue:!1,debugMode:!1,enableDragAndDrop:!1,enableMountQueryChange:!0,listsAsArrays:!1,preserveQueryStateOnUnmount:!1,resetOnFieldChange:!0,resetOnOperatorChange:!1,showCloneButtons:!1,showCombinatorsBetweenRules:!1,showLockButtons:!1,showMuteButtons:!1,showNotToggle:!1,showShiftActions:!1,showUndoRedo:!1,suppressStandardClassnames:!1},rr=new Set(["target-not-found","parent-not-found","parent-not-a-group","destination-not-found","root-not-allowed","not-a-combinator-slot","target-disabled","parent-disabled","max-levels-exceeded"]),or=(e,t=",")=>"string"==typeof e?e.split(`\\${t}`).map(e=>e.split(t)).reduce((e,n,r)=>0===r?n:[...e.slice(0,-1),`${e.at(-1)}${t}${n[0]}`,...n.slice(1)],[]):[],ar=(e,t=",")=>e.map(e=>`${e??""}`.replaceAll(t[0],`\\${t[0]}`)).join(t),ir=e=>"string"==typeof e?e.trim():e,lr=(e,{retainEmptyStrings:t}={})=>Array.isArray(e)?e.map(ir):"string"==typeof e?or(e,",").filter(t?()=>!0:e=>!/^\s*$/.test(e)).map(e=>e.trim()):"number"==typeof e?[e]:[];function sr(e){var t,n,r="";if("string"==typeof e||"number"==typeof e)r+=e;else if("object"==typeof e)if(Array.isArray(e)){var o=e.length;for(t=0;t"string"==typeof e?e.toLowerCase():e,dr=new RegExp(Me.source.replace(/^\^/,String.raw`^\s*`).replace(/\$$/,String.raw`\s*$`)),fr=e=>null!==e&&"object"==typeof e&&Object.getPrototypeOf(e)===Object.prototype,pr=e=>null==e||""===e,hr=e=>fr(e)&&Array.isArray(e.rules),mr=e=>hr(e)&&"string"==typeof e.combinator,gr=e=>hr(e)&&void 0===e.combinator,vr="~structural",br=(e,t)=>{const n=new Set([...Object.keys(e),...Object.keys(t)]);n.delete("rules");const r=[];for(const o of n)Object.is(e[o],t[o])||r.push(o);return r.toSorted()},yr=(e,t,n,r)=>{if(e===t)return"";if("string"==typeof e||"string"==typeof t)return typeof e==typeof t?`${n}:combinator[${r}]`:vr;const o=hr(e),a=hr(t);if(o!==a)return vr;if(!o||!a){const n=br(e,t);return 0===n.length?"":`${t.id??""}:${n.join(",")}`}if(e.rules.length!==t.rules.length)return vr;const i=br(e,t);let l=-1;for(const[n,r]of e.rules.entries())if(r!==t.rules[n]){if(-1!==l)return vr;l=n}return-1===l?0===i.length?"":`${t.id??""}:${i.join(",")}`:i.length>0?vr:yr(e.rules[l],t.rules[l],t.id??"",l)},wr=(e,t)=>yr(e,t,"",-1),Sr=["or","xor","and"],kr=(e,t)=>cr(e)===t,Er=(e,t=0)=>{const n=Sr[t];if(!e.rules.includes(n))return t{if(mr(e))return e;const t=Er(e),n=[];let r="and";for(const[e,o]of t.rules.entries())"string"==typeof o?1===e&&(r=o):n.push(hr(o)?Or(o):o);return{...t,combinator:r,rules:n}},Nr=e=>{if(gr(e))return e;const{combinator:t,...n}=e,r=[],{length:o}=e.rules;for(let n=0;ne.isOver,standardOnly:!0},{key:"dndCopy",when:e=>e.isOver&&"copy"===e.dropEffect},{key:"dndDropNotAllowed",when:e=>e.dropNotAllowed}]},shiftActions:["shiftActions"],undoRedoActions:["undoRedoActions"],undoAction:["actionElement","undoAction"],redoAction:["actionElement","redoAction"],dragHandle:["dragHandle"],combinators:["valueSelector","combinators"],notToggle:["notToggle"],addRule:["actionElement","addRule"],addGroup:["actionElement","addGroup"],cloneGroup:["actionElement","cloneGroup"],lockGroup:["actionElement","lockGroup"],muteGroup:["actionElement","muteGroup"],removeGroup:["actionElement","removeGroup"],body:["body"]},Ar=[{key:"disabled",when:e=>e.disabled},{key:"muted",when:e=>e.muted},{key:"dndDragging",when:e=>e.isDragging},{key:"dndOver",when:e=>e.isOver},{key:"dndCopy",when:e=>e.isOver&&"copy"===e.dropEffect},{key:"dndGroup",when:e=>e.isOver&&e.groupItems},{key:"dndDropNotAllowed",when:e=>e.dropNotAllowed},{key:"hasSubQuery",when:e=>e.hasSubQuery}],Tr=[{key:"disabled",when:e=>e.disabled},{key:"muted",when:e=>e.muted},{key:"dndDragging",when:e=>e.isDragging},{key:"dndGroup",when:e=>e.isOver&&e.groupItems}],$r=[{key:"disabled",when:e=>e.disabled},{key:"valid",when:e=>"boolean"==typeof e.validationResult&&e.validationResult},{key:"invalid",when:e=>"boolean"==typeof e.validationResult&&!e.validationResult}],Mr=(e,t,{classNames:n,suppressStandardClassnames:r})=>{const o=e.map(e=>[e,!!e.when(t)]);return[...o.filter(([e])=>!e.standardOnly).map(([e,t])=>t&&n?.[e.key]),r||Object.fromEntries(o.map(([e,t])=>[hn[e.key],t]))]},Rr=(e,t,n,r)=>{const{sources:o,conditions:a}=Array.isArray(t)?{sources:t}:t;return ur(r.suppressStandardClassnames||hn[e],...o.map(e=>r.classNames?.[e]),...a?Mr(a,n,r):[])},xr=(e,t,n)=>{const r={};for(const[o,a]of Object.entries(e))r[o]=Rr(o,a,t,n);return r},Ir=(e,t,n)=>{const{classNames:r,suppressStandardClassnames:o,leadingClassNames:a=[],validationClassName:i}=n;return ur(...a,o||hn[e],r?.[e],...Mr(t,n,n),i)},Pr=e=>{const t={},n=e=>{ +/* v8 ignore start -- @preserve */ +e.id&&t[e.id] +/* v8 ignore stop -- @preserve */},r=e=>{const o=[];if(0===e.rules.length)o.push(mn);else if(!mr(e)){let t=!1;for(let n=0;ne.name).includes(e.rules[n]))&&(t=!0);t&&o.push(vn)}mr(e)&&!fn.map(e=>e.name).includes(e.combinator)&&e.rules.length>1&&o.push(gn) +/* v8 ignore else -- @preserve */,e.id&&(t[e.id]=!(o.length>0)||{valid:!1,reasons:o});for(const t of e.rules)"string"==typeof t||(hr(t)?r(t):n(t))};return r(e),t},_r=Object.keys,Lr=Object.entries,Gr=e=>"__proto__"===e||"constructor"===e||"prototype"===e,Fr=e=>fr(e)&&"name"in e&&"string"==typeof e.name,zr=e=>fr(e)&&"value"in e&&"string"==typeof e.value;function jr(e,t,n){if("string"==typeof e)return{...t,name:e,value:e,label:n?.[e]??e};const r={};let o=!!t;return Fr(e)&&!zr(e)?(r.value=e.name,o=!0):!Fr(e)&&zr(e)&&(r.name=e.value,o=!0),o?Object.assign({},t,e,r):e}function qr(e,t,n){if(!Array.isArray(e))return[];const r=e;return Wr(r)?r.map(e=>({...e,options:e.options.map(e=>jr(e,t,n))})):r.map(e=>jr(e,t,n))}function Br(e,t){return Object.fromEntries(Object.entries(e).map(([e,n])=>[e,jr(n,t)]))}const Vr=e=>{const t=new Set,n=[];for(const r of e)t.has(r.value??r.name)||(t.add(r.value??r.name),n.push(r));return e.length===n.length?e:n},Hr=e=>Array.isArray(e)&&e.length>0&&fr(e[0])&&"options"in e[0]&&Array.isArray(e[0].options),Ur=e=>{let t=!1;if(Array.isArray(e))for(const n of e){if(!Fr(n)&&!zr(n))return!1;t=!0}return t},Wr=(e,{allowEmpty:t=!1}={})=>{let n=!1;if(Array.isArray(e))for(const r of e){if(!(fr(r)&&"options"in r&&(Ur(r.options)||t&&Array.isArray(r.options)&&0===r.options.length)))return!1;n=!0}return n};function Qr(e,t){return(Wr(e,{allowEmpty:!0})?e.flatMap(e=>e.options):e).find(e=>e.value===t||e.name===t)}function Kr(e){if(!Array.isArray(e)||0===e.length)return null;if(Wr(e,{allowEmpty:!0})){for(const t of e)if(t.options.length>0)return t.options[0].value??t.options[0].name;return null}return e[0].value??e[0].name}const Xr=e=>Vr(Hr(e)?e.flatMap(e=>e.options):e),Yr=e=>{const t=new Set,n=new Set,r=[];for(const o of e)if(!t.has(o.label)){t.add(o.label);const e=[];for(const t of o.options)n.has(t.value??t.name)||(n.add(t.value??t.name),e.push(jr(t)));r.push({...o,options:e})}return r},Jr=e=>{const{optionList:t,baseOption:n={},labelMap:r={},placeholder:{placeholderName:o="~",placeholderLabel:a=ln,placeholderGroupLabel:i=ln}={},autoSelectOption:l=!0}=e,s={id:o,name:o,value:o,label:a},u=t??[s];let c;const d=Array.isArray(u)?qr(u,n,r):_r(Br(u,n)).map(e=>({...u[e],name:e,value:e})).sort((e,t)=>e.label.localeCompare(t.label));c=Wr(d)?Yr(l?d:[{label:i,options:[s]},...d]):Vr(l?d:[s,...d]);let f={};if(Array.isArray(u))if(Wr(c))for(const e of c)for(const t of e.options)f[t.value?? +/* v8 ignore start -- @preserve */t.name]=jr(t,n);else for(const e of c)f[e.value?? +/* v8 ignore start -- @preserve */e.name]=jr(e,n);else{const e=Br(u,n);f=l?e:{...e,[o]:s}}return{defaultOption:s,optionList:c,optionsMap:f}},Zr=(e,t,n)=>{const r=jr(e),o=jr(n);return r.value!==o.value&&("string"==typeof r.comparator?r[r.comparator]===o[r.comparator]:r.comparator?.(o,t)?? +/* v8 ignore start -- @preserve */!1)},eo=(e,t,n)=>{if(!e.comparator){const n=t=>(t.value??/* v8 ignore start -- @preserve */t.name)!==(e.value?? +/* v8 ignore start -- @preserve */e.name);return Wr(t)?t.map(e=>({...e,options:e.options.filter(e=>n(e))})):t.filter(e=>n(e))}return Wr(t)?t.map(t=>({...t,options:t.options.filter(t=>Zr(e,n,t))})).filter(e=>e.options.length>0):t.filter(t=>Zr(e,n,t))},to=(e,t)=>t?.[e]??{name:e,value:e,label:e},no=e=>{const t=e?.arity;return"string"==typeof t&&"unary"===t||"number"==typeof t&&t<2},ro=(e,t,n)=>{const r=n(e.field,e.operator,{fieldData:t});return e.valueSource&&!Qr(r,e.valueSource)?[...r,{name:e.valueSource,value:e.valueSource,label:e.valueSource}]:r},oo=e=>e&&e.length>0?e:null,ao=(e,t,n,r)=>"field"===e.valueSource?"select":"parameter"===e.valueSource?n?"in"===cr(e.operator)||"notin"===cr(e.operator)?"multiselect":"select":"text":r(e.field,e.operator,{fieldData:t}),io=(e,t,n,r,o)=>{const a="field"===e.valueSource?eo(t,n,e.operator):"parameter"===e.valueSource?r??[]:o(e.field,e.operator,{fieldData:t});return Ur(a)||Wr(a)?qr(a):a},lo=(e,t,n={},r=e.id??"")=>n[r]??("function"==typeof t.validator?t.validator(e):null),so=(e,t)=>mr(e)?e.combinator:Kr(t)??"",uo=(e,{parseNumbers:t,bigIntOnOverflow:n}={})=>{if(!t||"bigint"==typeof e||"number"==typeof e)return e;if("native"===t)return Number.parseFloat(e);const r=He(e,{allowTrailingInvalid:"enhanced"===t,bigIntOnOverflow:n,romanNumerals:!1,round:!1});return"bigint"!=typeof r&&Number.isNaN(r)?e:r},co=new Set(["between","notBetween","in","notIn"]),fo=(e,t)=>"bigint"===e||"in"===t||"notIn"===t?"text":e||"text",po=(e,t,n)=>{const r={};for(const o in e){if(Gr(o))continue;const a=t[o];!1!==a&&(a&&o!==a?(Gr(a)||(r[a]=e[o]),n||(r[o]=e[o])):r[o]=e[o])}return r};function ho(e,t={}){const{ruleProcessor:n=e=>e,ruleGroupProcessor:r=e=>e,propertyMap:o={},combinatorMap:a={},operatorMap:i={},omitPath:l=!1,deleteRemappedProperties:s=!0}=t,u=e=>({...r(po({...e,...mr(e)?{combinator:a[e.combinator]??e.combinator}:{}},o,s)),...!1===o.rules?null:{[o.rules??"rules"]:e.rules.map((t,r)=>{const c=l?null:{path:[...e.path,r]};return"string"==typeof t?a[t]??t:hr(t)?u({...t,...c}):n(po({...t,...c,..."operator"in t?{operator:i[t.operator]??t.operator}:{}},o,s))})}});return u({...e,...l?null:{path:[]}})}const mo=e=>fr(e)&&"boolean"==typeof e.valid,go=(e,t,n)=>{if(e.muted)return!1;if("boolean"==typeof t)return t;if(mo(t))return t.valid;if("function"==typeof n&&!hr(e)){const t=n(e);if("boolean"==typeof t)return t;if(mo(t))return t.valid}return!0},vo=({parseNumbers:e,inputType:t})=>{if("string"==typeof e){const[n,r]=e.split("-");return"limited"===r?"number"===t&&n:n}return!!e&&"strict"},bo=(e,t,{concatOperator:n="||",quoteValuesWith:r,wrapValueWith:o=["",""]}={})=>{const[a,i]=(e=>{switch(e){case"contains":case"doesnotcontain":return["%","%"];case"beginswith":case"doesnotbeginwith":return["","%"];case"endswith":case"doesnotendwith":return["%",""];default:return["",""]}})(t),l=r||"'",s=e=>`${o[0]}${l}${e}${l}${o[1]}`,u=[a&&s(a),e,i&&s(i)].filter(Boolean);return u.length>1?((e="||")=>(...t)=>"CONCAT"===e.toUpperCase()?`CONCAT(${t.join(", ")})`:t.join(` ${e} `))(n)(...u):e},yo={"=":"$eq","!=":"$ne","<":"$lt","<=":"$lte",">":"$gt",">=":"$gte",in:"$in",notin:"$nin",notIn:"$nin"},wo={"=":"equals","!=":"not","<":"lt","<=":"lte",">":"gt",">=":"gte",in:"in",notin:"notIn"},So={and:"&&",or:"||"},ko=(e,t)=>({...e,rules:e.rules.map(e=>{if("string"==typeof e)return e;if(hr(e))return ko(e,t);const n=Qr(t.fields,e.field),r=vo({parseNumbers:t.parseNumbers,inputType:n?.inputType});if(Array.isArray(e.value))return{...e,value:e.value.map(e=>uo(e,{parseNumbers:r}))};const o=lr(e.value,{retainEmptyStrings:!0}).map(e=>uo(e,{parseNumbers:r}));if(o.every(e=>"number"==typeof e)){if(o.length>1)return{...e,value:o};if(1===o.length)return{...e,value:o[0]}}return e})}),Eo=e=>"string"==typeof e&&e.length>0||"number"==typeof e&&!Number.isNaN(e)||"string"!=typeof e&&"number"!=typeof e,Oo=(e,t)=>!!t&&("number"==typeof e||"bigint"==typeof e||"string"==typeof e&&dr.test(e)),No=(e=["",""])=>Array.isArray(e)?e:"string"==typeof e?[e,e]:e??["",""],Co=(e,{quoteFieldNamesWith:t,fieldIdentifierSeparator:n})=>{const[r,o]=No(t);return"string"==typeof n&&n.length>0?ar(or(e,n).map(e=>`${r}${e}${o}`),n):`${r}${e}${o}`},Do=["S","V","O"],Ao=(e,t=":")=>{const n=`${e??""}`;return t&&n.startsWith(t)?n:`${t}${n}`},To=(e,t=":")=>{const n=`${e??""}`;return t&&n.startsWith(t)?n.slice(t.length):n},$o={groupPrefix:"",groupPrefix_not_xor:"either zero or more than one of",groupPrefix_xor:"exactly one of",groupSuffix:"is true",groupSuffix_not:"is not true"},Mo=(e,t,n)=>t.startsWith(e)&&n.every(e=>t.includes(`_${e}`)&&t.match(/_/g)?.length===n.length),Ro=(e,t,n=[])=>0===n.length?t[e]??$o[e]?? +/* v8 ignore start -- @preserve */"":Object.entries(t).find(([t])=>Mo(e,t,n))?.[1]??Object.entries($o).find(([t])=>Mo(e,t,n))?.[1]??$o[e]?? +/* v8 ignore next -- @preserve */"",xo=e=>{const{mode:t,threshold:n}=e.match??{};if(!t)return null;if(!hr(e.value))return!1;const r=cr(t),o="atleast"===r&&1===n?"some":"atmost"===r&&0===n?"none":r;return("atleast"!==o&&"atmost"!==o&&"exactly"!==o||!("number"!=typeof n||n<0))&&{mode:o,threshold:n}},Io="elem_alias",Po=(e=0)=>e>0?`${Io}_${e}`:Io,_o=(e,t)=>"bigint"==typeof t?{$bigint:t.toString()}:t,Lo=(e,t)=>{const{fields:n,fallbackExpression:r,getParseNumberBoolean:o,placeholderFieldName:a,placeholderOperatorName:i,placeholderValueName:l,ruleProcessor:s,validateRule:u,validationMap:c}=t,d=(e,f)=>{if(!go(e,c[e.id??/* v8 ignore start -- @preserve */""]))return f?r:"";const p=[];let h="",m=!0;for(const r of e.rules){if("string"==typeof r){h=So[r];continue}if(hr(r)){const e=d(r);e&&(!m&&h&&(p.push(h),h=""),m=!1,p.push(e));continue}const[e,c]=u(r);if(!go(r,e,c)||r.field===a||r.operator===i||void 0!==l&&r.value===l)continue;const f=Qr(n,r.field),g=s(r,{...t,parseNumbers:o(f?.inputType),escapeQuotes:"value"===(r.valueSource??"value"),fieldData:f});g&&(!m&&h&&(p.push(h),h=""),m=!1,p.push(g))}const g=p.join(mr(e)?` ${So[e.combinator]} `:" "),[v,b]=e.not||!f?[(e.not?"!":"")+"(",")"]:["",""];return g?`${v}${g}${b}`:r};return d(e,!0)},Go=e=>e.startsWith("not")||e.startsWith("doesnot"),Fo=(e,t)=>"string"==typeof e&&t?e.replaceAll('"','\\"'):`${e}`,zo=(e,t={})=>{const{escapeQuotes:n,parseNumbers:r,preserveValueOrder:o}=t,{field:a,operator:i,value:l,valueSource:s}=e,u="field"===s||"parameter"===s,c=cr("="===i?"==":i),d="number"==typeof l||"boolean"==typeof l||"bigint"==typeof l||Oo(l,r),f=xo(e);if(!1===f)return"";if(f){const{mode:n,threshold:r}=f,o=t.subqueryDepth??0,i=Po(o),l=ho(e.value,{ruleProcessor:e=>({...e,field:`${i}${e.field?`.${e.field}`:""}`})}),s=Lo(l,{...t,subqueryDepth:o+1});switch(n){case"all":return`${a}.all(${i}, ${s})`;case"none":case"some":return`${"none"===n?"!":""}${a}.exists(${i}, ${s})`;case"atleast":case"atmost":case"exactly":{const e=`${a}.filter(${i}, ${s}).size()`,t="atleast"===n?">=":"atmost"===n?"<=":"==";return r>0&&r<1?`double(${e}) ${t} (${`double(${a}.size())`} * ${r})`:`${e} ${t} ${r}`}}}switch(c){case"<":case"<=":case"==":case"!=":case">":case">=":return`${a} ${c} ${u||d?ir(l):`"${Fo(l,n)}"`}`;case"contains":case"doesnotcontain":return`${Go(c)?"!":""}${a}.contains(${u?ir(l):`"${Fo(l,n)}"`})`;case"beginswith":case"doesnotbeginwith":return`${Go(c)?"!":""}${a}.startsWith(${u?ir(l):`"${Fo(l,n)}"`})`;case"endswith":case"doesnotendwith":return`${Go(c)?"!":""}${a}.endsWith(${u?ir(l):`"${Fo(l,n)}"`})`;case"null":return`${a} == null`;case"notnull":return`${a} != null`;case"in":case"notin":{const[e,t]=Go(c)?["!(",")"]:["",""];return`${e}${a} in [${lr(l).map(e=>u||Oo(e,r)?`${ir(e)}`:`"${Fo(e,n)}"`).join(", ")}]${t}`}case"between":case"notbetween":{const e=lr(l);if(e.length>=2&&!pr(e[0])&&!pr(e[1])){const[t,i]=e,l=!(!1===r),s=Oo(t,l)?uo(t,{parseNumbers:l}):NaN,d=Oo(i,l)?uo(i,{parseNumbers:l}):NaN;let f=Number.isNaN(s)?u?`${t}`:`"${Fo(t,n)}"`:s,p=Number.isNaN(d)?u?`${i}`:`"${Fo(i,n)}"`:d;if(!o&&f===s&&p===d&&d= ${f} && ${a} <= ${p})`:`(${a} < ${f} || ${a} > ${p})`}return""}}return""},jo={$and:[{$expr:!0}]},qo=(e,t,n)=>{const{context:r,fields:o,getParseNumberBoolean:a,placeholderFieldName:i,placeholderOperatorName:l,placeholderValueName:s,ruleProcessor:u,validateRule:c,validationMap:d}=t,{inExpressionContext:f}=r??{},p=(e,r)=>{if(!go(e,d[e.id??/* v8 ignore start -- @preserve */""]))return!!r&&jo;const h=`$${cr(e.combinator)}`;let m=!1;const g=e.rules.map(e=>{if(hr(e)){const t=p(e);return!!t&&(m=!0,t)}const[r,d]=c(e);if(!go(e,r,d)||e.field===i||e.operator===l||void 0!==s&&e.value===s)return!1;const f=Qr(o,e.field);return u(e,{...t,parseNumbers:a(f?.inputType),fieldData:f},n)}).filter(Boolean),v=g.length>0?1!==g.length||m?{[h]:g}:g[0]:jo;return e.not?f?{$not:v}:{$nor:[v]}:v};return p(Or(e),!0)},Bo=(e,t,n=!1)=>Oo(e,n||"bigint"==typeof e)?Number(uo(e,{parseNumbers:"strict"})):t,Vo=(e,t={})=>{const{field:n,operator:r,value:o,valueSource:a}=e,{parseNumbers:i,preserveValueOrder:l,context:s}=t,u="field"===a,{avoidFieldsAsKeys:c}=s??{},d=xo(e);if(!1===d)return;if(d){const{mode:e,threshold:r}=d,a={$size:{$ifNull:[`$${n}`,[]]}},i=qo(ho(o,{ruleProcessor:e=>({...e,field:e.field?`${n}.${e.field}`:n})}),{...t,ruleProcessor:Vo,context:{...t.context,avoidFieldsAsKeys:!1}}),l=qo(ho(o,{ruleProcessor:e=>({...e,field:e.field?`$item.${e.field}`:"$item"})}),{...t,ruleProcessor:Vo,context:{...t.context,avoidFieldsAsKeys:!0,inExpressionContext:!0}}),s={$size:{$ifNull:[{$filter:{input:`$${n}`,as:"item",cond:{$and:[l]}}},[]]}};switch(e){case"all":return{$expr:{$eq:[s,a]}};case"none":return{$nor:[i]};case"some":return i;case"atleast":case"atmost":case"exactly":{const t="atleast"===e?yo[">="]:"atmost"===e?yo["<="]:yo["="];return r>0&&r<1?{$expr:{[t]:[s,{$multiply:[a,r]}]}}:{$expr:{[t]:[s,r]}}}}}if("="===r&&!u)return c?{$eq:[`$${n}`,Bo(o,o,i)]}:{[n]:Bo(o,o,i)};const f=cr(r);switch(f){case"<":case"<=":case"=":case"!=":case">":case">=":{const e=yo[f];return u?{[e]:[`$${n}`,`$${o}`]}:c?{$and:[{$ne:[`$${n}`,null]},{[e]:[`$${n}`,Bo(o,o,i)]}]}:{[n]:{[e]:Bo(o,o,i)}}}case"contains":return u?{$where:`this.${n}.includes(this.${o})`}:c?{$regexMatch:{input:`$${n}`,regex:o}}:{[n]:{$regex:o}};case"beginswith":return u?{$where:`this.${n}.startsWith(this.${o})`}:c?{$regexMatch:{input:`$${n}`,regex:`^${o}`}}:{[n]:{$regex:`^${o}`}};case"endswith":return u?{$where:`this.${n}.endsWith(this.${o})`}:c?{$regexMatch:{input:`$${n}`,regex:`${o}$`}}:{[n]:{$regex:`${o}$`}};case"doesnotcontain":return u?{$where:`!this.${n}.includes(this.${o})`}:c?{$not:{$regexMatch:{input:`$${n}`,regex:o}}}:{[n]:{$not:{$regex:o}}};case"doesnotbeginwith":return u?{$where:`!this.${n}.startsWith(this.${o})`}:c?{$not:{$regexMatch:{input:`$${n}`,regex:`^${o}`}}}:{[n]:{$not:{$regex:`^${o}`}}};case"doesnotendwith":return u?{$where:`!this.${n}.endsWith(this.${o})`}:c?{$not:{$regexMatch:{input:`$${n}`,regex:`${o}$`}}}:{[n]:{$not:{$regex:`${o}$`}}};case"null":return c?{$eq:[`$${n}`,null]}:{[n]:null};case"notnull":return c?{$ne:[`$${n}`,null]}:{[n]:{$ne:null}};case"in":case"notin":{const e=lr(o);return u?{$where:`${"notin"===f?"!":""}[${e.map(e=>`this.${e}`).join(",")}].includes(this.${n})`}:c?"notin"===f?{$not:{[yo.in]:[`$${n}`,e.map(e=>Bo(e,e,i))]}}:{[yo[f]]:[`$${n}`,e.map(e=>Bo(e,e,i))]}:{[n]:{[yo[f]]:e.map(e=>Bo(e,e,i))}}}case"between":case"notbetween":{const e=lr(o);if(e.length>=2&&Eo(e[0])&&Eo(e[1])){const[t,r]=e,o=Bo(t,NaN,!0),a=Bo(r,NaN,!0);let i=u||Number.isNaN(o)?t:o,s=u||Number.isNaN(a)?r:a;if(!l&&i===o&&s===a&&a{const n=Vo(e,t);return n?JSON.stringify(n):""},Uo=(e,t)=>{const{fields:n,fallbackExpression:r,getParseNumberBoolean:o,placeholderFieldName:a,placeholderOperatorName:i,placeholderValueName:l,ruleProcessor:s,validateRule:u,validationMap:c}=t,d=(e,f)=>{if(!go(e,c[e.id??/* v8 ignore start -- @preserve */""]))return f?r:"";const p=[];let h="",m=!0;for(const r of e.rules){if("string"==typeof r){h=r;continue}if(hr(r)){const e=d(r);e&&(!m&&h&&(p.push(h),h=""),m=!1,p.push(e));continue}const[e,c]=u(r);if(!go(r,e,c)||r.field===a||r.operator===i||void 0!==l&&r.value===l)continue;const f=Qr(n,r.field),g=s(r,{...t,parseNumbers:o(f?.inputType),escapeQuotes:"value"===(r.valueSource??"value"),fieldData:f});g&&(!m&&h&&(p.push(h),h=""),m=!1,p.push(g))}const g=p.join(mr(e)?` ${e.combinator} `:" "),[v,b]=e.not||!f?[(e.not?"!":"")+"(",")"]:["",""];return g?`${v}${g}${b}`:r};return d(e,!0)},Wo=e=>e.startsWith("not")||e.startsWith("doesnot"),Qo=(e,t)=>t?`!(${e})`:e,Ko=(e,t)=>"string"==typeof e&&t?e.replaceAll("'","\\'"):`${e}`,Xo=(e,t={})=>{const{field:n,operator:r,value:o,valueSource:a}=e,{escapeQuotes:i,parseNumbers:l,preserveValueOrder:s}=t,u="field"===a||"parameter"===a,c=cr("="===r?"==":r),d="number"==typeof o||"boolean"==typeof o||"bigint"==typeof o||Oo(o,l),f=xo(e);if(!1===f)return"";if(f){const{mode:r,threshold:o}=f,a=Uo(ho(e.value,{ruleProcessor:e=>({...e,field:e.field||"#this"})}),t),i=`${n}.size()`,l=`${n}.?[${a}].size()`;switch(r){case"all":return`${l} == ${i}`;case"none":return`${l} == 0`;case"some":return`${l} >= 1`;case"atleast":case"atmost":case"exactly":{const e="atleast"===r?">=":"atmost"===r?"<=":"==";return o>0&&o<1?`${l} ${e} (${i} * ${o})`:`${l} ${e} ${o}`}}}switch(c){case"<":case"<=":case"==":case"!=":case">":case">=":return`${n} ${c} ${u||d?ir(o):`'${Ko(o,i)}'`}`;case"contains":case"doesnotcontain":{const e=u?`'.*'.concat(${ir(o)}).concat('.*')`:`'.*${Ko(o,i)}.*'`;return Qo(`${n} matches ${e}`,Wo(c))}case"beginswith":case"doesnotbeginwith":{const e=u?`'^'.concat(${ir(o)}).concat('.*')`:`'${"string"==typeof o&&!o.startsWith("^")||d?"^":""}${Ko(o,i)}.*'`;return Qo(`${n} matches ${e}`,Wo(c))}case"endswith":case"doesnotendwith":{const e=u?`'.*'.concat(${ir(o)}).concat('$')`:`'.*${Ko(o,i)}${"string"==typeof o&&!o.endsWith("$")||d?"$":""}'`;return Qo(`${n} matches ${e}`,Wo(c))}case"null":return`${n} == null`;case"notnull":return`${n} != null`;case"in":case"notin":{const e=Wo(c)?"!":"",t=lr(o);return t.length>0?`${e}(${t.map(e=>`${n} == ${u||Oo(e,l)?`${ir(e)}`:`'${Ko(e,i)}'`}`).join(" or ")})`:""}case"between":case"notbetween":{const e=lr(o);if(e.length>=2&&!pr(e[0])&&!pr(e[1])){const[t,r]=e,o=!(!1===l),a=Oo(t,o)?uo(t,{parseNumbers:o}):NaN,d=Oo(r,o)?uo(r,{parseNumbers:o}):NaN;let f=Number.isNaN(a)?u?`${t}`:`'${Ko(t,i)}'`:a,p=Number.isNaN(d)?u?`${r}`:`'${Ko(r,i)}'`:d;if(!s&&f===a&&p===d&&d= ${f} and ${n} <= ${p})`:`(${n} < ${f} or ${n} > ${p})`}return""}}return""},Yo=({operator:e,value:t,valueSource:n},{escapeQuotes:r,parseNumbers:o,preserveValueOrder:a,quoteFieldNamesWith:i,quoteValuesWith:l,concatOperator:s="||",fieldIdentifierSeparator:u,paramPrefix:c,wrapValueWith:d=["",""],translations:f}={})=>{if("parameter"===n){const n=cr(e);if("between"===n||"notbetween"===n){const e=lr(t,{retainEmptyStrings:!0});if(e.length<2||!Eo(e[0])||!Eo(e[1]))return"";const[n,r]=e;return[Ao(n,c),Ao(r,c)].join(` ${f?.betweenAnd??f?.and??"and"} `)}if("in"===n||"notin"===n){const e=lr(t);return 0===e.length?"":`(${e.map(e=>Ao(e,c)).join(", ")})`}const r=Ao(t,c);switch(n){case"contains":case"doesnotcontain":case"beginswith":case"doesnotbeginwith":case"endswith":case"doesnotendwith":return bo(r,n,{concatOperator:s,quoteValuesWith:l,wrapValueWith:d})}return r}const p="field"===n,h=cr(e),m=l||"'",g=e=>`${d[0]}${m}${e}${m}${d[1]}`,v=e=>((e,t,n)=>n&&"string"==typeof e?e.replaceAll(`${t}`,`${t}${t}`):e)(e,m,r),b=e=>g(v(e)),y=e=>Co(e,{quoteFieldNamesWith:i,fieldIdentifierSeparator:u}),w={concatOperator:s,quoteValuesWith:l,wrapValueWith:d};switch(h){case"null":case"notnull":return"";case"in":case"notin":{const e=lr(t);return e.length>0?`(${e.map(e=>p?y(e):Oo(e,o)?`${ir(e)}`:`${b(e)}`).join(", ")})`:""}case"between":case"notbetween":{const e=lr(t,{retainEmptyStrings:!0});if(e.length<2||!Eo(e[0])||!Eo(e[1]))return"";const[n,r]=e,i=Oo(n,o)?uo(n,{parseNumbers:"strict"}):NaN,l=Oo(r,o)?uo(r,{parseNumbers:"strict"}):NaN,s=Number.isNaN(i)?p?`${n}`:n:i,u=Number.isNaN(l)?p?`${r}`:r:l,c=[s,u];return!a&&s===i&&u===l&&ly(e)):c.every(e=>Oo(e,o))?c.map(e=>uo(e,{parseNumbers:"strict"})):c.map(e=>b(e))).join(` ${f?.betweenAnd??f?.and??"and"} `)}case"contains":case"doesnotcontain":return p?bo(y(t),h,w):g(`%${v(t)}%`);case"beginswith":case"doesnotbeginwith":return p?bo(y(t),h,w):g(`${v(t)}%`);case"endswith":case"doesnotendwith":return p?bo(y(t),h,w):g(`%${v(t)}`)}return"boolean"==typeof t?t?"TRUE":"FALSE":p?y(t):Oo(t,o)?`${ir(t)}`:`${b(t)}`},Jo={and:"AND",or:"OR"},Zo=(e,t,n)=>(n,r)=>{const{fields:o,getParseNumberBoolean:a,placeholderFieldName:i,placeholderOperatorName:l,placeholderValueName:s,validateRule:u,validationMap:c,ruleProcessor:d}=t;if(!n||!r)return;const{and:f,not:p,or:h}=r,m=(e,g)=>{if(!go(e,c[e.id??/* v8 ignore start -- @preserve */""]))return;const v=e.rules.map(e=>{if(hr(e))return m(e);const[c,f]=u(e);if(!go(e,c,f)||e.field===i||e.operator===l||void 0!==s&&e.value===s)return;const p=Qr(o,e.field);return d(e,{...t,parseNumbers:a(p?.inputType),fieldData:p,context:{...t.context,columns:n,drizzleOperators:r}})}).filter(Boolean);if(0===v.length)return;const b="or"===e.combinator?h(...v):f(...v);return e.not?p(b):b};return m(Or(e),!0)},ea=(e,t)=>{const{fields:n,getParseNumberBoolean:r,placeholderFieldName:o,placeholderOperatorName:a,placeholderValueName:i,ruleProcessor:l,validateRule:s,validationMap:u}=t,c=e=>{if(!go(e,u[e.id??/* v8 ignore start -- @preserve */""]))return!1;const d=e.rules.map(e=>{if(hr(e))return c(e);const[u,d]=s(e);if(!go(e,u,d)||e.field===o||e.operator===a||void 0!==i&&e.value===i)return!1;const f=Qr(n,e.field);return l(e,{...t,parseNumbers:r(f?.inputType),fieldData:f})}).filter(Boolean);return 0!==d.length&&{bool:e.not?{must_not:/^or$/i.test(e.combinator)?{bool:{should:d}}:d}:{[/^or$/i.test(e.combinator)?"should":"must"]:d}}},d=c(Or(e));return!1===d?{}:d},ta=(e,t)=>{const{fields:n,fallbackExpression:r,getParseNumberBoolean:o,placeholderFieldName:a,placeholderOperatorName:i,placeholderValueName:l,ruleProcessor:s,validateRule:u,validationMap:c}=t,d=(e,f)=>{if(!go(e,c[e.id??/* v8 ignore start -- @preserve */""]))return f?r:"";const p=[];let h="",m=!0;for(const r of e.rules){if("string"==typeof r){h=r;continue}if(hr(r)){const e=d(r);e&&(!m&&h&&(p.push(h),h=""),m=!1,p.push(e));continue}const[e,c]=u(r);if(!go(r,e,c)||r.field===a||r.operator===i||void 0!==l&&r.value===l)continue;const f=Qr(n,r.field),g=s(r,{...t,parseNumbers:o(f?.inputType),escapeQuotes:"value"===(r.valueSource??"value"),fieldData:f});g&&(!m&&h&&(p.push(h),h=""),m=!1,p.push(g))}const g=p.join(mr(e)?` ${e.combinator} `:" "),[v,b]=e.not||!f?[(e.not?"$not":"")+"(",")"]:["",""];return g?`${v}${g}${b}`:r};return d(e,!0)},na=(e,t)=>{const{fields:n,getParseNumberBoolean:r,placeholderFieldName:o,placeholderOperatorName:a,placeholderValueName:i,ruleProcessor:l,validateRule:s,validationMap:u}=t,c=Or(e),d=(e,c)=>{if(!go(e,u[e.id??/* v8 ignore start -- @preserve */""]))return!1;const f=e.rules.map(e=>{if(hr(e))return d(e);const[u,c]=s(e);if(!go(e,u,c)||e.field===o||e.operator===a||void 0!==i&&e.value===i)return!1;const f=Qr(n,e.field);return l(e,{...t,parseNumbers:r(f?.inputType),fieldData:f})}).filter(Boolean);if(0===f.length)return!1;const p={[e.combinator]:f};return e.not?{"!":p}:p};return d(c,!0)},ra=e=>e.startsWith("{")&&e.endsWith("}"),oa=(e,t)=>{const{fields:n,fallbackExpression:r,getParseNumberBoolean:o,placeholderFieldName:a,placeholderOperatorName:i,placeholderValueName:l,ruleProcessor:s,translations:u,validateRule:c,validationMap:d}=t,f=(e,p)=>{if(!go(e,d[e.id??/* v8 ignore start -- @preserve */""]))return p?r:"";const h=gr(e)&&e.rules.some(e=>"string"==typeof e&&"xor"===cr(e))?Or(e):e,m=[];let g="",v=!0;for(const e of h.rules){if("string"==typeof e){g=`${u.ruleSeparator??", "}${u[e]??e} `;continue}if(hr(e)){const t=f(e,1===h.rules.length&&!(h.not||/^xor$/i.test(h.combinator?? +/* v8 ignore start -- @preserve */"")));t&&(!v&&g&&(m.push(g),g=""),v=!1,m.push(t));continue}const[r,d]=c(e);if(!go(e,r,d)||e.field===a||e.operator===i||void 0!==l&&e.value===l)continue;const p="value"===(e.valueSource??"value"),b=Qr(n,e.field),y=s(e,{...t,parseNumbers:o(b?.inputType),escapeQuotes:p,fieldData:b});y&&(!v&&g&&(m.push(g),g=""),v=!1,m.push(y))}if(0===m.length)return r;const b="xor"===cr(h.combinator??""),y=b?h.combinator.slice(1):h.combinator,w=h.not||!p||b&&m.length>1,[S,k]=["groupPrefix","groupSuffix"].map(e=>h.not?Ro(e,u,b?["not","xor"]:["not"]):b?Ro(e,u,["xor"]):Ro(e,u)),E=w?`${S} (`.trim():"",O=w?`) ${k}`.trim():"",N=u.ruleSeparator??", ";return`${E}${m.join(mr(h)?`${N}${u[y]??y} `:"")}${O}`};return f(e,!0)},aa=(e,t)=>{const{format:n,fields:r,fallbackExpression:o,getParseNumberBoolean:a,placeholderFieldName:i,placeholderOperatorName:l,placeholderValueName:s,ruleProcessor:u,validateRule:c,validationMap:d}=t,f="parameterized"===n,p=[],h={},m=new Map,g=e=>{m.has(e)||m.set(e,new Set);const t=`${e}_${m.get(e).size+1}`;return m.get(e).add(t),t},v=e=>{const[o,d]=c(e);if(!go(e,o,d)||e.field===i||e.operator===l||void 0!==s&&e.value===s)return"";const f=Qr(r,e.field),v=Object.fromEntries([...m.entries()].map(([e,t])=>[e,[...t]])),b=u(e,{...t,parseNumbers:a(f?.inputType),getNextNamedParam:g,fieldParamNames:v,fieldData:f},{processedParams:p});if(!fr(b))return"";const{sql:y,params:w}=b;if("string"!=typeof y||!y)return"";if("parameterized"===n&&Array.isArray(w))p.push(...w);else if("parameterized_named"===n&&fr(w)){Object.assign(h,w);for(const t of Object.keys(w))m.get(e.field)?.add(t)}return y},b=(e,t)=>{if(!go(e,d[e.id??/* v8 ignore start -- @preserve */""]))return t?o:"";const n=[];let r="",a=!0;for(const t of e.rules){if("string"==typeof t){r=t;continue}if(hr(t)){const o=b(t,1===e.rules.length);o&&(!a&&r&&(n.push(r),r=""),a=!1,n.push(o));continue}const o=v(t);o&&(!a&&r&&(n.push(r),r=""),a=!1,n.push(o))}return 0===n.length?o:`${e.not?"NOT ":""}(${n.join(mr(e)?` ${e.combinator} `:" ")})`};return f?{sql:b(e,!0),params:p}:{sql:b(e,!0),params:h}},ia={},la=(e,t)=>{const{fields:n,fallbackExpression:r,getParseNumberBoolean:o,placeholderFieldName:a,placeholderOperatorName:i,placeholderValueName:l,ruleProcessor:s,validateRule:u,validationMap:c}=t,d=(e,f)=>{if(!go(e,c[e.id??/* v8 ignore start -- @preserve */""]))return f?r:"";const p=[];let h="",m=!0;for(const r of e.rules){if("string"==typeof r){h=r;continue}if(hr(r)){const t=d(r,1===e.rules.length);t&&(!m&&h&&(p.push(h),h=""),m=!1,p.push(t));continue}const[c,f]=u(r);if(!go(r,c,f)||r.field===a||r.operator===i||void 0!==l&&r.value===l)continue;const g="value"===(r.valueSource??"value"),v=Qr(n,r.field),b=s(r,{...t,parseNumbers:o(v?.inputType),escapeQuotes:g,fieldData:v});b&&(!m&&h&&(p.push(h),h=""),m=!1,p.push(b))}return 0===p.length?r:`${e.not?"NOT ":""}(${p.join(mr(e)?` ${e.combinator} `:" ")})`};return d(e,!0)},sa=new Set(["number","range","bigint"]),ua=/^\d{4}-\d{2}-\d{2}$/,ca=/^\d{2}:\d{2}(:\d{2}(\.\d+)?)?$/,da=/^\d{4}-\d{2}$/,fa=/^\d{4}-W\d{2}$/,pa=/^#([0-9a-f]{3}|[0-9a-f]{6})$/i,ha=/^[^\s@]+@[^\s@]+\.[^\s@]+$/,ma=(e,t,n)=>{const r=new Date(Date.UTC(e,t-1,n));return r.getUTCFullYear()===e&&r.getUTCMonth()===t-1&&r.getUTCDate()===n},ga=e=>{const t=e.split(":"),n=Number(t[0]),r=Number(t[1]),o=t[2]?Number.parseFloat(t[2]):0;return n>=0&&n<=23&&r>=0&&r<=59&&o>=0&&o<60},va=(e,t,n,r,o,a,i,l,s,u,c)=>{const d=e.id??"";if(e.muted&&a.push({id:d,path:t,code:"MUTED",message:"Rule is muted",source:"muted"}),e.field===i&&a.push({id:d,path:t,code:"PLACEHOLDER_FIELD",message:"Rule has a placeholder field",source:"placeholder"}),e.operator===l&&a.push({id:d,path:t,code:"PLACEHOLDER_OPERATOR",message:"Rule has a placeholder operator",source:"placeholder"}),void 0!==s&&e.value===s&&a.push({id:d,path:t,code:"PLACEHOLDER_VALUE",message:"Rule has a placeholder value",source:"placeholder"}),!e.muted&&e.field!==i&&e.operator!==l&&(void 0===s||e.value!==s))if("boolean"!=typeof n||n){if("boolean"!=typeof n&&mo(n)&&!n.valid){const e=n.reasons;a.push({id:d,path:t,code:"CUSTOM_VALIDATOR",message:e?`Invalid: ${e.join(", ")}`:"Rule failed validation",source:"query-validator"})}else if(!o&&"function"==typeof r){const n=r(e),o="boolean"!=typeof n&&mo(n)&&!n.valid?n.reasons:void 0;a.push({id:d,path:t,code:"CUSTOM_VALIDATOR",message:o?`Invalid: ${o.join(", ")}`:"Rule failed field validation",source:"field-validator"})}}else a.push({id:d,path:t,code:"CUSTOM_VALIDATOR",message:"Rule failed validation",source:"query-validator"});if(u&&!c.has(e.field)&&e.field!==i&&a.push({id:d,path:t,code:"UNDEFINED_FIELD",message:`Field "${e.field}" is not defined in the fields config`,source:"field-check"}),u){const n=c.get(e.field);if(n?.inputType){const r=((e,t)=>{if(null!=e&&""!==e){if(sa.has(t)){const t="string"==typeof e?e.trim():e;if("number"==typeof t||"bigint"==typeof t)return;if("string"==typeof t&&dr.test(t))return;return"VALUE_TYPE_MISMATCH"}if("date"===t){if("string"!=typeof e)return"VALUE_TYPE_MISMATCH";const t=e.trim();if(!ua.test(t))return"VALUE_TYPE_MISMATCH";const[n,r,o]=t.split("-").map(Number);return ma(n,r,o)?void 0:"VALUE_TYPE_MISMATCH"}if("datetime-local"===t){if("string"!=typeof e)return"VALUE_TYPE_MISMATCH";const t=e.trim().replace(/(Z|[+-]\d{2}:?\d{2}|[+-]\d{2})$/,""),n=t.indexOf("T");if(-1===n)return"VALUE_TYPE_MISMATCH";const r=t.slice(0,n),o=t.slice(n+1);if(!ua.test(r)||!ca.test(o))return"VALUE_TYPE_MISMATCH";const[a,i,l]=r.split("-").map(Number);return ma(a,i,l)&&ga(o)?void 0:"VALUE_TYPE_MISMATCH"}if("time"===t){if("string"!=typeof e)return"VALUE_TYPE_MISMATCH";const t=e.trim();return ca.test(t)&&ga(t)?void 0:"VALUE_TYPE_MISMATCH"}if("month"===t){if("string"!=typeof e)return"VALUE_TYPE_MISMATCH";const t=e.trim();if(!da.test(t))return"VALUE_TYPE_MISMATCH";const n=Number(t.slice(5));return n>=1&&n<=12?void 0:"VALUE_TYPE_MISMATCH"}if("week"===t){if("string"!=typeof e)return"VALUE_TYPE_MISMATCH";const t=e.trim();if(!fa.test(t))return"VALUE_TYPE_MISMATCH";const n=Number(t.slice(6));return n>=1&&n<=53?void 0:"VALUE_TYPE_MISMATCH"}if("color"===t)return"string"!=typeof e?"VALUE_TYPE_MISMATCH":pa.test(e.trim())?void 0:"VALUE_TYPE_MISMATCH";if("url"===t){if("string"!=typeof e)return"VALUE_TYPE_MISMATCH";try{return void new URL(e.trim())}catch{return"VALUE_TYPE_MISMATCH"}}return"email"===t?"string"!=typeof e?"VALUE_TYPE_MISMATCH":ha.test(e.trim())?void 0:"VALUE_TYPE_MISMATCH":void 0}})(e.value,n.inputType);r&&a.push({id:d,path:t,code:r,message:`Value "${e.value}" is not compatible with input type "${n.inputType}"`,source:"type-check"})}}},ba=e=>{if("boolean"!=typeof e&&mo(e)&&!e.valid&&e.reasons)return e.reasons},ya=(e,t)=>{if("function"==typeof t){const n=t(e);if("boolean"!=typeof n&&mo(n)&&!n.valid&&n.reasons)return n.reasons}},wa=(e,t={})=>{const{escapeQuotes:n,parseNumbers:r}=t,{field:o,operator:a,value:i,valueSource:l}=e,s="field"===l,u=a.toLowerCase(),c=e=>null==e?"null":"boolean"==typeof e||"bigint"==typeof e?String(e):s||"number"==typeof e||Oo(e,r)?ir(e):`'${((e,t)=>"string"==typeof e&&t?e.replaceAll("'","\\'"):`${e}`)(e,n)}'`;switch(u){case"=":return`${o} = ${c(i)}`;case"!=":case"<>":return`${o} <> ${c(i)}`;case"<":case">":case"<=":case">=":return`${o} ${u} ${c(i)}`;case"contains":return`${o} CONTAINS ${c(i)}`;case"doesnotcontain":return`NOT ${o} CONTAINS ${c(i)}`;case"beginswith":return`${o} STARTS WITH ${c(i)}`;case"doesnotbeginwith":return`NOT ${o} STARTS WITH ${c(i)}`;case"endswith":return`${o} ENDS WITH ${c(i)}`;case"doesnotendwith":return`NOT ${o} ENDS WITH ${c(i)}`;case"null":return`${o} IS NULL`;case"notnull":return`${o} IS NOT NULL`;case"in":case"notin":{const e=lr(i).map(c);return e.length?`${"notin"===u?"NOT ":""}${o} IN [${e.join(", ")}]`:""}case"between":case"notbetween":{const e=lr(i);if(e.length<2)return"";const[t,n]=[c(e[0]),c(e[1])],r=`${t} <= ${o} AND ${o} <= ${n}`;return"notbetween"===u?`NOT (${r})`:r}default:return`${o} ${a} ${c(i)}`}},Sa={"<":"lt","<=":"lte",">":"gt",">=":"gte"},ka=(e,t)=>e.startsWith("not")||e.startsWith("doesnot")?{bool:{must_not:t}}:t,Ea=e=>e?.replace(/('|\\)/g,"\\$1"),Oa={beginswith:"startsWith",doesnotbeginwith:"startsWith",doesnotcontain:"contains",doesnotendwith:"endsWith",endswith:"endsWith"},Na=(e,t,n)=>{const r=`doc['${e}'].value.${Oa[t]??t}(doc['${n}'].value)`;return t.startsWith("d")?`!${r}`:r},Ca=(e,t)=>"boolean"==typeof e?e:Oo(e,t)?uo(e,{parseNumbers:t}):e,Da=e=>e.startsWith("not")||e.startsWith("doesnot"),Aa=(e,t)=>`"${"string"==typeof e&&t?e.replaceAll('"','\\"'):`${e}`}"`,Ta=(e,t)=>t?`$not(${e})`:e,$a=e=>`${e}`.replaceAll(/[/$()*+.?[\\\]^{|}]/g,String.raw`\$&`).replaceAll("-",String.raw`\x2d`),Ma=e=>e.replace(/^(=)$/,"$1=").replace(/^notnull$/i,"!=").replace(/^null$/i,"=="),Ra=(e,t)=>e.startsWith("not")||e.startsWith("doesnot")?{"!":t}:t,xa=(e,t={})=>{const{field:n,operator:r,value:o,valueSource:a}=e,{parseNumbers:i,preserveValueOrder:l}=t,s="field"===a,u={var:n},c=e=>s?{var:`${e}`}:Oo(e,i)?uo(e,{parseNumbers:i}):e,d=xo(e);if(!1===d)return!1;if(d){const{mode:e,threshold:r}=d;switch(e){case"all":case"none":case"some":return{[e]:[{var:n},1!==o.rules.length||hr(o.rules[0])?na(o,t):xa(o.rules[0],t)]};case"atleast":case"atmost":case"exactly":{const a="atleast"===e?">=":"atmost"===e?"<=":"==",i={reduce:[{filter:[{var:n},1!==o.rules.length||hr(o.rules[0])?na(o,t):xa(o.rules[0],t)]},{"+":[1,{var:"accumulator"}]},0]};if(r>0&&r<1){const e={reduce:[{var:n},{"+":[1,{var:"accumulator"}]},0]};return{[a]:[i,{"*":[e,r]}]}}return{[a]:[i,r]}}}}const f=cr(r);switch(f){case"<":case"<=":case"=":case"!=":case">":case">=":return{[Ma(f)]:[u,c(o)]};case"null":case"notnull":return{[("notnull"===f?"!":"=")+"="]:[u,null]};case"in":case"notin":{const e=lr(o).map(e=>c(e));return Ra(f,{in:[u,e]})}case"between":case"notbetween":{const e=lr(o);if(e.length>=2&&Eo(e[0])&&Eo(e[1])){let[t,n]=e;const r=!(!1===i);if(!s&&Oo(t,r)&&Oo(n,r)){const e=uo(t,{parseNumbers:r}),o=uo(n,{parseNumbers:r});if(!l&&ot?`(!${e})`:`${e}`,Pa=e=>`${ir(e)}`.replaceAll(/[()&|=<>~*\\/]/g,e=>`\\${e.codePointAt(0).toString(16)}`),_a=(e,t={})=>{const{escapeQuotes:n,fields:r,parseNumbers:o,quoteFieldNamesWith:a,quoteValuesWith:i,fieldIdentifierSeparator:l,translations:s}=t,u="field"===e.valueSource,c=cr(e.operator),d=i||/* v8 ignore start -- @preserve */"'",f=e=>(e=>`${d}${e}${d}`)((e=>((e,t,n)=>n&&"string"==typeof e?e.replaceAll(`${t}`,`${t}${t}`):/* v8 ignore next -- @preserve */e)(e,d,n))(e)),p=e=>Co(e,{quoteFieldNamesWith:a,fieldIdentifierSeparator:l}),h=s??/* v8 ignore start -- @preserve */{},m=h.or??"or",g=h.true??"true",v=h.false??"false",b=h.listSeparator??", ",y=!h.listSeparator;switch(c){case"null":case"notnull":return"";case"between":case"notbetween":{if(!u)return Yo(e,t);const n=lr(e.value,{retainEmptyStrings:!0}).slice(0,2).map(e=>p(Qr(r?? +/* v8 ignore start -- @preserve */[],e)?.label??e));return n.length<2||!Eo(n[0])||!Eo(n[1])?"":Yo({...e,value:n},t)}case"in":case"notin":{const t=lr(e.value);if(0===t.length)return"";const n=t.map(e=>u?p(Qr(r?? +/* v8 ignore start -- @preserve */[],e)?.label??e):Oo(e,o)?`${ir(e)}`:`${f(e)}`);if(1===n.length)return n[0];const a=y&&n.length>2?",":"";return`(${n.slice(0,-1).join(b)}${a} ${m} ${n.at(-1)})`}}return"boolean"==typeof e.value?e.value?g:v:u?p(Qr(r?? +/* v8 ignore start -- @preserve */[],e.value)?.label??e.value):Oo(e.value,o)?`${ir(e.value)}`:`${f(e.value)}`},La={"=":["is","is the same as the value in"],"!=":["is not","is not the same as the value in"],"<":["is less than","is less than the value in"],">":["is greater than","is greater than the value in"],"<=":["is less than or equal to","is less than or equal to the value in"],">=":["is greater than or equal to","is greater than or equal to the value in"],contains:["contains","contains the value in"],beginswith:["starts with","starts with the value in"],endswith:["ends with","ends with the value in"],doesnotcontain:["does not contain","does not contain the value in"],doesnotbeginwith:["does not start with","does not start with the value in"],doesnotendwith:["does not end with","does not end with the value in"],null:"is null",notnull:"is not null",in:["is one of the values","is the same as a value in"],notin:["is not one of the values","is not the same as any value in"],between:["is between","is between the values in"],notbetween:["is not between","is not between the values in"]},Ga=()=>[],Fa=(e,t={})=>{const{field:n,operator:r,valueSource:o="value"}=e,{getOperators:a=Ga,operatorMap:i=La}=t,l=new Map(Object.entries(La));for(const[e,t]of Object.entries(i))l.set(cr(e),t);const s=Object.fromEntries(l),u=cr(r);var c=r;const d=1===lr(e.value).length;"in"===u&&d?c="=":"notin"===u&&d&&(c="!=");const{value:f,label:p}=Qr(qr(a(n,{fieldData:t.fieldData??{name:n,value:n,label:n}})??/* v8 ignore start -- @preserve */[]),c)??{name:c,value:c,label:c},h=s[f]??s[cr(f)]??[p,p];return"string"==typeof h?h:h["field"===o?1:0]},za=e=>cr((e=>{switch(cr(e)){case"null":return"is null";case"notnull":return"is not null";case"notin":return"not in";case"notbetween":return"not between";case"contains":case"beginswith":case"endswith":return"like";case"doesnotcontain":case"doesnotbeginwith":case"doesnotendwith":return"not like";default:return e}})(e.operator)),ja=(e,t={})=>{const{quoteFieldNamesWith:n=["",""],fieldIdentifierSeparator:r="",quoteValuesWith:o="'",operatorProcessor:a=za,valueProcessor:i=Yo,concatOperator:l="||"}=t,s=e=>Co(e,{quoteFieldNamesWith:n,fieldIdentifierSeparator:r}),u=s(e.field),c=xo(e);if(!1===c)return;if(c){if("postgresql"!==t?.preset)return"";const{mode:n,threshold:r}=c,o=t.subqueryDepth??0,a=Po(o),i=la(ho(e.value,{ruleProcessor:e=>({...e,field:a})}),{...t,subqueryDepth:o+1});switch(n){case"all":return`(select count(*) from unnest(${u}) as ${s(a)} where ${i}) = array_length(${u}, 1)`;case"none":return`not exists (select 1 from unnest(${u}) as ${s(a)} where ${i})`;case"some":return`exists (select 1 from unnest(${u}) as ${s(a)} where ${i})`;case"atleast":case"atmost":case"exactly":{const e="atleast"===n?">=":"atmost"===n?"<=":"=";return`(select count(*)${r>0&&r<1?`::float / array_length(${u}, 1)`:""} from unnest(${u}) as ${s(a)} where ${i}) ${e} ${r}`}}}const d=i(e,{...t,quoteFieldNamesWith:n,fieldIdentifierSeparator:r,quoteValuesWith:o,concatOperator:l}),f=a(e,t),p=cr(f);return"in"!==p&&"not in"!==p&&"between"!==p&&"not between"!==p||d?`${u} ${f} ${d}`.trim():""},qa=(e,t,n)=>{const{fieldData:r,format:o,getNextNamedParam:a,parseNumbers:i,paramPrefix:l,paramsKeepPrefix:s,numberedParams:u,quoteFieldNamesWith:c=["",""],fieldIdentifierSeparator:d,concatOperator:f,operatorProcessor:p=za,valueProcessor:h=Yo}=t??{},{processedParams:m=[]}=n??{},g="parameterized"===o,v=[],b={},y=e=>g?{sql:e,params:v}:{sql:e,params:b},w=e=>Co(e,{quoteFieldNamesWith:c,fieldIdentifierSeparator:d}),S=w(e.field),k=xo(e); +/* v8 ignore start -- @preserve */ +if(!1===k)return; +/* v8 ignore stop -- @preserve */if(k){if("postgresql"!==t?.preset)return y("");const{mode:n,threshold:r}=k,o=t.subqueryDepth??0,a=Po(o),{sql:i,params:l}=aa(ho(e.value,{ruleProcessor:e=>({...e,field:a})}),{...t,fields:[],subqueryDepth:o+1});switch(Array.isArray(l)?v.push(...l):Object.assign(b,l),n){case"all":return y(`(select count(*) from unnest(${S}) as ${w(a)} where ${i}) = array_length(${S}, 1)`);case"none":return y(`not exists (select 1 from unnest(${S}) as ${w(a)} where ${i})`);case"some":return y(`exists (select 1 from unnest(${S}) as ${w(a)} where ${i})`);case"atleast":case"atmost":case"exactly":{const e="atleast"===n?">=":"atmost"===n?"<=":"=";return y(`(select count(*)${r>0&&r<1?`::float / array_length(${S}, 1)`:""} from unnest(${S}) as ${w(a)} where ${i}) ${e} ${r}`)}}}const E=h(e,{parseNumbers:i,quoteFieldNamesWith:c,concatOperator:f,fieldData:r,format:o}),O=p(e,t),N=cr(O),[C,D]=c;if(!("in"!==N&&"not in"!==N&&"between"!==N&&"not between"!==N||E))return y("");if("is null"===N||"is not null"===N)return y(`${C}${e.field}${D} ${O}`);if("parameter"===e.valueSource){const t=e=>{const t=Ao(e,l);return g||(b[s?t:To(e,l)]=null),t};if("between"===N||"not between"===N){const[n,r]=lr(e.value,{retainEmptyStrings:!0}).slice(0,2);return y(`${C}${e.field}${D} ${O} ${t(n)} and ${t(r)}`.trim())}if("in"===N||"not in"===N){const n=lr(e.value);return y(`${C}${e.field}${D} ${O} (${n.map(e=>t(e)).join(", ")})`)}return y(`${C}${e.field}${D} ${O} ${bo(t(e.value),cr(e.operator),{concatOperator:f})}`.trim())}if("field"===e.valueSource)return y(`${C}${e.field}${D} ${O} ${E}`.trim());if("in"===N||"not in"===N){const t=lr(e.value);if(g){for(const e of t)v.push(Oo(e,i)?uo(e,{parseNumbers:i}):e);return y(`${C}${e.field}${D} ${O} (${t.map((e,n)=>u?`${l}${m.length+1+t.length-(t.length-n)}`:"?").join(", ")})`)}const n=[];for(const r of t){const t=a(e.field);n.push(`${l}${t}`),b[`${s?l:""}${t}`]=Oo(r,i)?uo(r,{parseNumbers:i}):r}return y(`${C}${e.field}${D} ${O} (${n.join(", ")})`)}if("between"===N||"not between"===N){const[t,n]=lr(e.value,{retainEmptyStrings:!0}).slice(0,2).map(e=>Oo(e,i)?uo(e,{parseNumbers:i}):e);if(g)return v.push(t,n),y(`${C}${e.field}${D} ${O} ${u?`${l}${m.length+1}`:"?"} and ${u?`${l}${m.length+2}`:"?"}`);const r=a(e.field),o=a(e.field);return b[`${s?l:""}${r}`]=t,b[`${s?l:""}${o}`]=n,y(`${C}${e.field}${D} ${O} ${l}${r} and ${l}${o}`)}let A=e.value;"string"==typeof e.value&&(A=Oo(e.value,i)?uo(e.value,{parseNumbers:i}):/^'.*'$/g.test(E)?E.replaceAll(/(^'|'$)/g,""):/* v8 ignore next -- @preserve */E);let T="";return g?v.push(A):(T=a(e.field),b[`${s?l:""}${T}`]=A),y(`${C}${e.field}${D} ${O} ${g?u?`${l}${m.length+1}`:"?":`${l}${T}`}`.trim())},Ba=(e,t,n)=>Oo(e,!!n||"bigint"==typeof e)?Number(uo(e,{parseNumbers:!!n})):t,Va=e=>/^[?<]/.test(e)||e.includes(":")?e:`?${e}`,Ha={ansi:{},sqlite:{paramsKeepPrefix:!0},oracle:{},mssql:{concatOperator:"+",quoteFieldNamesWith:["[","]"],fieldIdentifierSeparator:".",paramPrefix:"@"},mysql:{concatOperator:"CONCAT"},postgresql:{quoteFieldNamesWith:'"',numberedParams:!0,paramPrefix:"$"}},Ua={cel:zo,drizzle:(e,t)=>{const n=t??/* v8 ignore start -- @preserve */{},{parseNumbers:r,preserveValueOrder:o,context:a={}}=n,{columns:i,drizzleOperators:l,useRawFields:s}=a;if(!i||!l)return;const{between:u,eq:c,gt:d,gte:f,inArray:p,isNotNull:h,isNull:m,like:g,lt:v,lte:b,ne:y,notBetween:w,notInArray:S,notLike:k,sql:E}=l,{field:O,operator:N,value:C,valueSource:D}=e,A=s&&/^[a-z_][a-z0-9_.]*$/i.test(O)?E.raw(O):i[O],T=cr(N),$="field"===D,M=e=>$?i[e]:e,R=e=>$||!r?M(e):Oo(e,!0)?uo(e,{parseNumbers:!0}):e;if(!A)return;const x=xo(e);if(!1!==x){if(x){if("postgresql"!==n.preset)return;const{mode:t,threshold:r}=x,o=n.subqueryDepth??0,a=Po(o),i=ho(e.value,{ruleProcessor:e=>({...e,field:a})}),s=Zo(i,{...n,context:{...n.context,useRawFields:!0},subqueryDepth:o+1});switch(t){case"all":return E`(select count(*) from unnest(${A}) as ${E.raw(a)} where ${s({},l)}) = array_length(${A}, 1)`;case"none":return E`not exists (select 1 from unnest(${A}) as ${E.raw(a)} where ${s({},l)})`;case"some":return E`exists (select 1 from unnest(${A}) as ${E.raw(a)} where ${s({},l)})`;case"atleast":case"atmost":case"exactly":{const e="atleast"===t?">=":"atmost"===t?"<=":"=";return r>0&&r<1?E`(select count(*)::float / array_length(${A}, 1) from unnest(${A}) as ${E.raw(a)} where ${s({},l)}) ${E.raw(`${e} ${r}`)}`:E`(select count(*) from unnest(${A}) as ${E.raw(a)} where ${s({},l)}) ${E.raw(`${e} ${r}`)}`}}}switch(T){case"=":return c(A,R(C));case"!=":return y(A,R(C));case">":return d(A,R(C));case"<":return v(A,R(C));case">=":return f(A,R(C));case"<=":return b(A,R(C));case"beginswith":case"doesnotbeginwith":return("doesnotbeginwith"===T?k:g)(A,$?E`${M(C)} || '%'`:`${C}%`);case"contains":case"doesnotcontain":return("doesnotcontain"===T?k:g)(A,$?E`'%' || ${M(C)} || '%'`:`%${C}%`);case"endswith":case"doesnotendwith":return("doesnotendwith"===T?k:g)(A,$?E`'%' || ${M(C)}`:`%${C}`);case"null":return m(A);case"notnull":return h(A);case"in":case"notin":{const e=lr(C).map(e=>R(e));return"notin"===T?S(A,e):p(A,e)}case"between":case"notbetween":{const e=lr(C);if(e.length>=2&&Eo(e[0])&&Eo(e[1])){let[t,n]=e;const a=!(!1===r);if(!$&&Oo(t,a)&&Oo(n,a)){const e=uo(t,{parseNumbers:a}),r=uo(n,{parseNumbers:a});if(!o&&r{const{field:n,operator:r,value:o,valueSource:a}=e,{parseNumbers:i,preserveValueOrder:l}=t,s=cr(r),u=xo(e);if(!1===u)return!1;if(u){const{mode:r}=u,o=ea(ho(e.value,{ruleProcessor:e=>({...e,field:e.field?`${n}.${e.field}`:n})}),t);if(0===Object.keys(o).length)return!1;switch(r){case"some":return{nested:{path:n,query:o}};case"none":return{bool:{must_not:{nested:{path:n,query:o}}}};case"all":case"atleast":case"atmost":case"exactly":return!1}}if("field"===a){if(lr(o).some(e=>"string"!=typeof e))return!1;const e=Ea(n);switch(s){case"=":case"!=":case">":case">=":case"<":case"<=":{const t="="===s?"==":s,n=Ea(o);return!!n&&{bool:{filter:{script:{script:`doc['${e}'].value ${t} doc['${n}'].value`}}}}}case"in":case"notin":{const t=lr(o);if(t.length>0){const n=t.map(t=>({bool:{filter:{script:{script:`doc['${e}'].value == doc['${t}'].value`}}}}));return{bool:"in"===s?{should:n}:{must_not:n}}}return!1}case"between":case"notbetween":{const t=lr(o);if(t.length>=2&&t[0]&&t[1]){const n=`doc['${e}'].value >= doc['${t[0]}'].value && doc['${e}'].value <= doc['${t[1]}'].value`;return{bool:{filter:{script:{script:"notbetween"===s?`!(${n})`:n}}}}}return!1}case"contains":case"doesnotcontain":case"beginswith":case"doesnotbeginwith":case"endswith":case"doesnotendwith":{const t=Ea(o);return!!t&&{bool:{filter:{script:{script:Na(e,s,t)}}}}}}}switch(s){case"<":case"<=":case">":case">=":return{range:{[n]:{[Sa[s]]:Ca(o,i)}}};case"=":return{term:{[n]:Ca(o,i)}};case"!=":return{bool:{must_not:{term:{[n]:Ca(o,i)}}}};case"null":return{bool:{must_not:{exists:{field:n}}}};case"notnull":return{exists:{field:n}};case"in":case"notin":{const e=lr(o).map(e=>Ca(e,i));if(e.length>0){const t=e.map(e=>({term:{[n]:Ca(e,i)}}));return{bool:"in"===s?{should:t}:{must_not:t}}}return!1}case"between":case"notbetween":{const e=lr(o);if(e.length>=2&&Eo(e[0])&&Eo(e[1])){let[t,r]=e;const o=!(!1===i);if(Oo(t,o)&&Oo(r,o)){const e=uo(t,{parseNumbers:o}),n=uo(r,{parseNumbers:o});if(!l&&n{const{field:n,operator:r,value:o,valueSource:a}=e,{escapeQuotes:i,parseNumbers:l,preserveValueOrder:s,quoteFieldNamesWith:u=["",""],fieldIdentifierSeparator:c=""}=t,d="field"===a,f="number"==typeof o||"boolean"==typeof o||"bigint"==typeof o||Oo(o,l),p=e=>Co(e,{quoteFieldNamesWith:u,fieldIdentifierSeparator:c}),h=xo(e);if(!1===h)return;if(h){const{mode:e,threshold:r}=h,a=`$count(${p(n)})`,i=`$count($filter(${p(n)}, function($v) {${ta(ho(o,{ruleProcessor:e=>({...e,field:e.field?`$v.${e.field}`:"$v"})}),t)}}))`;switch(e){case"all":return`${i} = ${a}`;case"none":return`${i} = 0`;case"some":return`${i} > 0`;case"atleast":case"atmost":case"exactly":{const t="atleast"===e?">=":"atmost"===e?"<=":"=";return r>0&&r<1?`${i} ${t} (${a} * ${r})`:`${i} ${t} ${r}`}}}const m=cr(r);switch(m){case"<":case"<=":case"=":case"!=":case">":case">=":return`${p(n)} ${m} ${d?p(ir(o)):f?ir(o):Aa(o,i)}`;case"contains":case"doesnotcontain":return Ta(`$contains(${p(n)}, ${d?p(ir(o)):Aa(o,i)})`,Da(m));case"beginswith":case"doesnotbeginwith":return Ta(d?`$substring(${p(n)}, 0, $length(${p(ir(o))})) = ${p(ir(o))}`:`$contains(${p(n)}, /^${$a(o)}/)`,Da(m));case"endswith":case"doesnotendwith":return Ta(d?`$substring(${p(n)}, $length(${p(n)}) - $length(${p(ir(o))})) = ${p(ir(o))}`:`$contains(${p(n)}, /${$a(o)}$/)`,Da(m));case"null":return`${p(n)} = null`;case"notnull":return`${p(n)} != null`;case"in":case"notin":{const e=lr(o);return Ta(`${p(n)} in [${e.map(e=>d?`${p(ir(e))}`:Oo(e,l)?`${ir(e)}`:Aa(e,i)).join(", ")}]`,Da(m))}case"between":case"notbetween":{const e=lr(o);if(e.length<2||pr(e[0])||pr(e[1]))return"";const[t,r]=e,a=!(!1===l),u=Oo(t,a)?uo(t,{parseNumbers:a}):NaN,c=Oo(r,a)?uo(r,{parseNumbers:a}):NaN;let f=Number.isNaN(u)?d?`${t}`:t:u,h=Number.isNaN(c)?d?`${r}`:r:c;if(!s&&f===u&&h===c&&cd?p(e):g?t:Aa(t,i),b=`${p(n)} >= ${v(t,f)} and ${p(n)} <= ${v(r,h)}`;return"between"===m?`(${b})`:Ta(b,!0)}}return""},jsonlogic:xa,ldap:(e,t={})=>{const{field:n,operator:r,value:o,valueSource:a}=e,{preserveValueOrder:i}=t,l=cr(r);if("field"===a||pr(o)&&"null"!==l&&"notnull"!==l||xo(e))return"";switch(l){case"=":case"!=":return Ia(`(${n}=${Pa(o)})`,"!="===l);case">":case">=":return`(${n}>=${Pa(o)})`;case"<":case"<=":return`(${n}<=${Pa(o)})`;case"contains":case"doesnotcontain":return Ia(`(${n}=*${Pa(o)}*)`,"doesnotcontain"===l);case"beginswith":case"doesnotbeginwith":return Ia(`(${n}=${Pa(o)}*)`,"doesnotbeginwith"===l);case"endswith":case"doesnotendwith":return Ia(`(${n}=*${Pa(o)})`,"doesnotendwith"===l);case"null":case"notnull":return Ia(`(${n}=*)`,"notnull"===l);case"in":case"notin":{const e=lr(o);return Ia(`(|${e.map(e=>`(${n}=${Pa(e)})`).join("")})`,"notin"===l)}case"between":case"notbetween":{const e=lr(o);if(e.length<2||pr(e[0])||pr(e[1]))return"";const[t,r]=e,a=Oo(t,!0)?uo(t,{parseNumbers:!0}):NaN,s=Oo(r,!0)?uo(r,{parseNumbers:!0}):NaN;let u=Number.isNaN(a)?t:a,c=Number.isNaN(s)?r:s;if(!i&&u===a&&c===s&&s=${Pa(u)})(${n}<=${Pa(c)}))`,"notbetween"===l)}}return""},mongodb_query:Vo,mongodb:Ho,natural_language:(e,t)=>{const{field:n,operator:r}=e,{fieldData:o,quoteFieldNamesWith:a=["",""],fieldIdentifierSeparator:i="",quoteValuesWith:l="'",operatorProcessor:s=Fa,valueProcessor:u=_a,concatOperator:c="||",wordOrder:d="SVO"}=t??/* v8 ignore start -- @preserve */{},f=Co(o?.label??n,{quoteFieldNamesWith:a,fieldIdentifierSeparator:i}),p=xo(e);if(!1===p)return"";if(p){const{mode:n,threshold:r}=p,a=oa(e.value,{...t,fields:qr(o?.subproperties??[])}),i=(o?.subproperties?.length??0)>0;switch(n){case"all":return`(${i?"for ":""}every item in ${f}${i?",":""} ${a})`;case"none":return`(${i?"for ":""}no item in ${f}${i?",":""} ${a})`;case"some":return`(${i?"for ":""}at least one item in ${f}${i?",":""} ${a})`;case"atleast":case"atmost":case"exactly":{const e=n.replace("at","at ");return r>0&&r<1?`(${i?"for ":""}${e} ${100*r}% of the items in ${f}${i?",":""} ${a})`:`(${i?"for ":""}${e} ${r} of the items in ${f}${i?",":""} ${a})`}}}const h=u(e,{...t,quoteFieldNamesWith:a,fieldIdentifierSeparator:i,quoteValuesWith:l,concatOperator:c}),m=cr(r);if(("in"===m||"notin"===m||"between"===m||"notbetween"===m)&&!h)return"";const g={S:f,V:s(e,t),O:h},v=t?.translations??{},b={S:v.afterSubject??" ",V:v.afterVerb??" ",O:v.afterObject??" "},y=(e=>{const t=[],n=new Set(Do);for(const r of e.toUpperCase())if(n.has(r)&&(t.push(r),n.delete(r),0===n.size))break;for(const e of Do)n.has(e)&&t.push(e);return t})(d).filter(e=>""!==g[e]);return y.map((e,t)=>`${g[e]}${t{const{field:n,operator:r,value:o,valueSource:a}=e,{parseNumbers:i,preserveValueOrder:l}=t;if("field"===a||xo(e))return;const s=cr(r);switch(s){case"=":return{[n]:Ba(o,o,i)};case"!=":case"<":case"<=":case">":case">=":{const e=wo[s];return{[n]:{[e]:Ba(o,o,i)}}}case"contains":return{[n]:{contains:o}};case"beginswith":return{[n]:{startsWith:o}};case"endswith":return{[n]:{endsWith:o}};case"doesnotcontain":return{NOT:{[n]:{contains:o}}};case"doesnotbeginwith":return{NOT:{[n]:{startsWith:o}}};case"doesnotendwith":return{NOT:{[n]:{endsWith:o}}};case"null":return{[n]:null};case"notnull":return{[n]:{not:null}};case"in":case"notin":{const e=lr(o);return{[n]:{[wo[s]]:e.map(e=>Ba(e,e,i))}}}case"between":case"notbetween":{const e=lr(o);if(e.length>=2&&Eo(e[0])&&Eo(e[1])){const[t,r]=e,o=!(!1===i),a=Oo(t,o)?uo(t,{parseNumbers:i}):NaN,u=Oo(r,o)?uo(r,{parseNumbers:i}):NaN;let c=Number.isNaN(a)?t:a,d=Number.isNaN(u)?r:u;if(!l&&c===a&&d===u&&u{const{sequelizeOperators:o,sequelizeCol:a,sequelizeFn:i}=r;if(xo(e))return;const{field:l,operator:s,value:u,valueSource:c}=e,d="field"===c,f=cr(s);if(o&&(!d||a&&(i||!["contains","doesnotcontain","beginswith","doesnotbeginwith","endswith","doesnotendwith"].includes(f))))switch(f){case"=":case"!=":case"<":case"<=":case">":case">=":{const e={"=":o.eq,"!=":o.ne,"<":o.lt,"<=":o.lte,">":o.gt,">=":o.gte}[f];return{[l]:d&&"="===f?{[o.col]:u}:{[e]:d?a(u):Oo(u,t)?uo(u,{parseNumbers:"strict"}):u}}}case"contains":return{[l]:{[d?o.like:o.substring]:d?i("CONCAT","%",a(u),"%"):`${u}`}};case"beginswith":return{[l]:{[d?o.like:o.startsWith]:d?i("CONCAT",a(u),"%"):`${u}`}};case"endswith":return{[l]:{[d?o.like:o.endsWith]:d?i("CONCAT","%",a(u)):`${u}`}};case"doesnotcontain":return{[l]:{[o.notLike]:d?i("CONCAT","%",a(u),"%"):`%${u}%`}};case"doesnotbeginwith":return{[l]:{[o.notLike]:d?i("CONCAT",a(u),"%"):`${u}%`}};case"doesnotendwith":return{[l]:{[o.notLike]:d?i("CONCAT","%",a(u)):`%${u}`}};case"null":return{[l]:{[o.is]:null}};case"notnull":return{[l]:{[o.not]:null}};case"in":case"notin":{const e=lr(u);return{[l]:{["in"===f?o.in:o.notIn]:e.map(e=>d?a(e):Oo(e,t)?uo(e,{parseNumbers:"strict"}):e)}}}case"between":case"notbetween":{const e=lr(u,{retainEmptyStrings:!0});if(e.length<2||!Eo(e[0])||!Eo(e[1]))return;const[r,i]=e,s=Oo(r,t)?uo(r,{parseNumbers:"strict"}):NaN,c=Oo(i,t)?uo(i,{parseNumbers:"strict"}):NaN,p=Number.isNaN(s)?r:s,h=Number.isNaN(c)?i:c,m=[p,h];return!n&&p===s&&h===c&&ca(e)):m.every(e=>Oo(e,t))?m.map(e=>uo(e,{parseNumbers:"strict"})):m}}}}},spel:Xo,sql:ja,tanstack_db:(e,t)=>{const{parseNumbers:n,preserveValueOrder:r,context:o={}}=t??/* v8 ignore start -- @preserve */{},a=o.tanStackDbOperators,i=o._tanstackDbRefs,l=o._tanstackDbPrimaryRef;if(!a||!i||!l)return;const{and:s,eq:u,gt:c,gte:d,inArray:f,isNull:p,like:h,lt:m,lte:g,not:v}=a,b=e=>{const t=e.indexOf(".");if(t>0){const n=e.slice(0,t),r=e.slice(t+1);if(i[n])return i[n][r]}return i[l][e]},{field:y,operator:w,value:S,valueSource:k}=e,E=b(y),O=cr(w),N="field"===k,C=e=>N?b(e):e,D=e=>N||!n?C(e):Oo(e,!0)?uo(e,{parseNumbers:!0}):e;switch(O){case"=":return u(E,D(S));case"!=":return v(u(E,D(S)));case">":return c(E,D(S));case"<":return m(E,D(S));case">=":return d(E,D(S));case"<=":return g(E,D(S));case"beginswith":case"doesnotbeginwith":{const e=h(E,N?void 0:`${S}%`);return"doesnotbeginwith"===O?v(e):e}case"contains":case"doesnotcontain":{const e=h(E,N?void 0:`%${S}%`);return"doesnotcontain"===O?v(e):e}case"endswith":case"doesnotendwith":{const e=h(E,N?void 0:`%${S}`);return"doesnotendwith"===O?v(e):e}case"null":return p(E);case"notnull":return v(p(E));case"in":case"notin":{const e=f(E,lr(S).map(e=>D(e)));return"notin"===O?v(e):e}case"between":case"notbetween":{const e=lr(S);if(e.length>=2&&Eo(e[0])&&Eo(e[1])){let[t,o]=e;const a=!(!1===n);if(!N&&Oo(t,a)&&Oo(o,a)){const e=uo(t,{parseNumbers:a}),n=uo(o,{parseNumbers:a});if(!r&&n{const{escapeQuotes:n,parseNumbers:r}=t,{field:o,operator:a,value:i,valueSource:l}=e,s="field"===l,u=a.toLowerCase(),c=Va(o),d=e=>{if(null==e)return'""';if("boolean"==typeof e)return`"${e}"^^xsd:boolean`;if("bigint"==typeof e)return String(e);if(s)return Va(ir(e));if("number"==typeof e||Oo(e,r))return ir(e); +/* v8 ignore start -- @preserve */const t="string"==typeof e?e:JSON.stringify(e)??""; +/* v8 ignore stop -- @preserve */return t.startsWith("?")||t.startsWith("<")||t.includes(":")?t:`"${((e,t)=>"string"==typeof e&&t?e.replaceAll('"','\\"'):`${e}`)(t,n)}"`};switch(u){case"=":case"!=":case"<":case">":case"<=":case">=":return`${c} ${u} ${d(i)}`;case"<>":return`${c} != ${d(i)}`;case"contains":return`CONTAINS(${c}, ${d(i)})`;case"doesnotcontain":return`!CONTAINS(${c}, ${d(i)})`;case"beginswith":return`STRSTARTS(${c}, ${d(i)})`;case"doesnotbeginwith":return`!STRSTARTS(${c}, ${d(i)})`;case"endswith":return`STRENDS(${c}, ${d(i)})`;case"doesnotendwith":return`!STRENDS(${c}, ${d(i)})`;case"null":return`!BOUND(${c})`;case"notnull":return`BOUND(${c})`;case"in":{const e=lr(i).map(d);return e.length?`${c} IN (${e.join(", ")})`:""}case"notin":{const e=lr(i).map(d);return e.length?`${c} NOT IN (${e.join(", ")})`:""}case"between":{const e=lr(i);return e.length<2?"":`${c} >= ${d(e[0])} && ${c} <= ${d(e[1])}`}case"notbetween":{const e=lr(i);return e.length<2?"":`(${c} < ${d(e[0])} || ${c} > ${d(e[1])})`}default:return`${c} ${a} ${d(i)}`}},gremlin:(e,t={})=>{const{escapeQuotes:n,parseNumbers:r}=t,{field:o,operator:a,value:i,valueSource:l}=e,s="field"===l,u=a.toLowerCase(),c=o.includes(".")?o.split(".").pop():o,d=e=>null==e?"null":"boolean"==typeof e||"bigint"==typeof e?String(e):s||"number"==typeof e||Oo(e,r)?ir(e):`'${((e,t)=>"string"==typeof e&&t?e.replaceAll("'","\\'"):`${e}`)(e,n)}'`;switch(u){case"=":default:return`.has('${c}', ${d(i)})`;case"!=":case"<>":return`.has('${c}', neq(${d(i)}))`;case"<":return`.has('${c}', lt(${d(i)}))`;case">":return`.has('${c}', gt(${d(i)}))`;case"<=":return`.has('${c}', lte(${d(i)}))`;case">=":return`.has('${c}', gte(${d(i)}))`;case"contains":return`.has('${c}', containing(${d(i)}))`;case"doesnotcontain":return`.has('${c}', notContaining(${d(i)}))`;case"beginswith":return`.has('${c}', startingWith(${d(i)}))`;case"doesnotbeginwith":return`.has('${c}', notStartingWith(${d(i)}))`;case"endswith":return`.has('${c}', endingWith(${d(i)}))`;case"doesnotendwith":return`.has('${c}', notEndingWith(${d(i)}))`;case"null":return`.hasNot('${c}')`;case"notnull":return`.has('${c}')`;case"in":{const e=lr(i).map(d);return e.length?`.has('${c}', within(${e.join(", ")}))`:""}case"notin":{const e=lr(i).map(d);return e.length?`.has('${c}', without(${e.join(", ")}))`:""}case"between":{const e=lr(i);return e.length<2?"":`.has('${c}', between(${d(e[0])}, ${d(e[1])}))`}case"notbetween":{const e=lr(i);return e.length<2?"":`.has('${c}', outside(${d(e[0])}, ${d(e[1])}))`}}},diagnostics:ja},Wa=e=>e.operator,Qa={cel:Wa,drizzle:Wa,elasticsearch:Wa,json_without_ids:Wa,json:Wa,jsonata:Wa,jsonlogic:Wa,ldap:Wa,mongodb_query:Wa,mongodb:Wa,natural_language:Fa,parameterized_named:za,parameterized:za,prisma:Wa,sequelize:Wa,spel:Wa,sql:za,tanstack_db:Wa,cypher:Wa,gql:Wa,sparql:Wa,gremlin:Wa,diagnostics:Wa},Ka={cel:"1 == 1",ldap:"",mongodb:'"$and":[{"$expr":true}]',natural_language:"1 is 1",sparql:"1 = 1",gremlin:"",spel:"1 == 1",sql:"(1 = 1)"},Xa={format:"json",fields:[],quoteFieldNamesWith:["",""],fieldIdentifierSeparator:"",getOperators:()=>[],paramPrefix:":",paramsKeepPrefix:!1,numberedParams:!1,preserveValueOrder:!1,placeholderFieldName:"~",placeholderOperatorName:"~",quoteValuesWith:"'",concatOperator:"||",preset:"ansi",wordOrder:"SVO",translations:{},operatorMap:{}},Ya=new Set(["cel","drizzle","elasticsearch","jsonata","jsonlogic","ldap","mongodb_query","mongodb","prisma","sequelize","spel","tanstack_db"]),Ja=new Set(["sql","parameterized","parameterized_named","drizzle","prisma","sequelize"]);function Za(e,t={}){const n="string"==typeof t?{format:cr(t)}:t,r={...Xa,...!n.format||Ja.has(n.format)?Ha[n.preset??"ansi"]??null:null,...n,...!n.format&&(Object.keys(Ha).includes(n.preset??"")?{format:"sql"}:null)},o=cr(r.format),{fallbackExpression:a,getOperators:i,operatorProcessor:l,parseNumbers:s,quoteFieldNamesWith:u,ruleGroupProcessor:c,ruleProcessor:d,validator:f,valueProcessor:p,context:h}=r,m="function"==typeof l?l:Qa[o]??Wa,g="function"==typeof p?(e=>e.length>=3)(p)?e=>p(e.field,e.operator,e.value,e.valueSource):p:"natural_language"===o?_a:Ya.has(o)?d??Ua[o]:Yo,v=("function"==typeof d?d:null)??(Ya.has(o)&&"function"!=typeof d&&p?g:null)??Ua[o]??ja,b=No(u),y=qr(r.fields),w=a??Ka[o]??Ka.sql;let S={};if("function"==typeof f){const t=f(e);if("boolean"==typeof t){if(!t&&"diagnostics"!==o)return"parameterized"===o?{sql:w,params:[]}:"parameterized_named"===o?{sql:w,params:{}}:"mongodb"===o?`{${w}}`:"mongodb_query"===o?jo:"prisma"===o?ia:"jsonlogic"!==o&&("elasticsearch"===o?{}:"drizzle"===o||"sequelize"===o||"tanstack_db"===o?void 0:w)}else S=t}const k={},E=Xr(y);for(const e of E)"function"==typeof e.validator&&(k[e.value??/* v8 ignore start -- @preserve */e.name]=e.validator);const O={...r,fallbackExpression:w,fields:y,format:o,getOperators:(e,t)=>qr(i(e,t)?? +/* v8 ignore start -- @preserve */[]),getParseNumberBoolean:e=>"string"==typeof vo({parseNumbers:s,inputType:e})||("boolean"==typeof s?s:void 0),quoteFieldNamesWith:b,operatorProcessor:m,ruleProcessor:v,valueProcessor:g,validateRule:e=>{let t,n;if(e.id&&(t=S[e.id]),E.length>0){const t=E.filter(t=>t.name===e.field);if(t.length>0){const e=t[0];"function"==typeof e.validator&&(n=e.validator)}}if("parameter"===e.valueSource&&"function"==typeof r.getParameters){const n=Qr(y,e.field);new Set(Xr(qr(r.getParameters(e.field,e.operator,{fieldData:n}))).map(e=>To(e.value,r.paramPrefix))).has(To(e.value,r.paramPrefix))||(t=!1)}return[t,n]},validationMap:S,context:h};if("function"==typeof c)return c(e,O);switch(o){case"json":case"json_without_ids":{const t=s?ko(e,O):e;return"json_without_ids"===o?JSON.stringify(t,(e,t)=>"id"===e||"path"===e?void 0:_o(0,t)):JSON.stringify(t,_o,2)}case"sql":return la(e,O);case"parameterized":case"parameterized_named":return aa(e,O);case"mongodb":return((e,t,n)=>{const{fields:r,fallbackExpression:o,getParseNumberBoolean:a,placeholderFieldName:i,placeholderOperatorName:l,placeholderValueName:s,ruleProcessor:u,validateRule:c,validationMap:d}=t,f=(e,p)=>{if(!go(e,d[e.id??/* v8 ignore start -- @preserve */""]))return p?o:"";const h=`"$${cr(e.combinator)}"`;let m=!1;const g=e.rules.map(e=>{if(hr(e)){const t=f(e);return t?(m=!0,ra(t)?t:`{${t}}`):""}const[o,d]=c(e);if(!go(e,o,d)||e.field===i||e.operator===l||void 0!==s&&e.value===s)return"";const p=Qr(r,e.field);return u(e,{...t,parseNumbers:a(p?.inputType),fieldData:p},n)}).filter(Boolean),v=g.length>0?1!==g.length||m?`${h}:[${g.join(",")}]`:g[0]:o;return e.not?`"$nor":[${ra(v)?v:`{${v}}`}]`:v},p=f(Or(e),!0);return ra(p)?p:`{${p}}`})(e,O);case"mongodb_query":return qo(e,O);case"cel":return Lo(e,O);case"spel":return Uo(e,O);case"jsonata":return ta(e,O);case"jsonlogic":return na(e,O);case"elasticsearch":return ea(e,O);case"natural_language":return oa(e,O);case"ldap":return((e,t)=>{const{fields:n,fallbackExpression:r,getParseNumberBoolean:o,placeholderFieldName:a,placeholderOperatorName:i,placeholderValueName:l,ruleProcessor:s,validateRule:u,validationMap:c}=t,d=(e,f)=>{if(!go(e,c[e.id??/* v8 ignore start -- @preserve */""]))return f?r:"";const p=e.rules.map(e=>{if(hr(e))return d(e);const[r,c]=u(e);if(!go(e,r,c)||e.field===a||e.operator===i||void 0!==l&&e.value===l)return"";const f=Qr(n,e.field);return s(e,{...t,parseNumbers:o(f?.inputType),escapeQuotes:"value"===(e.valueSource??"value"),fieldData:f})}).filter(Boolean),h=p.join(""),[m,g]=e.not?["(!",")"]:["",""],[v,b]=p.length>1?[`${m}(${"or"===e.combinator?"|":"&"}`,`)${g}`]:[m,g];return h?`${v}${h}${b}`:r};return d(Or(e),!0)})(e,O);case"prisma":return((e,t)=>{const{fields:n,getParseNumberBoolean:r,placeholderFieldName:o,placeholderOperatorName:a,placeholderValueName:i,ruleProcessor:l,validateRule:s,validationMap:u}=t,c=(e,d)=>{if(!go(e,u[e.id??/* v8 ignore start -- @preserve */""]))return d?ia:void 0;const f=e.combinator.toUpperCase();let p=!1;const h=e.rules.map(e=>{if(hr(e)){const t=c(e);return t?(p=!0,t):void 0}const[u,d]=s(e);if(!go(e,u,d)||e.field===o||e.operator===a||void 0!==i&&e.value===i)return;const f=Qr(n,e.field);return l(e,{...t,parseNumbers:r(f?.inputType),fieldData:f})}).filter(Boolean);return h.length>0?1!==h.length||p?{[f]:h}:h[0]:ia},d=c(Or(e),!0);return e.not?{NOT:d}:d})(e,O);case"drizzle":return Zo(e,O);case"tanstack_db":return((e,t)=>n=>{const{fields:r,getParseNumberBoolean:o,placeholderFieldName:a,placeholderOperatorName:i,placeholderValueName:l,validateRule:s,validationMap:u,context:c={},ruleProcessor:d}=t,f=c.tanStackDbOperators;if(!f)return;const{and:p,eq:h,not:m,or:g}=f,v=h(1,1),b=Object.keys(n); +/* v8 ignore next -- @preserve */ +if(0===b.length)return v;const y=e=>{if(!go(e,u[e.id??/* v8 ignore start -- @preserve */""]))return;const f=e.rules.map(e=>{if(hr(e))return y(e);const[u,f]=s(e);if(!go(e,u,f)||e.field===a||e.operator===i||void 0!==l&&e.value===l)return;const p=Qr(r,e.field);return d(e,{...t,parseNumbers:o(p?.inputType),fieldData:p,context:{...c,_tanstackDbRefs:n,_tanstackDbPrimaryRef:b[0]}})}).filter(Boolean);if(0===f.length)return v;const h=1===f.length?f[0]:"or"===e.combinator?g(...f):p(...f);return e.not?m(h):h};return y(Or(e))??v})(e,O);case"sequelize":return((e,t)=>{const{fields:n,getParseNumberBoolean:r,placeholderFieldName:o,placeholderOperatorName:a,placeholderValueName:i,ruleProcessor:l,validateRule:s,validationMap:u,context:c={}}=t,{sequelizeOperators:d}=c;if(!d)return;const f=(e,c)=>{if(!go(e,u[e.id??/* v8 ignore start -- @preserve */""]))return;const p=e.combinator.toUpperCase();let h=!1;const m=e.rules.map(e=>{if(hr(e)){const t=f(e);return t?(h=!0,t):void 0}const[u,c]=s(e);if(!go(e,u,c)||e.field===o||e.operator===a||void 0!==i&&e.value===i)return;const d=Qr(n,e.field);return l(e,{...t,parseNumbers:r(d?.inputType),fieldData:d})}).filter(Boolean);if(0===m.length)return;const g=1!==m.length||h?{["or"===cr(p)?d.or:d.and]:m}:m[0];return e.not?{[d.not]:g}:g};return f(Or(e),!0)})(e,O);case"cypher":case"gql":return((e,t)=>{const{fields:n,fallbackExpression:r,getParseNumberBoolean:o,placeholderFieldName:a,placeholderOperatorName:i,placeholderValueName:l,ruleProcessor:s,validateRule:u,validationMap:c}=t,d=(e,f)=>{if(!go(e,c[e.id??/* v8 ignore start -- @preserve */""]))return f?r:"";const p=[];let h="",m=!0;for(const r of e.rules){if("string"==typeof r){h=Jo[r];continue}if(hr(r)){const e=d(r);e&&(!m&&h&&(p.push(h),h=""),m=!1,p.push(e));continue}const[e,c]=u(r);if(!go(r,e,c)||r.field===a||r.operator===i||void 0!==l&&r.value===l)continue;const f=Qr(n,r.field),g=s(r,{...t,parseNumbers:o(f?.inputType),escapeQuotes:"value"===(r.valueSource??"value"),fieldData:f});g&&(!m&&h&&(p.push(h),h=""),m=!1,p.push(g))}const g=p.join(mr(e)?` ${Jo[e.combinator]} `:" "),[v,b]=e.not||!f?[(e.not?"NOT ":"")+"(",")"]:["",""];return g?`${v}${g}${b}`:r};return d(e,!0)})(e,O);case"sparql":return((e,t)=>{const{fields:n,fallbackExpression:r,getParseNumberBoolean:o,placeholderFieldName:a,placeholderOperatorName:i,placeholderValueName:l,ruleProcessor:s,validateRule:u,validationMap:c}=t,d=(e,f)=>{if(!go(e,c[e.id??/* v8 ignore start -- @preserve */""]))return f?r:"";const p=[];let h="",m=!0;for(const r of e.rules){if("string"==typeof r){h=So[r];continue}if(hr(r)){const e=d(r);e&&(!m&&h&&(p.push(h),h=""),m=!1,p.push(e));continue}const[e,c]=u(r);if(!go(r,e,c)||r.field===a||r.operator===i||void 0!==l&&r.value===l)continue;const f=Qr(n,r.field),g=s(r,{...t,parseNumbers:o(f?.inputType),escapeQuotes:"value"===(r.valueSource??"value"),fieldData:f});g&&(!m&&h&&(p.push(h),h=""),m=!1,p.push(g))}const g=p.join(mr(e)?` ${So[e.combinator]} `:" "),[v,b]=e.not||!f?[(e.not?"!":"")+"(",")"]:["",""];return g?`${v}${g}${b}`:r};return d(e,!0)})(e,O);case"gremlin":return((e,t)=>{const{fields:n,fallbackExpression:r,getParseNumberBoolean:o,placeholderFieldName:a,placeholderOperatorName:i,placeholderValueName:l,ruleProcessor:s,validateRule:u,validationMap:c}=t,d=e=>{ +// v8 ignore next -- @preserve +if("string"==typeof e||hr(e))return;const[r,c]=u(e);if(!go(e,r,c)|| +/* v8 ignore next 2 -- @preserve */ +e.field===a||e.operator===i||void 0!==l&&e.value===l)return;const d=Qr(n,e.field);return s(e,{...t,parseNumbers:o(d?.inputType),escapeQuotes:"value"===(e.valueSource??"value"),fieldData:d})},f=e=>{if(!go(e,c[e.id??/* v8 ignore start -- @preserve */""]))return"";const t=[];for(const n of e.rules){if("string"==typeof n)continue;if(hr(n)){const e=f(n);e&&t.push(e);continue}const e=d(n);e&&t.push(e)}if(0===t.length)return"";const n=e.combinator??"and",r=e.not?"not":n;return 1!==t.length||e.not?`.${r}(${t.map(e=>e.startsWith(".")?`__${e}`:e).join(", ")})`:t[0]};if(!go(e,c[e.id??/* v8 ignore start -- @preserve */""]))return r;const p=[];for(const t of e.rules){if("string"==typeof t)continue;if(hr(t)){const e=f(t);e&&p.push(e);continue}const e=d(t);e&&p.push(e)}return 0===p.length?r:p.join("")})(e,O);case"diagnostics":return((e,t)=>{const{fields:n,placeholderFieldName:r,placeholderOperatorName:o,placeholderValueName:a,validateRule:i,validationMap:l}=t,s=[],u={totalRules:0,totalGroups:0,validRules:0,invalidRules:0,validGroups:0,invalidGroups:0},c={},d=Xr(n),f=new Map;for(const e of d)f.set(e.name,e);const p=f.size>0,h=(e,t)=>{u.totalGroups++;const n=t.length,d=l[e.id??""],m=go(e,d),g=ba(d);e.muted?s.push({id:e.id??"",path:t,code:"MUTED",message:"Group is muted",source:"muted"}):m||void 0===d||s.push({id:e.id??"",path:t,code:"CUSTOM_VALIDATOR",message:g?`Invalid: ${g.join(", ")}`:"Group failed validation",source:"query-validator"});let v=!0,b=0;const y=[];for(const n of e.rules){if("string"==typeof n){y.push(n),b++;continue}const e=[...t,b];if(hr(n)){const t=h(n,e);t.valid||(v=!1),y.push(t),b++;continue}u.totalRules++;const l=e.length,[d,m]=i(n),g=go(n,d,m)&&n.field!==r&&n.operator!==o&&!(void 0!==a&&n.value===a);va(n,e,d,m,g,s,r,o,a,p,f),g?u.validRules++:(v=!1,u.invalidRules++);const w=n.field;c[w]||(c[w]={ruleCount:0,invalidCount:0}),c[w].ruleCount++,g||c[w].invalidCount++;const S=ba(d)??ya(n,m),k={...n,valid:g,...S?{reasons:S}:null,path:e,level:l};y.push(k),b++}const w=m&&v;return w?u.validGroups++:u.invalidGroups++,mr(e),{...e,valid:w,...g?{reasons:g}:null,path:t,level:n,rules:y}},m=h(e,[]);if(p){const e=new Set(Object.keys(c));for(const[t]of f)e.has(t)||s.push({id:"",path:[],code:"UNREFERENCED_FIELD",message:`Field "${t}" is defined in the fields config but not used in the query`,source:"field-check"})}return{query:m,diagnostics:s,stats:u,fieldSummary:c}})(e,O);default:return""}}const ei=e=>(t,n,r,o)=>e({field:t,operator:n,value:r,valueSource:o},{parseNumbers:!1}),ti=(ei(Yo),ei(Ho),ei(zo),ei(Xo),(e,t)=>{let n=t,r=0;for(;r{if(t.id===e)return t;for(const n of t.rules)if("string"!=typeof n){if(n.id===e)return n;if(hr(n)){const t=ni(e,n);if(t)return t}}return null},ri=(e,t)=>{if(t.id===e)return[];const n=t.rules.findIndex(t=>!("string"==typeof t)&&t.id===e);if(n>=0)return[n];for(const[n,r]of Object.entries(t.rules))if(hr(r)){const t=ri(e,r);if(Array.isArray(t))return[Number.parseInt(n),...t]}return null},oi=e=>e.slice(0,-1),ai=(e,t)=>{if(e.length!==t.length)return!1;for(let n=0;n{if(e.length>=t.length)return!1;for(let n=0;n{const n=[],r=oi(e),o=oi(t);let a=0;for(;a{let n=!!t.disabled,r=t,o=0;for(;oai([],e.path)?"Query builder":`Rule group at path ${e.path.join("-")}`,ci=globalThis.crypto;let di=()=>"00-0-4-2-000".replaceAll(/[^-]/g,e=>(65536*(Math.random()+Math.trunc(e))>>Number.parseInt(e)).toString(16).padStart(4,"0"));if(ci)if("function"==typeof ci.randomUUID)di=()=>ci.randomUUID();else if("function"==typeof ci.getRandomValues){const e="89ab",t=new Uint32Array(32);di=()=>{ci.getRandomValues(t);let n=(t[0]%16).toString(16);for(let r=1;r<32;r++)n=12===r?`${n}4`:16===r?`${n}${e[t[17]%4]}`:`${n}${(t[r]%16).toString(16)}`,7!==r&&11!==r&&15!==r&&19!==r||(n=`${n}-`);return n}}const fi=new en({autoFreeze:!1}),pi=(e=!0)=>e?an:fi.produce,hi={name:"name",value:"name",matchModes:null,label:"label"},mi=(e,t)=>{const n=e?jr(e):/* v8 ignore next -- @preserve */hi;let r=n.matchModes??!1;return!r&&t&&(r=t(n.value,{fieldData:n})),!0===r?pn:!1===r?[]:Ur(r)?qr(r):r?.map(e=>pn.find(t=>t.value===cr(e))??{name:e,value:e,label:e})??[]},gi=(e,t,n)=>{const r=Kr(e);if("between"===t.operator||"notBetween"===t.operator){const e=[r,r];return n?e:ar(e.map(e=>e??/* v8 ignore start -- @preserve */""),",")}return r},vi=(e,t)=>{const{fieldData:n,fields:r,getValueEditorType:o,getValues:a,getDefaultValue:i,getParameters:l,listsAsArrays:s}=t;if(void 0!==n?.defaultValue&&null!==n.defaultValue)return n.defaultValue;if(i)return i(e,{fieldData:n});let u="";const c=a(e.field,e.operator,{fieldData:n});if("field"===e.valueSource){const t=eo(n,r,e.operator);u=t.length>0?gi(t,e,s):""}else if("parameter"===e.valueSource){const t=l?l(e.field,e.operator,{fieldData:n}):null;u=t&&t.length>0?Kr(t):""}else if(c.length>0){const t=o(e.field,e.operator,{fieldData:n});"multiselect"===t?u=s?[]:"":"select"!==t&&"radio"!==t||(u=gi(c,e,s))}else"checkbox"===o(e.field,e.operator,{fieldData:n})&&(u=!1);return u},bi=e=>{const t="boolean"==typeof e?e:"object"==typeof e&&null!==e?e.valid:null;return"boolean"==typeof t?t?hn.valid:hn.invalid:""},yi=[{name:"value",value:"value",label:"value"}],wi={name:"name",value:"name",valueSources:null,label:"label"},Si=(e,t,n)=>{const r=e?jr(e):wi;let o=r.valueSources??!1;return"function"==typeof o&&(o=o(t)),!o&&n&&(o=n(r.value,t,{fieldData:r})),o?Ur(o)?qr(o):o.map(e=>yi.find(t=>t.value===cr(e))??{name:e,value:e,label:e}):yi},ki=(e,t,n)=>{if(Gr(e))return;const r=Lr(t).map(([t,[r,o]])=>[t,r??o??n?.[e]?.[t]]).filter(e=>!Gr(e[0])&&!!e[1]);if(r.length>0||n){const t=n?.[e]??{},o=Object.assign({},t,Object.fromEntries(r));return{[e]:o}}},Ei=(e,t)=>{let n="";for(let r=0;rJr({optionList:t?.operators??n?.(e,{fieldData:t})??r,placeholder:o,baseOption:a,labelMap:un,autoSelectOption:i}).optionList,Ni=({field:e,fieldData:t,getDefaultOperator:n,getOperators:r})=>t?.defaultOperator?t.defaultOperator:n?"function"==typeof n?n(e,{fieldData:t}):n:Kr(r(e,{fieldData:t}))??"",Ci=({field:e,operator:t,fieldData:n,getValueEditorType:r})=>n?.valueEditorType?"function"==typeof n.valueEditorType?n.valueEditorType(t):n.valueEditorType:r?.(e,t,{fieldData:n})??"text",Di=({field:e,operator:t,fieldData:n,getValues:r,placeholder:o,baseOption:a,autoSelectOption:i})=>Jr({optionList:n?.values??r?.(e,t,{fieldData:n})??[],placeholder:o,baseOption:a,autoSelectOption:i}).optionList,Ai=(e,t)=>{if(Object.is(e,t))return!0;if(Array.isArray(e)||Array.isArray(t))return Array.isArray(e)&&Array.isArray(t)&&e.length===t.length&&e.every((e,n)=>Ai(e,t[n]));if("object"!=typeof e||"object"!=typeof t||null===e||null===t)return!1;if(Object.getPrototypeOf(e)!==Object.getPrototypeOf(t)||!(e=>{const t=Object.getPrototypeOf(e);return null===t||t===Object.prototype})(e))return!1;const n=Object.keys(e);return n.length===Object.keys(t).length&&n.every(n=>Ai(e[n],t[n]))},Ti=(e,t,n,r)=>r?t??n:e?((e,t)=>!(!1===e||!e&&!1===t))(t,n):((e,t)=>!!e||!1!==e&&!!t)(t,n),$i=(e,t,n)=>void 0!==t&&null!=t?t:void 0!==n&&null!=n?n:e,Mi=(e,{idGenerator:t=di}={})=>{const n=!e.id,r=xo(e);return n||r?{...e,...n&&{id:t()},...r&&{value:Ri(e.value,{idGenerator:t})}}:e},Ri=(e,{idGenerator:t=di}={})=>{const n=!e.id;let r=!1;const o=[];for(let n=0;nhr(e)?Ri(e,{idGenerator:t}):Mi(e,{idGenerator:t}),Ii=(e,{idGenerator:t=di}={})=>{if(!fr(e))return e;if(!hr(e))return structuredClone({...e,id:t()});const n={...e,id:t()};return Array.isArray(n.rules)&&(n.rules=e.rules.map(e=>"string"==typeof e?e:hr(e)?Ii(e,{idGenerator:t}):((e,{idGenerator:t=di}={})=>structuredClone({...e,id:t()}))(e,{idGenerator:t}))),n},Pi=(e,t,n={},{asParent:r=!1}={})=>{if(n.queryDisabled)return r?"parent-disabled":"target-disabled";if(!n.respectDisabled||void 0===t)return null;const o=Array.isArray(t)?t:ri(t,e);return o&&(si(o,e)||((e,t=[])=>t.some(t=>ai(t,e)||ii(t,e)))(o,n.disabledPaths))?r?"parent-disabled":"target-disabled":null},_i=(e,{maxLevels:t=1/0}={})=>!!e&&e.length>=t,Li=(e,t,n,r={})=>pi(r.freeze)(e,e=>Gi(e,t,n,r)),Gi=(e,t,n,r={})=>{const{combinators:o=fn,combinatorPreceding:a,idGenerator:i=di,onAbort:l}=r,s=Array.isArray(n)?ti(n,e):ni(n,e);if(!s)return l?.({reason:"parent-not-found",operation:"add",pathOrID:n}),e;if(!hr(s))return l?.({reason:"parent-not-a-group",operation:"add",pathOrID:n}),e;const u=Array.isArray(n)?n:ri(n,e),c=Pi(e,u,r,{asParent:!0});if(c)return l?.({reason:c,operation:"add",pathOrID:n}),e;if(hr(t)&&_i(u,r))return l?.({reason:"max-levels-exceeded",operation:"add",pathOrID:n}),e;if(gr(s)&&s.rules.length>0){const e=s.rules.at(-2);s.rules.push(a??("string"==typeof e?e:Kr(o)))}return s.rules.push(xi(t,{idGenerator:i})),e},Fi={field:0,operator:1,valueSource:2,value:4},zi=e=>e.toSorted((e,t)=>(Fi[e[0]]??3)-(Fi[t[0]]??3)),ji=(e,t,n,r,o)=>{const{entries:a,pathOrID:i,options:l}=((e,t,n,r)=>{if("string"==typeof e)return{entries:[[e,t]],pathOrID:n,options:r??{}};if(Array.isArray(e)){const o=t;return{entries:zi(e.map((e,t)=>[e,o[t]])),pathOrID:n,options:r??{}}}return{entries:zi(Object.entries(e)),pathOrID:t,options:n??{}}})(t,n,r,o);for(const[t,n]of a)Bi(e,t,n,i,l);return e},qi=(e,t,n,r,o)=>pi(((e,t,n,r)=>("string"==typeof e||Array.isArray(e)?r:n)??{})(t,0,r,o).freeze)(e,e=>{ji(e,t,n,r,o)}),Bi=(e,t,n,r,o={})=>{const{resetOnFieldChange:a=!0,resetOnOperatorChange:i=!1,getRuleDefaultOperator:l=()=>"=",getValueSources:s=()=>["value"],getRuleDefaultValue:u=()=>"",getMatchModes:c=()=>[],onAbort:d}=o;let f=a;const p=Array.isArray(r)?r:ri(r,e);if(!p)return d?.({reason:"target-not-found",operation:"update",pathOrID:r}),e;const h="disabled"===t?{queryDisabled:o.queryDisabled}:o,m=Pi(e,p,h);if(m)return d?.({reason:m,operation:"update",pathOrID:r}),e;if("combinator"===t&&!mr(e)){const t=ti(oi(p),e);if(!t||!hr(t))return d?.({reason:"target-not-found",operation:"update",pathOrID:r}),e;const o=t.rules;return p.at(-1)%2==1?o[p.at(-1)]===n?d?.({reason:"no-change",operation:"update",pathOrID:r}):o[p.at(-1)]=n:d?.({reason:"not-a-combinator-slot",operation:"update",pathOrID:r}),e}const g=ti(p,e);if(!g)return d?.({reason:"target-not-found",operation:"update",pathOrID:r}),e;const v=hr(g);if(g[t]===n)return d?.({reason:"no-change",operation:"update",pathOrID:r}),e;if("valueSource"!==t&&(g[t]=n),v)return e;let b=!1,y=!1;if("field"===t){const e=c(g.field),t=c(n);if(0===t.length)delete g.match;else{const e=g.match?.mode&&Qr(t,g.match.mode)?null:Kr(t);e&&(g.match={mode:e,threshold:1})}(e.length>0||t.length>0)&&(f=!0)}f&&"field"===t&&(g.operator=l(n),b=!0,y=!0),i&&"operator"===t&&(b=!0,y=!0);const w=Kr(Si({name:g.field,value:g.field,label:""},g.operator,s));return(b&&g.valueSource&&w!==g.valueSource||"valueSource"===t&&n!==g.valueSource)&&(y=!!g.valueSource||!g.valueSource&&n!==w,g.valueSource=b?w:n),y&&(g.value=u(g)),e},Vi=(e,t,n={})=>pi(n.freeze)(e,e=>Hi(e,t,n)),Hi=(e,t,n={})=>{const{onAbort:r}=n,o=Array.isArray(t)?t:ri(t,e);if(!o)return r?.({reason:"target-not-found",operation:"remove",pathOrID:t}),e;if(0===o.length)return r?.({reason:"root-not-allowed",operation:"remove",pathOrID:t}),e;const a=Pi(e,o,n);if(a)return r?.({reason:a,operation:"remove",pathOrID:t}),e;if(!ti(o,e))return r?.({reason:"target-not-found",operation:"remove",pathOrID:t}),e;const i=o.at(-1),l=ti(oi(o),e);if(!mr(l)&&l.rules.length>1){const e=0===i?0:i-1;l.rules.splice(e,2)}else l.rules.splice(i,1);return e},Ui=(e,t,n)=>{if(Array.isArray(n))return n;const r=gr(e);if("up"===n){if(ai(t,[0]))return t;if(0===t.at(-1)){const e=oi(t);return[...oi(e),Math.max(0,e.at(-1)-(r?1:0))]}{const n=[...oi(t),Math.max(0,t.at(-1)-(r?2:1))],o=ti(n,e);return hr(o)?[...n,o.rules.length]:[...oi(t),Math.max(0,t.at(-1)-(r?3:1))]}}if("down"===n){if(ai([e.rules.length-1],t))return t;if(t.at(-1)===ti(oi(t),e).rules.length-1){const e=oi(t);return[...oi(e),e.at(-1)+1]}{const n=[...oi(t),t.at(-1)+(r?2:1)],o=ti(n,e);return hr(o)?[...n,0]:[...oi(t),t.at(-1)+(r?3:2)]}}return t},Wi=(e,t,n,r={})=>pi(r.freeze)(e,e=>Qi(e,t,n,r)),Qi=(e,t,n,r={})=>{const{clone:o=!1,combinators:a=fn,idGenerator:i=di,onAbort:l}=r,s=Array.isArray(t)?t:ri(t,e);if(!s)return l?.({reason:"target-not-found",operation:"move",pathOrID:t}),e;const u=ti(s,e);if(!u)return l?.({reason:"target-not-found",operation:"move",pathOrID:t}),e;const c=Ui(e,s,n);if(0===s.length)return l?.({reason:"root-not-allowed",operation:"move",pathOrID:t}),e;const d=Pi(e,s,r);if(d)return l?.({reason:d,operation:"move",pathOrID:t}),e;if(ai(s,c))return l?.({reason:"same-location",operation:"move",pathOrID:t}),e;if(!ti(oi(c),e))return l?.({reason:"destination-not-found",operation:"move",pathOrID:n}),e;if(!o&&ii(s,c))return l?.({reason:"destination-not-found",operation:"move",pathOrID:n}),e;const f=o?Ii(ot(u)?nn(u):u,{idGenerator:i}):u,p=gr(e),h=ti(oi(s),e),m=s.at(-1),g=p&&m>0?h.rules[m-1]:null,v=p&&ms[y.length]&&(b[y.length]-=p?2:1);const w=oi(b),S=ti(w,e),k=b.at(-1),E=p&&k>0?k-1:k,O=(...e)=>S.rules.splice(E,0,...e);return 0!==S.rules.length&&p?0===k?O(f,0===m&&v?v:S.rules[1]??g??Kr(a)):O(g||(S.rules[E-2]??v??Kr(a)),f):O(f),e},Ki=(e,t,n,r={})=>pi(r.freeze)(e,e=>Xi(e,t,n,r)),Xi=(e,t,n,r={})=>{const{combinators:o=fn,combinatorPreceding:a,combinatorSucceeding:i,idGenerator:l=di,replace:s=!1,onAbort:u}=r,c=ti(oi(n),e);if(!c)return u?.({reason:"parent-not-found",operation:"insert",pathOrID:n}),e;if(!hr(c))return u?.({reason:"parent-not-a-group",operation:"insert",pathOrID:n}),e;const d=oi(n),f=Pi(e,d,r,{asParent:!0});if(f)return u?.({reason:f,operation:"insert",pathOrID:n}),e;if(hr(t)&&_i(d,r))return u?.({reason:"max-levels-exceeded",operation:"insert",pathOrID:n}),e;const p=Ii(t,{idGenerator:l}),h=gr(e),m=n.at(-1),g=(e,...t)=>c.rules.splice(e,s?t.length:0,...t);if(0!==c.rules.length&&h)if(s&&h)g(m+m%2,p);else if(0===m)0===p.path?.at(-1)&&i?g(m,p,i):g(m,p,c.rules[1]??a??Kr(o));else{const e=m%2==0?m-1:m;g(e,a||(c.rules[e-2]??i??Kr(o)),p)}else g(m,p);return e},Yi=(e,t,n,r={})=>pi(r.freeze)(e,e=>Ji(e,t,n,r)),Ji=(e,t,n,r={})=>{const{clone:o=!1,combinators:a=fn,idGenerator:i=di,onAbort:l}=r,s=Array.isArray(t)?t:ri(t,e),u=Array.isArray(n)?n:ri(n,e);if(!s)return l?.({reason:"target-not-found",operation:"group",pathOrID:t}),e;if(!u)return l?.({reason:"target-not-found",operation:"group",pathOrID:n}),e;const c=Ui(e,s,u);if(0===s.length)return l?.({reason:"root-not-allowed",operation:"group",pathOrID:t}),e;const d=Pi(e,s,r);if(d)return l?.({reason:d,operation:"group",pathOrID:t}),e;if(ai(s,c))return l?.({reason:"same-location",operation:"group",pathOrID:t}),e;if(!ti(oi(c),e))return l?.({reason:"destination-not-found",operation:"group",pathOrID:n}),e;if(!o&&ii(s,c))return l?.({reason:"destination-not-found",operation:"group",pathOrID:n}),e;const f=ti(s,e),p=ti(u,e);if(!f)return l?.({reason:"target-not-found",operation:"group",pathOrID:t}),e;if(!p)return l?.({reason:"target-not-found",operation:"group",pathOrID:n}),e;const h=o?Ii(ot(f)?nn(f):f,{idGenerator:i}):f,m=gr(e),g=ti(oi(s),e),v=s.at(-1);if(!o){const e=m?Math.max(0,v-1):v,t=m?2:1;g.rules.splice(e,t)}const b=[...c],y=li(s,c);!o&&s.length===y.length+1&&c[y.length]>s[y.length]&&(b[y.length]-=m?2:1);const w=oi(b),S=ti(w,e),k=b.at(-1);return S.rules.splice(k,1,xi(m?{rules:[p,Kr(a),h]}:{combinator:Kr(a),rules:[p,h]},{idGenerator:i})),e},Zi=({fields:e,getDefaultField:t,getRuleDefaultOperator:n,getValueSources:r,getMatchModes:o,getRuleDefaultValue:a,idGenerator:i=di})=>{let l=Kr(e)??"";if(t)if("function"==typeof t){const n=t(e);n&&(l=n)}else l=t;const s=Qr(e,l),u=n(l),c=Kr(r(l,u,{fieldData:s}))??"value",d=Kr(o(l,{fieldData:s})),f={id:i(),field:l,operator:u,valueSource:c,value:"",...d?{match:{mode:d,threshold:1}}:null};return{...f,value:a(f)}},el=({combinators:e,addRuleToNewGroups:t,createRule:n,idGenerator:r=di},o)=>{const a=r(),i=t?[n()]:[];return o?{id:a,rules:i,not:!1}:{id:a,rules:i,combinator:Kr(e)??"",not:!1}};var tl=class e extends Error{code;info;constructor(t){super(`QueryManager: "${t.operation}" aborted (${t.reason})${void 0===t.pathOrID?"":` for target ${JSON.stringify(t.pathOrID)}`}.`),this.name="QueryManagerError",this.code=t.reason,this.info=t,Object.setPrototypeOf(this,e.prototype)}};const nl=Symbol("QueryManager.state"),rl=Et({query:!0,config:!1}),ol=Et({query:!1,config:!0}),al=Et({query:!0,config:!0}),il=()=>{const e={listeners:new Set,configVersion:0,past:[],future:[],lastAt:0,batchDepth:0,historyBypassed:!1,pendingConfigNotify:!1};return Object.defineProperty(e,"__v_skip",{value:!0,enumerable:!1}),e};Symbol.iterator; +/* v8 ignore file -- @preserve */ +const ll=new Set(["shift","alt","meta","mod","ctrl"]),sl={esc:"escape",return:"enter",".":"period",",":"comma","-":"slash"," ":"space","`":"backquote","#":"backslash","+":"bracketright",ShiftLeft:"shift",ShiftRight:"shift",AltLeft:"alt",AltRight:"alt",MetaLeft:"meta",MetaRight:"meta",OSLeft:"meta",OSRight:"meta",ControlLeft:"ctrl",ControlRight:"ctrl"},ul=e=>cr((e&&sl[e]||e||"").trim()).replace(/key|digit|numpad|arrow/,""),cl=e=>ll.has(e),dl={"⌘":"meta",cmd:"meta",command:"meta","⊞":"meta",win:"meta",windows:"meta","⇧":"shift","⌥":"alt","⌃":"ctrl",control:"ctrl"};"undefined"!=typeof document&&(document.addEventListener("keydown",e=>{void 0!==e.key&&hl([ul(e.key),ul(e.code)])}),document.addEventListener("keyup",e=>{void 0!==e.key&&ml([ul(e.key),ul(e.code)])})),"undefined"!=typeof window&&window.addEventListener("blur",()=>{fl.clear()});const fl=new Set,pl=(e,t=",")=>{return(n=e,Array.isArray(n)?e:e.split(t)).every(e=>{const t=cr(e.trim());return fl.has(dl[t]??t)});var n},hl=e=>{const t=Array.isArray(e)?e:[e];if(fl.has("meta"))for(const e of fl)cl(e)||fl.delete(cr(e));for(const e of t)fl.add(cr(e))},ml=e=>{const t=Array.isArray(e)?e:[e];if("meta"===e)fl.clear();else for(const e of t)fl.delete(cr(e))},gl=({type:e,path:t,schema:n,copyModeModifierKey:r,groupModeModifierKey:o,copyModeOverride:a,groupModeOverride:i})=>{const{qbId:l,getQuery:s,dispatchQuery:u}=n;return{type:e,path:t,qbId:l,getQuery:s,dispatchQuery:u,groupItems:i||pl(o),dropEffect:a||pl(r)?"copy":"move"}},vl=({item:e,dropResult:t,schema:n,actions:r,copyModeModifierKey:o,groupModeModifierKey:a,copyModeOverride:i,groupModeOverride:l,onRuleDrop:s})=>{if(!t)return;const u=i||pl(o)?"copy":"move",c=l||pl(a),d=((e,t)=>{const n=oi(e.path),r=e.path.at(-1);return t?e.path:"ruleGroup"===e.type?[...e.path,0]:"inlineCombinator"===e.type?[...n,r]:[...n,r+1]})(t,c),f=n.qbId!==t.qbId;if(f){const n=t.getQuery();n&&(c?t.dispatchQuery(Yi(Li(n,e,[]),[n.rules.length],d,{clone:!1})):t.dispatchQuery(Ki(n,e,d)),"copy"!==u&&r.onRuleRemove(e.path))}else c?r.groupRule(e.path,d,"copy"===u):r.moveRule(e.path,d,"copy"===u);s?.({draggedItem:e,sourceQbId:n.qbId,targetQbId:t.qbId,sourcePath:e.path,targetPath:d,dropEffect:u,groupItems:c,isCrossBuilder:f})},bl=()=>"undefined"!=typeof window&&"ontouchstart"in window||"undefined"!=typeof navigator&&navigator.maxTouchPoints>0;let yl;const wl=()=>(yl||(yl=new Image,yl.src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="),yl),Sl=["rule","ruleGroup"],kl=(e,t,n)=>n(()=>({type:t,item:()=>({...ti(e.path,e.schema.getQuery()),path:e.path,qbId:e.schema.qbId}),canDrag:!e.disabled,previewOptions:{captureDraggingState:!!e.hideDefaultDragPreview},collect:t=>({isDragging:!e.disabled&&t.isDragging(),dragMonitorId:t.getHandlerId()??""}),end:(t,n)=>{vl({item:t,dropResult:n.getDropResult(),schema:e.schema,actions:e.actions,copyModeModifierKey:e.copyModeModifierKey,groupModeModifierKey:e.groupModeModifierKey,onRuleDrop:e.onRuleDrop})}}),[e.actions.groupRule,e.actions.moveRule,e.disabled,e.path]),El=e=>{const{useDrag:t,useDrop:n,DndProvider:r,DndContext:o}=e;let a=e.ReactDndBackend;a||(a=bl()?e.TouchBackend??e.HTML5Backend:e.HTML5Backend??e.TouchBackend);return{DndProvider:({debugMode:e,children:t})=>l().createElement(r,{backend:a,debugMode:e},l().createElement(o.Consumer,null,()=>l().createElement(l().Fragment,null,t))),useRuleDnD:e=>{const r=(0,l().useRef)(null),o=(0,l().useRef)(null),[{isDragging:a,dragMonitorId:i},s,u]=kl(e,"rule",t),[{isOver:c,dropMonitorId:d,dropEffect:f,groupItems:p,dropNotAllowed:h},m]=n(()=>({accept:Sl,canDrop:t=>(({dragging:e,path:t,schema:n,canDrop:r,groupModeModifierKey:o,disabled:a,rule:i})=>{if(pl(o)&&a||e&&"function"==typeof r&&!r({dragging:e,hovering:{...i,path:t,qbId:n.qbId}}))return!1;if(n.qbId!==e.qbId)return!0;const l=oi(t),s=oi(e.path),u=t.at(-1),c=e.path.at(-1);return!(ii(e.path,t)||ai(t,e.path)||!pl(o)&&ai(l,s)&&(u===c-1||n.independentCombinators&&u===c-2))})({dragging:t,path:e.path,schema:e.schema,canDrop:e.canDrop,groupModeModifierKey:e.groupModeModifierKey,disabled:e.disabled,rule:e.rule}),collect:t=>({dropNotAllowed:t.isOver()&&!t.canDrop(),isOver:t.canDrop()&&t.isOver(),dropMonitorId:t.getHandlerId()??"",dropEffect:pl(e.copyModeModifierKey)?"copy":"move",groupItems:pl(e.groupModeModifierKey)}),drop:()=>gl({type:"rule",path:e.path,schema:e.schema,copyModeModifierKey:e.copyModeModifierKey,groupModeModifierKey:e.groupModeModifierKey})}),[e.disabled,e.actions.moveRule,e.path,e.canDrop,e.rule,e.schema]);return l().useEffect(()=>{s(o),m(r),u(e.hideDefaultDragPreview?wl():r)},[s,m,e.hideDefaultDragPreview,u]),{isDragging:a,dragMonitorId:i,isOver:c,dropMonitorId:d,dndRef:r,dragRef:o,dropEffect:f,groupItems:p,dropNotAllowed:h}},useRuleGroupDnD:e=>{const r=(0,l().useRef)(null),o=(0,l().useRef)(null),a=(0,l().useRef)(null),[{isDragging:i,dragMonitorId:s},u,c]=kl(e,"ruleGroup",t),[{isOver:d,dropMonitorId:f,dropEffect:p,groupItems:h,dropNotAllowed:m},g]=n(()=>({accept:Sl,canDrop:t=>(({dragging:e,path:t,schema:n,canDrop:r,disabled:o,ruleGroup:a})=>{if(o||e&&"function"==typeof r&&!r({dragging:e,hovering:{...a,path:t,qbId:n.qbId}}))return!1;if(n.qbId!==e.qbId)return!0;const i=oi(e.path),l=e.path.at(-1);return!(ii(e.path,t)||ai(t,i)&&0===l||ai(t,e.path))})({dragging:t,path:e.path,schema:e.schema,canDrop:e.canDrop,disabled:e.disabled,ruleGroup:e.ruleGroup}),collect:t=>({dropNotAllowed:t.isOver()&&!t.canDrop(),isOver:t.canDrop()&&t.isOver(),dropMonitorId:t.getHandlerId()??"",dropEffect:pl(e.copyModeModifierKey)?"copy":"move",groupItems:pl(e.groupModeModifierKey)}),drop:()=>gl({type:"ruleGroup",path:e.path,schema:e.schema,copyModeModifierKey:e.copyModeModifierKey,groupModeModifierKey:e.groupModeModifierKey})}),[e.disabled,e.actions.groupRule,e.actions.moveRule,e.path,e.canDrop,e.ruleGroup,e.schema]);return l().useEffect(()=>{e.path.length>0&&(u(o),c(e.hideDefaultDragPreview?wl():r)),g(a)},[u,g,e.hideDefaultDragPreview,e.path.length,c]),{isDragging:i,dragMonitorId:s,isOver:d,dropMonitorId:f,previewRef:r,dragRef:o,dropRef:a,dropEffect:p,groupItems:h,dropNotAllowed:m}},useInlineCombinatorDnD:e=>{const t=(0,l().useRef)(null),r=(e.rules?? +/* v8 ignore start -- @preserve */[])[e.path.at(-1)-1],[{isOver:o,dropMonitorId:a,dropEffect:i,dropNotAllowed:s},u]=n(()=>({accept:["rule","ruleGroup"],canDrop:t=>(({dragging:e,path:t,schema:n,canDrop:r,groupModeModifierKey:o,hoveringItem:a})=>{const{path:i}=e;if(pl(o)||e&&"function"==typeof r&&!r({dragging:e,hovering:{...a,path:t,qbId:n.qbId}}))return!1;const l=oi(t),s=oi(i),u=t.at(-1),c=i.at(-1);return!(ii(i,t)||ai(i,t)||ai(l,s)&&u-1===c||n.independentCombinators&&ai(l,s)&&u===c-1)})({dragging:t,path:e.path,schema:e.schema,canDrop:e.canDrop,groupModeModifierKey:e.groupModeModifierKey,hoveringItem:r}),collect:t=>({dropNotAllowed:t.isOver()&&!t.canDrop(),isOver:t.canDrop()&&t.isOver(),dropMonitorId:t.getHandlerId()??"",dropEffect:pl(e.copyModeModifierKey)?"copy":"move",groupItems:pl(e.groupModeModifierKey)}),drop:()=>gl({type:"inlineCombinator",path:e.path,schema:e.schema,copyModeModifierKey:e.copyModeModifierKey,groupModeModifierKey:e.groupModeModifierKey})}),[e.canDrop,r,e.path,e.schema]);return l().useEffect(()=>{u(t)},[u]),{dropRef:t,dropMonitorId:a,isOver:o,dropEffect:i,dropNotAllowed:s}}}};function Ol(e){return`Minified Redux error #${e}; visit https://redux.js.org/Errors?code=${e} for the full message or use the non-minified dev environment for full errors. `}var Nl=(()=>"function"==typeof Symbol&&Symbol.observable||"@@observable")(),Cl=()=>Math.random().toString(36).substring(7).split("").join("."),Dl={INIT:`@@redux/INIT${Cl()}`,REPLACE:`@@redux/REPLACE${Cl()}`,PROBE_UNKNOWN_ACTION:()=>`@@redux/PROBE_UNKNOWN_ACTION${Cl()}`};function Al(e){if("object"!=typeof e||null===e)return!1;let t=e;for(;null!==Object.getPrototypeOf(t);)t=Object.getPrototypeOf(t);return Object.getPrototypeOf(e)===t||null===Object.getPrototypeOf(e)}function Tl(e,t,n){if("function"!=typeof e)throw new Error(Ol(2));if("function"==typeof t&&"function"==typeof n||"function"==typeof n&&"function"==typeof arguments[3])throw new Error(Ol(0));if("function"==typeof t&&void 0===n&&(n=t,t=void 0),void 0!==n){if("function"!=typeof n)throw new Error(Ol(1));return n(Tl)(e,t)}let r=e,o=t,a=new Map,i=a,l=0,s=!1;function u(){i===a&&(i=new Map,a.forEach((e,t)=>{i.set(t,e)}))}function c(){if(s)throw new Error(Ol(3));return o}function d(e){if("function"!=typeof e)throw new Error(Ol(4));if(s)throw new Error(Ol(5));let t=!0;u();const n=l++;return i.set(n,e),function(){if(t){if(s)throw new Error(Ol(6));t=!1,u(),i.delete(n),a=null}}}function f(e){if(!Al(e))throw new Error(Ol(7));if(void 0===e.type)throw new Error(Ol(8));if("string"!=typeof e.type)throw new Error(Ol(17));if(s)throw new Error(Ol(9));try{s=!0,o=r(o,e)}finally{s=!1}return(a=i).forEach(e=>{e()}),e}f({type:Dl.INIT});return{dispatch:f,subscribe:d,getState:c,replaceReducer:function(e){if("function"!=typeof e)throw new Error(Ol(10));r=e,f({type:Dl.REPLACE})},[Nl]:function(){const e=d;return{subscribe(t){if("object"!=typeof t||null===t)throw new Error(Ol(11));function n(){const e=t;e.next&&e.next(c())}n();return{unsubscribe:e(n)}},[Nl](){return this}}}}}function $l(e){const t=Object.keys(e),n={};for(let r=0;r{const n=e[t];if(void 0===n(void 0,{type:Dl.INIT}))throw new Error(Ol(12));if(void 0===n(void 0,{type:Dl.PROBE_UNKNOWN_ACTION()}))throw new Error(Ol(13))})}(n)}catch(e){o=e}return function(e={},t){if(o)throw o;let a=!1;const i={};for(let o=0;oe:1===e.length?e[0]:e.reduce((e,t)=>(...n)=>e(t(...n)))}function Rl(e){return({dispatch:t,getState:n})=>r=>o=>"function"==typeof o?o(t,n,e):r(o)}var xl=Rl(),Il=Rl,Pl="undefined"!=typeof window&&window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__:function(){if(0!==arguments.length)return"object"==typeof arguments[0]?Ml:Ml.apply(null,arguments)};"undefined"!=typeof window&&window.__REDUX_DEVTOOLS_EXTENSION__&&window.__REDUX_DEVTOOLS_EXTENSION__;function _l(e,t){function n(...n){if(t){let r=t(...n);if(!r)throw new Error(os(0));return{type:e,payload:r.payload,..."meta"in r&&{meta:r.meta},..."error"in r&&{error:r.error}}}return{type:e,payload:n[0]}}return n.toString=()=>`${e}`,n.type=e,n.match=t=>function(e){return Al(e)&&"type"in e&&"string"==typeof e.type}(t)&&t.type===e,n}var Ll=class e extends Array{constructor(...t){super(...t),Object.setPrototypeOf(this,e.prototype)}static get[Symbol.species](){return e}concat(...e){return super.concat.apply(this,e)}prepend(...t){return 1===t.length&&Array.isArray(t[0])?new e(...t[0].concat(this)):new e(...t.concat(this))}};function Gl(e){return at(e)?an(e,()=>{}):e}function Fl(e,t,n){return e.has(t)?e.get(t):e.set(t,n(t)).get(t)}var zl="RTK_autoBatch",jl=e=>t=>{setTimeout(t,e)},ql=(e={type:"raf"})=>t=>(...n)=>{const r=t(...n);let o=!0,a=!1,i=!1;const l=new Set,s="tick"===e.type?queueMicrotask:"raf"===e.type?"undefined"!=typeof window&&window.requestAnimationFrame?(u=window.requestAnimationFrame,c=100,e=>{let t=!1;const n=()=>{t||(t=!0,cancelAnimationFrame(r),clearTimeout(o),e())},r=u(n),o=setTimeout(n,c)}):jl(10):"callback"===e.type?e.queueNotification:jl(e.timeout);var u,c;const d=()=>{i=!1,a&&(a=!1,l.forEach(e=>e()))};return Object.assign({},r,{subscribe(e){const t=r.subscribe(()=>o&&e());return l.add(e),()=>{t(),l.delete(e)}},dispatch(e){try{return o=!e?.meta?.[zl],a=!o,a&&(i||(i=!0,s(d))),r.dispatch(e)}finally{o=!0}}})};function Bl(e){const t=function(e){const{thunk:t=!0,immutableCheck:n=!0,serializableCheck:r=!0,actionCreatorCheck:o=!0}=e??{};let a=new Ll;return t&&("boolean"==typeof t?a.push(xl):a.push(Il(t.extraArgument))),a},{reducer:n,middleware:r,devTools:o=!0,duplicateMiddlewareCheck:a=!0,preloadedState:i,enhancers:l}=e||{};let s,u;if("function"==typeof n)s=n;else{if(!Al(n))throw new Error(os(1));s=$l(n)}u="function"==typeof r?r(t):t();let c=Ml;o&&(c=Pl({trace:!1,..."object"==typeof o&&o}));const d=function(...e){return t=>(n,r)=>{const o=t(n,r);let a=()=>{throw new Error(Ol(15))};const i={getState:o.getState,dispatch:(e,...t)=>a(e,...t)},l=e.map(e=>e(i));return a=Ml(...l)(o.dispatch),{...o,dispatch:a}}}(...u),f=(e=>function(t){const{autoBatch:n=!0}=t??{};let r=new Ll(e);return n&&r.push(ql("object"==typeof n?n:void 0)),r})(d);return Tl(s,i,c(..."function"==typeof l?l(f):f()))}function Vl(e){const t={},n=[];let r;const o={addCase(e,n){const r="string"==typeof e?e:e.type;if(!r)throw new Error(os(28));if(r in t)throw new Error(os(29));return t[r]=n,o},addAsyncThunk:(e,r)=>(r.pending&&(t[e.pending.type]=r.pending),r.rejected&&(t[e.rejected.type]=r.rejected),r.fulfilled&&(t[e.fulfilled.type]=r.fulfilled),r.settled&&n.push({matcher:e.settled,reducer:r.settled}),o),addMatcher:(e,t)=>(n.push({matcher:e,reducer:t}),o),addDefaultCase:e=>(r=e,o)};return e(o),[t,n,r]}var Hl=(e=21)=>{let t="",n=e;for(;n--;)t+="ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW"[64*Math.random()|0];return t};var Ul=Symbol.for("rtk-slice-createasyncthunk");function Wl(e,t){return`${e}/${t}`}function Ql({creators:e}={}){const t=e?.asyncThunk?.[Ul];return function(e){const{name:n,reducerPath:r=n}=e;if(!n)throw new Error(os(11));const o=("function"==typeof e.reducers?e.reducers(function(){function e(e,t){return{_reducerDefinitionType:"asyncThunk",payloadCreator:e,...t}}return e.withTypes=()=>e,{reducer:e=>Object.assign({[e.name]:(...t)=>e(...t)}[e.name],{_reducerDefinitionType:"reducer"}),preparedReducer:(e,t)=>({_reducerDefinitionType:"reducerWithPrepare",prepare:e,reducer:t}),asyncThunk:e}}()):e.reducers)||{},a=Object.keys(o),i={sliceCaseReducersByName:{},sliceCaseReducersByType:{},actionCreators:{},sliceMatchers:[]},l={addCase(e,t){const n="string"==typeof e?e:e.type;if(!n)throw new Error(os(12));if(n in i.sliceCaseReducersByType)throw new Error(os(13));return i.sliceCaseReducersByType[n]=t,l},addMatcher:(e,t)=>(i.sliceMatchers.push({matcher:e,reducer:t}),l),exposeAction:(e,t)=>(i.actionCreators[e]=t,l),exposeCaseReducer:(e,t)=>(i.sliceCaseReducersByName[e]=t,l)};function s(){const[t={},n=[],r]="function"==typeof e.extraReducers?Vl(e.extraReducers):[e.extraReducers],o={...t,...i.sliceCaseReducersByType};return function(e,t){let n,[r,o,a]=Vl(t);if("function"==typeof e)n=()=>Gl(e());else{const t=Gl(e);n=()=>t}function i(e=n(),t){let i=[r[t.type],...o.filter(({matcher:e})=>e(t)).map(({reducer:e})=>e)];return 0===i.filter(e=>!!e).length&&(i=[a]),i.reduce((e,n)=>{if(n){if(ot(e)){const r=n(e,t);return void 0===r?e:r}if(at(e))return an(e,e=>n(e,t));{const r=n(e,t);if(void 0===r){if(null===e)return e;throw Error("A case reducer on a non-draftable value must not return undefined")}return r}}return e},e)}return i.getInitialState=n,i}(e.initialState,e=>{for(let t in o)e.addCase(t,o[t]);for(let t of i.sliceMatchers)e.addMatcher(t.matcher,t.reducer);for(let t of n)e.addMatcher(t.matcher,t.reducer);r&&e.addDefaultCase(r)})}a.forEach(r=>{const a=o[r],i={reducerName:r,type:Wl(n,r),createNotation:"function"==typeof e.reducers};!function(e){return"asyncThunk"===e._reducerDefinitionType}(a)?function({type:e,reducerName:t,createNotation:n},r,o){let a,i;if("reducer"in r){if(n&&!function(e){return"reducerWithPrepare"===e._reducerDefinitionType}(r))throw new Error(os(17));a=r.reducer,i=r.prepare}else a=r;o.addCase(e,a).exposeCaseReducer(t,a).exposeAction(t,i?_l(e,i):_l(e))}(i,a,l):function({type:e,reducerName:t},n,r,o){if(!o)throw new Error(os(18));const{payloadCreator:a,fulfilled:i,pending:l,rejected:s,settled:u,options:c}=n,d=o(e,a,c);r.exposeAction(t,d),i&&r.addCase(d.fulfilled,i);l&&r.addCase(d.pending,l);s&&r.addCase(d.rejected,s);u&&r.addMatcher(d.settled,u);r.exposeCaseReducer(t,{fulfilled:i||Yl,pending:l||Yl,rejected:s||Yl,settled:u||Yl})}(i,a,l,t)});const u=e=>e,c=new Map,d=new WeakMap;let f;function p(e,t){return f||(f=s()),f(e,t)}function h(){return f||(f=s()),f.getInitialState()}function m(t,n=!1){function r(e){let o=e[t];return void 0===o&&n&&(o=Fl(d,r,h)),o}function o(t=u){const r=Fl(c,n,()=>new WeakMap);return Fl(r,t,()=>{const r={};for(const[o,a]of Object.entries(e.selectors??{}))r[o]=Kl(a,t,()=>Fl(d,t,h),n);return r})}return{reducerPath:t,getSelectors:o,get selectors(){return o(r)},selectSlice:r}}const g={name:n,reducer:p,actions:i.actionCreators,caseReducers:i.sliceCaseReducersByName,getInitialState:h,...m(r),injectInto(e,{reducerPath:t,...n}={}){const o=t??r;return e.inject({reducerPath:o,reducer:p},n),{...g,...m(o,!0)}}};return g}}function Kl(e,t,n,r){function o(o,...a){let i=t(o);return void 0===i&&r&&(i=n()),e(i,...a)}return o.unwrapped=e,o}var Xl=Ql();function Yl(){}var{assign:Jl}=Object,Zl=Symbol.for("rtk-state-proxy-original"),es=new WeakMap,ts=e=>{if(!(t=e)||!t[Zl])throw new Error(os(25));var t;return e[Zl]},ns={},rs=(e=ns)=>e;function os(e){return`Minified Redux Toolkit error #${e}; visit https://redux-toolkit.js.org/Errors?code=${e} for the full message or use the non-minified dev environment for full errors. `}p();function as(e){e()}var is={notify(){},get:()=>[]};function ls(e,t){let n,r=is,o=0,a=!1;function i(){u.onStateChange&&u.onStateChange()}function l(){o++,n||(n=t?t.addNestedSub(i):e.subscribe(i),r=function(){let e=null,t=null;return{clear(){e=null,t=null},notify(){as(()=>{let t=e;for(;t;)t.callback(),t=t.next})},get(){const t=[];let n=e;for(;n;)t.push(n),n=n.next;return t},subscribe(n){let r=!0;const o=t={callback:n,next:null,prev:t};return o.prev?o.prev.next=o:e=o,function(){r&&null!==e&&(r=!1,o.next?o.next.prev=o.prev:t=o.prev,o.prev?o.prev.next=o.next:e=o.next)}}}}())}function s(){o--,n&&0===o&&(n(),n=void 0,r.clear(),r=is)}const u={addNestedSub:function(e){l();const t=r.subscribe(e);let n=!1;return()=>{n||(n=!0,t(),s())}},notifyNestedSubs:function(){r.notify()},handleChangeWrapper:i,isSubscribed:function(){return a},trySubscribe:function(){a||(a=!0,l())},tryUnsubscribe:function(){a&&(a=!1,s())},getListeners:()=>r};return u}var ss=(()=>!("undefined"==typeof window||void 0===window.document||void 0===window.document.createElement))(),us=(()=>"undefined"!=typeof navigator&&"ReactNative"===navigator.product)(),cs=(()=>ss||us?l().useLayoutEffect:l().useEffect)();Object.defineProperty,Object.getOwnPropertyNames,Object.getOwnPropertySymbols,Object.getOwnPropertyDescriptor,Object.getPrototypeOf,Object.prototype;var ds=Symbol.for("react-redux-context"),fs="undefined"!=typeof globalThis?globalThis:{};function ps(){if(!l().createContext)return{};const e=fs[ds]??=new Map;let t=e.get(l().createContext);return t||(t=l().createContext(null),e.set(l().createContext,t)),t}var hs=ps();var ms=function(e){const{children:t,context:n,serverState:r,store:o}=e,a=l().useMemo(()=>{const e=ls(o);return{store:o,subscription:e,getServerState:r?()=>r:void 0}},[o,r]),i=l().useMemo(()=>o.getState(),[o]);cs(()=>{const{subscription:e}=a;return e.onStateChange=e.notifyNestedSubs,e.trySubscribe(),i!==o.getState()&&e.notifyNestedSubs(),()=>{e.tryUnsubscribe(),e.onStateChange=void 0}},[a,i]);const s=n||hs;return l().createElement(s.Provider,{value:a},t)};function gs(e=hs){return function(){return l().useContext(e)}}var vs=gs();function bs(e=hs){const t=e===hs?vs:gs(e),n=()=>{const{store:e}=t();return e};return Object.assign(n,{withTypes:()=>n}),n}var ys=bs();function ws(e=hs){const t=e===hs?ys:bs(e),n=()=>t().dispatch;return Object.assign(n,{withTypes:()=>n}),n}var Ss=(e,t)=>e===t;function ks(e=hs){const t=e===hs?vs:gs(e),n=(e,n={})=>{const{equalityFn:r=Ss}="function"==typeof n?{equalityFn:n}:n;const o=t(),{store:a,subscription:i,getServerState:s}=o,u=(l().useRef(!0),l().useCallback({[e.name]:t=>e(t)}[e.name],[e])),c=(0,f().useSyncExternalStoreWithSelector)(i.addNestedSub,a.getState,s||a.getState,u,r);return l().useDebugValue(c),c};return Object.assign(n,{withTypes:()=>n}),n}const Es=e=>l().createElement("button",{type:"button","data-testid":e.testID,disabled:e.disabled&&!e.disabledTranslation,className:e.className,title:e.disabledTranslation&&e.disabled?e.disabledTranslation.title:e.title,onClick:t=>e.handleOnClick(t)},e.disabledTranslation&&e.disabled?e.disabledTranslation.label:e.label),Os=(0,l().forwardRef)((e,t)=>l().createElement("span",{...e.dragHandleAttributes,"data-testid":e.testID,ref:t,className:e.className,title:e.title},e.label)),Ns={name:"",value:"",label:""},Cs=e=>["atleast","atmost","exactly"].includes(cr(e)??""),Ds=[],As=e=>{const{match:t,handleOnChange:n}=e,r=l().useMemo(()=>"number"==typeof t.threshold?Math.max(0,t.threshold):1,[t.threshold]);return{thresholdNum:r,thresholdRule:l().useMemo(()=>({field:"",operator:"=",value:r}),[r]),thresholdSchema:l().useMemo(()=>({...e.schema,parseNumbers:!0}),[e.schema]),handleChangeMode:(0,l().useCallback)(e=>{Cs(e)&&"number"!=typeof t.threshold?n({...t,mode:e,threshold:1}):n({...t,mode:e})},[n,t]),handleChangeThreshold:(0,l().useCallback)(e=>{n({...t,threshold:uo(e,{parseNumbers:!0})})},[n,t])}},Ts="QueryBuilder was rendered with a truthy independentCombinators prop. This prop is deprecated and unnecessary. Furthermore, the initial query/defaultQuery prop was of type RuleGroupType instead of type RuleGroupIC. More info: https://react-querybuilder.js.org/docs/components/querybuilder#independent-combinators",$s="QueryBuilder was rendered with the deprecated and unnecessary independentCombinators prop. To use independent combinators, make sure the query/defaultQuery prop is of type RuleGroupIC when the component mounts. More info: https://react-querybuilder.js.org/docs/components/querybuilder#independent-combinators",Ms="A custom RuleGroup component has rendered a standard RuleGroup component with deprecated props. The combinator, not, and rules props should not be used. Instead, the full group object should be passed as the ruleGroup prop.",Rs="A custom RuleGroup component has rendered a standard Rule component with deprecated props. The field, operator, value, and valueSource props should not be used. Instead, the full rule object should be passed as the rule prop.",xs="QueryBuilder was rendered with both query and defaultQuery props. QueryBuilder must be either controlled or uncontrolled (specify either the query prop, or the defaultQuery prop, but not both). Decide between using a controlled or uncontrolled query builder and remove one of these props. More info: https://reactjs.org/link/controlled-components",Is="QueryBuilder is changing from an uncontrolled component to be controlled. This is likely caused by the query changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled query builder for the lifetime of the component. More info: https://reactjs.org/link/controlled-components",Ps="QueryBuilder is changing from a controlled component to be uncontrolled. This is likely caused by the query changing from defined to undefined, which should not happen. Decide between using a controlled or uncontrolled query builder for the lifetime of the component. More info: https://reactjs.org/link/controlled-components",_s="QueryBuilder was rendered with the enableDragAndDrop prop set to true, but either react-dnd was not detected or one of react-dnd-html5-backend or react-dnd-touch-backend was not detected. To enable drag-and-drop functionality, install react-dnd and one of the backend packages and wrap QueryBuilder in QueryBuilderDnD from @react-querybuilder/dnd.",Ls="Importing from react-querybuilder/debug is deprecated. To enable Redux DevTools for React Query Builder's internal store, set globalThis.__RQB_DEVTOOLS__ = true.",Gs="QueryBuilder was rendered with a qbId prop that is already in use by another mounted QueryBuilder. Each mounted query builder must have a unique qbId, so this query builder has fallen back to an automatically generated identifier. A qbId may be reused after the previous query builder has unmounted (see the preserveQueryStateOnUnmount prop), but not while it is still mounted.",Fs="QueryBuilder was rendered with the showUndoRedo prop set to true, but no undo/redo controls were available. To enable undo/redo, wrap QueryBuilder in the QueryBuilderHistory component from react-querybuilder/history, or provide your own controlElements.undoRedoActions component.",zs="QueryBuilder was rendered with a qbId prop that is different than the qbId it was mounted with. The qbId prop is only evaluated when the component mounts, so the new value has been ignored. To render a query builder with a different qbId, unmount the existing one first, e.g. by giving it a different React key.",js=Xl({name:"queries",initialState:{},reducers:{setQueryState:{prepare:(e,t)=>({payload:e,meta:{timestamp:t?.timestamp??Date.now(),fromHistory:!!t?.fromHistory}}),reducer:(e,{payload:{qbId:t,query:n}})=>{e[t]=n}},unsetQueryState:(e,{payload:{qbId:t}})=>{delete e[t]}},selectors:{getQuerySelectorById:(e,t)=>e[t]}}),qs=l().createContext(null),Bs=Xl({name:"warnings",initialState:{[Ts]:!1,[$s]:!1,[Ms]:!1,[Rs]:!1,[xs]:!1,[Is]:!1,[Ps]:!1,[_s]:!1,[Ls]:!1,[Gs]:!1,[zs]:!1,[Fs]:!1},reducers:{rqbWarn:(e,{payload:t})=>{e[t]||(console.error(t),e[t]=!0)}}}),Vs=function(...e){const t=Object.fromEntries((e=>e.flatMap(e=>{return"reducerPath"in(t=e)&&"string"==typeof t.reducerPath?[[e.reducerPath,e.reducer]]:Object.entries(e);var t}))(e)),n=()=>Object.keys(t).length?$l(t):rs;let r=n();function o(e,t){return r(e,t)}o.withLazyLoadedSlices=()=>o;const a={},i=Object.assign(function(e,n){return function(r,...o){return e(((e,t,n)=>Fl(es,e,()=>new Proxy(e,{get:(e,r,o)=>{if(r===Zl)return e;const a=Reflect.get(e,r,o);if(void 0===a){const e=n[r];if(void 0!==e)return e;const o=t[r];if(o){const e=o(void 0,{type:Hl()});if(void 0===e)throw new Error(os(24));return n[r]=e,e}}return a}})))(n?n(r,...o):r,t,a),...o)}},{original:ts});return Object.assign(o,{inject:(e,i={})=>{const{reducerPath:l,reducer:s}=e,u=t[l];return!i.overrideExisting&&u&&u!==s||(i.overrideExisting&&u!==s&&delete a[l],t[l]=s,r=n()),o},selector:i})}(js,Bs).withLazyLoadedSlices(),Hs=e=>ws(e),Us=e=>bs(e),Ws=e=>ks(e),Qs=new Map,Ks=e=>{const t=(Qs.get(e)??0)-1;return t>0?(Qs.set(e,t),t):(Qs.delete(e),0)},Xs=new Map,Ys=({payload:e,onQueryChange:t,options:n})=>r=>{r(js.actions.setQueryState(e,n)),"function"==typeof t&&t(e.query)},Js={useRQB_INTERNAL_QueryBuilderDispatch:Hs(Zs=qs),useRQB_INTERNAL_QueryBuilderStore:Us(Zs),useRQB_INTERNAL_QueryBuilderSelector:Ws(Zs)};var Zs;const eu=Js.useRQB_INTERNAL_QueryBuilderDispatch,tu=Js.useRQB_INTERNAL_QueryBuilderStore,nu=Js.useRQB_INTERNAL_QueryBuilderSelector,{rqbWarn:ru}=Bs.actions,ou={reducer:Vs,preloadedState:{queries:js.getInitialState(),warnings:Bs.getInitialState()},middleware:e=>e({serializableCheck:{ignoredActions:[js.actions.setQueryState.type],ignoredPaths:[/^queries\b.*\.rules\.\d+\.value$/]}})},au=e=>{eu();const{defaultQuery:t,queryProp:n}=e;(e=>{const[t,n]=(0,l().useState)(e),[r,o]=(0,l().useState)(null);e!==t&&(o(t),n(e))})(!!n)};function iu(e,t,n){eu()}const lu=e=>{const{optionList:t,optionsMap:n,defaultOption:r}=(0,l().useMemo)(()=>Jr({placeholder:e.translations.fields,optionList:e.fields,autoSelectOption:e.autoSelectField,baseOption:e.baseField}),[e.autoSelectField,e.baseField,e.fields,e.translations.fields]);return{fields:t,fieldMap:n,defaultField:r}};let su=null;function uu(e){if(!su){const t=e||globalThis?.__RQB_DEVTOOLS__;su=(e=>{const t=Bl({...ou,devTools:!!e&&/* v8 ignore next -- @preserve */{name:"React Query Builder"}});return t.addSlice=e=>{Vs.inject(e),t.dispatch({type:crypto.randomUUID().slice(0,8),meta:`Initializing state for slice "${e.name}"`})},t})(t)}return su}const cu=e=>t=>js.selectors.getQuerySelectorById({queries:t.queries},e),du={},fu={},pu=[],hu={},mu=()=>null,gu=()=>"",vu=()=>!0,bu=()=>{},yu=(...e)=>{console.log(...e)};function wu(e,t){const{query:n,defaultQuery:r,getValueEditorSeparator:o=mu,getRuleClassname:a=gu,getRuleGroupClassname:i=gu,onAddRule:s=vu,onAddGroup:u=vu,onMoveRule:c=vu,onMoveGroup:d=vu,onGroupRule:f=vu,onGroupGroup:p=vu,onRemove:h=vu,onQueryChange:m,showCombinatorsBetweenRules:g=!1,showNotToggle:v=!1,showShiftActions:b=!1,showCloneButtons:y=!1,showLockButtons:w=!1,showMuteButtons:S=!1,suppressStandardClassnames:k=!1,preserveQueryStateOnUnmount:E=!1,resetOnFieldChange:O=!0,resetOnOperatorChange:N=!1,autoSelectField:C=!0,autoSelectOperator:D=!0,autoSelectValue:A=!0,addRuleToNewGroups:T=!1,listsAsArrays:$=!1,parseNumbers:M=!1,disabled:R=!1,validator:x,onLog:I=yu,idGenerator:P,accessibleDescriptionGenerator:_=ui}=e,{qbId:L,resolveQbIdCollision:G,rqbContext:F,fields:z,fieldMap:j,combinators:q,getParameters:B,getOperatorsMain:V,getMatchModesMain:H,getRuleDefaultOperator:U,getSubQueryBuilderPropsMain:W,getValueEditorTypeMain:Q,getValueSourcesMain:K,getValuesMain:X,getRuleDefaultValue:Y,getInputTypeMain:J,createRule:Z,createRuleGroup:ee}=t,{controlClassnames:te,controlElements:ne,debugMode:re,enableDragAndDrop:oe,enableMountQueryChange:ae,showUndoRedo:ie,translations:le}=F,se=!!g,ue=!!v,ce=!!b,de=!!ie,fe=!!y,pe=!!w,he=!!S,me=!!O,ge=!!N,ve=!!C,be=!!D,ye=!!A,we=!!T,Se=!!$,ke=!!k,Ee=!!E,Oe=(e.maxLevels??0)>0?Number(e.maxLevels):1/0,Ne=(0,l().useCallback)((...e)=>{re&&I(...e)},[re,I]);qu(de,!!ne.undoRedoActions),au({defaultQuery:r,queryProp:n});const Ce=tu(),De=eu(),Ae=(0,l().useMemo)(()=>cu(L),[L]),Te=((e,t)=>{const n=l().useContext(ku);return nu(e,t)??n?.initialQuery})(Ae),$e=(0,l().useCallback)(()=>Ae(Ce.getState()),[Ce,Ae]),Me=(0,l().useMemo)(()=>ee(),[ee]),Re=((e,t)=>{const n=e.query??e.storeQuery??e.defaultQuery??e.fallbackQuery;return n.id?n:Ri(n,t)})({query:n,storeQuery:Te,defaultQuery:r,fallbackQuery:Me},{idGenerator:P}),[xe]=(0,l().useState)(Re),Ie=(0,l().useMemo)(()=>({...F,initialQuery:xe}),[F,xe]);(0,l().useEffect)(()=>{n&&!Object.is(n,Te)&&De(Ys({payload:{qbId:L,query:n},onQueryChange:void 0}))},[n,L,Te,De]);const Pe=(0,l().useMemo)(()=>gr(Re),[Re]),_e=!!e.independentCombinators&&!Pe;iu(0,_e||!_e&&e.independentCombinators);const Le=(0,l().useRef)(!1);(0,l().useEffect)(()=>{Le.current||(Le.current=!0,De(Ys({payload:{qbId:L,query:Re},onQueryChange:ae&&"function"==typeof m?m:void 0})))},[ae,m,L,De,Re]);const Ge=(0,l().useRef)(Re);(0,l().useEffect)(()=>{Ge.current=Re},[Re]);const Fe=(0,l().useRef)(Ee);(0,l().useEffect)(()=>{Fe.current=Ee},[Ee]),(0,l().useEffect)(()=>(e=>{const t=(Qs.get(e)??0)+1;return Qs.set(e,t),t})(L)>1?(Ks(L),G(),bu):(Ae(Ce.getState())||De(Ys({payload:{qbId:L,query:Ge.current},onQueryChange:void 0})),()=>{0!==Ks(L)||Fe.current||De(js.actions.unsetQueryState({qbId:L}))}),[L,De,Ce,Ae,G]);const ze=(0,l().useCallback)((e,t)=>{De(Ys({payload:{qbId:L,query:e},onQueryChange:m,options:t}))},[m,L,De]),je=(0,l().useRef)(ze);(0,l().useEffect)(()=>{je.current=ze},[ze]),(0,l().useEffect)(()=>(((e,t)=>{Xs.set(e,t)})(L,(e,t)=>je.current(e,t)),()=>(e=>{Xs.delete(e)})(L)),[L]);const qe=Array.isArray(R)&&R||pu,Be=!0===R,Ve=Re.disabled||qe.some(e=>0===e.length),He=(0,l().useMemo)(()=>((e={})=>{const{qbId:t,combinators:n,idGenerator:r,maxLevels:o=1/0,queryDisabled:a,respectDisabled:i=!0,disabledPaths:l,resetOnFieldChange:s,resetOnOperatorChange:u,getRuleDefaultOperator:c,getValueSources:d,getRuleDefaultValue:f,getMatchModes:p,freeze:h,onAddRule:m,onAddGroup:g,onRemove:v,onMoveRule:b,onMoveGroup:y,onGroupRule:w,onGroupGroup:S,onLog:k}=e,E=e=>k?.({qbId:t,...e}),O={respectDisabled:i,queryDisabled:a,disabledPaths:l},N=(e,t,n=!1)=>!!Pi(e,t,O,{asParent:n});return{addRule:(e,t,o,a)=>{if(N(e,o,!0))return void E({type:jn,rule:t,parentPath:o,query:e});const i=!m||m(t,o,e,a);if(!i)return void E({type:Bn,rule:t,parentPath:o,query:e});const l=!0===i?t:i,s=Li(e,l,o,{combinators:n,combinatorPreceding:l.combinatorPreceding,idGenerator:r,freeze:h});return E({type:Xn,query:e,newQuery:s,newRule:l,parentPath:o}),s},addGroup:(e,t,a,i)=>{if(a.length>=o)return;if(N(e,a,!0))return void E({type:jn,ruleGroup:t,parentPath:a,query:e});const l=!g||g(t,a,e,i);if(!l)return void E({type:Vn,ruleGroup:t,parentPath:a,query:e});const s=!0===l?t:l,u=Li(e,s,a,{combinators:n,combinatorPreceding:s.combinatorPreceding??void 0,idGenerator:r,freeze:h});return E({type:Xn,query:e,newQuery:u,newGroup:s,parentPath:a}),u},propChange:(e,t,n,r)=>{if(Pi(e,r,"disabled"===t?{queryDisabled:a}:O))return void E({type:qn,path:r,prop:t,value:n,query:e});const o=qi(e,t,n,r,{resetOnFieldChange:s,resetOnOperatorChange:u,getRuleDefaultOperator:c,getValueSources:d,getRuleDefaultValue:f,getMatchModes:p,freeze:h});return E({type:Jn,query:e,newQuery:o,prop:t,value:n,path:r}),o},removeRuleOrGroup:(e,t,n)=>{if(N(e,t))return void E({type:qn,path:t,query:e});const r=ti(t,e); +/* v8 ignore next -- @preserve */if(!r)return;if(v&&!v(r,t,e,n))return void E({type:Kn,ruleOrGroup:r,path:t,query:e});const o=Vi(e,t,{freeze:h});return E({type:Yn,query:e,newQuery:o,path:t,ruleOrGroup:r}),o},moveRule:(e,t,o,a,i)=>{if(N(e,t))return void E({type:qn,oldPath:t,newPath:o,query:e});const l=ti(t,e);if(!l)return;const s=Wi(e,t,o,{clone:a,combinators:n,idGenerator:r,freeze:h}),u=hr(l),c=u?y:b,d=!c||c(l,t,o,e,s,{clone:a,combinators:n},i);if(!d)return void E({type:u?Qn:Wn,ruleOrGroup:l,oldPath:t,newPath:o,clone:a,query:e,nextQuery:s});const f=hr(d)?d:s;return E({type:Zn,query:e,newQuery:f,oldPath:t,newPath:o,clone:a}),f},groupRule:(e,t,o,a,i)=>{if(N(e,t))return void E({type:qn,sourcePath:t,targetPath:o,query:e});const l=ti(t,e);if(!l)return;const s=Yi(e,t,o,{clone:a,combinators:n,idGenerator:r,freeze:h}),u=hr(l),c=u?S:w,d=!c||c(l,t,o,e,s,{clone:a,combinators:n},i);if(!d)return void E({type:u?Un:Hn,ruleOrGroup:l,sourcePath:t,targetPath:o,clone:a,query:e,nextQuery:s});const f=hr(d)?d:s;return E({type:er,query:e,newQuery:f,sourcePath:t,targetPath:o,clone:a}),f}}})({qbId:L,combinators:q,idGenerator:P,maxLevels:Oe,queryDisabled:Be,disabledPaths:qe,resetOnFieldChange:me,resetOnOperatorChange:ge,getRuleDefaultOperator:U,getValueSources:K,getRuleDefaultValue:Y,getMatchModes:H,onAddRule:s,onAddGroup:u,onRemove:h,onMoveRule:c,onMoveGroup:d,onGroupRule:f,onGroupGroup:p,onLog:Ne}),[q,H,U,Y,K,P,Ne,Oe,u,s,p,f,d,c,h,L,Be,qe,me,ge]),Ue=(0,l().useCallback)(e=>{const t=cu(L)(Ce.getState());if(!t)return;const n=e(t);n&&ze(n)},[ze,L,Ce]),We=(0,l().useCallback)((e,t,n)=>{Ue(r=>He.addRule(r,e,t,n))},[He,Ue]),Qe=(0,l().useCallback)((e,t,n)=>{Ue(r=>He.addGroup(r,e,t,n))},[He,Ue]),Ke=(0,l().useCallback)((e,t,n)=>{Ue(r=>He.propChange(r,e,t,n))},[He,Ue]),Xe=(0,l().useCallback)((e,t)=>{Ue(n=>He.removeRuleOrGroup(n,e,t))},[He,Ue]),Ye=(0,l().useCallback)((e,t,n,r)=>{Ue(o=>He.moveRule(o,e,t,n,r))},[He,Ue]),Je=(0,l().useCallback)((e,t,n,r)=>{Ue(o=>He.groupRule(o,e,t,n,r))},[He,Ue]),{validationResult:Ze,validationMap:et}=(0,l().useMemo)(()=>{const e="function"==typeof x&&Re?x(Re):du;return{validationResult:e,validationMap:"boolean"==typeof e?fu:e}},[Re,x]),tt=oe?"enabled":"disabled",nt=Pe||se?"enabled":"disabled",rt=(0,l().useMemo)(()=>"string"==typeof Re.combinator?{combinator:Re.combinator}:hu,[Re.combinator]),ot=(0,l().useMemo)(()=>Ir("queryBuilder",$r,{classNames:te,suppressStandardClassnames:ke,disabled:Be,validationResult:Ze}),[te,Be,ke,Ze]),at=(0,l().useCallback)(e=>ee(e??Pe),[ee,Pe]),it=(0,l().useMemo)(()=>({addRuleToNewGroups:we,accessibleDescriptionGenerator:_,autoSelectField:ve,autoSelectOperator:be,autoSelectValue:ye,classNames:te,combinators:q,getParameters:B,controls:ne,createRule:Z,createRuleGroup:at,disabledPaths:qe,enableDragAndDrop:oe,fieldMap:j,fields:z,dispatchQuery:ze,getQuery:$e,getInputType:J,getOperators:V,getMatchModes:H,getRuleClassname:a,getRuleDefaultOperator:U,getRuleDefaultValue:Y,getRuleGroupClassname:i,getSubQueryBuilderProps:W,getValueEditorSeparator:o,getValueEditorType:Q,getValues:X,getValueSources:K,independentCombinators:Pe,listsAsArrays:Se,maxLevels:Oe,parseNumbers:M,qbId:L,resetOnFieldChange:me,resetOnOperatorChange:ge,showCloneButtons:fe,showCombinatorsBetweenRules:se,showLockButtons:pe,showMuteButtons:he,showNotToggle:ue,showShiftActions:ce,showUndoRedo:de,suppressStandardClassnames:ke,validationMap:et}),[_,we,ve,be,ye,q,te,ne,Z,at,qe,ze,oe,j,z,J,V,H,$e,a,U,Y,i,W,o,Q,X,K,B,Pe,Se,Oe,M,L,me,ge,fe,se,pe,he,ue,ce,de,ke,et]);return{actions:(0,l().useMemo)(()=>({moveRule:Ye,onGroupAdd:Qe,onGroupRemove:Xe,onPropChange:Ke,onRuleAdd:We,onRuleRemove:Xe,groupRule:Je}),[Je,Ye,Qe,Ke,We,Xe]),rootGroup:Re,rootGroupDisabled:Ve,queryDisabled:Be,rqbContext:Ie,schema:it,translations:le,wrapperClassName:ot,dndEnabledAttr:tt,inlineCombinatorsAttr:nt,combinatorPropObject:rt}}const Su=e=>wu(e,(e=>{const{qbId:t,resolveQbIdCollision:n}=Fu(e.qbId),{fields:r,baseField:o,operators:a,baseOperator:i,combinators:s,baseCombinator:u,getParameters:c,translations:d,enableMountQueryChange:f=!0,controlClassnames:p,controlElements:h,getDefaultField:m,getDefaultOperator:g,getDefaultValue:v,getMatchModes:b,getOperators:y,getSubQueryBuilderProps:w,getValueEditorType:S,getValueSources:k,getInputType:E,getValues:O,autoSelectField:N=!0,autoSelectOperator:C=!0,autoSelectValue:D=!0,addRuleToNewGroups:A=!1,enableDragAndDrop:T,showUndoRedo:$,listsAsArrays:M=!1,debugMode:R=!1,idGenerator:x=di}=e,[I]=(0,l().useState)(e.query??e.defaultQuery),P=Lu({controlClassnames:p,controlElements:h,debugMode:R,enableDragAndDrop:T,enableMountQueryChange:f,showUndoRedo:$,translations:d,initialQuery:I,qbId:t,finalize:!0}),{translations:_}=P,{fields:L,fieldMap:G}=lu({fields:r,baseField:o,autoSelectField:N,translations:_}),{optionList:F}=(0,l().useMemo)(()=>Jr({optionList:s??fn,labelMap:cn,baseOption:u,autoSelectOption:!0}),[u,s]),z=(0,l().useCallback)((e,t,n)=>Jr({optionList:c?.(e,t,n)??[],autoSelectOption:!0}).optionList,[c]),{optionList:j}=(0,l().useMemo)(()=>Jr({optionList:a??dn,placeholder:_.operators,labelMap:un,baseOption:i,autoSelectOption:C}),[C,i,a,_.operators]),q=(0,l().useCallback)((e,{fieldData:t})=>Oi({field:e,fieldData:t,getOperators:y,operators:j,placeholder:_.operators,baseOption:i,autoSelectOption:C}),[C,i,y,j,_.operators]),B=(0,l().useCallback)(e=>Ni({field:e,fieldData:G[e],getDefaultOperator:g,getOperators:(e,t)=>q(e,t)}),[G,g,q]),V=(0,l().useCallback)((e,t,{fieldData:n})=>Ci({field:e,operator:t,fieldData:n,getValueEditorType:S}),[S]),H=(0,l().useCallback)((e,t,n)=>Si(G[e],t,k),[G,k]),U=(0,l().useCallback)((e,t)=>mi(G[e],b),[G,b]),W=(0,l().useCallback)((e,t)=>w?.(e,t)??{},[w]),Q=(0,l().useCallback)((e,t,{fieldData:n})=>Di({field:e,operator:t,fieldData:n,getValues:O,placeholder:_.values,autoSelectOption:D}),[D,O,_.values]),K=(0,l().useCallback)(e=>vi(e,{fieldData:G[e.field]??{},fields:L,getParameters:(e,t,n)=>z(e,t,n),listsAsArrays:M,getValueEditorType:(e,t,n)=>V(e,t,n),getValues:(e,t,n)=>Q(e,t,n),getDefaultValue:v&&((e,t)=>v(e,t))}),[G,L,v,V,Q,M,z]),X=(0,l().useCallback)((e,t,{fieldData:n})=>{if(E){const r=E(e,t,{fieldData:n});if(r)return r}return"text"},[E]),Y=(0,l().useCallback)(()=>Zi({fields:L,getDefaultField:m,getRuleDefaultOperator:e=>B(e),getValueSources:(e,t,n)=>H(e,t,n),getMatchModes:(e,t)=>U(e,t),getRuleDefaultValue:e=>K(e),idGenerator:x}),[L,m,U,B,K,H,x]);return{qbId:t,resolveQbIdCollision:n,rqbContext:P,fields:L,fieldMap:G,combinators:F,getParameters:z,getMatchModesMain:U,getOperatorsMain:q,getRuleDefaultOperator:B,getSubQueryBuilderPropsMain:W,getValueEditorTypeMain:V,getValueSourcesMain:H,getValuesMain:Q,getRuleDefaultValue:K,getInputTypeMain:X,createRule:Y,createRuleGroup:(0,l().useCallback)(e=>el({combinators:F,addRuleToNewGroups:A,createRule:Y,idGenerator:x},e),[A,F,Y,x])}})(e)),ku=(0,l().createContext)({}),Eu=l().memo(function(e){const t=Cu(e),{previewRef:n,dropRef:r,schema:{controls:{ruleGroupBodyElements:o,ruleGroupHeaderElements:a}}}=t,i=ju(t.addRule),s=ju(t.addGroup),u=ju(t.cloneGroup),c=ju(t.toggleLockGroup),d=ju(t.toggleMuteGroup),f=ju(t.removeGroup),p=ju(t.shiftGroupUp),h=ju(t.shiftGroupDown),m=(0,l().useMemo)(()=>({addRule:i,addGroup:s,cloneGroup:u,toggleLockGroup:c,toggleMuteGroup:d,removeGroup:f,shiftGroupUp:p,shiftGroupDown:h}),[i,s,u,c,d,f,p,h]);return l().createElement("div",{ref:n,title:t.accessibleDescription,className:t.outerClassName,"data-testid":yn,"data-not":t.ruleGroup.not?"true":void 0,"data-dragmonitorid":t.dragMonitorId,"data-dropmonitorid":t.dropMonitorId,"data-rule-group-id":t.id,"data-level":t.path.length,"data-path":JSON.stringify(t.path)},l().createElement("div",{ref:r,className:t.classNames.header},l().createElement(a,{...t,...m})),l().createElement("div",{className:t.classNames.body},l().createElement(o,{...t,...m})))}),Ou=l().memo(function(e){const{dragRef:t,schema:{controls:{shiftActions:n,undoRedoActions:r,dragHandle:o,combinatorSelector:a,notToggle:i,addRuleAction:s,addGroupAction:u,cloneGroupAction:c,lockGroupAction:d,muteGroupAction:f,removeGroupAction:p}}}=e,h=(0,l().useMemo)(()=>({level:e.path.length,path:e.path,disabled:e.disabled,context:e.context,validation:e.validationResult,schema:e.schema}),[e.path,e.disabled,e.context,e.validationResult,e.schema]),m=(0,l().useMemo)(()=>e.schema.showShiftActions?{shiftUp:e.translations.shiftActionUp.title,shiftDown:e.translations.shiftActionDown.title}:void 0,[e.schema.showShiftActions,e.translations]),g=(0,l().useMemo)(()=>e.schema.showShiftActions?{shiftUp:e.translations.shiftActionUp.label,shiftDown:e.translations.shiftActionDown.label}:void 0,[e.schema.showShiftActions,e.translations]),v=(0,l().useMemo)(()=>e.schema.showUndoRedo?{undo:e.translations.undo.title,redo:e.translations.redo.title}:void 0,[e.schema.showUndoRedo,e.translations]),b=(0,l().useMemo)(()=>e.schema.showUndoRedo?{undo:e.classNames.undoAction,redo:e.classNames.redoAction}:void 0,[e.classNames.redoAction,e.classNames.undoAction,e.schema.showUndoRedo]),y=(0,l().useMemo)(()=>e.schema.showUndoRedo?{undo:e.translations.undo.label,redo:e.translations.redo.label}:void 0,[e.schema.showUndoRedo,e.translations]);return l().createElement(l().Fragment,null,e.schema.showShiftActions&&e.path.length>0&&l().createElement(n,{key:Rn,...h,testID:Rn,titles:m,labels:g,className:e.classNames.shiftActions,shiftUp:e.shiftGroupUp,shiftDown:e.shiftGroupDown,shiftUpDisabled:e.shiftUpDisabled,shiftDownDisabled:e.shiftDownDisabled,ruleOrGroup:e.ruleGroup}),e.path.length>0&&e.schema.enableDragAndDrop&&l().createElement(o,{key:In,...h,testID:In,ref:t,title:e.translations.dragHandle.title,label:e.translations.dragHandle.label,className:e.classNames.dragHandle,dragHandleAttributes:e.dragHandleAttributes,ruleOrGroup:e.ruleGroup}),!e.schema.showCombinatorsBetweenRules&&!e.schema.independentCombinators&&l().createElement(a,{key:Dn,...h,testID:Dn,options:e.schema.combinators,value:e.combinator,title:e.translations.combinators.title,className:e.classNames.combinators,handleOnChange:e.onCombinatorChange,rules:e.ruleGroup.rules,ruleGroup:e.ruleGroup}),e.schema.showNotToggle&&l().createElement(i,{key:Mn,...h,testID:Mn,className:e.classNames.notToggle,title:e.translations.notToggle.title,label:e.translations.notToggle.label,checked:e.ruleGroup.not,handleOnChange:e.onNotToggleChange,ruleGroup:e.ruleGroup}),l().createElement(s,{key:Nn,...h,testID:Nn,label:e.translations.addRule.label,title:e.translations.addRule.title,className:e.classNames.addRule,handleOnClick:e.addRule,rules:e.ruleGroup.rules,ruleOrGroup:e.ruleGroup}),e.schema.maxLevels>e.path.length&&l().createElement(u,{key:Sn,...h,testID:Sn,label:e.translations.addGroup.label,title:e.translations.addGroup.title,className:e.classNames.addGroup,handleOnClick:e.addGroup,rules:e.ruleGroup.rules,ruleOrGroup:e.ruleGroup}),e.schema.showCloneButtons&&e.path.length>0&&l().createElement(c,{key:En,...h,testID:En,label:e.translations.cloneRuleGroup.label,title:e.translations.cloneRuleGroup.title,className:e.classNames.cloneGroup,handleOnClick:e.cloneGroup,rules:e.ruleGroup.rules,ruleOrGroup:e.ruleGroup}),e.schema.showLockButtons&&l().createElement(d,{key:_n,...h,testID:_n,label:e.translations.lockGroup.label,title:e.translations.lockGroup.title,className:e.classNames.lockGroup,handleOnClick:e.toggleLockGroup,rules:e.ruleGroup.rules,disabledTranslation:e.parentDisabled?void 0:e.translations.lockGroupDisabled,ruleOrGroup:e.ruleGroup}),e.schema.showMuteButtons&&l().createElement(f,{key:Gn,...h,testID:Gn,label:e.ruleGroup.muted?e.translations.unmuteGroup.label:e.translations.muteGroup.label,title:e.ruleGroup.muted?e.translations.unmuteGroup.title:e.translations.muteGroup.title,className:e.classNames.muteGroup,handleOnClick:e.toggleMuteGroup,rules:e.ruleGroup.rules,ruleOrGroup:e.ruleGroup}),e.schema.showUndoRedo&&0===e.path.length&&r&&l().createElement(r,{key:xn,...h,testID:xn,titles:v,labels:y,className:e.classNames.undoRedoActions,classNames:b,ruleOrGroup:e.ruleGroup}),e.path.length>0&&l().createElement(p,{key:kn,...h,testID:kn,label:e.translations.removeGroup.label,title:e.translations.removeGroup.title,className:e.classNames.removeGroup,handleOnClick:e.removeGroup,rules:e.ruleGroup.rules,ruleOrGroup:e.ruleGroup}))}),Nu=l().memo(function(e){const{schema:{controls:{combinatorSelector:t,inlineCombinator:n,ruleGroup:r,rule:o}}}=e;return l().createElement(l().Fragment,null,e.ruleGroup.rules.map((a,i,{length:s})=>{const u=e.pathsMemo[i],c=u.path,d=u.disabled||"string"!=typeof a&&a.disabled,f=0===e.path.length&&0===i,p=0===e.path.length&&i===s-1,h="string"==typeof a?[...c,a].join("-"):a.id;return l().createElement(l().Fragment,{key:h},i>0&&!e.schema.independentCombinators&&e.schema.showCombinatorsBetweenRules&&l().createElement(n,{key:wn,options:e.schema.combinators,value:e.combinator,title:e.translations.combinators.title,className:e.classNames.combinators,handleOnChange:e.onCombinatorChange,rules:e.ruleGroup.rules,level:e.path.length,context:e.context,validation:e.validationResult,component:t,path:c,disabled:e.disabled,schema:e.schema,ruleGroup:e.ruleGroup}),"string"==typeof a?l().createElement(n,{key:`${wn}-independent`,options:e.schema.combinators,value:a,title:e.translations.combinators.title,className:e.classNames.combinators,handleOnChange:t=>e.onIndependentCombinatorChange(t,i),rules:e.ruleGroup.rules,level:e.path.length,context:e.context,validation:e.validationResult,component:t,path:c,disabled:d,schema:e.schema,ruleGroup:e.ruleGroup}):hr(a)?l().createElement(r,{key:yn,id:a.id,schema:e.schema,actions:e.actions,path:c,translations:e.translations,ruleGroup:a,rules:a.rules,combinator:mr(a)?a.combinator:void 0,not:!!a.not,disabled:d,parentDisabled:e.parentDisabled||e.disabled,parentMuted:e.parentMuted||e.muted,shiftUpDisabled:f,shiftDownDisabled:p,context:e.context}):l().createElement(o,{key:bn,id:a.id,rule:a,field:a.field,operator:a.operator,value:a.value,valueSource:a.valueSource,schema:e.schema,actions:e.actions,path:c,disabled:d,parentDisabled:e.parentDisabled||e.disabled,parentMuted:e.parentMuted||e.muted,translations:e.translations,shiftUpDisabled:f,shiftDownDisabled:p,context:e.context}))}))}),Cu=e=>{const{id:t,path:n,ruleGroup:r,schema:{qbId:o,accessibleDescriptionGenerator:a,classNames:i,combinators:s,createRule:u,createRuleGroup:c,disabledPaths:d,independentCombinators:f,validationMap:p,enableDragAndDrop:h,getRuleGroupClassname:m,suppressStandardClassnames:g},actions:{onGroupAdd:v,onGroupRemove:b,onPropChange:y,onRuleAdd:w,moveRule:S},disabled:k,parentDisabled:E,parentMuted:O,shiftUpDisabled:N,shiftDownDisabled:C,combinator:D,rules:A,not:T,dropEffect:$="move",groupItems:M=!1,dragMonitorId:R="",dropMonitorId:x="",previewRef:I=null,dragRef:P=null,dropRef:_=null,isDragging:L=!1,isOver:G=!1,dropNotAllowed:F=!1}=e;iu(),zu(h,!!(R||x||I||P||_));const z=!!E||!!k,j=!!O||!!r?.muted,q=(0,l().useMemo)(()=>r?so(r,s):D??Kr(s),[D,s,r]),B=(0,l().useMemo)(()=>{if(r){if(r.combinator===q||f)return r;const e=structuredClone(r);return e.combinator=q,e}return{rules:A,not:T}},[q,f,T,r,A]),V=(0,l().useMemo)(()=>{return xr(Dr,e={classNames:i,suppressStandardClassnames:g,isOver:G,dropEffect:$,dropNotAllowed:F},e);var e},[i,$,F,G,g]),H=(0,l().useCallback)(e=>{z||y("combinator",e,n)},[z,y,n]),U=(0,l().useCallback)((e,t,r)=>{z||y("combinator",e,[...n,t])},[z,y,n]),W=(0,l().useCallback)((e,t)=>{z||y("not",e,n)},[z,y,n]),Q=(0,l().useCallback)((e,t)=>{if(!z){const e=u();w(e,n,t)}},[u,z,w,n]),K=(0,l().useCallback)((e,t)=>{if(!z){const e=c();v(e,n,t)}},[c,z,v,n]),X=(0,l().useCallback)(()=>{if(!z){const e=[...oi(n),n.at(-1)+1];S(n,e,!0)}},[z,S,n]),Y=(0,l().useCallback)((e,t)=>{z||N||S(n,"up",e?.altKey)},[z,S,n,N]),J=(0,l().useCallback)((e,t)=>{z||C||S(n,"down",e?.altKey)},[z,S,n,C]),Z=(0,l().useCallback)(()=>{y("disabled",!z,n)},[z,y,n]),ee=(0,l().useCallback)(()=>{y("muted",!B.muted,n)},[B.muted,y,n]),te=(0,l().useCallback)(()=>{z||b(n)},[z,b,n]),ne=p[t??/* v8 ignore start -- @preserve */""],re=(0,l().useMemo)(()=>bi(ne),[ne]),oe=(0,l().useMemo)(()=>f?null:Qr(s,q)?.className??"",[q,s,f]),ae=(0,l().useMemo)(()=>m(B),[m,B]),ie=(0,l().useMemo)(()=>Ir("ruleGroup",Tr,{classNames:i,suppressStandardClassnames:g,leadingClassNames:[ae,oe],disabled:z,muted:j,isDragging:L,isOver:G,groupItems:M,validationClassName:re}),[i,oe,z,j,M,L,G,ae,g,re]),le=Gu({disabled:z,disabledPaths:d,path:n,nestedArray:B.rules}),se=(0,l().useMemo)(()=>a({path:n,qbId:o}),[a,n,o]);return{...e,addGroup:K,addRule:Q,accessibleDescription:se,classNames:V,cloneGroup:X,combinator:q,disabled:z,dragMonitorId:R,dragRef:P,dropMonitorId:x,dropRef:_,isDragging:L,isOver:G,muted:j,onCombinatorChange:H,onGroupAdd:v,onIndependentCombinatorChange:U,onNotToggleChange:W,outerClassName:ie,parentDisabled:E,pathsMemo:le,previewRef:I,removeGroup:te,ruleGroup:B,shiftGroupUp:Y,shiftGroupDown:J,toggleLockGroup:Z,toggleMuteGroup:ee,validationClassName:re,validationResult:ne}},Du=({name:e,disabled:t,checked:n,label:r,handleOnChange:o})=>{const a=l().useId();return l().createElement("label",{htmlFor:a},l().createElement("input",{id:a,type:"radio",value:e,disabled:t,checked:n,onChange:e=>o(e.target.value)}),r)},Au=e=>{const{operator:t,value:n,handleOnChange:r,title:o,className:a,type:i="text",inputType:s,values:u=[],listsAsArrays:c,fieldData:d,disabled:f,separator:p=null,testID:h,selectorComponent:m=e.schema.controls.valueSelector,parseNumbers:g,skipHook:v,valueSource:b,...y}=e,{valueAsArray:w,multiValueHandler:S,bigIntValueHandler:k,parseNumberMethod:E,valueListItemClassName:O,inputTypeCoerced:N}=Tu(e);if("null"===t||"notNull"===t)return null;const C=d?.placeholder??"";if(!("between"!==t&&"notBetween"!==t||"select"!==i&&"text"!==i)){const t=["from","to"].map((t,n)=>"text"===i?l().createElement("input",{key:t,type:N,placeholder:C,value:w[n]??"",title:o,className:O,disabled:f,onChange:e=>S(e.target.value,n)}):l().createElement(m,{key:t,...y,schema:e.schema,title:o,className:O,handleOnChange:e=>S(e,n),disabled:f,value:w[n]??Kr(u),options:u,listsAsArrays:c}));return l().createElement("span",{"data-testid":h,className:a,title:o},t[0],p,t[1])}switch(i){case"select":case"multiselect":return l().createElement(m,{...y,schema:e.schema,testID:h,className:a,title:o,handleOnChange:r,disabled:f,value:n,options:u,multiple:"multiselect"===i,listsAsArrays:c});case"textarea":return l().createElement("textarea",{"data-testid":h,placeholder:C,value:n,title:o,className:a,disabled:f,onChange:e=>r(e.target.value)});case"switch":case"checkbox":return l().createElement("input",{"data-testid":h,type:"checkbox",className:a,title:o,onChange:e=>r(e.target.checked),checked:!!n,disabled:f});case"radio":return l().createElement("span",{"data-testid":h,className:a,title:o},u.map(e=>l().createElement(Du,{key:e.name,name:e.name,disabled:f,checked:n===e.name,handleOnChange:r,label:e.label})))}return"bigint"===s?l().createElement("input",{"data-testid":h,type:N,placeholder:C,value:`${n}`,title:o,className:a,disabled:f,onChange:e=>k(e.target.value)}):l().createElement("input",{"data-testid":h,type:N,placeholder:C,value:n,title:o,className:a,disabled:f,onChange:e=>r(uo(e.target.value,{parseNumbers:E}))})},Tu=e=>{const{handleOnChange:t,inputType:n,operator:r,value:o,listsAsArrays:a,parseNumbers:i,values:s,type:u,skipHook:c,schema:{classNames:d,suppressStandardClassnames:f}}=e;(0,l().useEffect)(()=>{const{reset:e,value:a}=(({skipHook:e,type:t,operator:n,value:r,inputType:o})=>e||"multiselect"===t||co.has(n)||!(Array.isArray(r)||"number"===o&&"string"==typeof r&&r.includes(","))?{reset:!1,value:r}:{reset:!0,value:lr(r,{retainEmptyStrings:!0})[0]??""})({skipHook:c,type:u??void 0,operator:r,value:o,inputType:n});e&&t(a)},[t,n,r,c,u,o]);const p=(0,l().useMemo)(()=>lr(o,{retainEmptyStrings:!0}),[o]),h=(0,l().useMemo)(()=>vo({parseNumbers:i,inputType:n}),[n,i]);return{valueAsArray:p,multiValueHandler:(0,l().useCallback)((e,n)=>{t((({value:e,index:t,valueAsArray:n,operator:r,values:o,listsAsArrays:a,parseNumberMethod:i})=>{const l=uo(e,{parseNumbers:i}),s=0===t&&(e=>"between"===e||"notBetween"===e)(r)&&(n.length<2||void 0===n[1]);if(n[t]===l&&!s)return a?n:ar(n,",");const u=[...n];return u[t]=l,s&&(u[1]=Kr(o)??""),a?u:ar(u,",")})({value:e,index:n,valueAsArray:p,operator:r,values:s,listsAsArrays:a,parseNumberMethod:h}))},[t,a,r,h,p,s]),bigIntValueHandler:(0,l().useCallback)(e=>{t(((e,t)=>{const n=uo(e,{parseNumbers:t,bigIntOnOverflow:!0});try{return BigInt(n)}catch{return n}})(e,h))},[t,h]),parseNumberMethod:h,valueListItemClassName:(m="valueListItem",g={classNames:d,suppressStandardClassnames:f},Rr(m,Cr[m],{},g)),inputTypeCoerced:fo(n,r)};var m,g},$u=e=>{const t=Lu(e);return l().createElement(ku.Provider,{value:t},e.children)},Mu=e=>{const{onChange:t,val:n}=Ru(e),r=(e=>{const{multiple:t,onChange:n}=e;return(0,l().useMemo)(()=>t?e=>n(Array.from(e.target.selectedOptions).map(e=>e.value)):e=>n(e.target.value),[t,n])})({multiple:e.multiple,onChange:t});return l().createElement("select",{"data-testid":e.testID,className:e.className,value:n,title:e.title,disabled:e.disabled,multiple:!!e.multiple,onChange:r},(o=e.options,Hr(o)?o.map(e=>l().createElement("optgroup",{key:e.label,label:e.label},e.options.map(e=>l().createElement("option",{key:e.name,value:e.name,disabled:e.disabled},e.label)))):Array.isArray(o)?o.map(e=>l().createElement("option",{key:e.name,value:e.name,disabled:e.disabled},e.label)):null));var o},Ru=e=>{const{handleOnChange:t,listsAsArrays:n=!1,multiple:r=!1,value:o}=e;return{onChange:(0,l().useCallback)(e=>{t(((e,{multiple:t,listsAsArrays:n}={})=>{if(!t)return e;const r=lr(e);return n?r:ar(r,",")})(e,{multiple:r,listsAsArrays:n}))},[t,n,r]),val:(0,l().useMemo)(()=>((e,t)=>t?lr(e).map(String):e)(o,r),[r,o])}},xu=({props:e})=>{const t=Su(e),n=t.schema.controls.ruleGroup,r=ku;return l().createElement(r.Provider,{key:t.dndEnabledAttr,value:t.rqbContext},l().createElement("div",{role:"form",className:t.wrapperClassName,"data-dnd":t.dndEnabledAttr,"data-inlinecombinators":t.inlineCombinatorsAttr},l().createElement(n,{translations:t.translations,ruleGroup:t.rootGroup,rules:t.rootGroup.rules,...t.combinatorPropObject,not:!!t.rootGroup.not,schema:t.schema,actions:t.actions,id:t.rootGroup.id,path:tr,disabled:t.rootGroupDisabled,shiftUpDisabled:!0,shiftDownDisabled:!0,parentDisabled:t.queryDisabled,context:e.context})))},Iu=()=>null,Pu=(0,l().forwardRef)(Iu),_u={},Lu=({finalize:e,...t})=>{const n=(0,l().useContext)(ku),r=(0,l().useMemo)(()=>((e={},t={},n)=>Lr(nr).reduce((r,[o,a])=>(r[o]=Ti(a,e[o],t[o],!n),r),{}))(t,n,e),[t,n,e]),o=e?!1!==n.enableDragAndDrop&&Ti(!1,t.enableDragAndDrop,n.enableDragAndDrop):t.enableDragAndDrop??n.enableDragAndDrop,a=(0,l().useMemo)(()=>((...e)=>({queryBuilder:Ei("queryBuilder",e),ruleGroup:Ei("ruleGroup",e),header:Ei("header",e),body:Ei("body",e),combinators:Ei("combinators",e),addRule:Ei("addRule",e),addGroup:Ei("addGroup",e),cloneRule:Ei("cloneRule",e),cloneGroup:Ei("cloneGroup",e),removeGroup:Ei("removeGroup",e),rule:Ei("rule",e),fields:Ei("fields",e),operators:Ei("operators",e),value:Ei("value",e),removeRule:Ei("removeRule",e),notToggle:Ei("notToggle",e),shiftActions:Ei("shiftActions",e),undoRedoActions:Ei("undoRedoActions",e),undoAction:Ei("undoAction",e),redoAction:Ei("redoAction",e),dragHandle:Ei("dragHandle",e),lockRule:Ei("lockRule",e),lockGroup:Ei("lockGroup",e),muteRule:Ei("muteRule",e),muteGroup:Ei("muteGroup",e),muted:Ei("muted",e),valueSource:Ei("valueSource",e),actionElement:Ei("actionElement",e),valueSelector:Ei("valueSelector",e),betweenRules:Ei("betweenRules",e),valid:Ei("valid",e),invalid:Ei("invalid",e),dndDragging:Ei("dndDragging",e),dndOver:Ei("dndOver",e),dndCopy:Ei("dndCopy",e),dndGroup:Ei("dndGroup",e),dndDropNotAllowed:Ei("dndDropNotAllowed",e),dndPreviewPosition:Ei("dndPreviewPosition",e),dndHidden:Ei("dndHidden",e),disabled:Ei("disabled",e),valueListItem:Ei("valueListItem",e),matchMode:Ei("matchMode",e),matchThreshold:Ei("matchThreshold",e),branches:Ei("branches",e),hasSubQuery:Ei("hasSubQuery",e),loading:Ei("loading",e),valueDateTimeRelative:Ei("valueDateTimeRelative",e)}))(n.controlClassnames,t.controlClassnames),[n.controlClassnames,t.controlClassnames]),i=(0,l().useMemo)(()=>({actionElement:a.actionElement,addGroup:a.addGroup,addRule:a.addRule,body:a.body,cloneGroup:a.cloneGroup,cloneRule:a.cloneRule,combinators:a.combinators,dragHandle:a.dragHandle,fields:a.fields,header:a.header,lockGroup:a.lockGroup,lockRule:a.lockRule,muteGroup:a.muteGroup,muteRule:a.muteRule,muted:a.muted,notToggle:a.notToggle,operators:a.operators,queryBuilder:a.queryBuilder,removeGroup:a.removeGroup,removeRule:a.removeRule,rule:a.rule,ruleGroup:a.ruleGroup,shiftActions:a.shiftActions,undoRedoActions:a.undoRedoActions,undoAction:a.undoAction,redoAction:a.redoAction,value:a.value,valueSelector:a.valueSelector,valueSource:a.valueSource,betweenRules:a.betweenRules,valid:a.valid,invalid:a.invalid,dndDragging:a.dndDragging,dndOver:a.dndOver,dndCopy:a.dndCopy,dndGroup:a.dndGroup,dndDropNotAllowed:a.dndDropNotAllowed,dndPreviewPosition:a.dndPreviewPosition,dndHidden:a.dndHidden,disabled:a.disabled,valueListItem:a.valueListItem,matchMode:a.matchMode,matchThreshold:a.matchThreshold,branches:a.branches,hasSubQuery:a.hasSubQuery,loading:a.loading,valueDateTimeRelative:a.valueDateTimeRelative}),[a.actionElement,a.addGroup,a.addRule,a.betweenRules,a.body,a.branches,a.cloneGroup,a.cloneRule,a.combinators,a.disabled,a.dndCopy,a.dndDropNotAllowed,a.dndPreviewPosition,a.dndHidden,a.dndGroup,a.dndDragging,a.dndOver,a.dragHandle,a.fields,a.hasSubQuery,a.header,a.invalid,a.loading,a.lockGroup,a.lockRule,a.muteGroup,a.muteRule,a.muted,a.matchMode,a.matchThreshold,a.notToggle,a.operators,a.queryBuilder,a.removeGroup,a.removeRule,a.rule,a.ruleGroup,a.shiftActions,a.undoRedoActions,a.undoAction,a.redoAction,a.valid,a.value,a.valueDateTimeRelative,a.valueListItem,a.valueSelector,a.valueSource]),s=n.controlElements??_u,u=t.controlElements??_u,c=(0,l().useCallback)((t,n,r)=>{const o="dragHandle"===t?Pu:Iu,a=(t.endsWith("Action")&&u.actionElement?u.actionElement:void 0)??(t.endsWith("Selector")&&u.valueSelector?u.valueSelector:void 0),i=(t.endsWith("Action")&&s.actionElement?s.actionElement:void 0)??(t.endsWith("Selector")&&s.valueSelector?s.valueSelector:void 0),l=null===n?o:n??(e?a:void 0)??(null===r?o:r??(e?i:void 0));return l?{[t]:l}:e?{[t]:Xu[t]}:_u},[s.actionElement,s.valueSelector,e,u.actionElement,u.valueSelector]),d=(0,l().useMemo)(()=>Object.assign({},c("addGroupAction",u.addGroupAction,s.addGroupAction),c("addRuleAction",u.addRuleAction,s.addRuleAction),c("cloneGroupAction",u.cloneGroupAction,s.cloneGroupAction),c("cloneRuleAction",u.cloneRuleAction,s.cloneRuleAction),c("combinatorSelector",u.combinatorSelector,s.combinatorSelector),c("dragHandle",u.dragHandle,s.dragHandle),c("fieldSelector",u.fieldSelector,s.fieldSelector),c("inlineCombinator",u.inlineCombinator,s.inlineCombinator),c("lockGroupAction",u.lockGroupAction,s.lockGroupAction),c("lockRuleAction",u.lockRuleAction,s.lockRuleAction),c("muteGroupAction",u.muteGroupAction,s.muteGroupAction),c("muteRuleAction",u.muteRuleAction,s.muteRuleAction),c("notToggle",u.notToggle,s.notToggle),c("operatorSelector",u.operatorSelector,s.operatorSelector),c("removeGroupAction",u.removeGroupAction,s.removeGroupAction),c("removeRuleAction",u.removeRuleAction,s.removeRuleAction),c("shiftActions",u.shiftActions,s.shiftActions),c("undoRedoActions",u.undoRedoActions,s.undoRedoActions),{valueEditor:null===u.valueEditor?Iu:u.valueEditor??(null===s.valueEditor?Iu:s.valueEditor)??Xu.valueEditor},c("valueSourceSelector",u.valueSourceSelector,s.valueSourceSelector),c("matchModeEditor",u.matchModeEditor,s.matchModeEditor),c("rule",u.rule,s.rule),c("ruleGroup",u.ruleGroup,s.ruleGroup),c("ruleGroupBodyElements",u.ruleGroupBodyElements,s.ruleGroupBodyElements),c("ruleGroupHeaderElements",u.ruleGroupHeaderElements,s.ruleGroupHeaderElements),{actionElement:u.actionElement??s.actionElement??(e?Xu.actionElement:void 0)},{valueSelector:u.valueSelector??s.valueSelector??(e?Xu.valueSelector:void 0)}),[s.actionElement,s.addGroupAction,s.addRuleAction,s.cloneGroupAction,s.cloneRuleAction,s.combinatorSelector,s.dragHandle,s.fieldSelector,s.inlineCombinator,s.lockGroupAction,s.lockRuleAction,s.muteGroupAction,s.muteRuleAction,s.matchModeEditor,s.notToggle,s.operatorSelector,s.removeGroupAction,s.removeRuleAction,s.rule,s.ruleGroup,s.ruleGroupBodyElements,s.ruleGroupHeaderElements,s.shiftActions,s.undoRedoActions,s.valueEditor,s.valueSelector,s.valueSourceSelector,c,e,u.actionElement,u.addGroupAction,u.addRuleAction,u.cloneGroupAction,u.cloneRuleAction,u.combinatorSelector,u.dragHandle,u.fieldSelector,u.inlineCombinator,u.lockGroupAction,u.lockRuleAction,u.muteGroupAction,u.muteRuleAction,u.matchModeEditor,u.notToggle,u.operatorSelector,u.removeGroupAction,u.removeRuleAction,u.rule,u.ruleGroup,u.ruleGroupBodyElements,u.ruleGroupHeaderElements,u.shiftActions,u.undoRedoActions,u.valueEditor,u.valueSelector,u.valueSourceSelector]),f=t.translations??_u,p=n.translations??_u;return{...r,controlClassnames:i,controlElements:d,enableDragAndDrop:o,translations:(0,l().useMemo)(()=>Object.assign({},ki("addGroup",{label:[f.addGroup?.label,p.addGroup?.label],title:[f.addGroup?.title,p.addGroup?.title]},e?sn:void 0),ki("addRule",{label:[f.addRule?.label,p.addRule?.label],title:[f.addRule?.title,p.addRule?.title]},e?sn:void 0),ki("cloneRule",{label:[f.cloneRule?.label,p.cloneRule?.label],title:[f.cloneRule?.title,p.cloneRule?.title]},e?sn:void 0),ki("cloneRuleGroup",{label:[f.cloneRuleGroup?.label,p.cloneRuleGroup?.label],title:[f.cloneRuleGroup?.title,p.cloneRuleGroup?.title]},e?sn:void 0),ki("combinators",{title:[f.combinators?.title,p.combinators?.title]},e?sn:void 0),ki("dragHandle",{label:[f.dragHandle?.label,p.dragHandle?.label],title:[f.dragHandle?.title,p.dragHandle?.title]},e?sn:void 0),ki("fields",{placeholderGroupLabel:[f.fields?.placeholderGroupLabel,p.fields?.placeholderGroupLabel],placeholderLabel:[f.fields?.placeholderLabel,p.fields?.placeholderLabel],placeholderName:[f.fields?.placeholderName,p.fields?.placeholderName],title:[f.fields?.title,p.fields?.title]},e?sn:void 0),ki("lockGroup",{label:[f.lockGroup?.label,p.lockGroup?.label],title:[f.lockGroup?.title,p.lockGroup?.title]},e?sn:void 0),ki("lockGroupDisabled",{label:[f.lockGroupDisabled?.label,p.lockGroupDisabled?.label],title:[f.lockGroupDisabled?.title,p.lockGroupDisabled?.title]},e?sn:void 0),ki("lockRule",{label:[f.lockRule?.label,p.lockRule?.label],title:[f.lockRule?.title,p.lockRule?.title]},e?sn:void 0),ki("lockRuleDisabled",{label:[f.lockRuleDisabled?.label,p.lockRuleDisabled?.label],title:[f.lockRuleDisabled?.title,p.lockRuleDisabled?.title]},e?sn:void 0),ki("muteGroup",{label:[f.muteGroup?.label,p.muteGroup?.label],title:[f.muteGroup?.title,p.muteGroup?.title]},e?sn:void 0),ki("unmuteGroup",{label:[f.unmuteGroup?.label,p.unmuteGroup?.label],title:[f.unmuteGroup?.title,p.unmuteGroup?.title]},e?sn:void 0),ki("muteRule",{label:[f.muteRule?.label,p.muteRule?.label],title:[f.muteRule?.title,p.muteRule?.title]},e?sn:void 0),ki("unmuteRule",{label:[f.unmuteRule?.label,p.unmuteRule?.label],title:[f.unmuteRule?.title,p.unmuteRule?.title]},e?sn:void 0),ki("notToggle",{label:[f.notToggle?.label,p.notToggle?.label],title:[f.notToggle?.title,p.notToggle?.title]},e?sn:void 0),ki("operators",{placeholderGroupLabel:[f.operators?.placeholderGroupLabel,p.operators?.placeholderGroupLabel],placeholderLabel:[f.operators?.placeholderLabel,p.operators?.placeholderLabel],placeholderName:[f.operators?.placeholderName,p.operators?.placeholderName],title:[f.operators?.title,p.operators?.title]},e?sn:void 0),ki("values",{placeholderGroupLabel:[f.values?.placeholderGroupLabel,p.values?.placeholderGroupLabel],placeholderLabel:[f.values?.placeholderLabel,p.values?.placeholderLabel],placeholderName:[f.values?.placeholderName,p.values?.placeholderName],title:[f.values?.title,p.values?.title]},e?sn:void 0),ki("removeGroup",{label:[f.removeGroup?.label,p.removeGroup?.label],title:[f.removeGroup?.title,p.removeGroup?.title]},e?sn:void 0),ki("removeRule",{label:[f.removeRule?.label,p.removeRule?.label],title:[f.removeRule?.title,p.removeRule?.title]},e?sn:void 0),ki("shiftActionDown",{label:[f.shiftActionDown?.label,p.shiftActionDown?.label],title:[f.shiftActionDown?.title,p.shiftActionDown?.title]},e?sn:void 0),ki("shiftActionUp",{label:[f.shiftActionUp?.label,p.shiftActionUp?.label],title:[f.shiftActionUp?.title,p.shiftActionUp?.title]},e?sn:void 0),ki("undo",{label:[f.undo?.label,p.undo?.label],title:[f.undo?.title,p.undo?.title]},e?sn:void 0),ki("redo",{label:[f.redo?.label,p.redo?.label],title:[f.redo?.title,p.redo?.title]},e?sn:void 0),ki("matchMode",{title:[f.matchMode?.title,p.matchMode?.title]},e?sn:void 0),ki("matchThreshold",{title:[f.matchThreshold?.title,p.matchThreshold?.title],placeholderName:[f.matchThreshold?.placeholderName,p.matchThreshold?.placeholderName]},e?sn:void 0),ki("value",{title:[f.value?.title,p.value?.title]},e?sn:void 0),ki("valueSourceSelector",{title:[f.valueSourceSelector?.title,p.valueSourceSelector?.title]},e?sn:void 0)),[p.addGroup?.label,p.addGroup?.title,p.addRule?.label,p.addRule?.title,p.cloneRule?.label,p.cloneRule?.title,p.cloneRuleGroup?.label,p.cloneRuleGroup?.title,p.combinators?.title,p.dragHandle?.label,p.dragHandle?.title,p.fields?.placeholderGroupLabel,p.fields?.placeholderLabel,p.fields?.placeholderName,p.fields?.title,p.lockGroup?.label,p.lockGroup?.title,p.lockGroupDisabled?.label,p.lockGroupDisabled?.title,p.lockRule?.label,p.lockRule?.title,p.lockRuleDisabled?.label,p.lockRuleDisabled?.title,p.muteGroup?.label,p.muteGroup?.title,p.unmuteGroup?.label,p.unmuteGroup?.title,p.muteRule?.label,p.muteRule?.title,p.unmuteRule?.label,p.unmuteRule?.title,p.matchMode?.title,p.matchThreshold?.title,p.matchThreshold?.placeholderName,p.notToggle?.label,p.notToggle?.title,p.operators?.placeholderGroupLabel,p.operators?.placeholderLabel,p.operators?.placeholderName,p.operators?.title,p.removeGroup?.label,p.removeGroup?.title,p.removeRule?.label,p.removeRule?.title,p.shiftActionDown?.label,p.shiftActionDown?.title,p.shiftActionUp?.label,p.shiftActionUp?.title,p.undo?.label,p.undo?.title,p.redo?.label,p.redo?.title,p.value?.title,p.values?.placeholderGroupLabel,p.values?.placeholderLabel,p.values?.placeholderName,p.values?.title,p.valueSourceSelector?.title,e,f.addGroup?.label,f.addGroup?.title,f.addRule?.label,f.addRule?.title,f.cloneRule?.label,f.cloneRule?.title,f.cloneRuleGroup?.label,f.cloneRuleGroup?.title,f.combinators?.title,f.dragHandle?.label,f.dragHandle?.title,f.fields?.placeholderGroupLabel,f.fields?.placeholderLabel,f.fields?.placeholderName,f.fields?.title,f.lockGroup?.label,f.lockGroup?.title,f.lockGroupDisabled?.label,f.lockGroupDisabled?.title,f.lockRule?.label,f.lockRule?.title,f.lockRuleDisabled?.label,f.lockRuleDisabled?.title,f.muteGroup?.label,f.muteGroup?.title,f.unmuteGroup?.label,f.unmuteGroup?.title,f.muteRule?.label,f.muteRule?.title,f.unmuteRule?.label,f.unmuteRule?.title,f.matchMode?.title,f.matchThreshold?.title,f.matchThreshold?.placeholderName,f.notToggle?.label,f.notToggle?.title,f.operators?.placeholderGroupLabel,f.operators?.placeholderLabel,f.operators?.placeholderName,f.operators?.title,f.removeGroup?.label,f.removeGroup?.title,f.removeRule?.label,f.removeRule?.title,f.shiftActionDown?.label,f.shiftActionDown?.title,f.shiftActionUp?.label,f.shiftActionUp?.title,f.undo?.label,f.undo?.title,f.redo?.label,f.redo?.title,f.value?.title,f.values?.placeholderGroupLabel,f.values?.placeholderLabel,f.values?.placeholderName,f.values?.title,f.valueSourceSelector?.title]),initialQuery:t.initialQuery,qbId:t.qbId}},Gu=({disabled:e,path:t,nestedArray:n,disabledPaths:r})=>{const o=n.length;return(0,l().useMemo)(()=>((e,t,{disabled:n=!1,disabledPaths:r=[]}={})=>{const o=[];for(let a=0;aai(t,e))}}return o})(t,o,{disabled:e,disabledPaths:r}),[e,t,o,r])},Fu=e=>{const t=eu(),[n,r]=(0,l().useState)(()=>e??di()),[o]=(0,l().useState)(e);return{qbId:n,resolveQbIdCollision:(0,l().useCallback)(()=>{r(di())},[t])}};const zu=(e,t)=>{0},ju=e=>(0,l().useCallback)((t,n)=>{t?.preventDefault(),t?.stopPropagation(),e(t,n)},[e]);const qu=(e,t)=>{0},Bu={mode:"all"},Vu=[{name:"",value:"",label:""}],Hu=l().memo(function(e){const t=Ku(e),{dndRef:n}=t,r=ju(t.cloneRule),o=ju(t.toggleLockRule),a=ju(t.toggleMuteRule),i=ju(t.removeRule),s=ju(t.shiftRuleUp),u=ju(t.shiftRuleDown),c=(0,l().useMemo)(()=>({cloneRule:r,toggleLockRule:o,toggleMuteRule:a,removeRule:i,shiftRuleUp:s,shiftRuleDown:u}),[r,i,u,s,o,a]);return l().createElement("div",{ref:n,"data-testid":bn,"data-dragmonitorid":t.dragMonitorId,"data-dropmonitorid":t.dropMonitorId,className:t.outerClassName,"data-rule-id":t.id,"data-level":t.path.length,"data-path":JSON.stringify(t.path)},t.matchModes.length>0?l().createElement(Qu,{...t,...c}):l().createElement(Uu,{...t,...c}))}),Uu=l().memo(function(e){const{dragRef:t,schema:{controls:{shiftActions:n,dragHandle:r,fieldSelector:o,matchModeEditor:a,operatorSelector:i,valueSourceSelector:s,valueEditor:u,cloneRuleAction:c,lockRuleAction:d,muteRuleAction:f,removeRuleAction:p,ruleGroupBodyElements:h,ruleGroupHeaderElements:m}},groupComponentsWrapper:g=l().Fragment}=e,v=(0,l().useMemo)(()=>({level:e.path.length,path:e.path,disabled:e.disabled,context:e.context,validation:e.validationResult,schema:e.schema,rule:e.rule}),[e.path,e.disabled,e.context,e.validationResult,e.schema,e.rule]),b=(0,l().useMemo)(()=>!(1===e.schema.fields.length&&fr(e.schema.fields[0])&&"value"in e.schema.fields[0]&&""===e.schema.fields[0].value),[e.schema.fields]),y=(0,l().useMemo)(()=>e.schema.showShiftActions?{shiftUp:e.translations.shiftActionUp.title,shiftDown:e.translations.shiftActionDown.title}:void 0,[e.schema.showShiftActions,e.translations]),w=(0,l().useMemo)(()=>e.schema.showShiftActions?{shiftUp:e.translations.shiftActionUp.label,shiftDown:e.translations.shiftActionDown.label}:void 0,[e.schema.showShiftActions,e.translations]);return l().createElement(l().Fragment,null,e.schema.showShiftActions&&l().createElement(n,{key:Rn,...v,testID:Rn,titles:y,labels:w,className:e.classNames.shiftActions,ruleOrGroup:e.rule,shiftUp:e.shiftRuleUp,shiftDown:e.shiftRuleDown,shiftUpDisabled:e.shiftUpDisabled,shiftDownDisabled:e.shiftDownDisabled}),e.schema.enableDragAndDrop&&l().createElement(r,{key:In,...v,testID:In,ref:t,title:e.translations.dragHandle.title,label:e.translations.dragHandle.label,className:e.classNames.dragHandle,dragHandleAttributes:e.dragHandleAttributes,ruleOrGroup:e.rule}),b&&l().createElement(o,{key:An,...v,testID:An,options:e.schema.fields,title:e.translations.fields.title,value:e.rule.field,operator:e.rule.operator,className:e.classNames.fields,handleOnChange:e.onChangeField}),(e.schema.autoSelectField||e.rule.field!==e.translations.fields.placeholderName)&&(e.subQuery?l().createElement(a,{key:zn,...v,testID:zn,field:e.rule.field,fieldData:e.fieldData,title:e.translations.matchMode.title,options:e.matchModes,thresholdPlaceholder:e.translations.matchThreshold.placeholderName,match:e.rule.match??/* v8 ignore start -- @preserve */Bu,className:e.classNames.matchMode,classNames:e.classNames,handleOnChange:e.onChangeMatchMode}):l().createElement(l().Fragment,null,l().createElement(i,{key:Tn,...v,testID:Tn,field:e.rule.field,fieldData:e.fieldData,title:e.translations.operators.title,options:e.operators,value:e.rule.operator,className:e.classNames.operators,handleOnChange:e.onChangeOperator}),(e.schema.autoSelectOperator||e.rule.operator!==e.translations.operators.placeholderName)&&!e.hideValueControls&&l().createElement(l().Fragment,null,!["null","notnull"].includes(cr(`${e.rule.operator}`))&&e.valueSources.length>1&&l().createElement(s,{key:Fn,...v,testID:Fn,field:e.rule.field,fieldData:e.fieldData,title:e.translations.valueSourceSelector.title,options:e.valueSourceOptions,value:e.rule.valueSource??"value",className:e.classNames.valueSource,handleOnChange:e.onChangeValueSource}),l().createElement(u,{key:$n,...v,testID:$n,field:e.rule.field,fieldData:e.fieldData,title:e.translations.value.title,operator:e.rule.operator,value:e.rule.value,valueSource:e.rule.valueSource??"value",type:e.valueEditorType,inputType:e.inputType,values:e.values,listsAsArrays:e.schema.listsAsArrays,parseNumbers:e.schema.parseNumbers,separator:e.valueEditorSeparator,className:e.classNames.value,handleOnChange:e.onChangeValue})))),e.subQuery&&l().createElement(g,{className:e.subQuery.classNames.header},l().createElement(m,e.subQuery)),e.schema.showCloneButtons&&l().createElement(c,{key:On,...v,testID:On,label:e.translations.cloneRule.label,title:e.translations.cloneRule.title,className:e.classNames.cloneRule,ruleOrGroup:e.rule,handleOnClick:e.cloneRule}),e.schema.showLockButtons&&l().createElement(d,{key:Pn,...v,testID:Pn,label:e.translations.lockRule.label,title:e.translations.lockRule.title,className:e.classNames.lockRule,ruleOrGroup:e.rule,handleOnClick:e.toggleLockRule,disabledTranslation:e.parentDisabled?void 0:e.translations.lockRuleDisabled}),e.schema.showMuteButtons&&l().createElement(f,{key:Ln,...v,testID:Ln,label:e.rule.muted?e.translations.unmuteRule.label:e.translations.muteRule.label,title:e.rule.muted?e.translations.unmuteRule.title:e.translations.muteRule.title,className:e.classNames.muteRule,ruleOrGroup:e.rule,handleOnClick:e.toggleMuteRule}),l().createElement(p,{key:Cn,...v,testID:Cn,label:e.translations.removeRule.label,title:e.translations.removeRule.title,className:e.classNames.removeRule,ruleOrGroup:e.rule,handleOnClick:e.removeRule}),e.subQuery&&l().createElement(g,{className:e.subQuery.classNames.body},l().createElement(h,e.subQuery)))}),Wu=e=>l().createElement("div",e),Qu=l().memo(function(e){const t=(0,l().useMemo)(()=>e.schema.createRuleGroup(),[e.schema]),n=Su({...e.subQueryBuilderProps,enableDragAndDrop:!1,disabled:e.disabled,fields:e.subproperties.fields,enableMountQueryChange:!hr(e.rule.value)||!e.rule.value.id,query:hr(e.rule.value)?e.rule.value:t,onQueryChange:e.onChangeValue}),r=Cu({...n,ruleGroup:n.rootGroup,path:tr,disabled:e.disabled,parentDisabled:n.queryDisabled,id:n.rootGroup.id,shiftUpDisabled:!0,shiftDownDisabled:!0}),o=ju(r.addRule),a=ju(r.addGroup),i=ju(r.cloneGroup),s=ju(r.toggleLockGroup),u=ju(r.removeGroup),c=ju(r.shiftGroupUp),d=ju(r.shiftGroupDown),f=(0,l().useMemo)(()=>({...r,addGroup:a,addRule:o,cloneGroup:i,removeGroup:u,shiftGroupDown:d,shiftGroupUp:c,toggleLockGroup:s}),[a,o,i,u,d,c,r,s]);return l().createElement(Uu,{...e,groupComponentsWrapper:e.groupComponentsWrapper??Wu,subQuery:f})}),Ku=e=>{const{id:t,path:n,rule:r,schema:{classNames:o,fields:a,fieldMap:i,getParameters:s,getInputType:u,getMatchModes:c,getOperators:d,getSubQueryBuilderProps:f,getValueEditorType:p,getValueEditorSeparator:h,getValueSources:m,getValues:g,validationMap:v,enableDragAndDrop:b,getRuleClassname:y,suppressStandardClassnames:w},actions:{moveRule:S,onPropChange:k,onRuleRemove:E},disabled:O,parentDisabled:N,parentMuted:C,shiftUpDisabled:D,shiftDownDisabled:A,field:T,operator:$,value:M,valueSource:R,dropEffect:x="move",groupItems:I=!1,dragMonitorId:P="",dropMonitorId:_="",dndRef:L=null,dragRef:G=null,isDragging:F=!1,isOver:z=!1,dropNotAllowed:j=!1}=e;iu(),zu(b,!!(P||_||L||G));const q=!!N||!!O,B=!!C||!!r?.muted,V=(0,l().useMemo)(()=>r??{id:t,field:T??/* v8 ignore start -- @preserve */"",operator:$??/* v8 ignore start -- @preserve */"",value:M,valueSource:R},[T,t,$,r,M,R]),H=(0,l().useMemo)(()=>xr(Cr,{},{classNames:o,suppressStandardClassnames:w}),[o,w]),U=(0,l().useCallback)(e=>(t,r)=>{q||k(e,t,n,r)},[q,k,n]),W=(0,l().useMemo)(()=>U("field"),[U]),Q=(0,l().useMemo)(()=>U("operator"),[U]),K=(0,l().useMemo)(()=>U("match"),[U]),X=(0,l().useMemo)(()=>U("valueSource"),[U]),Y=(0,l().useMemo)(()=>U("value"),[U]),J=(0,l().useCallback)((e,t)=>{if(!q){const e=[...oi(n),n.at(-1)+1];S(n,e,!0,t)}},[q,S,n]),Z=(0,l().useCallback)((e,t)=>k("disabled",!q,n,t),[q,k,n]),ee=(0,l().useCallback)((e,t)=>k("muted",!V.muted,n,t),[V.muted,k,n]),te=(0,l().useCallback)((e,t)=>{q||E(n)},[q,E,n]),ne=(0,l().useCallback)((e,t)=>{q||D||S(n,"up",e?.altKey,t)},[q,S,n,D]),re=(0,l().useCallback)((e,t)=>{q||A||S(n,"down",e?.altKey,t)},[q,S,n,A]),oe=(0,l().useMemo)(()=>to(V.field,i),[i,V.field]),ae=(0,l().useMemo)(()=>((e,t,n,r)=>n.inputType??r(e,t,{fieldData:n}))(V.field,V.operator,oe,u),[oe,u,V.field,V.operator]),ie=(0,l().useMemo)(()=>c(V.field,{fieldData:oe}),[oe,c,V.field]),le=(0,l().useMemo)(()=>d(V.field,{fieldData:oe}),[oe,d,V.field]),se=(0,l().useMemo)(()=>Qr(le,V.operator),[le,V.operator]),ue=no(se),ce=(0,l().useMemo)(()=>ro({field:V.field,operator:V.operator,valueSource:V.valueSource},oe,m),[oe,m,V.field,V.operator,V.valueSource]),de=(0,l().useMemo)(()=>ce.map(({value:e})=>e),[ce]),fe=(0,l().useMemo)(()=>s(V.field,V.operator,{fieldData:oe}),[oe,s,V.field,V.operator]),pe=(0,l().useMemo)(()=>oo(fe),[fe]),he=(0,l().useMemo)(()=>ao({field:V.field,operator:V.operator,valueSource:V.valueSource},oe,pe,p),[oe,p,pe,V.field,V.operator,V.valueSource]),me=(0,l().useMemo)(()=>h(V.field,V.operator,{fieldData:oe}),[oe,h,V.field,V.operator]),ge=(0,l().useMemo)(()=>io({field:V.field,operator:V.operator,valueSource:V.valueSource},oe,a,pe,g),[oe,a,g,pe,V.field,V.operator,V.valueSource]),ve=(0,l().useMemo)(()=>f(V.field,{fieldData:oe}),[oe,f,V.field]),be=lu({translations:e.translations,fields:oe.subproperties??ve.fields??Vu,autoSelectField:e.schema.autoSelectField||!!oe.subproperties}),ye=(0,l().useMemo)(()=>lo(V,oe,v,t??/* v8 ignore start -- @preserve */""),[oe,t,V,v]),we=(0,l().useMemo)(()=>bi(ye),[ye]),Se=oe?.className??"",ke=se?.className??"",Ee=ie.length>0,Oe=(0,l().useMemo)(()=>{return e={classNames:o,suppressStandardClassnames:w,leadingClassNames:[y(V,{fieldData:oe}),Se,ke],disabled:q,muted:B,isDragging:F,isOver:z,dropEffect:x,groupItems:I,dropNotAllowed:j,hasSubQuery:Ee,validationClassName:we},Ir("rule",Ar,e);var e},[o,q,x,j,B,Se,oe,y,I,Ee,F,z,ke,V,w,we]);return{...e,classNames:H,cloneRule:J,disabled:q,dndRef:L,dragMonitorId:P,dragRef:G,dropMonitorId:_,fieldData:oe,generateOnChangeHandler:U,onChangeField:W,onChangeMatchMode:K,onChangeOperator:Q,onChangeValueSource:X,onChangeValue:Y,hideValueControls:ue,inputType:ae,matchModes:ie,muted:B,operators:le,outerClassName:Oe,removeRule:te,rule:V,shiftRuleUp:ne,shiftRuleDown:re,subproperties:be,subQueryBuilderProps:ve,toggleLockRule:Z,toggleMuteRule:ee,validationResult:ye,valueEditorSeparator:me,valueEditorType:he,values:ge,valueSourceOptions:ce,valueSources:de}},Xu={actionElement:Es,addGroupAction:Es,addRuleAction:Es,cloneGroupAction:Es,cloneRuleAction:Es,combinatorSelector:Mu,dragHandle:Os,fieldSelector:Mu,inlineCombinator:e=>{const{component:t,...n}=e,r=ur(n.schema.suppressStandardClassnames||hn.betweenRules,n.schema.classNames.betweenRules);return l().createElement("div",{className:r,"data-testid":wn},l().createElement(t,{...n,testID:Dn}))},lockGroupAction:Es,lockRuleAction:Es,matchModeEditor:e=>{const{match:t,options:n,title:r,className:o,disabled:a,testID:i,schema:s,thresholdPlaceholder:u,selectorComponent:c=e.schema.controls.valueSelector,numericEditorComponent:d=e.schema.controls.valueEditor}=e,{thresholdNum:f,thresholdRule:p,thresholdSchema:h,handleChangeMode:m,handleChangeThreshold:g}=As(e),v=(0,l().useMemo)(()=>u?{...Ns,placeholder:u}:Ns,[u]);return l().createElement(l().Fragment,null,l().createElement(c,{schema:s,testID:i,className:o,title:r,handleOnChange:m,disabled:a,value:t.mode,options:n,multiple:!1,listsAsArrays:!1,path:Ds,level:0}),Cs(t.mode)&&l().createElement(d,{skipHook:!0,testID:i,inputType:"number",title:r,className:o,disabled:a,handleOnChange:g,field:"",operator:"",value:f,valueSource:"value",fieldData:v,schema:h,path:Ds,level:0,rule:p}))},muteGroupAction:Es,muteRuleAction:Es,notToggle:e=>{const t=l().useId();return l().createElement("label",{"data-testid":e.testID,className:e.className,title:e.title,htmlFor:t},l().createElement("input",{id:t,type:"checkbox",onChange:t=>e.handleOnChange(t.target.checked),checked:!!e.checked,disabled:e.disabled}),e.label)},operatorSelector:Mu,removeGroupAction:Es,removeRuleAction:Es,rule:Hu,ruleGroup:Eu,ruleGroupBodyElements:Nu,ruleGroupHeaderElements:Ou,shiftActions:e=>l().createElement("div",{"data-testid":e.testID,className:e.className},l().createElement("button",{type:"button",disabled:e.disabled||e.shiftUpDisabled,onClick:e.shiftUp,title:e.titles?.shiftUp},e.labels?.shiftUp),l().createElement("button",{type:"button",disabled:e.disabled||e.shiftDownDisabled,onClick:e.shiftDown,title:e.titles?.shiftDown},e.labels?.shiftDown)),undoRedoActions:null,valueEditor:Au,valueSelector:Mu,valueSourceSelector:Mu};const Yu=e=>"object"==typeof e&&null!==e&&"DndProvider"in e&&"useRuleDnD"in e&&"useRuleGroupDnD"in e&&"useInlineCombinatorDnD"in e,{rule:Ju,ruleGroup:Zu,combinatorSelector:ec}=Xu,tc=(0,l().createContext)({baseControls:{rule:Ju,ruleGroup:Zu,combinatorSelector:ec}}),nc=({component:e,...t})=>{const{adapter:n,canDrop:r,copyModeModifierKey:o,copyModeAfterHoverMs:a,groupModeModifierKey:i,groupModeAfterHoverMs:s}=(0,l().useContext)(tc),{dragPreviewState:u}=(0,l().useContext)(Ce),c=null!==u,{dropRef:d,dropMonitorId:f,isOver:p}=n.useInlineCombinatorDnD({path:t.path,schema:t.schema,rules:t.rules,canDrop:r,copyModeModifierKey:o??"alt",copyModeAfterHoverMs:a,groupModeModifierKey:i??"ctrl",groupModeAfterHoverMs:s}),h=!c&&p,m=[t.schema.suppressStandardClassnames||hn.betweenRules,h&&!t.schema.classNames.dndOver||!1,h&&!t.schema.suppressStandardClassnames&&hn.dndOver||!1].filter(e=>"string"==typeof e).join(" ");return l().createElement("div",{key:"dnd",ref:d,className:m,"data-dropmonitorid":f,"data-testid":wn},l().createElement(e,{...t,testID:Dn}))},rc=e=>{const t=(0,l().useContext)(tc),{dragPreviewState:n}=(0,l().useContext)(Ce),{adapter:r,canDrop:o,copyModeModifierKey:a,copyModeAfterHoverMs:i,groupModeModifierKey:s,groupModeAfterHoverMs:u,hideDefaultDragPreview:c,onRuleDrop:d}=t,f=!!e.parentDisabled||!!e.disabled,p=r.useRuleDnD({path:e.path,disabled:f,schema:e.schema,actions:e.actions,rule:e.rule,canDrop:o,copyModeModifierKey:a??"alt",copyModeAfterHoverMs:i,groupModeModifierKey:s??"ctrl",groupModeAfterHoverMs:u,hideDefaultDragPreview:c,onRuleDrop:d}),h=n?{...p,isDragging:!1,isOver:!1,dropNotAllowed:!1}:p,{rule:m}=t.baseControls;return l().createElement(tc.Provider,{value:t},l().createElement(m,{...e,...h}))},oc=e=>{const t=(0,l().useContext)(tc),{dragPreviewState:n}=(0,l().useContext)(Ce),{adapter:r,canDrop:o,baseControls:{ruleGroup:a},copyModeModifierKey:i,copyModeAfterHoverMs:s,groupModeModifierKey:u,groupModeAfterHoverMs:c,hideDefaultDragPreview:d,onRuleDrop:f}=t,p=(0,l().useMemo)(()=>{if(0===e.path.length&&n){const t=n.shadowQuery;return{...e,ruleGroup:t,rules:t.rules}}return e},[e,n]),h=r.useRuleGroupDnD({disabled:!!p.parentDisabled||!!p.disabled,path:p.path,schema:p.schema,actions:p.actions,ruleGroup:p.ruleGroup,canDrop:o,copyModeModifierKey:i??"alt",copyModeAfterHoverMs:s,groupModeModifierKey:u??"ctrl",groupModeAfterHoverMs:c,hideDefaultDragPreview:d,onRuleDrop:f}),m=n?{...h,isDragging:!1,isOver:!1,dropNotAllowed:!1}:h;return l().createElement(a,{...p,...m})},ac=e=>{const{controlClassnames:t,controlElements:n,debugMode:r,enableDragAndDrop:o,enableMountQueryChange:a,translations:i}=e,s=Lu({controlClassnames:t,controlElements:n,debugMode:r,enableDragAndDrop:o??!0,enableMountQueryChange:a,translations:i??{}}),{enableDragAndDrop:u}=s,c=lc(e.dnd),d=u&&c?"dnd":"no-dnd",f=(0,l().useMemo)(()=>({...s,enableDragAndDrop:!1,debugMode:r}),[s,r]),p=(0,l().useMemo)(()=>({...s,enableDragAndDrop:u,debugMode:r}),[s,r,u]);if(!u||!c)return l().createElement(ku.Provider,{key:d,value:f},e.children);const{DndProvider:h}=c;return l().createElement(h,{key:d,debugMode:r,updateWhileDragging:e.updateWhileDragging,copyModeAfterHoverMs:e.copyModeAfterHoverMs,groupModeAfterHoverMs:e.groupModeAfterHoverMs},l().createElement(ku.Provider,{key:d,value:p},l().createElement(ic,{dnd:c,canDrop:e.canDrop,copyModeModifierKey:e.copyModeModifierKey,copyModeAfterHoverMs:e.copyModeAfterHoverMs,groupModeModifierKey:e.groupModeModifierKey,groupModeAfterHoverMs:e.groupModeAfterHoverMs,hideDefaultDragPreview:e.hideDefaultDragPreview,updateWhileDragging:e.updateWhileDragging,onDragMove:e.onDragMove,onRuleDrop:e.onRuleDrop},e.children)))},ic=e=>{const t=(0,l().useContext)(ku),n=(0,l().useContext)(tc),r=lc(e.dnd)??n.adapter,o=$i(void 0,e.copyModeModifierKey,n.copyModeModifierKey),a=$i(void 0,e.groupModeModifierKey,n.groupModeModifierKey),i=$i(void 0,e.copyModeAfterHoverMs,n.copyModeAfterHoverMs),s=$i(void 0,e.groupModeAfterHoverMs,n.groupModeAfterHoverMs),u=Ti(!0,e.enableDragAndDrop,t.enableDragAndDrop),c=Ti(!1,e.debugMode,t.debugMode),d=Ti(!1,e.hideDefaultDragPreview,n.hideDefaultDragPreview),f=$i(void 0,e.canDrop,n.canDrop),p=Ti(!1,e.updateWhileDragging,n.updateWhileDragging),h=$i(void 0,e.onDragMove,n.onDragMove),m=$i(void 0,e.onRuleDrop,n.onRuleDrop),g=u&&r?"dnd":"no-dnd",v=(0,l().useMemo)(()=>({rule:e.controlElements?.rule??t.controlElements?.rule??n.baseControls.rule,ruleGroup:e.controlElements?.ruleGroup??t.controlElements?.ruleGroup??n.baseControls.ruleGroup,combinatorSelector:e.controlElements?.combinatorSelector??t.controlElements?.combinatorSelector??n.baseControls.combinatorSelector}),[e.controlElements?.combinatorSelector,e.controlElements?.rule,e.controlElements?.ruleGroup,t.controlElements?.combinatorSelector,t.controlElements?.rule,t.controlElements?.ruleGroup,n.baseControls.combinatorSelector,n.baseControls.rule,n.baseControls.ruleGroup]),b=(0,l().useMemo)(()=>({...t,enableDragAndDrop:u,debugMode:c,controlElements:{...t.controlElements,ruleGroup:oc,rule:rc,inlineCombinator:nc}}),[c,u,t]),y=(0,l().useMemo)(()=>({baseControls:v,canDrop:f,copyModeModifierKey:o,copyModeAfterHoverMs:i,groupModeModifierKey:a,groupModeAfterHoverMs:s,hideDefaultDragPreview:d,updateWhileDragging:p,onDragMove:h,onRuleDrop:m,adapter:r}),[v,f,o,i,a,s,d,p,h,m,r]),w=(0,l().useMemo)(()=>({...t,enableDragAndDrop:!1,debugMode:c}),[t,c]);return u&&r?l().createElement(ku.Provider,{key:g,value:b},l().createElement(tc.Provider,{value:y},e.children)):l().createElement(ku.Provider,{key:g,value:w},e.children)};const lc=e=>{const t=e&&Yu(e)?e:null,n=(0,l().useMemo)(()=>e&&!Yu(e)?El(e):null,[e]),r=sc(null!==t||null!==n);return t??n??r},sc=e=>{const[t,r]=(0,l().useState)(null);return(0,l().useEffect)(()=>{if(e)return;let o=!1;return t||(async()=>{const[e,t,a]=await Promise.all(["","-html5-backend","-touch-backend"].map(e=>n(290)(`react-dnd${e}`).catch( +/* v8 ignore next -- @preserve */ +()=>null)));if(!o)if(e){let n;if(t&&(!a||a&&!bl()))n={...e,...t,...a,ReactDndBackend:t.HTML5Backend};else{if(!a)return;n={...e,...a,...t,ReactDndBackend:a.TouchBackend}}r(()=>El(n))}else 0})(),()=>{o=!0}},[t,e]),e?null:t},uc=(0,l().createContext)({dragDropManager:void 0});function cc(e,t,...n){if("undefined"!=typeof process&&void 0===t)throw new Error("invariant requires an error message argument");if(!e){let e;if(void 0===t)e=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{let r=0;e=new Error(t.replace(/%s/g,function(){return n[r++]})),e.name="Invariant Violation"}throw e.framesToPop=1,e}}function dc(e){return"object"==typeof e}u();const fc="dnd-core/INIT_COORDS",pc="dnd-core/BEGIN_DRAG",hc="dnd-core/PUBLISH_DRAG_SOURCE",mc="dnd-core/HOVER",gc="dnd-core/DROP",vc="dnd-core/END_DRAG";function bc(e,t){return{type:fc,payload:{sourceClientOffset:t||null,clientOffset:e||null}}}const yc={type:fc,payload:{clientOffset:null,sourceClientOffset:null}};function wc(e){return function(t=[],n={publishSource:!0}){const{publishSource:r=!0,clientOffset:o,getSourceClientOffset:a}=n,i=e.getMonitor(),l=e.getRegistry();e.dispatch(bc(o)),function(e,t,n){cc(!t.isDragging(),"Cannot call beginDrag while dragging."),e.forEach(function(e){cc(n.getSource(e),"Expected sourceIds to be registered.")})}(t,i,l);const s=function(e,t){let n=null;for(let r=e.length-1;r>=0;r--)if(t.canDragSource(e[r])){n=e[r];break}return n}(t,i);if(null==s)return void e.dispatch(yc);let u=null;if(o){if(!a)throw new Error("getSourceClientOffset must be defined");!function(e){cc("function"==typeof e,"When clientOffset is provided, getSourceClientOffset must be a function.")}(a),u=a(s)}e.dispatch(bc(o,u));const c=l.getSource(s).beginDrag(i,s);if(null==c)return;!function(e){cc(dc(e),"Item must be an object.")}(c),l.pinSource(s);const d=l.getSourceType(s);return{type:pc,payload:{itemType:d,item:c,sourceId:s,clientOffset:o||null,sourceClientOffset:u||null,isSourcePublic:!!r}}}}function Sc(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function kc(e){for(var t=1;t{const i=function(e,t,n,r){const o=n.getTarget(e);let a=o?o.drop(r,e):void 0;(function(e){cc(void 0===e||dc(e),"Drop result must either be an object or undefined.")})(a),void 0===a&&(a=0===t?{}:r.getDropResult());return a}(o,a,r,n),l={type:gc,payload:{dropResult:kc({},t,i)}};e.dispatch(l)})}}function Oc(e){return function(){const t=e.getMonitor(),n=e.getRegistry();!function(e){cc(e.isDragging(),"Cannot call endDrag while not dragging.")}(t);const r=t.getSourceId();if(null!=r){n.getSource(r,!0).endDrag(t,r),n.unpinSource()}return{type:vc}}}function Nc(e,t){return null===t?null===e:Array.isArray(e)?e.some(e=>e===t):e===t}function Cc(e){return function(t,{clientOffset:n}={}){!function(e){cc(Array.isArray(e),"Expected targetIds to be an array.")}(t);const r=t.slice(0),o=e.getMonitor(),a=e.getRegistry();return function(e,t,n){for(let r=e.length-1;r>=0;r--){const o=e[r];Nc(t.getTargetType(o),n)||e.splice(r,1)}}(r,a,o.getItemType()),function(e,t,n){cc(t.isDragging(),"Cannot call hover while not dragging."),cc(!t.didDrop(),"Cannot call hover after drop.");for(let t=0;t{const a=n[o];var i;return r[o]=(i=a,(...n)=>{const r=i.apply(e,n);void 0!==r&&t(r)}),r},{})}dispatch(e){this.store.dispatch(e)}constructor(e,t){this.isSetUp=!1,this.handleRefCountChange=()=>{const e=this.store.getState().refCount>0;this.backend&&(e&&!this.isSetUp?(this.backend.setup(),this.isSetUp=!0):!e&&this.isSetUp&&(this.backend.teardown(),this.isSetUp=!1))},this.store=e,this.monitor=t,e.subscribe(this.handleRefCountChange)}}function Tc(e,t){return{x:e.x-t.x,y:e.y-t.y}}const $c=[],Mc=[];$c.__IS_NONE__=!0,Mc.__IS_ALL__=!0;class Rc{subscribeToStateChange(e,t={}){const{handlerIds:n}=t;cc("function"==typeof e,"listener must be a function."),cc(void 0===n||Array.isArray(n),"handlerIds, when specified, must be an array of strings.");let r=this.store.getState().stateId;return this.store.subscribe(()=>{const t=this.store.getState(),o=t.stateId;try{const a=o===r||o===r+1&&!function(e,t){return e!==$c&&(e===Mc||void 0===t||(n=e,t.filter(e=>n.indexOf(e)>-1)).length>0);var n}(t.dirtyHandlerIds,n);a||e()}finally{r=o}})}subscribeToOffsetChange(e){cc("function"==typeof e,"listener must be a function.");let t=this.store.getState().dragOffset;return this.store.subscribe(()=>{const n=this.store.getState().dragOffset;n!==t&&(t=n,e())})}canDragSource(e){if(!e)return!1;const t=this.registry.getSource(e);return cc(t,`Expected to find a valid source. sourceId=${e}`),!this.isDragging()&&t.canDrag(this,e)}canDropOnTarget(e){if(!e)return!1;const t=this.registry.getTarget(e);if(cc(t,`Expected to find a valid target. targetId=${e}`),!this.isDragging()||this.didDrop())return!1;return Nc(this.registry.getTargetType(e),this.getItemType())&&t.canDrop(this,e)}isDragging(){return Boolean(this.getItemType())}isDraggingSource(e){if(!e)return!1;const t=this.registry.getSource(e,!0);if(cc(t,`Expected to find a valid source. sourceId=${e}`),!this.isDragging()||!this.isSourcePublic())return!1;return this.registry.getSourceType(e)===this.getItemType()&&t.isDragging(this,e)}isOverTarget(e,t={shallow:!1}){if(!e)return!1;const{shallow:n}=t;if(!this.isDragging())return!1;const r=this.registry.getTargetType(e),o=this.getItemType();if(o&&!Nc(r,o))return!1;const a=this.getTargetIds();if(!a.length)return!1;const i=a.indexOf(e);return n?i===a.length-1:i>-1}getItemType(){return this.store.getState().dragOperation.itemType}getItem(){return this.store.getState().dragOperation.item}getSourceId(){return this.store.getState().dragOperation.sourceId}getTargetIds(){return this.store.getState().dragOperation.targetIds}getDropResult(){return this.store.getState().dragOperation.dropResult}didDrop(){return this.store.getState().dragOperation.didDrop}isSourcePublic(){return Boolean(this.store.getState().dragOperation.isSourcePublic)}getInitialClientOffset(){return this.store.getState().dragOffset.initialClientOffset}getInitialSourceClientOffset(){return this.store.getState().dragOffset.initialSourceClientOffset}getClientOffset(){return this.store.getState().dragOffset.clientOffset}getSourceClientOffset(){return function(e){const{clientOffset:t,initialClientOffset:n,initialSourceClientOffset:r}=e;return t&&n&&r?Tc((a=r,{x:(o=t).x+a.x,y:o.y+a.y}),n):null;var o,a}(this.store.getState().dragOffset)}getDifferenceFromInitialOffset(){return function(e){const{clientOffset:t,initialClientOffset:n}=e;return t&&n?Tc(t,n):null}(this.store.getState().dragOffset)}constructor(e,t){this.store=e,this.registry=t}}const xc=void 0!==n.g?n.g:self,Ic=xc.MutationObserver||xc.WebKitMutationObserver;function Pc(e){return function(){const t=setTimeout(r,0),n=setInterval(r,50);function r(){clearTimeout(t),clearInterval(n),e()}}}const _c="function"==typeof Ic?function(e){let t=1;const n=new Ic(e),r=document.createTextNode("");return n.observe(r,{characterData:!0}),function(){t=-t,r.data=t}}:Pc;class Lc{call(){try{this.task&&this.task()}catch(e){this.onError(e)}finally{this.task=null,this.release(this)}}constructor(e,t){this.onError=e,this.release=t,this.task=null}}const Gc=new class{enqueueTask(e){const{queue:t,requestFlush:n}=this;t.length||(n(),this.flushing=!0),t[t.length]=e}constructor(){this.queue=[],this.pendingErrors=[],this.flushing=!1,this.index=0,this.capacity=1024,this.flush=()=>{const{queue:e}=this;for(;this.indexthis.capacity){for(let t=0,n=e.length-this.index;t{this.pendingErrors.push(e),this.requestErrorThrow()},this.requestFlush=_c(this.flush),this.requestErrorThrow=Pc(()=>{if(this.pendingErrors.length)throw this.pendingErrors.shift()})}},Fc=new class{create(e){const t=this.freeTasks,n=t.length?t.pop():new Lc(this.onError,e=>t[t.length]=e);return n.task=e,n}constructor(e){this.onError=e,this.freeTasks=[]}}(Gc.registerPendingError);const zc="dnd-core/ADD_SOURCE",jc="dnd-core/ADD_TARGET",qc="dnd-core/REMOVE_SOURCE",Bc="dnd-core/REMOVE_TARGET";function Vc(e,t){t&&Array.isArray(e)?e.forEach(e=>Vc(e,!1)):cc("string"==typeof e||"symbol"==typeof e,t?"Type can only be a string, a symbol, or an array of either.":"Type can only be a string or a symbol.")}var Hc;!function(e){e.SOURCE="SOURCE",e.TARGET="TARGET"}(Hc||(Hc={}));let Uc=0;function Wc(e){const t=(Uc++).toString();switch(e){case Hc.SOURCE:return`S${t}`;case Hc.TARGET:return`T${t}`;default:throw new Error(`Unknown Handler Role: ${e}`)}}function Qc(e){switch(e[0]){case"S":return Hc.SOURCE;case"T":return Hc.TARGET;default:throw new Error(`Cannot parse handler ID: ${e}`)}}function Kc(e,t){const n=e.entries();let r=!1;do{const{done:e,value:[,o]}=n.next();if(o===t)return!0;r=!!e}while(!r);return!1}class Xc{addSource(e,t){Vc(e),function(e){cc("function"==typeof e.canDrag,"Expected canDrag to be a function."),cc("function"==typeof e.beginDrag,"Expected beginDrag to be a function."),cc("function"==typeof e.endDrag,"Expected endDrag to be a function.")}(t);const n=this.addHandler(Hc.SOURCE,e,t);return this.store.dispatch(function(e){return{type:zc,payload:{sourceId:e}}}(n)),n}addTarget(e,t){Vc(e,!0),function(e){cc("function"==typeof e.canDrop,"Expected canDrop to be a function."),cc("function"==typeof e.hover,"Expected hover to be a function."),cc("function"==typeof e.drop,"Expected beginDrag to be a function.")}(t);const n=this.addHandler(Hc.TARGET,e,t);return this.store.dispatch(function(e){return{type:jc,payload:{targetId:e}}}(n)),n}containsHandler(e){return Kc(this.dragSources,e)||Kc(this.dropTargets,e)}getSource(e,t=!1){cc(this.isSourceId(e),"Expected a valid source ID.");return t&&e===this.pinnedSourceId?this.pinnedSource:this.dragSources.get(e)}getTarget(e){return cc(this.isTargetId(e),"Expected a valid target ID."),this.dropTargets.get(e)}getSourceType(e){return cc(this.isSourceId(e),"Expected a valid source ID."),this.types.get(e)}getTargetType(e){return cc(this.isTargetId(e),"Expected a valid target ID."),this.types.get(e)}isSourceId(e){return Qc(e)===Hc.SOURCE}isTargetId(e){return Qc(e)===Hc.TARGET}removeSource(e){var t;cc(this.getSource(e),"Expected an existing source."),this.store.dispatch(function(e){return{type:qc,payload:{sourceId:e}}}(e)),t=()=>{this.dragSources.delete(e),this.types.delete(e)},Gc.enqueueTask(Fc.create(t))}removeTarget(e){cc(this.getTarget(e),"Expected an existing target."),this.store.dispatch(function(e){return{type:Bc,payload:{targetId:e}}}(e)),this.dropTargets.delete(e),this.types.delete(e)}pinSource(e){const t=this.getSource(e);cc(t,"Expected an existing source."),this.pinnedSourceId=e,this.pinnedSource=t}unpinSource(){cc(this.pinnedSource,"No source is pinned at the time."),this.pinnedSourceId=null,this.pinnedSource=null}addHandler(e,t,n){const r=Wc(e);return this.types.set(r,t),e===Hc.SOURCE?this.dragSources.set(r,n):e===Hc.TARGET&&this.dropTargets.set(r,n),r}constructor(e){this.types=new Map,this.dragSources=new Map,this.dropTargets=new Map,this.pinnedSourceId=null,this.pinnedSource=null,this.store=e}}const Yc=(e,t)=>e===t;function Jc(e=$c,t){switch(t.type){case mc:break;case zc:case jc:case Bc:case qc:return $c;default:return Mc}const{targetIds:n=[],prevTargetIds:r=[]}=t.payload,o=function(e,t){const n=new Map,r=e=>{n.set(e,n.has(e)?n.get(e)+1:1)};e.forEach(r),t.forEach(r);const o=[];return n.forEach((e,t)=>{1===e&&o.push(t)}),o}(n,r);if(!(o.length>0||!function(e,t,n=Yc){if(e.length!==t.length)return!1;for(let r=0;re!==o))});case gc:return rd({},e,{dropResult:n.dropResult,didDrop:!0,targetIds:[]});case vc:return rd({},e,{itemType:null,item:null,sourceId:null,dropResult:null,didDrop:!1,isSourcePublic:null,targetIds:[]});default:return e}var r,o}function id(e=0,t){switch(t.type){case zc:case jc:return e+1;case qc:case Bc:return e-1;default:return e}}function ld(e=0){return e+1}function sd(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function ud(e){for(var t=1;te&&e[t]?e[t]:o||null,n))})}),dragOffset:td(e.dragOffset,t),refCount:id(e.refCount,t),dragOperation:ad(e.dragOperation,t),stateId:ld(e.stateId)};var n,r,o}function dd(e,t=void 0,n={},r=!1){const o=function(e){const t="undefined"!=typeof window&&window.__REDUX_DEVTOOLS_EXTENSION__;return Tl(cd,e&&t&&t({name:"dnd-core",instanceId:"dnd-core"}))}(r),a=new Rc(o,new Xc(o)),i=new Ac(o,a),l=e(i,t,n);return i.receiveBackend(l),i}function fd(e,t){if(null==e)return{};var n,r,o=function(e,t){if(null==e)return{};var n,r,o={},a=Object.keys(e);for(r=0;r=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}let pd=0;const hd=Symbol.for("__REACT_DND_CONTEXT_INSTANCE__");var md=(0,l().memo)(function(e){var{children:t}=e,n=fd(e,["children"]);const[r,o]=function(e){if("manager"in e){return[{dragDropManager:e.manager},!1]}const t=function(e,t=gd(),n,r){const o=t;o[hd]||(o[hd]={dragDropManager:dd(e,t,n,r)});return o[hd]}(e.backend,e.context,e.options,e.debugMode),n=!e.context;return[t,n]}(n);return(0,l().useEffect)(()=>{if(o){const e=gd();return++pd,()=>{0===--pd&&(e[hd]=null)}}},[]),(0,i().jsx)(uc.Provider,{value:r,children:t})});function gd(){return void 0!==n.g?n.g:window}const vd=(0,l().memo)(function({connect:e,src:t}){return(0,l().useEffect)(()=>{if("undefined"==typeof Image)return;let n=!1;const r=new Image;return r.src=t,r.onload=()=>{e(r),n=!0},()=>{n&&e(null)}}),null});e();const bd="undefined"!=typeof window?l().useLayoutEffect:l().useEffect;function yd(t,n,r){const[o,a]=(0,l().useState)(()=>n(t)),i=(0,l().useCallback)(()=>{const i=n(t);e()(o,i)||(a(i),r&&r())},[o,t,r]);return bd(i),[o,i]}function wd(e,t,n){return function(e,t,n){const[r,o]=yd(e,t,n);return bd(function(){const t=e.getHandlerId();if(null!=t)return e.subscribeToStateChange(o,{handlerIds:[t]})},[e,o]),r}(t,e||(()=>({})),()=>n.reconnect())}function Sd(e,t){const n=[...t||[]];return null==t&&"function"!=typeof e&&n.push(e),(0,l().useMemo)(()=>"function"==typeof e?e():e,n)}function kd(e){return(0,l().useMemo)(()=>e.hooks.dragSource(),[e])}function Ed(e){return(0,l().useMemo)(()=>e.hooks.dragPreview(),[e])}function Od(e,t,n,r){let o=n?n.call(r,e,t):void 0;if(void 0!==o)return!!o;if(e===t)return!0;if("object"!=typeof e||!e||"object"!=typeof t||!t)return!1;const a=Object.keys(e),i=Object.keys(t);if(a.length!==i.length)return!1;const l=Object.prototype.hasOwnProperty.bind(t);for(let i=0;i{if(!(0,l().isValidElement)(t)){const r=t;return e(r,n),r}const r=t;!function(e){if("string"==typeof e.type)return;const t=e.type.displayName||e.type.name||"the component";throw new Error(`Only native element nodes can now be passed to React DnD connectors.You can either wrap ${t} into a
, or turn it into a drag source or a drop target itself.`)}(r);return function(e,t){const n=e.ref;return cc("string"!=typeof n,"Cannot connect React DnD to an element with an existing string ref. Please convert it to use a callback ref instead, or wrap it into a or
. Read more: https://reactjs.org/docs/refs-and-the-dom.html#callback-refs"),n?(0,l().cloneElement)(e,{ref:e=>{Ad(n,e),Ad(t,e)}}):(0,l().cloneElement)(e,{ref:t})}(r,n?t=>e(t,n):e)}}function Dd(e){const t={};return Object.keys(e).forEach(n=>{const r=e[n];if(n.endsWith("Ref"))t[n]=e[n];else{const e=Cd(r);t[n]=()=>e}}),t}function Ad(e,t){"function"==typeof e?e(t):e.current=t}class Td{receiveHandlerId(e){this.handlerId!==e&&(this.handlerId=e,this.reconnect())}get connectTarget(){return this.dragSource}get dragSourceOptions(){return this.dragSourceOptionsInternal}set dragSourceOptions(e){this.dragSourceOptionsInternal=e}get dragPreviewOptions(){return this.dragPreviewOptionsInternal}set dragPreviewOptions(e){this.dragPreviewOptionsInternal=e}reconnect(){const e=this.reconnectDragSource();this.reconnectDragPreview(e)}reconnectDragSource(){const e=this.dragSource,t=this.didHandlerIdChange()||this.didConnectedDragSourceChange()||this.didDragSourceOptionsChange();return t&&this.disconnectDragSource(),this.handlerId?e?(t&&(this.lastConnectedHandlerId=this.handlerId,this.lastConnectedDragSource=e,this.lastConnectedDragSourceOptions=this.dragSourceOptions,this.dragSourceUnsubscribe=this.backend.connectDragSource(this.handlerId,e,this.dragSourceOptions)),t):(this.lastConnectedDragSource=e,t):t}reconnectDragPreview(e=!1){const t=this.dragPreview,n=e||this.didHandlerIdChange()||this.didConnectedDragPreviewChange()||this.didDragPreviewOptionsChange();n&&this.disconnectDragPreview(),this.handlerId&&(t?n&&(this.lastConnectedHandlerId=this.handlerId,this.lastConnectedDragPreview=t,this.lastConnectedDragPreviewOptions=this.dragPreviewOptions,this.dragPreviewUnsubscribe=this.backend.connectDragPreview(this.handlerId,t,this.dragPreviewOptions)):this.lastConnectedDragPreview=t)}didHandlerIdChange(){return this.lastConnectedHandlerId!==this.handlerId}didConnectedDragSourceChange(){return this.lastConnectedDragSource!==this.dragSource}didConnectedDragPreviewChange(){return this.lastConnectedDragPreview!==this.dragPreview}didDragSourceOptionsChange(){return!Od(this.lastConnectedDragSourceOptions,this.dragSourceOptions)}didDragPreviewOptionsChange(){return!Od(this.lastConnectedDragPreviewOptions,this.dragPreviewOptions)}disconnectDragSource(){this.dragSourceUnsubscribe&&(this.dragSourceUnsubscribe(),this.dragSourceUnsubscribe=void 0)}disconnectDragPreview(){this.dragPreviewUnsubscribe&&(this.dragPreviewUnsubscribe(),this.dragPreviewUnsubscribe=void 0,this.dragPreviewNode=null,this.dragPreviewRef=null)}get dragSource(){return this.dragSourceNode||this.dragSourceRef&&this.dragSourceRef.current}get dragPreview(){return this.dragPreviewNode||this.dragPreviewRef&&this.dragPreviewRef.current}clearDragSource(){this.dragSourceNode=null,this.dragSourceRef=null}clearDragPreview(){this.dragPreviewNode=null,this.dragPreviewRef=null}constructor(e){this.hooks=Dd({dragSource:(e,t)=>{this.clearDragSource(),this.dragSourceOptions=t||null,Nd(e)?this.dragSourceRef=e:this.dragSourceNode=e,this.reconnectDragSource()},dragPreview:(e,t)=>{this.clearDragPreview(),this.dragPreviewOptions=t||null,Nd(e)?this.dragPreviewRef=e:this.dragPreviewNode=e,this.reconnectDragPreview()}}),this.handlerId=null,this.dragSourceRef=null,this.dragSourceOptionsInternal=null,this.dragPreviewRef=null,this.dragPreviewOptionsInternal=null,this.lastConnectedHandlerId=null,this.lastConnectedDragSource=null,this.lastConnectedDragSourceOptions=null,this.lastConnectedDragPreview=null,this.lastConnectedDragPreviewOptions=null,this.backend=e}}function $d(){const{dragDropManager:e}=(0,l().useContext)(uc);return cc(null!=e,"Expected drag drop context"),e}let Md=!1,Rd=!1;class xd{receiveHandlerId(e){this.sourceId=e}getHandlerId(){return this.sourceId}canDrag(){cc(!Md,"You may not call monitor.canDrag() inside your canDrag() implementation. Read more: http://react-dnd.github.io/react-dnd/docs/api/drag-source-monitor");try{return Md=!0,this.internalMonitor.canDragSource(this.sourceId)}finally{Md=!1}}isDragging(){if(!this.sourceId)return!1;cc(!Rd,"You may not call monitor.isDragging() inside your isDragging() implementation. Read more: http://react-dnd.github.io/react-dnd/docs/api/drag-source-monitor");try{return Rd=!0,this.internalMonitor.isDraggingSource(this.sourceId)}finally{Rd=!1}}subscribeToStateChange(e,t){return this.internalMonitor.subscribeToStateChange(e,t)}isDraggingSource(e){return this.internalMonitor.isDraggingSource(e)}isOverTarget(e,t){return this.internalMonitor.isOverTarget(e,t)}getTargetIds(){return this.internalMonitor.getTargetIds()}isSourcePublic(){return this.internalMonitor.isSourcePublic()}getSourceId(){return this.internalMonitor.getSourceId()}subscribeToOffsetChange(e){return this.internalMonitor.subscribeToOffsetChange(e)}canDragSource(e){return this.internalMonitor.canDragSource(e)}canDropOnTarget(e){return this.internalMonitor.canDropOnTarget(e)}getItemType(){return this.internalMonitor.getItemType()}getItem(){return this.internalMonitor.getItem()}getDropResult(){return this.internalMonitor.getDropResult()}didDrop(){return this.internalMonitor.didDrop()}getInitialClientOffset(){return this.internalMonitor.getInitialClientOffset()}getInitialSourceClientOffset(){return this.internalMonitor.getInitialSourceClientOffset()}getSourceClientOffset(){return this.internalMonitor.getSourceClientOffset()}getClientOffset(){return this.internalMonitor.getClientOffset()}getDifferenceFromInitialOffset(){return this.internalMonitor.getDifferenceFromInitialOffset()}constructor(e){this.sourceId=null,this.internalMonitor=e.getMonitor()}}class Id{beginDrag(){const e=this.spec,t=this.monitor;let n=null;return n="object"==typeof e.item?e.item:"function"==typeof e.item?e.item(t):{},null!=n?n:null}canDrag(){const e=this.spec,t=this.monitor;return"boolean"==typeof e.canDrag?e.canDrag:"function"!=typeof e.canDrag||e.canDrag(t)}isDragging(e,t){const n=this.spec,r=this.monitor,{isDragging:o}=n;return o?o(r):t===e.getSourceId()}endDrag(){const e=this.spec,t=this.monitor,n=this.connector,{end:r}=e;r&&r(t.getItem(),t),n.reconnect()}constructor(e,t,n){this.spec=e,this.monitor=t,this.connector=n}}function Pd(e,t,n){const r=$d(),o=function(e,t,n){const r=(0,l().useMemo)(()=>new Id(e,t,n),[t,n]);return(0,l().useEffect)(()=>{r.spec=e},[e]),r}(e,t,n),a=function(e){return(0,l().useMemo)(()=>{const t=e.type;return cc(null!=t,"spec.type must be defined"),t},[e])}(e);bd(function(){if(null!=a){const[e,i]=function(e,t,n){const r=n.getRegistry(),o=r.addSource(e,t);return[o,()=>r.removeSource(o)]}(a,o,r);return t.receiveHandlerId(e),n.receiveHandlerId(e),i}},[r,t,n,o,a])}function _d(e,t){const n=Sd(e,t);cc(!n.begin,"useDrag::spec.begin was deprecated in v14. Replace spec.begin() with spec.item(). (see more here - https://react-dnd.github.io/react-dnd/docs/api/use-drag)");const r=function(){const e=$d();return(0,l().useMemo)(()=>new xd(e),[e])}(),o=function(e,t){const n=$d(),r=(0,l().useMemo)(()=>new Td(n.getBackend()),[n]);return bd(()=>(r.dragSourceOptions=e||null,r.reconnect(),()=>r.disconnectDragSource()),[r,e]),bd(()=>(r.dragPreviewOptions=t||null,r.reconnect(),()=>r.disconnectDragPreview()),[r,t]),r}(n.options,n.previewOptions);return Pd(n,r,o),[wd(n.collect,r,o),kd(o),Ed(o)]}function Ld(e){const t=$d().getMonitor(),[n,r]=yd(t,e);return(0,l().useEffect)(()=>t.subscribeToOffsetChange(r)),(0,l().useEffect)(()=>t.subscribeToStateChange(r)),n}function Gd(e){return(0,l().useMemo)(()=>e.hooks.dropTarget(),[e])}class Fd{get connectTarget(){return this.dropTarget}reconnect(){const e=this.didHandlerIdChange()||this.didDropTargetChange()||this.didOptionsChange();e&&this.disconnectDropTarget();const t=this.dropTarget;this.handlerId&&(t?e&&(this.lastConnectedHandlerId=this.handlerId,this.lastConnectedDropTarget=t,this.lastConnectedDropTargetOptions=this.dropTargetOptions,this.unsubscribeDropTarget=this.backend.connectDropTarget(this.handlerId,t,this.dropTargetOptions)):this.lastConnectedDropTarget=t)}receiveHandlerId(e){e!==this.handlerId&&(this.handlerId=e,this.reconnect())}get dropTargetOptions(){return this.dropTargetOptionsInternal}set dropTargetOptions(e){this.dropTargetOptionsInternal=e}didHandlerIdChange(){return this.lastConnectedHandlerId!==this.handlerId}didDropTargetChange(){return this.lastConnectedDropTarget!==this.dropTarget}didOptionsChange(){return!Od(this.lastConnectedDropTargetOptions,this.dropTargetOptions)}disconnectDropTarget(){this.unsubscribeDropTarget&&(this.unsubscribeDropTarget(),this.unsubscribeDropTarget=void 0)}get dropTarget(){return this.dropTargetNode||this.dropTargetRef&&this.dropTargetRef.current}clearDropTarget(){this.dropTargetRef=null,this.dropTargetNode=null}constructor(e){this.hooks=Dd({dropTarget:(e,t)=>{this.clearDropTarget(),this.dropTargetOptions=t,Nd(e)?this.dropTargetRef=e:this.dropTargetNode=e,this.reconnect()}}),this.handlerId=null,this.dropTargetRef=null,this.dropTargetOptionsInternal=null,this.lastConnectedHandlerId=null,this.lastConnectedDropTarget=null,this.lastConnectedDropTargetOptions=null,this.backend=e}}let zd=!1;class jd{receiveHandlerId(e){this.targetId=e}getHandlerId(){return this.targetId}subscribeToStateChange(e,t){return this.internalMonitor.subscribeToStateChange(e,t)}canDrop(){if(!this.targetId)return!1;cc(!zd,"You may not call monitor.canDrop() inside your canDrop() implementation. Read more: http://react-dnd.github.io/react-dnd/docs/api/drop-target-monitor");try{return zd=!0,this.internalMonitor.canDropOnTarget(this.targetId)}finally{zd=!1}}isOver(e){return!!this.targetId&&this.internalMonitor.isOverTarget(this.targetId,e)}getItemType(){return this.internalMonitor.getItemType()}getItem(){return this.internalMonitor.getItem()}getDropResult(){return this.internalMonitor.getDropResult()}didDrop(){return this.internalMonitor.didDrop()}getInitialClientOffset(){return this.internalMonitor.getInitialClientOffset()}getInitialSourceClientOffset(){return this.internalMonitor.getInitialSourceClientOffset()}getSourceClientOffset(){return this.internalMonitor.getSourceClientOffset()}getClientOffset(){return this.internalMonitor.getClientOffset()}getDifferenceFromInitialOffset(){return this.internalMonitor.getDifferenceFromInitialOffset()}constructor(e){this.targetId=null,this.internalMonitor=e.getMonitor()}}class qd{canDrop(){const e=this.spec,t=this.monitor;return!e.canDrop||e.canDrop(t.getItem(),t)}hover(){const e=this.spec,t=this.monitor;e.hover&&e.hover(t.getItem(),t)}drop(){const e=this.spec,t=this.monitor;if(e.drop)return e.drop(t.getItem(),t)}constructor(e,t){this.spec=e,this.monitor=t}}function Bd(e,t,n){const r=$d(),o=function(e,t){const n=(0,l().useMemo)(()=>new qd(e,t),[t]);return(0,l().useEffect)(()=>{n.spec=e},[e]),n}(e,t),a=function(e){const{accept:t}=e;return(0,l().useMemo)(()=>(cc(null!=e.accept,"accept must be defined"),Array.isArray(t)?t:[t]),[t])}(e);bd(function(){const[e,i]=function(e,t,n){const r=n.getRegistry(),o=r.addTarget(e,t);return[o,()=>r.removeTarget(o)]}(a,o,r);return t.receiveHandlerId(e),n.receiveHandlerId(e),i},[r,t,o,n,a.map(e=>e.toString()).join("|")])}function Vd(e,t){const n=Sd(e,t),r=function(){const e=$d();return(0,l().useMemo)(()=>new jd(e),[e])}(),o=function(e){const t=$d(),n=(0,l().useMemo)(()=>new Fd(t.getBackend()),[t]);return bd(()=>(n.dropTargetOptions=e||null,n.reconnect(),()=>n.disconnectDropTarget()),[e]),n}(n.options);return Bd(n,r,o),[wd(n.collect,r,o),Gd(o)]}function Hd(e){let t=null;return()=>(null==t&&(t=e()),t)}class Ud{enter(e){const t=this.entered.length;return this.entered=function(e,t){const n=new Set,r=e=>n.add(e);e.forEach(r),t.forEach(r);const o=[];return n.forEach(e=>o.push(e)),o}(this.entered.filter(t=>this.isNodeInDocument(t)&&(!t.contains||t.contains(e))),[e]),0===t&&this.entered.length>0}leave(e){const t=this.entered.length;var n,r;return this.entered=(n=this.entered.filter(this.isNodeInDocument),r=e,n.filter(e=>e!==r)),t>0&&0===this.entered.length}reset(){this.entered=[]}constructor(e){this.entered=[],this.isNodeInDocument=e}}class Wd{initializeExposedProperties(){Object.keys(this.config.exposeProperties).forEach(e=>{Object.defineProperty(this.item,e,{configurable:!0,enumerable:!0,get:()=>(console.warn(`Browser doesn't allow reading "${e}" until the drop event.`),null)})})}loadDataTransfer(e){if(e){const t={};Object.keys(this.config.exposeProperties).forEach(n=>{const r=this.config.exposeProperties[n];null!=r&&(t[n]={value:r(e,this.config.matchesTypes),configurable:!0,enumerable:!0})}),Object.defineProperties(this.item,t)}}canDrag(){return!0}beginDrag(){return this.item}isDragging(e,t){return t===e.getSourceId()}endDrag(){}constructor(e){this.config=e,this.item={},this.initializeExposedProperties()}}const Qd="__NATIVE_FILE__",Kd="__NATIVE_URL__",Xd="__NATIVE_TEXT__",Yd="__NATIVE_HTML__";function Jd(e,t,n){const r=t.reduce((t,n)=>t||e.getData(n),"");return null!=r?r:n}const Zd={[Qd]:{exposeProperties:{files:e=>Array.prototype.slice.call(e.files),items:e=>e.items,dataTransfer:e=>e},matchesTypes:["Files"]},[Yd]:{exposeProperties:{html:(e,t)=>Jd(e,t,""),dataTransfer:e=>e},matchesTypes:["Html","text/html"]},[Kd]:{exposeProperties:{urls:(e,t)=>Jd(e,t,"").split("\n"),dataTransfer:e=>e},matchesTypes:["Url","text/uri-list"]},[Xd]:{exposeProperties:{text:(e,t)=>Jd(e,t,""),dataTransfer:e=>e},matchesTypes:["Text","text/plain"]}};function ef(e){if(!e)return null;const t=Array.prototype.slice.call(e.types||[]);return Object.keys(Zd).filter(e=>{const n=Zd[e];return!!(null==n?void 0:n.matchesTypes)&&n.matchesTypes.some(e=>t.indexOf(e)>-1)})[0]||null}const tf=Hd(()=>/firefox/i.test(navigator.userAgent)),nf=Hd(()=>Boolean(window.safari));class rf{interpolate(e){const{xs:t,ys:n,c1s:r,c2s:o,c3s:a}=this;let i=t.length-1;if(e===t[i])return n[i];let l,s=0,u=a.length-1;for(;s<=u;){l=Math.floor(.5*(s+u));const r=t[l];if(re))return n[l];u=l-1}}i=Math.max(0,u);const c=e-t[i],d=c*c;return n[i]+r[i]*c+o[i]*d+a[i]*c*d}constructor(e,t){const{length:n}=e,r=[];for(let e=0;ee[t]{let e=new rf([0,.5,1],[u.y,u.y/d*m,u.y+m-d]).interpolate(p);return nf()&&a&&(e+=(window.devicePixelRatio-1)*m),e})()}}class sf{get window(){return this.globalContext?this.globalContext:"undefined"!=typeof window?window:void 0}get document(){var e;return(null===(e=this.globalContext)||void 0===e?void 0:e.document)?this.globalContext.document:this.window?this.window.document:void 0}get rootElement(){var e;return(null===(e=this.optionsArgs)||void 0===e?void 0:e.rootElement)||this.window}constructor(e,t){this.ownerDocument=null,this.globalContext=e,this.optionsArgs=t}}function uf(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function cf(e){for(var t=1;t{this.sourcePreviewNodes.delete(e),this.sourcePreviewNodeOptions.delete(e)}}connectDragSource(e,t,n){this.sourceNodes.set(e,t),this.sourceNodeOptions.set(e,n);const r=t=>this.handleDragStart(t,e),o=e=>this.handleSelectStart(e);return t.setAttribute("draggable","true"),t.addEventListener("dragstart",r),t.addEventListener("selectstart",o),()=>{this.sourceNodes.delete(e),this.sourceNodeOptions.delete(e),t.removeEventListener("dragstart",r),t.removeEventListener("selectstart",o),t.setAttribute("draggable","false")}}connectDropTarget(e,t){const n=t=>this.handleDragEnter(t,e),r=t=>this.handleDragOver(t,e),o=t=>this.handleDrop(t,e);return t.addEventListener("dragenter",n),t.addEventListener("dragover",r),t.addEventListener("drop",o),()=>{t.removeEventListener("dragenter",n),t.removeEventListener("dragover",r),t.removeEventListener("drop",o)}}addEventListeners(e){e.addEventListener&&(e.addEventListener("dragstart",this.handleTopDragStart),e.addEventListener("dragstart",this.handleTopDragStartCapture,!0),e.addEventListener("dragend",this.handleTopDragEndCapture,!0),e.addEventListener("dragenter",this.handleTopDragEnter),e.addEventListener("dragenter",this.handleTopDragEnterCapture,!0),e.addEventListener("dragleave",this.handleTopDragLeaveCapture,!0),e.addEventListener("dragover",this.handleTopDragOver),e.addEventListener("dragover",this.handleTopDragOverCapture,!0),e.addEventListener("drop",this.handleTopDrop),e.addEventListener("drop",this.handleTopDropCapture,!0))}removeEventListeners(e){e.removeEventListener&&(e.removeEventListener("dragstart",this.handleTopDragStart),e.removeEventListener("dragstart",this.handleTopDragStartCapture,!0),e.removeEventListener("dragend",this.handleTopDragEndCapture,!0),e.removeEventListener("dragenter",this.handleTopDragEnter),e.removeEventListener("dragenter",this.handleTopDragEnterCapture,!0),e.removeEventListener("dragleave",this.handleTopDragLeaveCapture,!0),e.removeEventListener("dragover",this.handleTopDragOver),e.removeEventListener("dragover",this.handleTopDragOverCapture,!0),e.removeEventListener("drop",this.handleTopDrop),e.removeEventListener("drop",this.handleTopDropCapture,!0))}getCurrentSourceNodeOptions(){const e=this.monitor.getSourceId(),t=this.sourceNodeOptions.get(e);return cf({dropEffect:this.altKeyPressed?"copy":"move"},t||{})}getCurrentDropEffect(){return this.isDraggingNativeItem()?"copy":this.getCurrentSourceNodeOptions().dropEffect}getCurrentSourcePreviewNodeOptions(){const e=this.monitor.getSourceId();return cf({anchorX:.5,anchorY:.5,captureDraggingState:!1},this.sourcePreviewNodeOptions.get(e)||{})}isDraggingNativeItem(){const e=this.monitor.getItemType();return Object.keys(g).some(t=>g[t]===e)}beginDragNativeItem(e,t){this.clearCurrentDragSourceNode(),this.currentNativeSource=function(e,t){const n=Zd[e];if(!n)throw new Error(`native type ${e} has no configuration`);const r=new Wd(n);return r.loadDataTransfer(t),r}(e,t),this.currentNativeHandle=this.registry.addSource(e,this.currentNativeSource),this.actions.beginDrag([this.currentNativeHandle])}setCurrentDragSourceNode(e){this.clearCurrentDragSourceNode(),this.currentDragSourceNode=e;this.mouseMoveTimeoutTimer=setTimeout(()=>{var e;return null===(e=this.rootElement)||void 0===e?void 0:e.addEventListener("mousemove",this.endDragIfSourceWasRemovedFromDOM,!0)},1e3)}clearCurrentDragSourceNode(){if(this.currentDragSourceNode){var e;if(this.currentDragSourceNode=null,this.rootElement)null===(e=this.window)||void 0===e||e.clearTimeout(this.mouseMoveTimeoutTimer||void 0),this.rootElement.removeEventListener("mousemove",this.endDragIfSourceWasRemovedFromDOM,!0);return this.mouseMoveTimeoutTimer=null,!0}return!1}handleDragStart(e,t){e.defaultPrevented||(this.dragStartSourceIds||(this.dragStartSourceIds=[]),this.dragStartSourceIds.unshift(t))}handleDragEnter(e,t){this.dragEnterTargetIds.unshift(t)}handleDragOver(e,t){null===this.dragOverTargetIds&&(this.dragOverTargetIds=[]),this.dragOverTargetIds.unshift(t)}handleDrop(e,t){this.dropTargetIds.unshift(t)}constructor(e,t,n){this.sourcePreviewNodes=new Map,this.sourcePreviewNodeOptions=new Map,this.sourceNodes=new Map,this.sourceNodeOptions=new Map,this.dragStartSourceIds=null,this.dropTargetIds=[],this.dragEnterTargetIds=[],this.currentNativeSource=null,this.currentNativeHandle=null,this.currentDragSourceNode=null,this.altKeyPressed=!1,this.mouseMoveTimeoutTimer=null,this.asyncEndDragFrameId=null,this.dragOverTargetIds=null,this.lastClientOffset=null,this.hoverRafId=null,this.getSourceClientOffset=e=>{const t=this.sourceNodes.get(e);return t&&of(t)||null},this.endDragNativeItem=()=>{this.isDraggingNativeItem()&&(this.actions.endDrag(),this.currentNativeHandle&&this.registry.removeSource(this.currentNativeHandle),this.currentNativeHandle=null,this.currentNativeSource=null)},this.isNodeInDocument=e=>Boolean(e&&this.document&&this.document.body&&this.document.body.contains(e)),this.endDragIfSourceWasRemovedFromDOM=()=>{const e=this.currentDragSourceNode;null==e||this.isNodeInDocument(e)||(this.clearCurrentDragSourceNode()&&this.monitor.isDragging()&&this.actions.endDrag(),this.cancelHover())},this.scheduleHover=e=>{null===this.hoverRafId&&"undefined"!=typeof requestAnimationFrame&&(this.hoverRafId=requestAnimationFrame(()=>{this.monitor.isDragging()&&this.actions.hover(e||[],{clientOffset:this.lastClientOffset}),this.hoverRafId=null}))},this.cancelHover=()=>{null!==this.hoverRafId&&"undefined"!=typeof cancelAnimationFrame&&(cancelAnimationFrame(this.hoverRafId),this.hoverRafId=null)},this.handleTopDragStartCapture=()=>{this.clearCurrentDragSourceNode(),this.dragStartSourceIds=[]},this.handleTopDragStart=e=>{if(e.defaultPrevented)return;const{dragStartSourceIds:t}=this;this.dragStartSourceIds=null;const n=af(e);this.monitor.isDragging()&&(this.actions.endDrag(),this.cancelHover()),this.actions.beginDrag(t||[],{publishSource:!1,getSourceClientOffset:this.getSourceClientOffset,clientOffset:n});const{dataTransfer:r}=e,o=ef(r);if(this.monitor.isDragging()){if(r&&"function"==typeof r.setDragImage){const e=this.monitor.getSourceId(),t=this.sourceNodes.get(e),o=this.sourcePreviewNodes.get(e)||t;if(o){const{anchorX:e,anchorY:a,offsetX:i,offsetY:l}=this.getCurrentSourcePreviewNodeOptions(),s=lf(t,o,n,{anchorX:e,anchorY:a},{offsetX:i,offsetY:l});r.setDragImage(o,s.x,s.y)}}try{null==r||r.setData("application/json",{})}catch(e){}this.setCurrentDragSourceNode(e.target);const{captureDraggingState:t}=this.getCurrentSourcePreviewNodeOptions();t?this.actions.publishDragSource():setTimeout(()=>this.actions.publishDragSource(),0)}else if(o)this.beginDragNativeItem(o);else{if(r&&!r.types&&(e.target&&!e.target.hasAttribute||!e.target.hasAttribute("draggable")))return;e.preventDefault()}},this.handleTopDragEndCapture=()=>{this.clearCurrentDragSourceNode()&&this.monitor.isDragging()&&this.actions.endDrag(),this.cancelHover()},this.handleTopDragEnterCapture=e=>{var t;(this.dragEnterTargetIds=[],this.isDraggingNativeItem())&&(null===(t=this.currentNativeSource)||void 0===t||t.loadDataTransfer(e.dataTransfer));if(!this.enterLeaveCounter.enter(e.target)||this.monitor.isDragging())return;const{dataTransfer:n}=e,r=ef(n);r&&this.beginDragNativeItem(r,n)},this.handleTopDragEnter=e=>{const{dragEnterTargetIds:t}=this;if(this.dragEnterTargetIds=[],!this.monitor.isDragging())return;this.altKeyPressed=e.altKey,t.length>0&&this.actions.hover(t,{clientOffset:af(e)});t.some(e=>this.monitor.canDropOnTarget(e))&&(e.preventDefault(),e.dataTransfer&&(e.dataTransfer.dropEffect=this.getCurrentDropEffect()))},this.handleTopDragOverCapture=e=>{var t;(this.dragOverTargetIds=[],this.isDraggingNativeItem())&&(null===(t=this.currentNativeSource)||void 0===t||t.loadDataTransfer(e.dataTransfer))},this.handleTopDragOver=e=>{const{dragOverTargetIds:t}=this;if(this.dragOverTargetIds=[],!this.monitor.isDragging())return e.preventDefault(),void(e.dataTransfer&&(e.dataTransfer.dropEffect="none"));this.altKeyPressed=e.altKey,this.lastClientOffset=af(e),this.scheduleHover(t);(t||[]).some(e=>this.monitor.canDropOnTarget(e))?(e.preventDefault(),e.dataTransfer&&(e.dataTransfer.dropEffect=this.getCurrentDropEffect())):this.isDraggingNativeItem()?e.preventDefault():(e.preventDefault(),e.dataTransfer&&(e.dataTransfer.dropEffect="none"))},this.handleTopDragLeaveCapture=e=>{this.isDraggingNativeItem()&&e.preventDefault();this.enterLeaveCounter.leave(e.target)&&(this.isDraggingNativeItem()&&setTimeout(()=>this.endDragNativeItem(),0),this.cancelHover())},this.handleTopDropCapture=e=>{var t;(this.dropTargetIds=[],this.isDraggingNativeItem())?(e.preventDefault(),null===(t=this.currentNativeSource)||void 0===t||t.loadDataTransfer(e.dataTransfer)):ef(e.dataTransfer)&&e.preventDefault();this.enterLeaveCounter.reset()},this.handleTopDrop=e=>{const{dropTargetIds:t}=this;this.dropTargetIds=[],this.actions.hover(t,{clientOffset:af(e)}),this.actions.drop({dropEffect:this.getCurrentDropEffect()}),this.isDraggingNativeItem()?this.endDragNativeItem():this.monitor.isDragging()&&this.actions.endDrag(),this.cancelHover()},this.handleSelectStart=e=>{const t=e.target;"function"==typeof t.dragDrop&&("INPUT"===t.tagName||"SELECT"===t.tagName||"TEXTAREA"===t.tagName||t.isContentEditable||(e.preventDefault(),t.dragDrop()))},this.options=new sf(t,n),this.actions=e.getActions(),this.monitor=e.getMonitor(),this.registry=e.getRegistry(),this.enterLeaveCounter=new Ud(this.isNodeInDocument)}}let ff;function pf(){return ff||(ff=new Image,ff.src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="),ff}const hf=function(e,t,n){return new df(e,t,n)};var mf;!function(e){e.mouse="mouse",e.touch="touch",e.keyboard="keyboard"}(mf||(mf={}));class gf{get delay(){var e;return null!==(e=this.args.delay)&&void 0!==e?e:0}get scrollAngleRanges(){return this.args.scrollAngleRanges}get getDropTargetElementsAtPoint(){return this.args.getDropTargetElementsAtPoint}get ignoreContextMenu(){var e;return null!==(e=this.args.ignoreContextMenu)&&void 0!==e&&e}get enableHoverOutsideTarget(){var e;return null!==(e=this.args.enableHoverOutsideTarget)&&void 0!==e&&e}get enableKeyboardEvents(){var e;return null!==(e=this.args.enableKeyboardEvents)&&void 0!==e&&e}get enableMouseEvents(){var e;return null!==(e=this.args.enableMouseEvents)&&void 0!==e&&e}get enableTouchEvents(){var e;return null===(e=this.args.enableTouchEvents)||void 0===e||e}get touchSlop(){return this.args.touchSlop||0}get delayTouchStart(){var e,t,n,r;return null!==(r=null!==(n=null===(e=this.args)||void 0===e?void 0:e.delayTouchStart)&&void 0!==n?n:null===(t=this.args)||void 0===t?void 0:t.delay)&&void 0!==r?r:0}get delayMouseStart(){var e,t,n,r;return null!==(r=null!==(n=null===(e=this.args)||void 0===e?void 0:e.delayMouseStart)&&void 0!==n?n:null===(t=this.args)||void 0===t?void 0:t.delay)&&void 0!==r?r:0}get window(){return this.context&&this.context.window?this.context.window:"undefined"!=typeof window?window:void 0}get document(){var e;return(null===(e=this.context)||void 0===e?void 0:e.document)?this.context.document:this.window?this.window.document:void 0}get rootElement(){var e;return(null===(e=this.args)||void 0===e?void 0:e.rootElement)||this.document}constructor(e,t){this.args=e,this.context=t}}const vf=1,bf=0;function yf(e){return void 0===e.button||e.button===bf}function wf(e){return!!e.targetTouches}function Sf(e,t){return wf(e)?function(e,t){return 1===e.targetTouches.length?Sf(e.targetTouches[0]):t&&1===e.touches.length&&e.touches[0].target===t.target?Sf(e.touches[0]):void 0}(e,t):{x:e.clientX,y:e.clientY}}const kf=(()=>{let e=!1;try{addEventListener("test",()=>{},Object.defineProperty({},"passive",{get:()=>(e=!0,!0)}))}catch(e){}return e})(),Ef={[mf.mouse]:{start:"mousedown",move:"mousemove",end:"mouseup",contextmenu:"contextmenu"},[mf.touch]:{start:"touchstart",move:"touchmove",end:"touchend"},[mf.keyboard]:{keydown:"keydown"}};class Of{profile(){var e;return{sourceNodes:this.sourceNodes.size,sourcePreviewNodes:this.sourcePreviewNodes.size,sourcePreviewNodeOptions:this.sourcePreviewNodeOptions.size,targetNodes:this.targetNodes.size,dragOverTargetIds:(null===(e=this.dragOverTargetIds)||void 0===e?void 0:e.length)||0}}get document(){return this.options.document}setup(){const e=this.options.rootElement;e&&(cc(!Of.isSetUp,"Cannot have two Touch backends at the same time."),Of.isSetUp=!0,this.addEventListener(e,"start",this.getTopMoveStartHandler()),this.addEventListener(e,"start",this.handleTopMoveStartCapture,!0),this.addEventListener(e,"move",this.handleTopMove),this.addEventListener(e,"move",this.handleTopMoveCapture,!0),this.addEventListener(e,"end",this.handleTopMoveEndCapture,!0),this.options.enableMouseEvents&&!this.options.ignoreContextMenu&&this.addEventListener(e,"contextmenu",this.handleTopMoveEndCapture),this.options.enableKeyboardEvents&&this.addEventListener(e,"keydown",this.handleCancelOnEscape,!0))}teardown(){const e=this.options.rootElement;e&&(Of.isSetUp=!1,this._mouseClientOffset={},this.removeEventListener(e,"start",this.handleTopMoveStartCapture,!0),this.removeEventListener(e,"start",this.handleTopMoveStart),this.removeEventListener(e,"move",this.handleTopMoveCapture,!0),this.removeEventListener(e,"move",this.handleTopMove),this.removeEventListener(e,"end",this.handleTopMoveEndCapture,!0),this.options.enableMouseEvents&&!this.options.ignoreContextMenu&&this.removeEventListener(e,"contextmenu",this.handleTopMoveEndCapture),this.options.enableKeyboardEvents&&this.removeEventListener(e,"keydown",this.handleCancelOnEscape,!0),this.uninstallSourceNodeRemovalObserver())}addEventListener(e,t,n,r=!1){const o=kf?{capture:r,passive:!1}:r;this.listenerTypes.forEach(function(r){const a=Ef[r][t];a&&e.addEventListener(a,n,o)})}removeEventListener(e,t,n,r=!1){const o=kf?{capture:r,passive:!1}:r;this.listenerTypes.forEach(function(r){const a=Ef[r][t];a&&e.removeEventListener(a,n,o)})}connectDragSource(e,t){const n=this.handleMoveStart.bind(this,e);return this.sourceNodes.set(e,t),this.addEventListener(t,"start",n),()=>{this.sourceNodes.delete(e),this.removeEventListener(t,"start",n)}}connectDragPreview(e,t,n){return this.sourcePreviewNodeOptions.set(e,n),this.sourcePreviewNodes.set(e,t),()=>{this.sourcePreviewNodes.delete(e),this.sourcePreviewNodeOptions.delete(e)}}connectDropTarget(e,t){const n=this.options.rootElement;if(!this.document||!n)return()=>{};const r=r=>{if(!this.document||!n||!this.monitor.isDragging())return;let o;switch(r.type){case Ef.mouse.move:o={x:r.clientX,y:r.clientY};break;case Ef.touch.move:var a,i;o={x:(null===(a=r.touches[0])||void 0===a?void 0:a.clientX)||0,y:(null===(i=r.touches[0])||void 0===i?void 0:i.clientY)||0}}const l=null!=o?this.document.elementFromPoint(o.x,o.y):void 0,s=l&&t.contains(l);return l===t||s?this.handleMove(r,e):void 0};return this.addEventListener(this.document.body,"move",r),this.targetNodes.set(e,t),()=>{this.document&&(this.targetNodes.delete(e),this.removeEventListener(this.document.body,"move",r))}}getTopMoveStartHandler(){return this.options.delayTouchStart||this.options.delayMouseStart?this.handleTopMoveStartDelay:this.handleTopMoveStart}installSourceNodeRemovalObserver(e){this.uninstallSourceNodeRemovalObserver(),this.draggedSourceNode=e,this.draggedSourceNodeRemovalObserver=new MutationObserver(()=>{e&&!e.parentElement&&(this.resurrectSourceNode(),this.uninstallSourceNodeRemovalObserver())}),e&&e.parentElement&&this.draggedSourceNodeRemovalObserver.observe(e.parentElement,{childList:!0})}resurrectSourceNode(){this.document&&this.draggedSourceNode&&(this.draggedSourceNode.style.display="none",this.draggedSourceNode.removeAttribute("data-reactid"),this.document.body.appendChild(this.draggedSourceNode))}uninstallSourceNodeRemovalObserver(){this.draggedSourceNodeRemovalObserver&&this.draggedSourceNodeRemovalObserver.disconnect(),this.draggedSourceNodeRemovalObserver=void 0,this.draggedSourceNode=void 0}constructor(e,t,n){this.getSourceClientOffset=e=>{const t=this.sourceNodes.get(e);return t&&function(e){const t=1===e.nodeType?e:e.parentElement;if(!t)return;const{top:n,left:r}=t.getBoundingClientRect();return{x:r,y:n}}(t)},this.handleTopMoveStartCapture=e=>{yf(e)&&(this.moveStartSourceIds=[])},this.handleMoveStart=e=>{Array.isArray(this.moveStartSourceIds)&&this.moveStartSourceIds.unshift(e)},this.handleTopMoveStart=e=>{if(!yf(e))return;const t=Sf(e);t&&(wf(e)&&(this.lastTargetTouchFallback=e.targetTouches[0]),this._mouseClientOffset=t),this.waitingForDelay=!1},this.handleTopMoveStartDelay=e=>{if(!yf(e))return;const t=e.type===Ef.touch.start?this.options.delayTouchStart:this.options.delayMouseStart;this.timeout=setTimeout(this.handleTopMoveStart.bind(this,e),t),this.waitingForDelay=!0},this.handleTopMoveCapture=()=>{this.dragOverTargetIds=[]},this.handleMove=(e,t)=>{this.dragOverTargetIds&&this.dragOverTargetIds.unshift(t)},this.handleTopMove=e=>{if(this.timeout&&clearTimeout(this.timeout),!this.document||this.waitingForDelay)return;const{moveStartSourceIds:t,dragOverTargetIds:n}=this,r=this.options.enableHoverOutsideTarget,o=Sf(e,this.lastTargetTouchFallback);if(!o)return;if(this._isScrolling||!this.monitor.isDragging()&&function(e,t,n,r,o){if(!o)return!1;const a=180*Math.atan2(r-t,n-e)/Math.PI+180;for(let e=0;e=t.start)&&(null==t.end||a<=t.end))return!0}return!1}(this._mouseClientOffset.x||0,this._mouseClientOffset.y||0,o.x,o.y,this.options.scrollAngleRanges))return void(this._isScrolling=!0);var a,i,l,s;if(!this.monitor.isDragging()&&this._mouseClientOffset.hasOwnProperty("x")&&t&&(a=this._mouseClientOffset.x||0,i=this._mouseClientOffset.y||0,l=o.x,s=o.y,Math.sqrt(Math.pow(Math.abs(l-a),2)+Math.pow(Math.abs(s-i),2))>(this.options.touchSlop?this.options.touchSlop:0))&&(this.moveStartSourceIds=void 0,this.actions.beginDrag(t,{clientOffset:this._mouseClientOffset,getSourceClientOffset:this.getSourceClientOffset,publishSource:!1})),!this.monitor.isDragging())return;const u=this.sourceNodes.get(this.monitor.getSourceId());this.installSourceNodeRemovalObserver(u),this.actions.publishDragSource(),e.cancelable&&e.preventDefault();const c=(n||[]).map(e=>this.targetNodes.get(e)).filter(e=>!!e),d=this.options.getDropTargetElementsAtPoint?this.options.getDropTargetElementsAtPoint(o.x,o.y,c):this.document.elementsFromPoint(o.x,o.y),f=[];for(const e in d){if(!d.hasOwnProperty(e))continue;let t=d[e];for(null!=t&&f.push(t);t;)t=t.parentElement,t&&-1===f.indexOf(t)&&f.push(t)}const p=f.filter(e=>c.indexOf(e)>-1).map(e=>this._getDropTargetId(e)).filter(e=>!!e).filter((e,t,n)=>n.indexOf(e)===t);if(r)for(const e in this.targetNodes){const t=this.targetNodes.get(e);if(u&&t&&t.contains(u)&&-1===p.indexOf(e)){p.unshift(e);break}}p.reverse(),this.actions.hover(p,{clientOffset:o})},this._getDropTargetId=e=>{const t=this.targetNodes.keys();let n=t.next();for(;!1===n.done;){const r=n.value;if(e===this.targetNodes.get(r))return r;n=t.next()}},this.handleTopMoveEndCapture=e=>{this._isScrolling=!1,this.lastTargetTouchFallback=void 0,function(e){return void 0===e.buttons||0===(e.buttons&vf)}(e)&&(this.monitor.isDragging()&&!this.monitor.didDrop()?(e.cancelable&&e.preventDefault(),this._mouseClientOffset={},this.uninstallSourceNodeRemovalObserver(),this.actions.drop(),this.actions.endDrag()):this.moveStartSourceIds=void 0)},this.handleCancelOnEscape=e=>{"Escape"===e.key&&this.monitor.isDragging()&&(this._mouseClientOffset={},this.uninstallSourceNodeRemovalObserver(),this.actions.endDrag())},this.options=new gf(n,t),this.actions=e.getActions(),this.monitor=e.getMonitor(),this.sourceNodes=new Map,this.sourcePreviewNodes=new Map,this.sourcePreviewNodeOptions=new Map,this.targetNodes=new Map,this.listenerTypes=[],this._mouseClientOffset={},this._isScrolling=!1,this.options.enableMouseEvents&&this.listenerTypes.push(mf.mouse),this.options.enableTouchEvents&&this.listenerTypes.push(mf.touch),this.options.enableKeyboardEvents&&this.listenerTypes.push(mf.keyboard)}}const Nf=function(e,t={},n={}){return new Of(e,t,n)},Cf=(uu(),e=>l().createElement(ms,{context:qs,store:uu()},e.children)),Df=e=>l().createElement(Cf,null,l().createElement(xu,{props:e})),Af=e=>{const t=l().useId();return l().createElement("div",{className:"form-check form-check-inline"},l().createElement("input",{className:"form-check-input",type:"radio",id:t,value:e.name,checked:e.checked,disabled:e.disabled,onChange:t=>e.handleOnChange(t.target.value)}),l().createElement("label",{className:"form-check-label",htmlFor:t},e.label))},Tf=(e=>{const t=t=>{const n=Lu(e);return l().createElement(ku.Provider,{value:n},t.children)};return e=>l().createElement(t,null,l().createElement($u,e,e.children))})({controlClassnames:{actionElement:"btn btn-sm",addGroup:"btn-secondary",addRule:"btn-primary",cloneGroup:"btn-secondary",cloneRule:"btn-secondary",lockGroup:"btn-secondary",lockRule:"btn-secondary",removeGroup:"btn-danger",removeRule:"btn-danger",undoAction:"btn-secondary",redoAction:"btn-secondary",valueSelector:"form-select form-select-sm"},controlElements:{notToggle:({className:e,handleOnChange:t,title:n,label:r,checked:o,disabled:a})=>{const i=(0,l().useId)();return l().createElement("div",{className:`form-check-inline form-switch ${e}`},l().createElement("input",{id:i,className:"form-check-input",type:"checkbox",onChange:e=>t(e.target.checked),checked:!!o,disabled:a}),l().createElement("label",{title:n,htmlFor:i,className:"form-check-label"},r))},valueEditor:e=>{const{valueListItemClassName:t}=Tu(e);if("null"===e.operator||"notNull"===e.operator)return null;if(!("between"!==e.operator&&"notBetween"!==e.operator||"select"!==e.type&&"text"!==e.type)){const n="text"===e.type?"form-control form-control-sm":"form-select form-select-sm";return l().createElement(Au,{...e,skipHook:!0,schema:{...e.schema,classNames:{...e.schema.classNames,valueListItem:`${t} ${n}`}}})}switch(e.type){case"select":case"multiselect":return l().createElement(Au,{...e,skipHook:!0,className:`${e.className} form-select form-select-sm`});case"switch":return l().createElement("span",{className:`${e.className} custom-control custom-switch`},l().createElement("input",{type:"checkbox",className:"form-check-input custom-control-input",title:e.title,disabled:e.disabled,onChange:t=>e.handleOnChange(t.target.checked),checked:!!e.value}));case"checkbox":return l().createElement(Au,{...e,skipHook:!0,className:`${e.className} form-check-input`});case"radio":return l().createElement("span",{title:e.title,className:hn.value},e.values?.map(t=>l().createElement(Af,{key:t.name,name:t.name,disabled:e.disabled,checked:e.value===t.name,handleOnChange:e.handleOnChange,label:t.label})))}return l().createElement(Au,{...e,skipHook:!0,className:`${e.className} form-control form-control-sm`})}},translations:{removeGroup:{label:l().createElement("i",{className:"bi bi-x"})},removeRule:{label:l().createElement("i",{className:"bi bi-x"})},cloneRule:{label:l().createElement("i",{className:"bi bi-copy"})},cloneRuleGroup:{label:l().createElement("i",{className:"bi bi-copy"})},dragHandle:{label:l().createElement("i",{className:"bi bi-grip-vertical"})},lockGroup:{label:l().createElement("i",{className:"bi bi-unlock"})},lockRule:{label:l().createElement("i",{className:"bi bi-unlock"})},lockGroupDisabled:{label:l().createElement("i",{className:"bi bi-lock"})},lockRuleDisabled:{label:l().createElement("i",{className:"bi bi-lock"})},shiftActionUp:{label:l().createElement("i",{className:"bi bi-chevron-compact-up"})},shiftActionDown:{label:l().createElement("i",{className:"bi bi-chevron-compact-down"})},undo:{label:l().createElement("i",{className:"bi bi-arrow-counterclockwise"})},redo:{label:l().createElement("i",{className:"bi bi-arrow-clockwise"})}}}),$f=e=>"string"==typeof e?e.toLowerCase():e,Mf=(new RegExp(Me.source.replace(/^\^/,String.raw`^\s*`).replace(/\$$/,String.raw`\s*$`)),e=>null!==e&&"object"==typeof e&&Object.getPrototypeOf(e)===Object.prototype),Rf=e=>Mf(e)&&Array.isArray(e.rules),xf=e=>Rf(e)&&"string"==typeof e.combinator,If=Object.keys;Object.entries;const Pf={"=":"!=","!=":"=","<":">=","<=":">",">":"<=",">=":"<",beginsWith:"doesNotBeginWith",doesNotBeginWith:"beginsWith",endsWith:"doesNotEndWith",doesNotEndWith:"endsWith",contains:"doesNotContain",doesNotContain:"contains",between:"notBetween",notBetween:"between",in:"notIn",notIn:"in",notNull:"null",null:"notNull"},_f=(e,t=",")=>e.map(e=>`${e??""}`.replaceAll(t[0],`\\${t[0]}`)).join(t),Lf=e=>Mf(e)&&"name"in e&&"string"==typeof e.name,Gf=e=>Mf(e)&&"value"in e&&"string"==typeof e.value;function Ff(e,t,n){if("string"==typeof e)return{...t,name:e,value:e,label:n?.[e]??e};const r={};let o=!!t;return Lf(e)&&!Gf(e)?(r.value=e.name,o=!0):!Lf(e)&&Gf(e)&&(r.name=e.value,o=!0),o?Object.assign({},t,e,r):e}function zf(e,t,n){if(!Array.isArray(e))return[];const r=e;return Bf(r)?r.map(e=>({...e,options:e.options.map(e=>Ff(e,t,n))})):r.map(e=>Ff(e,t,n))}const jf=e=>{const t=new Set,n=[];for(const r of e)t.has(r.value??r.name)||(t.add(r.value??r.name),n.push(r));return e.length===n.length?e:n},qf=e=>{let t=!1;if(Array.isArray(e))for(const n of e){if(!Lf(n)&&!Gf(n))return!1;t=!0}return t},Bf=(e,{allowEmpty:t=!1}={})=>{let n=!1;if(Array.isArray(e))for(const r of e){if(!(Mf(r)&&"options"in r&&(qf(r.options)||t&&Array.isArray(r.options)&&0===r.options.length)))return!1;n=!0}return n};const Vf=e=>jf((e=>Array.isArray(e)&&e.length>0&&Mf(e[0])&&"options"in e[0]&&Array.isArray(e[0].options))(e)?e.flatMap(e=>e.options):e);const Hf=e=>{if((e=>Rf(e)&&void 0===e.combinator)(e))return e;const{combinator:t,...n}=e,r=[],{length:o}=e.rules;for(let n=0;n{const r=Ff(e),o=Ff(n);return r.value!==o.value&&("string"==typeof r.comparator?r[r.comparator]===o[r.comparator]:r.comparator?.(o,t)?? +/* v8 ignore start -- @preserve */!1)},Wf=globalThis.crypto;let Qf=()=>"00-0-4-2-000".replaceAll(/[^-]/g,e=>(65536*(Math.random()+Math.trunc(e))>>Number.parseInt(e)).toString(16).padStart(4,"0"));if(Wf)if("function"==typeof Wf.randomUUID)Qf=()=>Wf.randomUUID();else if("function"==typeof Wf.getRandomValues){const e="89ab",t=new Uint32Array(32);Qf=()=>{Wf.getRandomValues(t);let n=(t[0]%16).toString(16);for(let r=1;r<32;r++)n=12===r?`${n}4`:16===r?`${n}${e[t[17]%4]}`:`${n}${(t[r]%16).toString(16)}`,7!==r&&11!==r&&15!==r&&19!==r||(n=`${n}-`);return n}}const Kf=[{name:"value",value:"value",label:"value"}],Xf={name:"name",value:"name",valueSources:null,label:"label"},Yf=(e,{idGenerator:t=Qf}={})=>{const n=!e.id,r=(e=>{const{mode:t,threshold:n}=e.match??{};if(!t)return null;if(!Rf(e.value))return!1;const r=$f(t),o="atleast"===r&&1===n?"some":"atmost"===r&&0===n?"none":r;return("atleast"!==o&&"atmost"!==o&&"exactly"!==o||!("number"!=typeof n||n<0))&&{mode:o,threshold:n}})(e);return n||r?{...e,...n&&{id:t()},...r&&{value:Jf(e.value,{idGenerator:t})}}:e},Jf=(e,{idGenerator:t=Qf}={})=>{const n=!e.id;let r=!1;const o=[];for(let n=0;n{const t=((e,t,n)=>{const r=e?Ff(e):Xf;let o=r.valueSources??!1;return"function"==typeof o&&(o=o(t)),!o&&n&&(o=n(r.value,t,{fieldData:r})),o?qf(o)?zf(o):o.map(e=>Kf.find(t=>t.value===$f(e))??{name:e,value:e,label:e}):Kf})(s,r,a);return qf(t)&&t.some(t=>t.value===e||t.name===e)};if(0===t.length)return!0;let l=!1;const s=Ff(t.find(e=>e.name===n));return s&&(l=!(!o&&"notNull"!==r&&"null"!==r&&!i("value")),l&&o&&(i("field")&&n!==o&&((e,t,n)=>{if(!e.comparator){const n=t=>(t.value??/* v8 ignore start -- @preserve */t.name)!==(e.value?? +/* v8 ignore start -- @preserve */e.name);return Bf(t)?t.map(e=>({...e,options:e.options.filter(e=>n(e))})):t.filter(e=>n(e))}return Bf(t)?t.map(t=>({...t,options:t.options.filter(t=>Uf(e,n,t))})).filter(e=>e.options.length>0):t.filter(t=>Uf(e,n,t))})(s,t,r).some(e=>e.name===o)||(l=!1))),l}const ep=e=>(e=>Mf(e)&&"var"in e)(e)&&"string"==typeof e.var,tp=e=>Mf(e)&&"=="in e,np=e=>Mf(e)&&"==="in e,rp=e=>Mf(e)&&"!="in e,op=e=>Mf(e)&&"!=="in e,ap=e=>Mf(e)&&">"in e,ip=e=>Mf(e)&&">="in e,lp=e=>Mf(e)&&"<"in e&&2===e["<"].length,sp=e=>Mf(e)&&"<="in e&&2===e["<="].length,up=e=>Mf(e)&&"in"in e&&!Array.isArray(e.in[1]),cp=e=>Mf(e)&&"none"in e,dp=e=>Mf(e)&&"some"in e,fp=e=>Mf(e)&&"<"in e&&Array.isArray(e["<"])&&3===e["<"].length,pp=e=>Mf(e)&&"startsWith"in e,hp=e=>Mf(e)&&"endsWith"in e,mp=e=>Mf(e)&&!ep(e),gp={combinator:"and",rules:[]};function vp(e,t={}){const n=(e=>{const t=e?Array.isArray(e)?e:Object.keys(e).map(t=>Object.assign({},e[t],{name:t})).sort((e,t)=>e.label.localeCompare(t.label)):[];return Vf(t)})(t.fields),{getValueSources:r,listsAsArrays:o,jsonLogicOperations:a,getExpression:i}=t,l=(e,t,o)=>Zf({fieldName:e,fieldsFlat:n,operator:t,subordinateFieldName:o,getValueSources:r}),s={fieldExists:e=>0===n.length||n.some(t=>t.name===e)};const u=t.generateIDs?Jf:e=>e;let c=e;if("string"==typeof e)try{c=JSON.parse(e)}catch{return u(gp)}const d=function e(t,n,r){if(n&&!Mf(t))return!1;const[u,c]=Object.entries(t||{})?.[0]??[];if(a&&If(a).includes(u)){const e=a[u](c);return!!e&&(n&&!Rf(e)?{combinator:"and",rules:[e]}:e)}if((e=>Mf(e)&&"and"in e)(t))return{combinator:"and",rules:t.and.map(t=>e(t,!1,r)).filter(Boolean)};if((e=>Mf(e)&&"or"in e)(t))return{combinator:"or",rules:t.or.map(t=>e(t,!1,r)).filter(Boolean)};if((e=>Mf(e)&&"!"in e)(t)){const o=e(t["!"],!1,r);if(o){if(!(xf(o)||"between"!==o.operator&&"in"!==o.operator&&"contains"!==o.operator&&"beginsWith"!==o.operator&&"endsWith"!==o.operator)){const e={...o,operator:Pf[o.operator]};return n?{combinator:"and",rules:[e]}:e}return fp(t["!"])||xf(o)?{...o,not:!0}:{combinator:"and",rules:[o],not:!0}}return!1}if((e=>Mf(e)&&"!!"in e)(t))return e(t["!!"],!1,r)||!1;let d,f=!1,p="",h="=",m="";if(tp(t)||np(t)||rp(t)||op(t)||ap(t)||ip(t)||lp(t)||sp(t)||up(t)||pp(t)||hp(t)){const[e,o]=c,a=tp(t)||np(t)||rp(t)||op(t)||ap(t)||ip(t)||lp(t)||sp(t),g=mp(e),v=mp(o);if(i&&a&&(g||v)){const a=tp(t)||np(t)?"=":rp(t)||op(t)?"!=":u;if(g&&v){const t=i(e,s),n=i(o,s);t&&n&&(f={field:"",operator:a,lhs:t,value:n,valueSource:"expression"})}else if(g&&!Mf(o)){const t=i(e,s);t&&(f={field:"",operator:a,lhs:t,value:o})}else if(ep(e)&&v){const t=i(o,s);t&&(r||l(e.var,a))&&(f={field:e.var,operator:a,value:t,valueSource:"expression"})}return!!f&&(n?{combinator:"and",rules:[f]}:f)}if(ep(e)&&!Mf(o))p=e.var,m=o;else if(!Mf(e)&&ep(o))p=o.var,m=e;else{if(!ep(e)||!ep(o))return!1;p=e.var,m=o.var,d="field"}h=tp(t)||np(t)?null===m?"null":"=":rp(t)||op(t)?null===m?"notNull":"!=":up(t)?"contains":pp(t)?"beginsWith":hp(t)?"endsWith":u,(r||l(p,h,"field"===d?m:void 0))&&(f={field:p,operator:h,value:m,valueSource:d})}else if((e=>Mf(e)&&"all"in e)(t)&&ep(t.all[0])||cp(t)&&ep(t.none[0])||dp(t)&&ep(t.some[0])){const n={mode:cp(t)?"none":dp(t)?"some":"all"},[{var:r},o]=t[n.mode],a=e(o,!1,!0);if(!a)return!1;f={field:r,operator:"=",match:n,value:Rf(a)?a:{combinator:"and",rules:[a]}}}else if(fp(t)&&ep(t["<"][1])){p=t["<"][1].var;const n=[t["<"][0],t["<"][2]];if(n.every(e=>ep(e))||n.every(e=>"string"==typeof e)||n.every(e=>"number"==typeof e)||n.every(e=>"boolean"==typeof e))return e({and:[{">":[{var:p},n[0]]},{"<":[{var:p},n[1]]}]})||/* v8 ignore start -- @preserve */!1}else if((e=>Mf(e)&&"<="in e&&Array.isArray(e["<="])&&3===e["<="].length)(t)&&ep(t["<="][1])){p=t["<="][1].var,h="between";const e=[t["<="][0],t["<="][2]];if(i&&(mp(e[0])||mp(e[1]))){const t=i(e[0],s),r=i(e[1],s);return t&&r&&l(p,h)&&(f={field:p,operator:h,value:[t,r],valueSource:"expression"}),!!f&&(n?{combinator:"and",rules:[f]}:f)}if(t["<="].every(e=>ep(e))){d="field";const t=e.map(e=>e.var).filter(e=>l(p,h,e));m=o?t:_f(t,",")}else(e.every(e=>"string"==typeof e)||e.every(e=>"number"==typeof e)||e.every(e=>"boolean"==typeof e))&&(m=o?e:_f(e.map(e=>`${e}`),","));l(p,h)&&m.length>=2&&(f={field:p,operator:h,value:m,valueSource:d})}else if((e=>Mf(e)&&"in"in e&&Array.isArray(e.in[1]))(t)&&ep(c[0])){if(p=c[0].var,h="in",t.in[1].every(e=>ep(e))){d="field";const e=t.in[1].map(e=>e.var).filter(e=>l(p,h,e));m=o?e:_f(e,",")}else(t.in[1].every(e=>"string"==typeof e)||t.in[1].every(e=>"number"==typeof e)||t.in[1].every(e=>"boolean"==typeof e))&&(m=o?t.in[1]:_f(t.in[1].map(e=>`${e}`),","));m.length>0&&(f={field:p,operator:h,value:m,valueSource:d})}return!!f&&(n?{combinator:"and",rules:[f]}:f)}(c,!0)||gp;return u(t.independentCombinators?Hf(d):d)}Promise.resolve().then(()=>n(236));const bp=["=","null","contains","doesNotContain","beginsWith","endsWith",">",">=","<","<=","in"],yp=class extends Oe{#e;#t=[];#n=[];#r={};#o={};#a=bp;#i=!1;#l="en";#s={en:{fields:{title:"Fields"},operators:{title:"Operators","=":"is equal to","!=":"is not equal to","<":"is less than",">":"is greater than","<=":"is less than or equal to",">=":"is greater than or equal to",between:"between",notBetween:"not between",contains:"contains",doesNotContain:"does not contain",beginsWith:"begins with",endsWith:"ends with",in:"in",notIn:"not in",null:"is empty",notNull:"is not empty",regex:"matches the regular expression",notRegex:"does not match the regular expression","<=ndays":"is less than or equal to N day(s)",">=ndays":"is greater than or equal to N day(s)",valuesList:"List of values"},value:{title:"Value"},addRule:{label:"+ Rule",title:"Add a rule"},removeRule:{label:"⨯",title:"Remove the rule"},addGroup:{label:"+ Group",title:"Add a group"},removeGroup:{label:"⨯",title:"Remove the group"},and:"AND",or:"OR",not:"NOT",validationError:"This rule is incomplete"},fr:{fields:{title:"Champs"},operators:{title:"Opérateurs","=":"est égal à","!=":"n'est pas égal à","<":"est inférieur à",">":"est supérieur à","<=":"est inférieur ou égal à",">=":"est supérieur ou égal à",between:"entre",notBetween:"n'est pas entre",contains:"contient",doesNotContain:"ne contient pas",beginsWith:"commence par",endsWith:"se termine par",in:"dans",notIn:"pas dans",null:"est vide",notNull:"n'est pas vide",regex:"correspond à l'expression régulière",notRegex:"ne correspond pas à l'expression régulière","<=ndays":"est inférieure ou égale à N jour(s)",">=ndays":"est supérieure ou égale à N jour(s)",valuesList:"Liste de valeurs"},value:{title:"Valeur"},addRule:{label:"+ Règle",title:"Ajouter une règle"},removeRule:{label:"⨯",title:"Supprimer la règle"},addGroup:{label:"+ Groupe",title:"Ajouter un groupe"},removeGroup:{label:"⨯",title:"Supprimer le groupe"},and:"ET",or:"OU",not:"NON",validationError:"Cette règle est incomplète"}};static values={conditions:Object,fields:Array,operators:Array,processor:String,lang:{type:String,default:"fr"}};static targets=["queryBuilder","input"];#u=null;#c=null;#d=null;connect(){const e=this.#f();this.#d=this.processorValue,this.#u=this.#p(),this.#h(e,this.#u)}disconnect(){this.#e?.unmount(),this.#e=null}fieldsValueChanged(){this.#m()}operatorsValueChanged(){this.#m()}langValueChanged(){this.#m()}processorValueChanged(){this.#e&&this.processorValue!==this.#d&&(this.#d=this.processorValue,this.#g(this.#u))}#m(){if(!this.#e||this.#v()===this.#c)return;const e=this.#f(),t=this.#b(this.#u,this.#y(e));this.#e.unmount(),this.#h(e,t),this.#g(t)}#v(){return JSON.stringify([this.fieldsValue,this.operatorsValue,this.langValue])}#f(){this.#w();const e=this.#S();return this.#c=this.#v(),e}#w(){this.#i=this.operatorsValue.length>0,this.#a=this.#i?this.operatorsValue:bp}#S(){this.#t=[],this.#n=[],this.#r={},this.#o={};const e=[];for(const t of this.fieldsValue)this.#k(t),e.push({name:t.name,label:this.#E(t),...this.#O(t),...this.#N(t)});return e.sort((e,t)=>e.label.localeCompare(t.label,this.langValue)),e}#y(e){const t=this.#C();return new Map(e.map(e=>[e.name,new Set(t(e.name,{fieldData:e}).map(e=>e.name))]))}#b(e,t){const n=[];for(const r of e.rules??[]){if(r&&Array.isArray(r.rules)){const e=this.#b(r,t);e.rules.length&&n.push(e);continue}if(!r||"object"!=typeof r||"~"===r.field){n.push(r);continue}const e=t.get(r.field);if(e){if("valuesList"===r.operator){const t=null!=r.value&&""!==r.value&&"~"!==r.value;!e.has("valuesList")||t&&!(this.#r[r.field]??[]).includes(r.value)?e.has("=")&&n.push({...r,operator:"=",value:t?r.value:""}):n.push(r);continue}("~"===r.operator||e.has(r.operator))&&n.push(r)}}return{...e,rules:n}}#h(e,n){const r=l().createElement(Df,{fields:e,onQueryChange:e=>this.#g(e),defaultQuery:n,combinators:this.#D(),getOperators:this.#C(),getValueEditorType:this.#A(),getInputType:this.#T(),translations:this.#$(),autoSelectField:!1,autoSelectValue:!1,listsAsArrays:!1,enableDragAndDrop:!0,enableMountQueryChange:!1,controlClassnames:{queryBuilder:"queryBuilder-branches"}});this.#e=(0,t().createRoot)(this.queryBuilderTarget),this.#e.render(l().createElement(ac,{dnd:{...h,...v,...b},children:[l().createElement(Tf,{},r)]}))}#g(e){if(this.#u=e,"native"===this.processorValue){const t=this.#M(e);return void this.#R(JSON.stringify(t.rules.length?t:{}))}let t=this.#x();const n=Za(e,{format:"jsonlogic",ruleProcessor:t});if("jsonLogic"===this.processorValue&&this.#R(JSON.stringify(!1===n?{}:n)),"parameterized"===this.processorValue){t=this.#I();const r=Za(e,{format:"parameterized",ruleProcessor:t});if(!1===n)return void this.#R(JSON.stringify({}));const o={conditionTree:n,parameterizedSql:r};this.#R(JSON.stringify(o))}}#R(e){this.inputTarget.value!==e&&(this.inputTarget.value=e,this.inputTarget.dispatchEvent(new Event("input",{bubbles:!0})),this.inputTarget.dispatchEvent(new Event("change",{bubbles:!0})))}#E(e){let t=e.label??e.name;return e.labelInformation&&(t+=`     (${e.labelInformation})`),t}#O(e){switch("date"===e.type&&this.#t.push(e.name),"number"===e.type&&this.#n.push(e.name),e.type){case"date":return this.#P();case"number":return this.#_();case"boolean":return this.#L();default:return this.#G()}}#G(){return{inputType:"text",datatype:"string"}}#P(){return{datatype:"date"}}#_(){return{inputType:"number",datatype:"number"}}#L(){return{inputType:"boolean",datatype:"boolean",valueEditorType:"checkbox",defaultValue:!1}}#N(e){if(!e.values?.length)return{};const t=e.values.map(e=>({name:e.value??e.name,label:e.label??e.name}));return this.#r[e.name]=t.map(e=>e.name),{values:t}}#k(e){e.operators?.length&&(this.#o[e.name]=e.operators)}#D(){return fn.map(e=>"and"===e.name?{...e,label:this.#F("and")}:"or"===e.name?{...e,label:this.#F("or")}:e)}#C(){return(e,{fieldData:t})=>{if(this.#o[e])return this.#z(this.#o[e],t);let n=dn.filter(e=>this.#a.includes(e.name));return n.sort((e,t)=>this.#a.indexOf(e.name)-this.#a.indexOf(t.name)),n=n.map(e=>({...e,label:this.#j(e.name,e.label)})),["number","date"].includes(t.datatype)||(n=n.filter(e=>![">",">=","<","<="].includes(e.name))),"string"!==t.datatype&&(n=n.filter(e=>!["contains","doesNotContain","beginsWith","endsWith","in"].includes(e.name))),"text"===t.inputType&&(this.#q("regex")&&n.push({name:"regex",label:this.#j("regex"),value:"regex"}),this.#q("notRegex")&&n.push({name:"notRegex",label:this.#j("notRegex"),value:"notRegex"})),"date"===t.datatype&&(this.#q("<=ndays")&&n.push({name:"<=ndays",label:this.#j("<=ndays"),value:"<=ndays"}),this.#q(">=ndays")&&n.push({name:">=ndays",label:this.#j(">=ndays"),value:">=ndays"})),e&&t.values?.length&&this.#q("valuesList")&&n.push({name:"valuesList",label:this.#j("valuesList"),value:"valuesList"}),this.#i&&n.sort((e,t)=>this.#a.indexOf(e.name)-this.#a.indexOf(t.name)),n}}#z(e,t){const n=[];for(const r of e){if("valuesList"===r&&!t.values?.length)continue;const e=dn.find(e=>e.name===r);n.push(e?{...e,label:this.#j(r,e.label)}:{name:r,label:this.#j(r),value:r})}return n}#q(e,t=null){return t&&this.#o[t]?this.#o[t].includes(e):!this.#i||this.#a.includes(e)}#j(e,t=null){return`${this.#F("operators")[e]||t||e}   ( ${e} )`}#A(){return(e,t)=>{switch(t){case"valuesList":return"select";case"in":case"notIn":return"textarea";default:return"text"}}}#T(){return(e,t,{fieldData:n})=>n.datatype&&"date"===n.datatype?["<=ndays",">=ndays"].includes(t)?"number":"date":n.inputType||"text"}#x(){return(e,t)=>{if("valuesList"===e.operator)return{"==":[{var:e.field},e.value]};if(["regex","notRegex"].includes(e.operator))return{[e.operator]:[{var:e.field},this.#B(e.value)]};if(["<=ndays",">=ndays"].includes(e.operator)){const t=isNaN(e.value)?"":e.value;return{[e.operator]:[{var:e.field},t]}}return this.#t.includes(e.field)&&(e={...e,value:this.#V(e)}),["in","notIn"].includes(e.operator)&&"string"==typeof e.value&&(e={...e,value:e.value.replaceAll(/\n/g,",")}),"null"===e.operator&&this.#n.includes(e.field)&&(e={...e,operator:"in",value:[null,0]}),"notNull"===e.operator&&this.#n.includes(e.field)?{notNull:[{var:e.field},[null,0]]}:xa(e,t)}}#V(e){const t=e=>/^\d{4}-\d{2}-\d{2}$/.test(e);if(["between","notBetween"].includes(e.operator)){const n=String(e.value).split(",");return 2===n.length&&n.every(t)?e.value:""}return t(e.value)?e.value:""}#I(){return(e,t)=>{if("null"===e.operator&&this.#n.includes(e.field))return{sql:"( "+e.field+" is null or "+e.field+" = ? )",params:[0]};if("notNull"===e.operator&&this.#n.includes(e.field))return{sql:"( "+e.field+" is not null and "+e.field+" <> ? )",params:[0]};if("null"===e.operator)return{sql:"( "+e.field+" is null )",operator:"",params:null};if("valuesList"===e.operator)return{sql:"( "+e.field+" = ? )",params:[e.value]};if("regex"===e.operator)return{sql:"( lower("+e.field+") regexp lower(?) )",params:[e.value]};if("notRegex"===e.operator)return{sql:"( lower("+e.field+") not regexp lower(?) )",params:[e.value]};if(["<=ndays",">=ndays"].includes(e.operator)&&(e={...e,field:"datediff( curdate(), "+e.field+" )",operator:e.operator.replace("ndays","")}),["in","notIn"].includes(e.operator)&&"string"==typeof e.value&&(e={...e,value:e.value.replaceAll(/\n/g,",")}),["contains","doesNotContain"].includes(e.operator)){const t="contains"===e.operator?"like":"not like";return{sql:"( "+e.field+" "+t+" ? )",params:[`%${e.value}%`]}}const n=qa(e,t);return n&&n.sql?{sql:"( "+n.sql+" )",params:n.params}:n}}#M(e){const t=[];for(const n of e.rules??[])if(n&&"object"==typeof n){if(Array.isArray(n.rules)){const e=this.#M(n);e.rules.length&&t.push(e);continue}"~"!==n.field&&"~"!==n.operator&&t.push(this.#H(n))}return{combinator:e.combinator,...e.not?{not:!0}:{},rules:t}}#H(e){if("valuesList"===e.operator)return{field:e.field,operator:"=",value:e.value};let t=e.value;return["<=ndays",">=ndays"].includes(e.operator)?t=isNaN(t)?"":t:this.#t.includes(e.field)&&(t=this.#V(e)),["in","notIn"].includes(e.operator)&&"string"==typeof t&&(t=t.replaceAll(/\n/g,",")),{field:e.field,operator:e.operator,value:t}}#p(){const e=e=>Array.isArray(e)&&2===e.length&&[null,0].every(t=>e.includes(t)),t=e=>e[1].var&&"string"==typeof e[0]?e[1].var:e[0].var,n=function(t){return t[1].var&&"string"==typeof t[0]?"contains":e(t[1])?"null":"in"},r=function(t){return t[1].var&&"string"==typeof t[0]?t[0]:e(t[1])?null:o(t[1])},o=e=>Array.isArray(e)?e.join("\n"):e,a=null!==this.conditionsValue&&"object"==typeof this.conditionsValue?this.conditionsValue:{},i="conditionTree"in a?a.conditionTree:a;return null!==i&&"object"==typeof i&&Array.isArray(i.rules)?this.#U(i):this.#U(vp(i,{jsonLogicOperations:{in:e=>({field:t(e),operator:n(e),value:r(e)}),notIn:e=>({field:e[0].var,operator:"notIn",value:o(e[1])}),notNull:e=>({field:e[0].var,operator:"notNull",value:""}),regex:e=>({field:e[0].var,operator:"regex",value:this.#W(e[1])}),notRegex:e=>({field:e[0].var,operator:"notRegex",value:this.#W(e[1])}),"<=ndays":e=>({field:e[0].var,operator:"<=ndays",value:e[1]}),">=ndays":e=>({field:e[0].var,operator:">=ndays",value:e[1]})}}))}#B(e){const t="string"==typeof e?e:String(e??"");if(/^\/(?:\\.|[^\\/])*\/[iums]*$/s.test(t))return t;return`/${t.replace(/\\.|\/|\\$/gs,e=>"/"===e?"\\/":"\\"===e?"\\\\":e)}/iu`}#W(e){if("string"!=typeof e)return e;const t=/^\/(.*)\/iu$/s.exec(e);return null===t?e:t[1].replace(/\\./gs,e=>"\\/"===e?"/":e)}#U(e){return e&&Array.isArray(e.rules)?{...e,rules:e.rules.map(e=>e&&Array.isArray(e.rules)?this.#U(e):e&&["in","notIn"].includes(e.operator)&&"string"==typeof e.value?{...e,value:e.value.replaceAll(",","\n")}:e&&"="===e.operator&&this.#q("valuesList",e.field)&&(this.#r[e.field]??[]).includes(e.value)?{...e,operator:"valuesList"}:e)}:e}#$(){return{...sn,...this.#Q(["fields","operators","value","addRule","removeRule","addGroup","removeGroup"])}}#Q(e=null){const t=this.#s[this.langValue]??this.#s[this.#l];if(!e)return t;let n={};return e.forEach(e=>{n[e]=t[e]||e}),n}#F(e){return this.#Q([e])[e]||e}};n.dn(yp);const wp={combinator:"and",rules:[]},Sp=["in","notIn","between","notBetween"],kp=["null","notNull"];function Ep(e){if(!e||"null"===e)return wp;try{const t=JSON.parse(e);return t&&"object"==typeof t&&Array.isArray(t.rules)?t:wp}catch{return wp}}function Op(e){return e&&Array.isArray(e.rules)?e.rules.reduce((e,t)=>e+(t&&Array.isArray(t.rules)?Op(t):t&&t.field?1:0),0):0}function Np(e,t,n){const r=document.createElement(e);return t&&(r.className=t),void 0!==n&&(r.textContent=n),r}function Cp(e,t,n){const r=Array.isArray(e?.values)?e.values:[],o=n.values||{};return function(e){const t=e.value;return Array.isArray(t)?t:Sp.includes(e.operator)&&"string"==typeof t?t.split(",").map(e=>e.trim()).filter(e=>""!==e):null==t?[]:[t]}(t).map(e=>{if(!0===e||"true"===e)return o.true||"true";if(!1===e||"false"===e)return o.false||"false";const t=r.find(t=>String(t.value??t.name)===String(e));return t?t.label??t.name:String(e)})}function Dp(e,t,n){const r=t.find(t=>t.name===e.field),o=(n.operators||{})[e.operator]||e.operator,a=n.values||{},i=Np("li","qb-summary-rule"),l=Np("span",r?"qb-summary-field":"qb-summary-field qb-summary-field-unknown",r?r.label:e.field);if(r||(l.title=a.unknownField||""),i.appendChild(l),i.appendChild(Np("span","qb-summary-operator",o)),kp.includes(e.operator))return i;const s=Cp(r,e,n).filter(e=>""!==e);if(0===s.length)return i.appendChild(Np("span","qb-summary-missing",a.missing||"")),i;const u="between"===e.operator||"notBetween"===e.operator,c=n.join||{};return s.forEach((e,t)=>{t>0&&i.appendChild(Np("span","qb-summary-join",u?c.and||"and":c.or||"or")),i.appendChild(Np("span","qb-summary-value",e))}),i}function Ap(e,t,n,r){const o=Array.isArray(e.rules)?e.rules:[],a="or"===e.combinator?"or":"and",i=Np("div",`qb-summary-group qb-summary-depth-${Math.min(r,3)}`),l=n.groups||{},s=n.combinators||{};if(o.length>1||r>0||e.not){const t=Np("div","qb-summary-heading");t.appendChild(Np("span",`qb-summary-combinator qb-summary-combinator-${a}`,s[a]||a.toUpperCase())),t.appendChild(Np("span","qb-summary-heading-text",l[e.not?`${a}Not`:a]||"")),i.appendChild(t)}const u=Np("ul","qb-summary-list");for(const e of o)if(e&&"object"==typeof e){if(Array.isArray(e.rules)){const o=Np("li","qb-summary-nested");o.appendChild(Ap(e,t,n,r+1)),u.appendChild(o);continue}e.field&&u.appendChild(Dp(e,t,n))}return i.appendChild(u),i}const Tp=class extends Oe{static targets=["context","contextHelp","hookChoice","hookCount","hookEmpty","counter","summary","summaryText","emptyHint"];static values={fieldsByContext:Object,labels:Object};connect(){this.editor=this.element.querySelector('[data-controller~="query-builder"]'),this.input=this.element.querySelector('[data-query-builder-target="input"]'),this.onInputChange=()=>this.refreshFeedback(),this.input?.addEventListener("change",this.onInputChange),this.previousContext=this.hasContextTarget?this.contextTarget.value:null,this.syncContextHelp(),this.refreshHooks(),this.refreshFeedback()}disconnect(){this.input?.removeEventListener("change",this.onInputChange)}changeContext(){const e=this.contextTarget.value,t=this.labelsValue.confirmContextChange;Op(this.currentQuery())>0&&t&&!window.confirm(t)?this.contextTarget.value=this.previousContext:(this.previousContext=e,this.editor&&(this.editor.dataset.queryBuilderFieldsValue=JSON.stringify(this.currentFields())),this.syncContextHelp(),this.refreshHooks(),this.refreshFeedback())}syncContextHelp(){if(!this.hasContextTarget||!this.hasContextHelpTarget)return;const e=this.contextTarget.options[this.contextTarget.selectedIndex];this.contextHelpTarget.textContent=e&&e.dataset.description||""}currentContext(){return this.hasContextTarget?this.contextTarget.value:Object.keys(this.fieldsByContextValue)[0]??null}currentFields(){return this.fieldsByContextValue[this.currentContext()]||[]}refreshHooks(){const e=this.currentContext(),t=[];for(const n of this.hookChoiceTargets){const r=n.dataset.context===e;n.hidden=!r;const o=n.querySelector('input[type="checkbox"]');o&&(r?t.push(o):o.checked=!1,n.classList.toggle("qb-hook-checked",r&&o.checked))}if(this.hasHookCountTarget){const e=t.filter(e=>e.checked).length;this.hookCountTarget.textContent=(this.labelsValue.hooks?.count||"%checked% / %total%").replace("%checked%",String(e)).replace("%total%",String(t.length))}this.hasHookEmptyTarget&&(this.hookEmptyTarget.hidden=t.length>0)}checkAllHooks(e){e.preventDefault(),this.setVisibleHooks(!0)}uncheckAllHooks(e){e.preventDefault(),this.setVisibleHooks(!1)}setVisibleHooks(e){for(const t of this.hookChoiceTargets){if(t.hidden)continue;const n=t.querySelector('input[type="checkbox"]');n&&(n.checked=e)}this.refreshHooks()}currentQuery(){return Ep(this.input?.value)}refreshFeedback(){const e=this.currentQuery(),t=Op(e),n=this.labelsValue;if(this.hasCounterTarget){const e=n.counter||{};this.counterTarget.textContent=0===t?e.none||"":1===t?e.one||"1":(e.many||"%count%").replace("%count%",String(t))}if(this.hasEmptyHintTarget&&this.emptyHintTarget.classList.toggle("qb-empty-hint-visible",0===t),this.hasSummaryTarget){this.summaryTarget.classList.toggle("qb-summary-visible",t>0);const r=this.hasSummaryTextTarget?this.summaryTextTarget:this.summaryTarget;r.replaceChildren(),t>0&&r.appendChild(Ap(e,this.currentFields(),n,0))}}};n.dn(Tp);const $p=class extends Oe{static targets=["code","displayParams","discountParams","cartDiscountParams","treeGroup"];static values={discountCode:String,cartDiscountCode:String};connect(){this.sync()}sync(){const e=this.codeTarget.value,t=e===this.discountCodeValue,n=e===this.cartDiscountCodeValue;this.displayParamsTarget.hidden=n,this.discountParamsTarget.hidden=!t,this.cartDiscountParamsTarget.hidden=!n,this.treeGroupTarget.hidden=n}};n.dn($p);const Mp=class extends Oe{static targets=["trigger"];static values={fields:Array,labels:Object,title:String,emptyHint:String};connect(){const e=window.bootstrap?.Popover;this.popovers=[];for(const t of this.triggerTargets){const n="tree"in t.dataset,r=Ep(t.dataset.tree),o=Rp(t.dataset.details),a=(!n||0===Op(r))&&0===o.length;t.classList.toggle("qb-action-summary-trigger-empty",a),e&&this.popovers.push(new e(t,{html:!0,sanitize:!1,title:this.titleValue,content:this.buildContent(n?r:null,o),trigger:"hover focus",placement:"right",container:"body",customClass:"qb-action-summary-popover"}))}}disconnect(){this.popovers.forEach(e=>e.dispose()),this.popovers=[]}buildContent(e,t){const n=document.createElement("div");if(n.className="qb-action-summary",null!==e)if(0===Op(e)){const e=document.createElement("p");e.className="qb-action-summary-empty mb-0",e.textContent=this.emptyHintValue,n.appendChild(e)}else n.appendChild(Ap(e,this.fieldsValue,this.labelsValue,0));if(t.length>0){const e=document.createElement("ul");e.className="qb-action-summary-details";for(const n of t){const t=document.createElement("li");t.textContent=n,e.appendChild(t)}n.appendChild(e)}return n}};function Rp(e){if(!e)return[];try{const t=JSON.parse(e);return Array.isArray(t)?t.filter(e=>"string"==typeof e&&""!==e):[]}catch{return[]}}n.dn(Mp);const xp=class{constructor(e=document.documentElement,t=pe){this.logger=console,this.debug=!1,this.logDebugActivity=(e,t,n={})=>{this.debug&&this.logFormattedMessage(e,t,n)},this.element=e,this.schema=t,this.dispatcher=new w(this),this.router=new fe(this),this.actionDescriptorFilters=Object.assign({},S)}static start(e,t){const n=new this(e,t);return n.start(),n}async start(){await new Promise(e=>{"loading"==document.readyState?document.addEventListener("DOMContentLoaded",()=>e()):e()}),this.logDebugActivity("application","starting"),this.dispatcher.start(),this.router.start(),this.logDebugActivity("application","start")}stop(){this.logDebugActivity("application","stopping"),this.dispatcher.stop(),this.router.stop(),this.logDebugActivity("application","stop")}register(e,t){this.load({identifier:e,controllerConstructor:t})}registerActionOption(e,t){this.actionDescriptorFilters[e]=t}load(e,...t){(Array.isArray(e)?e:[e,...t]).forEach(e=>{e.controllerConstructor.shouldLoad&&this.router.loadDefinition(e)})}unload(e,...t){(Array.isArray(e)?e:[e,...t]).forEach(e=>this.router.unloadIdentifier(e))}get controllers(){return this.router.contexts.map(e=>e.controller)}getControllerForElementAndIdentifier(e,t){const n=this.router.getContextForElementAndIdentifier(e,t);return n?n.controller:null}handleError(e,t,n){var r;this.logger.error("%s\n\n%o\n\n%o",t,e,n),null===(r=window.onerror)||void 0===r||r.call(window,t,"",0,0,e)}logFormattedMessage(e,t,n={}){n=Object.assign({application:this},n),this.logger.groupCollapsed(`${e} #${t}`),this.logger.log("details:",Object.assign({},n)),this.logger.groupEnd()}}.start();xp.register("query-builder",yp),xp.register("qb-editor",Tp),xp.register("qb-action-form",$p),xp.register("qb-action-summary",Mp)})()})(); \ No newline at end of file diff --git a/templates/frontOffice/default/QueryBuilder/cart-discount.html.twig b/templates/frontOffice/default/QueryBuilder/cart-discount.html.twig new file mode 100644 index 0000000..c83908e --- /dev/null +++ b/templates/frontOffice/default/QueryBuilder/cart-discount.html.twig @@ -0,0 +1,53 @@ +{# Rule discounts of the cart, rendered once on the checkout pages (checkout.top, + cart.bottom): the cart discount (label, amount, free shipping) and the product + discounts charged on the lines (label, products). Texts are translated by the + module; the amount is formatted with twig/intl-extra, shipped with Flexy. A theme + overrides this file at templates/frontOffice//modules/QueryBuilder/cart-discount.html.twig + (variables: hook, label, rate, amount, free_shipping, currency_code, locale, + line_discounts [{label, rate, products}], texts). #} + +{% set className = '' %} +{% if hook == 'checkout.top' %} + {% set className = 'flex justify-center gap-5' %} +{% elseif hook == 'cart.bottom' %} + {% set className = 'my-5' %} +{% endif %} + +
+ {% if amount %} +
+ + {% if hook == 'cart.bottom' %} +

+ −{{ currency_code ? amount|format_currency(currency_code, locale: locale) : amount }} + ({{ texts.rate_off }}) +

+ {% endif %} +
+ {% endif %} + {% if free_shipping %} +

+ +

+ {% endif %} + {% for line in line_discounts|default([]) %} +

+ + {% if hook == 'cart.bottom' and line.products %} + {{ line.products|join(', ') }} + {% endif %} +

+ {% endfor %} +
diff --git a/templates/frontOffice/default/QueryBuilder/product-list.html.twig b/templates/frontOffice/default/QueryBuilder/product-list.html.twig new file mode 100644 index 0000000..caedb3a --- /dev/null +++ b/templates/frontOffice/default/QueryBuilder/product-list.html.twig @@ -0,0 +1,8 @@ +{# Default rendering of a DisplayProductsList action at a theme_hook() point. + Relies on the Flexy cross-selling strip, which fetches the products and keeps + the picked order. A theme overrides this file by shipping its own copy at + templates/frontOffice//modules/QueryBuilder/product-list.html.twig + (variables: hook, rule_name, action_name, product_ids). #} +
+ +
diff --git a/tests/Integration/BackHookMenuEntryTest.php b/tests/Integration/BackHookMenuEntryTest.php new file mode 100644 index 0000000..8dc8f7f --- /dev/null +++ b/tests/Integration/BackHookMenuEntryTest.php @@ -0,0 +1,115 @@ +session()->clearAdminUser(); + + parent::tearDown(); + } + + #[Test] + public function theEntryShowsForAnAdministratorGrantedOnTheModuleOnly(): void + { + $admin = $this->createFixtureFactory()->restrictedAdmin([AdminResources::TOOLS => [AccessManager::VIEW]]); + $this->grantModule($admin, [AccessManager::VIEW]); + + self::assertSame(['tools_menu_query_builder'], $this->renderedEntryIds($admin)); + } + + #[Test] + public function theEntryStaysHiddenFromAnAdministratorWithoutTheModuleRight(): void + { + $admin = $this->createFixtureFactory()->restrictedAdmin([ + AdminResources::TOOLS => [AccessManager::VIEW], + AdminResources::MODULE => [AccessManager::VIEW], + ]); + + self::assertSame([], $this->renderedEntryIds($admin)); + } + + #[Test] + public function theEntryStaysHiddenFromAnAdministratorGrantedOnTheModuleWithoutView(): void + { + $admin = $this->createFixtureFactory()->restrictedAdmin([AdminResources::TOOLS => [AccessManager::VIEW]]); + $this->grantModule($admin, [AccessManager::UPDATE]); + + self::assertSame([], $this->renderedEntryIds($admin)); + } + + /** + * @param list $accesses + */ + private function grantModule(Admin $admin, array $accesses): void + { + $module = ModuleQuery::create()->findOneByCode(QueryBuilder::getModuleCode()); + + if ($module === null) { + self::markTestSkipped('The QueryBuilder module is not registered in the test database.'); + } + + $accessManager = new AccessManager(0); + $accessManager->build($accesses); + + (new ProfileModule()) + ->setProfileId($admin->getProfileId()) + ->setModuleId($module->getId()) + ->setAccess($accessManager->getAccessValue()) + ->save($this->getPropelConnection()); + } + + /** + * @return list + */ + private function renderedEntryIds(Admin $admin): array + { + $this->session()->setAdminUser($admin); + + $hook = new BackHook(new SecurityContext($this->requestStack())); + $event = new HookRenderBlockEvent('main.top-menu-tools', [], ['id', 'class', 'url', 'title']); + $hook->onMainTopMenuTools($event); + + return array_map(static fn (Fragment $fragment): string => (string) $fragment->get('id'), iterator_to_array($event->get(), false)); + } + + private function requestStack(): RequestStack + { + return static::getContainer()->get('request_stack'); + } + + private function session(): Session + { + $session = $this->requestStack()->getMainRequest()?->getSession(); + self::assertInstanceOf(Session::class, $session); + + return $session; + } +} diff --git a/tests/Integration/ModuleActivationTest.php b/tests/Integration/ModuleActivationTest.php new file mode 100644 index 0000000..7d29fac --- /dev/null +++ b/tests/Integration/ModuleActivationTest.php @@ -0,0 +1,85 @@ +findOneByCode(QueryBuilder::getModuleCode()); + + if ($module === null) { + self::markTestSkipped('The QueryBuilder module is not registered in the test database.'); + } + + // createInstance() hands back a bare module: activate() reaches for the + // cache directory, the dispatcher and the kernel through the container, + // the way Thelia\Action\Module wires it before toggling a module. + $instance = $module->createInstance(); + $instance->setContainer(static::getContainer()); + + if ((int) $module->getActivate() === BaseModule::IS_ACTIVATED) { + $instance->deActivate($module); + } + + // Counted rather than asserted to zero: the suite also runs on a shop that + // already holds rules, and what is under test is that activation adds none. + $rulesBeforeActivation = QueryBuilderRuleQuery::create()->count(); + + $instance->activate($module); + self::assertSame(BaseModule::IS_ACTIVATED, (int) $this->reloadModule()->getActivate()); + $this->assertTablesExist(); + self::assertSame($rulesBeforeActivation, QueryBuilderRuleQuery::create()->count(), 'the activation ships no rule of its own'); + + $instance->deActivate($this->reloadModule()); + self::assertSame(BaseModule::IS_NOT_ACTIVATED, (int) $this->reloadModule()->getActivate()); + $this->assertTablesExist('the rule tables survive a deactivation'); + + //Second activation: the SQL is not replayed on an initialized shop + $instance->activate($this->reloadModule()); + self::assertSame(BaseModule::IS_ACTIVATED, (int) $this->reloadModule()->getActivate()); + $this->assertTablesExist(); + self::assertSame($rulesBeforeActivation, QueryBuilderRuleQuery::create()->count(), 'the second activation replays no SQL'); + } + + private function assertTablesExist(string $message = ''): void + { + $connection = $this->getPropelConnection(); + + foreach (self::TABLES as $table) { + $statement = $connection->query(sprintf("SHOW TABLES LIKE '%s'", $table)); + + self::assertNotFalse($statement->fetch(), $message !== '' ? $message : sprintf('table %s is missing', $table)); + } + } + + private function reloadModule(): \Thelia\Model\Module + { + $module = ModuleQuery::create()->findOneByCode(QueryBuilder::getModuleCode()); + self::assertNotNull($module); + + return $module; + } +} diff --git a/tests/Support/DictionaryFactory.php b/tests/Support/DictionaryFactory.php new file mode 100644 index 0000000..dace5cb --- /dev/null +++ b/tests/Support/DictionaryFactory.php @@ -0,0 +1,43 @@ +files; + } +} diff --git a/tests/Unit/Api/FrontReadTest.php b/tests/Unit/Api/FrontReadTest.php new file mode 100644 index 0000000..52c6778 --- /dev/null +++ b/tests/Unit/Api/FrontReadTest.php @@ -0,0 +1,40 @@ + ['front:product:read', 'front:product:read:single']])); + } + + #[Test] + public function anAdminGroupMakesTheWholeReadAnAdminOne(): void + { + self::assertFalse(FrontRead::isFrontRead(['groups' => ['front:product:read', 'admin:product:read']])); + self::assertFalse(FrontRead::isFrontRead(['groups' => ['admin:product:read']])); + } + + #[Test] + public function aReadWithoutFrontGroupIsNotAFrontRead(): void + { + self::assertFalse(FrontRead::isFrontRead([])); + self::assertFalse(FrontRead::isFrontRead(['groups' => []])); + self::assertFalse(FrontRead::isFrontRead(['groups' => ['Default', 42]])); + } + + #[Test] + public function aSingleGroupGivenAsAStringIsAccepted(): void + { + self::assertTrue(FrontRead::isFrontRead(['groups' => 'front:cart:read'])); + self::assertFalse(FrontRead::isFrontRead(['groups' => 'admin:cart:read'])); + } +} diff --git a/tests/Unit/Discount/CartLineDiscountPolicyTest.php b/tests/Unit/Discount/CartLineDiscountPolicyTest.php new file mode 100644 index 0000000..f5c7bf3 --- /dev/null +++ b/tests/Unit/Discount/CartLineDiscountPolicyTest.php @@ -0,0 +1,71 @@ +policy = new CartLineDiscountPolicy(); + } + + #[Test] + public function aRuleDiscountsTheCatalogPriceOfALineWithoutPromotion(): void + { + $line = $this->policy->discountedLine(new Discount(10.0, 'Loyalty', false), new LinePrices(100.0, 0.0, 0)); + + self::assertEquals(new LinePrices(100.0, 90.0, 1), $line); + } + + #[Test] + public function aNonStackableRuleLeavesABetterCatalogPromotionAlone(): void + { + self::assertNull($this->policy->discountedLine(new Discount(10.0, null, false), new LinePrices(100.0, 85.0, 1))); + self::assertNull($this->policy->discountedLine(new Discount(10.0, null, false), new LinePrices(100.0, 90.0, 1)), 'an equal promotion is not replaced'); + } + + #[Test] + public function aNonStackableRuleReplacesAWorseCatalogPromotion(): void + { + $line = $this->policy->discountedLine(new Discount(20.0, null, false), new LinePrices(100.0, 90.0, 1)); + + self::assertEquals(new LinePrices(100.0, 80.0, 1), $line); + } + + #[Test] + public function aStackableRuleAppliesOnTopOfTheCatalogPromotion(): void + { + $line = $this->policy->discountedLine(new Discount(10.0, null, true), new LinePrices(100.0, 80.0, 1)); + + self::assertEquals(new LinePrices(100.0, 72.0, 1), $line); + } + + #[Test] + public function aPromotionTheCatalogDoesNotExplainWasWrittenByARule(): void + { + $catalogWithoutPromo = new LinePrices(100.0, 0.0, 0); + $catalogWithPromo = new LinePrices(100.0, 80.0, 1); + + self::assertTrue($this->policy->carriesRuleDiscount(new LinePrices(100.0, 90.0, 1), $catalogWithoutPromo)); + self::assertTrue($this->policy->carriesRuleDiscount(new LinePrices(100.0, 72.0, 1), $catalogWithPromo)); + self::assertFalse($this->policy->carriesRuleDiscount(new LinePrices(100.0, 80.0, 1), $catalogWithPromo), 'the catalog promotion itself'); + self::assertFalse($this->policy->carriesRuleDiscount(new LinePrices(100.0, 0.0, 0), $catalogWithoutPromo), 'no promotion at all'); + } + + #[Test] + public function linesCompareWithinACentTolerance(): void + { + self::assertTrue($this->policy->sameLine(new LinePrices(100.0, 90.0, 1), new LinePrices(100.004, 89.996, 1))); + self::assertFalse($this->policy->sameLine(new LinePrices(100.0, 90.0, 1), new LinePrices(100.0, 90.0, 0))); + } +} diff --git a/tests/Unit/Query/RuntimeContextTest.php b/tests/Unit/Query/RuntimeContextTest.php new file mode 100644 index 0000000..b04867a --- /dev/null +++ b/tests/Unit/Query/RuntimeContextTest.php @@ -0,0 +1,34 @@ +getBindableParameters(); + + self::assertSame(7, $parameters['customer_id']); + self::assertSame(0, $parameters['product_id']); + self::assertSame([1, 2], $parameters['cart_product_ids']); + self::assertSame(42.5, $parameters['cart_total']); + self::assertSame(64, $parameters['delivery_country_id']); + self::assertSame('fr_FR', $parameters['locale']); + } + + #[Test] + public function projectParametersAreMergedAfterTheBuiltInOnes(): void + { + $parameters = (new RuntimeContext(productId: 3, parameters: ['customer_typology_id' => 12, 'product_id' => 99]))->getBindableParameters(); + + self::assertSame(12, $parameters['customer_typology_id']); + self::assertSame(99, $parameters['product_id'], 'a project provider may override a built-in placeholder'); + } +} diff --git a/tests/Unit/QueryBuilderTest.php b/tests/Unit/QueryBuilderTest.php new file mode 100644 index 0000000..ab6d54a --- /dev/null +++ b/tests/Unit/QueryBuilderTest.php @@ -0,0 +1,39 @@ +expectException(\RuntimeException::class); + $this->expectExceptionMessage('Thelia 3.0.0 or later'); + $this->expectExceptionMessage('2.5.4'); + + QueryBuilder::assertSupportedCoreVersion('2.5.4'); + } + + #[Test] + public function aThelia3CoreIsAccepted(): void + { + QueryBuilder::assertSupportedCoreVersion('3.0.0'); + QueryBuilder::assertSupportedCoreVersion('3.1.0-beta1'); + + $this->addToAssertionCount(2); + } + + #[Test] + public function anUnknownCoreVersionIsRefused(): void + { + $this->expectException(\RuntimeException::class); + + QueryBuilder::assertSupportedCoreVersion(null); + } +} diff --git a/tests/Unit/Service/CartDiscountLedgerTest.php b/tests/Unit/Service/CartDiscountLedgerTest.php new file mode 100644 index 0000000..233a462 --- /dev/null +++ b/tests/Unit/Service/CartDiscountLedgerTest.php @@ -0,0 +1,61 @@ +nextTotal(1, 5.0, 10.0)); + } + + #[Test] + public function aNestedPassOnAnUntouchedColumnDoesNotAccumulate(): void + { + $ledger = new CartDiscountLedger(); + $written = $ledger->nextTotal(1, 5.0, 10.0); + + //The cart events nest: the column still holds what the first pass wrote + self::assertSame(15.0, $ledger->nextTotal(1, $written, 10.0)); + self::assertSame(15.0, $ledger->nextTotal(1, 15.0, 10.0)); + } + + #[Test] + public function aRecomputedRuleAmountReplacesThePreviousOne(): void + { + $ledger = new CartDiscountLedger(); + $written = $ledger->nextTotal(1, 5.0, 10.0); + + //Quantity changed: the rule amount is now 12, the coupon part (5) is kept once + self::assertSame(17.0, $ledger->nextTotal(1, $written, 12.0)); + } + + #[Test] + public function anUpstreamResetOfTheColumnStartsFromItsNewValue(): void + { + $ledger = new CartDiscountLedger(); + $ledger->nextTotal(1, 5.0, 10.0); + + //The core rewrote the column (a coupon was added): our part is gone, add it again + self::assertSame(18.0, $ledger->nextTotal(1, 8.0, 10.0)); + } + + #[Test] + public function cartsAreTrackedSeparatelyAndTotalsAreRoundedToCents(): void + { + $ledger = new CartDiscountLedger(); + + self::assertSame(10.0, $ledger->nextTotal(1, 0.0, 10.0)); + self::assertSame(3.33, $ledger->nextTotal(2, 0.0, 3.333)); + self::assertSame(3.33, $ledger->nextTotal(2, 3.33, 3.333)); + } +} diff --git a/tests/Unit/Service/DataDictionaryTest.php b/tests/Unit/Service/DataDictionaryTest.php new file mode 100644 index 0000000..f432d0e --- /dev/null +++ b/tests/Unit/Service/DataDictionaryTest.php @@ -0,0 +1,219 @@ +getHooks(Context::PRODUCT))); + self::assertSame(['cart.top', 'cart.bottom'], array_keys($dictionary->getHooks(Context::CART))); + self::assertSame(Context::CUSTOMER, $dictionary->getContextForHook('account.bottom')); + self::assertNull($dictionary->getContextForHook('unknown.hook')); + } + + #[Test] + public function aGlobalRuleSeesEveryDeclaredHook(): void + { + $globalHooks = DictionaryFactory::base()->getHooks(Context::GLOBAL_SCOPE); + + foreach (['home.top', 'category.top', 'brand.bottom', 'product.top', 'cart.bottom', 'account.top'] as $hookCode) { + self::assertArrayHasKey($hookCode, $globalHooks); + } + } + + #[Test] + public function anOverrideMergesHooksByCodeAndTheLastLabelWins(): void + { + $dictionary = DictionaryFactory::withOverrides( + <<getHooks(Context::PRODUCT); + self::assertSame('Project label', $productHooks['product.top']); + self::assertSame(['product.top', 'product.details.bottom', 'product.bottom'], array_keys($productHooks), 'the base hooks stay, in order'); + self::assertSame('cart.recommendations', $dictionary->getHooks(Context::CART)['cart.recommendations'], 'a bare code is its own label'); + self::assertSame(Context::CART, $dictionary->getContextForHook('cart.recommendations')); + } + + #[Test] + public function anOverrideCanDisableABaseFieldAndAddItsOwn(): void + { + $dictionary = DictionaryFactory::withOverrides( + <<getField('product_visible')); + self::assertNotNull($dictionary->getField('erp_family')); + self::assertArrayHasKey('erp_family', $dictionary->getFields(Context::PRODUCT)); + self::assertArrayNotHasKey('erp_family', $dictionary->getFields(Context::CART)); + } + + #[Test] + public function aFieldWithoutContextsIsGlobalAndOfferedInEveryContextAndEditor(): void + { + $field = DictionaryFactory::base()->getField('product_ref'); + + self::assertNotNull($field); + self::assertSame([Context::GLOBAL_SCOPE], $field->contexts); + self::assertSame(FieldDefinition::USAGES, $field->usages); + self::assertSame('product', $field->getGroup()); + + foreach (Context::cases() as $context) { + self::assertTrue($field->isAvailableInContext($context), $context->value); + } + } + + #[Test] + public function aFieldRestrictedToAContextIsHiddenFromTheOthersAndFromGlobalRules(): void + { + $dictionary = DictionaryFactory::base(); + + self::assertArrayHasKey('context_product_same_brand', $dictionary->getFields(Context::PRODUCT)); + self::assertArrayNotHasKey('context_product_same_brand', $dictionary->getFields(Context::CART)); + self::assertArrayNotHasKey('context_product_same_brand', $dictionary->getFields(Context::GLOBAL_SCOPE)); + self::assertArrayHasKey('product_ref', $dictionary->getFields(Context::GLOBAL_SCOPE), 'a GLOBAL field stays in GLOBAL rules'); + } + + #[Test] + public function theUsageReservesAFieldToOneEditor(): void + { + $dictionary = DictionaryFactory::withOverrides( + <<getFields(Context::PRODUCT, FieldDefinition::USAGE_RULE); + $actionFields = $dictionary->getFields(Context::PRODUCT, FieldDefinition::USAGE_ACTION); + + self::assertArrayNotHasKey('selection_only', $ruleFields); + self::assertArrayHasKey('selection_only', $actionFields); + self::assertArrayHasKey('product_ref', $ruleFields, 'a field without usage is offered to both editors'); + self::assertArrayHasKey('product_ref', $actionFields); + self::assertArrayNotHasKey('context_category_id', $actionFields, 'the context object fields stay in the rule editor'); + self::assertArrayHasKey('context_category_id', $dictionary->getFields(Context::CATEGORY, FieldDefinition::USAGE_RULE)); + self::assertArrayNotHasKey('context_category_product', $dictionary->getFields(Context::CATEGORY, FieldDefinition::USAGE_RULE)); + } + + #[Test] + public function everyShippedContextFieldIsRestrictedToItsPlaceholderContext(): void + { + $placeholderContexts = [ + ':product_id' => Context::PRODUCT, + ':category_id' => Context::CATEGORY, + ':brand_id' => Context::BRAND, + ':order_id' => Context::ORDER, + ]; + + //Reads the cart of the visit, offered to the PRODUCT product lists by design + $sessionBoundContextFields = ['context_product_in_cart' => Context::PRODUCT]; + + foreach (DictionaryFactory::base()->getFields() as $field) { + if (!str_starts_with($field->code, 'context_')) { + continue; + } + + if (isset($sessionBoundContextFields[$field->code])) { + self::assertSame([$sessionBoundContextFields[$field->code]], $field->contexts, $field->code); + continue; + } + + $expected = null; + foreach ($placeholderContexts as $placeholder => $context) { + if (str_contains((string) $field->expression, $placeholder)) { + $expected = $context; + } + } + + self::assertNotNull($expected, sprintf('%s reads no context placeholder', $field->code)); + self::assertSame([$expected], $field->contexts, $field->code); + } + } + + #[Test] + #[DataProvider('invalidDefinitions')] + public function anInvalidDefinitionIsRefused(string $yaml, string $message): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage($message); + + DictionaryFactory::withOverrides($yaml)->getFields(); + } + + /** @return iterable */ + public static function invalidDefinitions(): iterable + { + yield 'field and expression together' => [ + "fields:\n broken:\n field: product.ref\n expression: \"1\"\n", + 'requires exactly one of "field" or "expression"', + ]; + yield 'unknown type' => [ + "fields:\n broken:\n field: product.ref\n type: json\n", + 'unknown type "json"', + ]; + yield 'values on a boolean' => [ + "fields:\n broken:\n field: product.visible\n type: boolean\n values_query: \"SELECT 1 AS value\"\n", + '"values_query" does not apply', + ]; + yield ':value expression without polarity operators' => [ + "fields:\n broken:\n expression: \"product.id IN (:value)\"\n", + 'its "operators" must be declared among [in, notIn]', + ]; + yield 'join without a source column' => [ + "joins:\n broken_table:\n to: id\n", + 'requires a "from" in "table.column" form', + ]; + yield 'unknown context' => [ + "fields:\n broken:\n field: product.ref\n contexts: [SHOP]\n", + 'unknown context "SHOP"', + ]; + yield 'contexts not a list' => [ + "fields:\n broken:\n field: product.ref\n contexts: PRODUCT\n", + 'expects a list of contexts', + ]; + yield 'unknown usage' => [ + "fields:\n broken:\n field: product.ref\n usage: [editor]\n", + 'unknown usage "editor"', + ]; + yield 'malformed hook entry' => [ + "contexts:\n PRODUCT:\n - 42\n", + 'invalid hook entry in context "PRODUCT"', + ]; + } +} diff --git a/tests/Unit/Service/DeliveryModuleOptionPostageClearerTest.php b/tests/Unit/Service/DeliveryModuleOptionPostageClearerTest.php new file mode 100644 index 0000000..0386113 --- /dev/null +++ b/tests/Unit/Service/DeliveryModuleOptionPostageClearerTest.php @@ -0,0 +1,49 @@ +setPostage(6.9)->setPostageTax(1.15)->setPostageUntaxed(5.75); + $pickup = (new DeliveryModuleOption())->setPostage(3.5)->setPostageTax(0.58)->setPostageUntaxed(2.92); + + (new DeliveryModuleOptionPostageClearer())->clear([$home, $pickup]); + + foreach ([$home, $pickup] as $option) { + self::assertSame(0.0, $option->getPostage()); + self::assertSame(0.0, $option->getPostageTax()); + self::assertSame(0.0, $option->getPostageUntaxed()); + } + } + + #[Test] + public function anInvalidOptionWithoutPriceBecomesAZeroPriceToo(): void + { + $option = (new DeliveryModuleOption())->setValid(false); + + (new DeliveryModuleOptionPostageClearer())->clear([$option]); + + self::assertSame(0.0, $option->getPostage()); + self::assertFalse($option->isValid()); + } + + #[Test] + public function foreignEntriesAreSkipped(): void + { + $option = (new DeliveryModuleOption())->setPostage(6.9); + + (new DeliveryModuleOptionPostageClearer())->clear(['not an option', $option]); + + self::assertSame(0.0, $option->getPostage()); + } +} diff --git a/tests/Unit/Service/SqlBuilderTest.php b/tests/Unit/Service/SqlBuilderTest.php new file mode 100644 index 0000000..1013be5 --- /dev/null +++ b/tests/Unit/Service/SqlBuilderTest.php @@ -0,0 +1,255 @@ +builder()->compile(self::rule('product_ref', '=', 'ABC'), new RuntimeContext()); + + self::assertStringStartsWith('SELECT DISTINCT `product`.`id` FROM `product`', $compiled->sql); + self::assertStringContainsString('WHERE (`product`.`ref` = :qb_0)', $compiled->sql); + self::assertSame(['qb_0' => 'ABC'], $compiled->parameters); + } + + #[Test] + public function aFieldOfAJoinedTableAddsTheJoinChainAndBindsTheLocale(): void + { + $compiled = $this->builder()->compile(self::rule('product_title', 'contains', 'tee'), new RuntimeContext(locale: 'en_US')); + + self::assertStringContainsString( + 'INNER JOIN `product_i18n` ON `product`.`id` = `product_i18n`.`id` AND product_i18n.locale = :locale', + $compiled->sql + ); + self::assertStringContainsString('`product_i18n`.`title` LIKE :qb_0', $compiled->sql); + self::assertSame(['qb_0' => '%tee%', 'locale' => 'en_US'], $compiled->parameters); + } + + #[Test] + public function aNegativeOperatorOnAMultivaluedJoinExcludesTheWholeProduct(): void + { + $compiled = $this->builder()->compile(self::rule('product_category_title', 'notIn', ['Shoes']), new RuntimeContext()); + + self::assertStringContainsString('NOT EXISTS (SELECT 1 FROM `product_category`', $compiled->sql); + self::assertStringContainsString('WHERE `product_category`.`product_id` = `product`.`id` AND `category_i18n`.`title` IN (:qb_0))', $compiled->sql); + self::assertStringNotContainsString("\nINNER JOIN `product_category`", $compiled->sql, 'a multivalued join must not be added to the main FROM'); + self::assertSame('Shoes', $compiled->parameters['qb_0']); + } + + #[Test] + public function aListPlaceholderIsExpandedToOneParameterPerItem(): void + { + $compiled = $this->builder()->compile(self::rule('product_in_cart', '=', true), new RuntimeContext(cartProductIds: [4, 9])); + + self::assertStringContainsString('(product.id IN (:cart_product_ids_0, :cart_product_ids_1)) = :qb_0', $compiled->sql); + self::assertSame(['qb_0' => 1, 'cart_product_ids_0' => 4, 'cart_product_ids_1' => 9], $compiled->parameters); + } + + #[Test] + public function anEmptyListPlaceholderBindsASentinelThatNeverMatches(): void + { + $compiled = $this->builder()->compile(self::rule('product_in_cart', '=', 'true'), new RuntimeContext()); + + self::assertStringContainsString('IN (:cart_product_ids_0)', $compiled->sql); + self::assertSame(-1, $compiled->parameters['cart_product_ids_0']); + } + + #[Test] + public function aMissingRuntimePlaceholderIsRefused(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage(':customer_id'); + + $this->builder()->compile(self::rule('customer_purchased_last_3_months', '=', true), new RuntimeContext(customerId: null)); + } + + #[Test] + public function theCurrentProductPlaceholderFallsBackToZeroOutsideAProductPage(): void + { + $compiled = $this->builder()->compile(self::rule('product_is_current', '=', false), new RuntimeContext()); + + self::assertSame(0, $compiled->parameters['product_id']); + self::assertSame(0, $compiled->parameters['qb_0']); + } + + #[Test] + public function aDatetimeColumnIsComparedOnItsDatePart(): void + { + $compiled = $this->builder()->compile(self::rule('product_created_at', '>=', '2026-01-01'), new RuntimeContext()); + + self::assertStringContainsString('DATE(`product`.`created_at`) >= :qb_0', $compiled->sql); + self::assertSame('2026-01-01', $compiled->parameters['qb_0']); + } + + #[Test] + public function theCartTotalIsComparedNumerically(): void + { + $compiled = $this->builder()->compile(self::rule('cart_total_amount', '>=', '100'), new RuntimeContext(cartTotal: 120.5)); + + self::assertStringContainsString('(CAST(:cart_total AS DECIMAL(16, 6))) >= :qb_0', $compiled->sql); + self::assertSame(100, $compiled->parameters['qb_0']); + self::assertSame(120.5, $compiled->parameters['cart_total']); + } + + #[Test] + public function aCommaSeparatedListFromTheEditorIsSplit(): void + { + $compiled = $this->builder()->compile(self::rule('product_ref', 'in', 'A, B'), new RuntimeContext()); + + self::assertStringContainsString('`product`.`ref` IN (:qb_0, :qb_1)', $compiled->sql); + self::assertSame(['qb_0' => 'A', 'qb_1' => 'B'], $compiled->parameters); + } + + #[Test] + public function aNegatedGroupWrapsItsClausesInNot(): void + { + $tree = ['combinator' => 'or', 'not' => true, 'rules' => [ + ['field' => 'product_ref', 'operator' => '=', 'value' => 'A'], + ['field' => 'product_ref', 'operator' => 'beginsWith', 'value' => 'B'], + ]]; + + $compiled = $this->builder()->compile($tree, new RuntimeContext()); + + self::assertStringContainsString('WHERE NOT (`product`.`ref` = :qb_0 OR `product`.`ref` LIKE :qb_1)', $compiled->sql); + self::assertSame('B%', $compiled->parameters['qb_1']); + } + + #[Test] + public function anEmptyGroupIsAlwaysTrueAndTheLimitIsAppended(): void + { + $compiled = $this->builder()->compile(['combinator' => 'and', 'rules' => []], new RuntimeContext(), 3, ['`product`.`visible` = 1']); + + self::assertStringContainsString("WHERE 1=1\n AND `product`.`visible` = 1\nLIMIT 3", $compiled->sql); + } + + #[Test] + public function anUnknownFieldIsRefused(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('unknown field "product.ref; DROP TABLE product"'); + + $this->builder()->validateTree(self::rule('product.ref; DROP TABLE product', '=', 'x')); + } + + #[Test] + public function anOperatorOutsideTheFieldWhitelistIsRefused(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('operator "contains" is not allowed for field "product_visible"'); + + $this->builder()->validateTree(self::rule('product_visible', 'contains', 'x')); + } + + #[Test] + public function aBetweenNeedsExactlyTwoValues(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('exactly two values'); + + $this->builder()->compile(self::rule('product_price', 'between', '10'), new RuntimeContext()); + } + + #[Test] + public function aFieldRestrictedToAnotherContextIsRefusedOnValidation(): void + { + $builder = $this->builder(DictionaryFactory::withOverrides(self::PROJECT_OVERRIDE)); + $tree = self::rule('cart_only_field', '=', 'x'); + + $builder->validateTree($tree, Context::CART); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('not available in context PRODUCT'); + $builder->validateTree($tree, Context::PRODUCT); + } + + #[Test] + public function aFieldReservedToTheActionEditorIsRefusedInTheRuleEditor(): void + { + $builder = $this->builder(DictionaryFactory::withOverrides(self::PROJECT_OVERRIDE)); + $tree = self::rule('selection_only', '=', 'x'); + + $builder->validateTree($tree, Context::PRODUCT, FieldDefinition::USAGE_ACTION); + $builder->validateTree($tree, Context::PRODUCT); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('not available in the rule editor'); + $builder->validateTree($tree, Context::PRODUCT, FieldDefinition::USAGE_RULE); + } + + #[Test] + public function aContextFieldBindsTheContextPlaceholderInsideItsExistsSubquery(): void + { + $compiled = $this->builder()->compile( + self::rule('context_product_same_brand', '=', true), + new RuntimeContext(productId: 42) + ); + + self::assertStringContainsString('(EXISTS (SELECT 1 FROM product qb_ctx WHERE qb_ctx.id = :product_id AND qb_ctx.brand_id IS NOT NULL AND qb_ctx.brand_id = product.brand_id)) = :qb_0', $compiled->sql); + self::assertSame(42, $compiled->parameters['product_id']); + self::assertSame(1, $compiled->parameters['qb_0']); + } + + #[Test] + public function aValueExpressionConsumesTheEnteredListAndTheOperatorOnlySetsThePolarity(): void + { + $builder = $this->builder(DictionaryFactory::withOverrides(self::PROJECT_OVERRIDE)); + + $kept = $builder->compile(self::rule('bought_category', 'in', ['A', 'B']), new RuntimeContext()); + self::assertStringContainsString('WHERE ((product.id IN (SELECT p.id FROM product p WHERE p.ref IN (:qb_0, :qb_1))))', $kept->sql); + self::assertSame(['qb_0' => 'A', 'qb_1' => 'B'], $kept->parameters); + + $excluded = $builder->compile(self::rule('bought_category', 'notIn', ['A']), new RuntimeContext()); + self::assertStringContainsString('WHERE (NOT (product.id IN (SELECT p.id FROM product p WHERE p.ref IN (:qb_0))))', $excluded->sql); + + $emptyKept = $builder->compile(self::rule('bought_category', 'in', []), new RuntimeContext()); + self::assertStringContainsString('WHERE (1=0)', $emptyKept->sql, 'an empty list keeps nothing'); + + $emptyExcluded = $builder->compile(self::rule('bought_category', 'notIn', []), new RuntimeContext()); + self::assertStringContainsString('WHERE (1=1)', $emptyExcluded->sql, 'an empty list excludes nothing'); + } + + private function builder(?DataDictionary $dictionary = null): SqlBuilder + { + return new SqlBuilder($dictionary ?? DictionaryFactory::base()); + } + + /** @return array{combinator: string, rules: list} */ + private static function rule(string $field, string $operator, mixed $value): array + { + return ['combinator' => 'and', 'rules' => [['field' => $field, 'operator' => $operator, 'value' => $value]]]; + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..949cd26 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,108 @@ +/phpunit.xml.dist --testsuite integration\n", + THELIA_ROOT, + $shopBootstrap, + )); + exit(1); +} + +// The shop kernel reads its parameters from the environment (DEFAULT_URI, the database): +// load the shop .env the way the shop test bootstrap does. In test mode Dotenv skips +// .env.local by design, where the database access of a development shop usually lives: +// bridge the DATABASE_* variables from it when nothing else defined them. +$shopEnvFile = dirname($autoload, 2) . '/.env'; + +if (is_file($shopEnvFile) && class_exists(\Symfony\Component\Dotenv\Dotenv::class)) { + (new \Symfony\Component\Dotenv\Dotenv())->bootEnv($shopEnvFile); + + $shopEnvLocalFile = $shopEnvFile . '.local'; + + if (empty($_SERVER['DATABASE_HOST']) && is_file($shopEnvLocalFile)) { + $localVariables = (new \Symfony\Component\Dotenv\Dotenv())->parse((string) file_get_contents($shopEnvLocalFile)); + + foreach (['DATABASE_HOST', 'DATABASE_PORT', 'DATABASE_NAME', 'DATABASE_USER', 'DATABASE_PASSWORD'] as $key) { + if (isset($localVariables[$key]) && empty($_SERVER[$key])) { + $_SERVER[$key] = $_ENV[$key] = $localVariables[$key]; + } + } + } +} + +// The module and its tests, when the shop autoloader does not map them already +// (module checked out outside local/modules). Longest prefix first. +spl_autoload_register(static function (string $class): void { + $prefixes = [ + 'QueryBuilder\\Tests\\' => __DIR__ . '/', + 'QueryBuilder\\' => dirname(__DIR__) . '/', + ]; + + foreach ($prefixes as $prefix => $directory) { + if (!str_starts_with($class, $prefix)) { + continue; + } + + $file = $directory . str_replace('\\', '/', substr($class, \strlen($prefix))) . '.php'; + + if (is_file($file)) { + require $file; + } + + return; + } +});