diff --git a/CHANGELOG.md b/CHANGELOG.md index 25aa575..7d53a04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,45 @@ ## Не выпущено -Новые изменения будут добавлены после build 0.40. +Новые изменения будут добавлены после build 0.41. + +## 3.3 build 0.41 — 7 сентября 2026 + +### Контент и каталог + +- Уточнены роли товарных полей и связи между разделами каталога, документами и + нативными характеристиками; редакторы получили более понятные переходы к + связанным настройкам. +- Массовые и быстрые изменения документов используют единый безопасный путь + записи и корректно обновляют производные данные и кеш. +- Улучшены диагностика качества, выборки товаров и совместимость запросов с + MariaDB 10.3. + +### Публичный сайт и фиды + +- Фиды корректно обновляются после изменения товара, устойчивее генерируются + параллельно и учитывают видимость служебного контента. +- Поиск и товарные выборки точнее обрабатывают доступность, роли полей и + специальные условия показа. + +### Панель управления + +- Раздел 404 поддерживает безопасный редирект на произвольный локальный путь и + выбор любого доступного публичного документа, а таблицы журнала адаптированы + для мобильного экрана. +- Исправлено мобильное отображение журнала событий; виджет заказов по статусам + теперь использует тот же период 14 дней, что и общая статистика. +- Дополнены переводы, подсказки и пользовательская справка для изменённых + разделов. + +### Надёжность + +- Операции корзины, оформления заказа, оплаты и доставки получили дополнительные + проверки целостности и регрессионные тесты. +- Для build 0.41 подготовлены чистая core-сборка, компактный установщик, + накопительное обновление с build 0.20 и обновлённый каталог модулей. +- Проектные торговые расширения и ИИ-помощник не входят в публичную core-сборку + и репозиторий модулей. ## 3.3 build 0.40 — 25 августа 2026 diff --git a/PACKAGE-MANIFEST.json b/PACKAGE-MANIFEST.json index 4808a43..522e44d 100644 --- a/PACKAGE-MANIFEST.json +++ b/PACKAGE-MANIFEST.json @@ -2,35 +2,35 @@ "format": "ave-core-package-v2", "product": "AVE.cms", "version": "3.3", - "build": "0.40", + "build": "0.41", "package": "core", - "built_at": "2026-08-25T11:42:07+00:00", + "built_at": "2026-09-07T04:07:54+00:00", "installed_modules": 0, "bundled_optional_modules": [], "optional_after_install": [ "setup/" ], - "core_fingerprint": "045c60e55daab45dcf75d86b72e01dd6aed8ca7103efa4293dd1676f0be349ab", + "core_fingerprint": "7896669cffd40910cf19bca24b6784de5653e80ca4feee3c182c309971f9dcb8", "files": { ".gitignore": "20d4fb177d47da6944a7a1c2dbf5918fd62766881aa52b00901f9054b1db4b49", ".htaccess": "71c06b7bc41b09c347a562ca72768acaa0ee1ef8388c749fc67956c793d8a826", - "CHANGELOG.md": "4adedd967bb28883655dfb981ebcc7fdc1e32367a79efb4e4417e3fd70d4f04e", + "CHANGELOG.md": "ae00742770c10c9ed32f028b7df22df1ce55627635569b38de6f2043e3bcd093", "README.md": "19b820f1b092780039aae011e133dc6a1e5f1cf213b435e0ff1962b592b9095b", "adminx/.htaccess": "d03ea2d2972af8b7545a2d57c6253f303d15adeb30b775e161d887ca5ff73378", "adminx/Support/AdminLocale.php": "a7fe84d86d65c9f021106172b1789c8bc6221c36b2fe39cf883ca9a505e3e8ad", "adminx/Support/BulkActionExecutor.php": "82859d10eec39a53091ed46604d6819e7111b7d879e5f0f1595c26f8a7ff1a60", "adminx/Support/CodeEditor.php": "51532980b1990e595b796d884c1a8bcc46f77f88344386e5153b4bef4cae973c", "adminx/Support/GlobalSearchRegistry.php": "7f0cb38a76ecb4c71af2891176406b1d46004c7b778e44cd2fce8367407e9da1", - "adminx/Support/InterfaceSettings.php": "e42cdc1157d8a55760278ab46d603316039fad0680eadf7c95824984f45f63ea", - "adminx/Support/ModuleExtensions.php": "36de2286496c8327ee22d806f98a6bc9521a2d81cd1bb4182122c7925ed8d0a7", + "adminx/Support/InterfaceSettings.php": "752bd345a6595604b3ef87bd93bd0259a76f144ed887c7ff443a4fe53651494d", + "adminx/Support/ModuleExtensions.php": "e7925a287351ef338a3733551e36646cb914c3ce069bed5f3a0c24aec7596b9a", "adminx/Support/Notifications.php": "ab08805d571cd3d8a4b2a09fdc6ce96fb4c7b5ec75691322cf8a4e1a142112a3", - "adminx/Support/PhpCode.php": "5cdd56c7deddd713dd06412b64da729c8a411aa99f46958fc29121055740cd07", + "adminx/Support/PhpCode.php": "a9b53565414c9784919ca06ce6d3926f939ddbb42b097f62bb8111b8d235dd71", "adminx/Support/ReAuth.php": "f8c2146fb4b8f03b7d0cd2f3576e3f39fe2749823c5d6f1dca494268dcaba303", "adminx/Support/Roles.php": "6be335dd643317f77bd7c2db92f25895e601fe72818befd62f6aeb3ecac9fce7", "adminx/Support/RouteGuard.php": "e9e20c097c0db354851546585be72bfc71a5d14652871f8bd1160e845d4dab1b", "adminx/Support/SavedViews.php": "4548ca129f270184e61cb670e7f44a1c103099a6a6c2820bed1a11bc18f227c4", - "adminx/Support/SectionHelp.php": "4ce72b8895ad4f813d3a49e56a895c20f2821ad7f546c87f6e891793592dee4c", - "adminx/Support/SensitiveRoutes.php": "d14d809310b43fdb66ec141b0573ec1020aea3720d20269d39f201f411d9f9c5", + "adminx/Support/SectionHelp.php": "91d58793f2d6db4d78fd8f45fd6602dc3a5bd67a97456befdba156afabfc365f", + "adminx/Support/SensitiveRoutes.php": "3359bb50323e98433ff1e830e9c62645c5b346027dea869c58fc1dbc13e4d84b", "adminx/Support/SystemFeatures.php": "f18ada623710f8c3efc6588c037227f6074e75bad9a3820aeef60b4d8d4b20a4", "adminx/Support/SystemHealth.php": "286aee452e90f71ad64437bd39d6dd985d312eb85bbb711490d3a0b631a16aea", "adminx/Support/Twig/AdminConstantExpression.php": "44bb7d2d978e3c873d457ba1ca7b8021c6f5292e1a4f8b74e49348d01dc84b1d", @@ -39,7 +39,7 @@ "adminx/Support/Twig/AdminTextNode.php": "e51d8d3ee49411f42c8a874cd283f60c1fa0af6506b34568a119ebe2d18d7268", "adminx/Support/ViewOverrideEditor.php": "9704c974f36b6f9e204b6c609582d61b0a0f65aa182a3f1381fe7a7d1c0b15e7", "adminx/Support/WorkItemContext.php": "d4b87b78ff1f2bdd89d77bc0483ebb46e5c11616635211ead9a43b34568e3a59", - "adminx/assets/css/adminkit.css": "0883ec02682b3069765388c61e0d9f8262786599fbd8bec51701affbab585a5e", + "adminx/assets/css/adminkit.css": "5cbcb6bf1e506d35afa6ffccbbc56385c8feb31a5eec52af5054c315335b71a2", "adminx/assets/fonts/NunitoSans-Bold.woff2": "2d48580b7e1be60fa94b8092ebe86471fe0adfe5ca75242a7af2042ac9da825f", "adminx/assets/fonts/NunitoSans-ExtraBold.woff2": "66612ee9d1cdc341c9eec8a43104bbf515e7cc52821558bfc7dfcf33c8cb0cc1", "adminx/assets/fonts/NunitoSans-Regular.woff2": "878adc25278872933f004783f0ebbe092cebb3f9161aa30cffb2b9a8a07db040", @@ -47,7 +47,7 @@ "adminx/assets/fonts/OpenSans-Regular.woff2": "a26ed465673123933c5510730a7286f49016a40fa94062021d1dc4c9bfeddbd3", "adminx/assets/fonts/OpenSans-SemiBold.woff2": "7d963e1d2f61c11c2e8bd2eb528777b5a55569708d50a7ee7bd097d7fafe6da4", "adminx/assets/img/logo.svg": "685d9bf037b33fbcfc200947d5051c5325762f8ab8a29ac9e1e587076321e463", - "adminx/assets/js/adminx.js": "34d38f1291372e5ce10d051d9cbc144f61020f833fce263f5041fdc6926c20d1", + "adminx/assets/js/adminx.js": "0f2efdf3ba50652f1e979bfc546d562e38e44e13f2bfb7c42f666e8b5d944a17", "adminx/assets/js/bulk-actions.js": "d7ee80678e6703aff2418a4679ba8e5027e7220cb089848bcced6d9f100b3aae", "adminx/assets/js/command-palette.js": "f62f0bad5ddf2b6e813e7eda094d73df63dce09a6e581025971e8f10430e8d9f", "adminx/assets/js/editor-codemirror.js": "90a04c219e84f5391a40b0d1fc807ecfe4ca582a198bc202bdc5aa6dccec51fa", @@ -142,13 +142,13 @@ "adminx/assets/vendor/tabler/fonts/tabler-icons.woff2": "cce741fcb6c526dfea3c0d48ff486279ac723617cd5b4de2bd5d726e94cef477", "adminx/assets/vendor/tabler/tabler-icons.min.css": "676509f34c06d34b75e4227e4c36105e5d220b458f609fffec247f20a8d72a8d", "adminx/core-permissions.php": "084fbff2f7625f0e1b6a6a15d00fce536212adbdf40f6a7fa2eef34cab54394b", - "adminx/index.php": "bf4c543d4b32cf24df073318151ae6a7192132897ccbb81089e7c78398247462", - "adminx/language/en/client.xml": "de712c66a939fcae607c087e0941dfbf824152a41be85aeae0a0406cc0ef3053", + "adminx/index.php": "267b4eb5ee65f64e1388acaf6e64ce27608b4b405744ff4fed6eda5f656bc7fb", + "adminx/language/en/client.xml": "7a6a03448ca15df594f38e3ccb63f11cabc9c3c5ce301022074a14490e3dbee5", "adminx/language/en/common.xml": "e5f9972786f1d2a8e05f86913b07c361b82811f99cf51bee6d38e9ed275fc995", "adminx/language/en/interface.xml": "80ccb298e1ae8ea31c533832072b99c1eb1369871f6d1b357635aa59622d9d52", "adminx/language/en/runtime.xml": "68b00572d69484240e1716007ce64c2febe66cae4e021b09c428e758de990ae2", "adminx/language/en/section-help.xml": "f82ba8d3b78d06d26b92afb81e036b241c8dd3ef9e4ded6158a0be10491e90b9", - "adminx/language/ru/client.xml": "b69308cbe26e7da2c10084ce7163e3aacb24cf35447a70a3474963ab0a29ff77", + "adminx/language/ru/client.xml": "606dac2a0fec6fdb5fbd4ca8201d1fef5f6607556a27d249688646c3dc595fe4", "adminx/language/ru/common.xml": "d27d863bf82591dbe857317d97a888a23980c87529e2b4c1d4125a6c0ba20966", "adminx/language/ru/interface.xml": "03fa6351745d2e47c2b3407da65c8b7b048b8e862409dc9bb934e939b9439c75", "adminx/language/ru/runtime.xml": "3c411a18695af7a63dd0685a8351ac3a51695aea7d1fa2550fb24cc9e3e82be6", @@ -185,24 +185,37 @@ "adminx/modules/Blocks/migrations/012_merge_visual_blocks.php": "5c9cd6efd66a027134621859da1059b2c403bacb732885d6b405699cbce8fc14", "adminx/modules/Blocks/module.php": "53eb434566d820814f5b25382200b75ed02b0067c066c8f8cb62bee13529b964", "adminx/modules/Blocks/view/index.twig": "39f9225b6a95c65549d0c861afb7db7879ca338c251290653dded6ab7cb715cc", - "adminx/modules/Catalog/Controller.php": "dea3f7ac5cdb8bd1757e18f95bcbeb2dd433123536d6f0766bc584bb1c09fcbf", - "adminx/modules/Catalog/Model.php": "d524730981ad388928cfc29208a821ed1d474bd01d6db1749fd37fdfb4e4958e", - "adminx/modules/Catalog/assets/catalog-drawer.css": "b3e4f798d7bdf329aa91987ae5be435910ddccfeb8a32211059846ab9dc6b7c4", + "adminx/modules/Catalog/Controller.php": "f9bf3bf72e15cf0b886157a9b421c4f6a2364b35a70464e7285259a151d51bbe", + "adminx/modules/Catalog/EditorContextLinks.php": "0319a6f237aa3aa627aba1f44a3efe4c6de007b26a9e75ba0df6b3b2ccc5478e", + "adminx/modules/Catalog/Model.php": "937cec97bcb67aad4ce4da40e9da2b1ad691de6969ae5b12bbec2118b997653d", + "adminx/modules/Catalog/assets/catalog-drawer.css": "27af3ab3f1b3cfe6425467528350489a870388b34427a79b0b2c4e94af9f1cd6", "adminx/modules/Catalog/assets/catalog.css": "49e7a33fc444f8330806e1cc5cbb0d523b673c64faafd25dbf0fe33067643efd", - "adminx/modules/Catalog/assets/catalog.js": "1fc4acc30b05fd702dee9ff62958141aeab7593ca38d6f6360567af7c3dc9742", - "adminx/modules/Catalog/language/en/client.xml": "9b156a155a1e6f1a2024f4be5ab8d1051b1ec2adf4b9a0ede71afe11e6d4558f", + "adminx/modules/Catalog/assets/catalog.js": "fe5b3f7df7c8c70348badc433dab5b91268a69bf081a2bc87e52dd253923fad4", + "adminx/modules/Catalog/assets/context-links.css": "71a563572960db0574e99fe1c81bf1227e1851ab902afdac7ffeb3b5a0d8e877", + "adminx/modules/Catalog/language/en/client.xml": "775af8a1a4deb887ef9671e64f97e9f43b69b045c3bbcf2a52c3ab184dfd4ec5", + "adminx/modules/Catalog/language/en/context-links.xml": "1e465bcee8e3331ef7ea978b530a657bf1087ff1484d909354f0f8366ff99331", "adminx/modules/Catalog/language/en/interface.xml": "1007d3fb9158846dd36f519d13909502e16d3c0a9ca2c49dc24bec088c487ea7", + "adminx/modules/Catalog/language/en/product-roles.xml": "054da578bcc51de37ed41222eb40f758f5cf5045d0ad97e6fc6e931af76886d8", + "adminx/modules/Catalog/language/en/product-selection.xml": "8150677ed1b3f6aa192acdecb27958c69880c390768ac7e5a60ae37aa3f82c33", "adminx/modules/Catalog/language/en/runtime.xml": "f30a0b64665a64de1682251402c720bc9e526138f682c5dd2e9fbf3ffb87f2b0", - "adminx/modules/Catalog/language/ru/client.xml": "a4747370c4510ad0a56344ce3ee79d460585363253e983053b6743e613132a51", + "adminx/modules/Catalog/language/ru/client.xml": "3091a480bff89529fbe28efb55aa87b59944f7605f38f614d71780103cbe63e0", + "adminx/modules/Catalog/language/ru/context-links.xml": "33a0cd281d0586e85ac6144933767067540f62919df57c3dbf41c5bdfb439ec0", "adminx/modules/Catalog/language/ru/interface.xml": "15d8de452eef296375ba40d557ce15ac0363881a7b763b7111c8cc221e9861ff", + "adminx/modules/Catalog/language/ru/product-roles.xml": "9f80012acbb4d98ae26d88edb484ee6f446748a41b6549fcebdbf08d85af5cf2", + "adminx/modules/Catalog/language/ru/product-selection.xml": "42773ca81d5f35780b8a791868bea289d2d04f0e9eba579cae23094406ff1f5e", "adminx/modules/Catalog/language/ru/runtime.xml": "fe59945f4ceb2bf2d85699623369e7e5889613b8cecd8551a390c78023531d14", "adminx/modules/Catalog/migrations/005_add_catalog_purpose.sql": "006ff237e25042a4fa9b9f74d5079741eeee1aa52db0cd996ea4c8b817e6700f", - "adminx/modules/Catalog/module.php": "c36086c3162d1eba433af05c879dcfb9cff830ee3c53da38705a72cc19b3777b", - "adminx/modules/Catalog/view/edit.twig": "13bd9ebabdc8bb470a6c8ceb813b122d33da5c5604a23bb7ad3a840cc061cde2", + "adminx/modules/Catalog/migrations/006_add_item_sources.php": "97a023140d1380ef2ef6a2ab1dd1a86b0901689fa2f1ccdf0fc76608dd18e546", + "adminx/modules/Catalog/module.php": "f393b86b20c962f5262840381a397533e34e4c7f5b8bb786ae9aaa6de70f5d05", + "adminx/modules/Catalog/view/_context-links.twig": "fa079e6dbc48c87a901dcb9272b00728b0697884c6607ba93c36fc56bef2ea5f", + "adminx/modules/Catalog/view/_product-role-preview.twig": "9c3b5ce52dc23fe5af3acf682ee1470a07bef1bd8cee8cca946fb36745a3f026", + "adminx/modules/Catalog/view/_product-roles.twig": "905b2ed079e74e481e0ae7aa9782cc2d339dda75109be097b3994aef85a9571a", + "adminx/modules/Catalog/view/_product-selection-preview.twig": "9942891a9991a28f99072ef66ea7a8910ff3548972c2e601510e5abda250fba4", + "adminx/modules/Catalog/view/edit.twig": "776e68b63576fc389487d4f03467df6a259f3903d2bd2b59ac7017e18b8b5cac", "adminx/modules/Catalog/view/index.twig": "754c7c4dc81176e8c99b3d9e8c1fda2f62f03df341c3df1f7393f72d43c6f8e2", "adminx/modules/Console/Controller.php": "af0309a50d7f4366279be8ea12959cf7718a3c68b895a65ec5846554fd6a4857", "adminx/modules/Console/Model.php": "53aad480b8d4e5d68c4e9345d219ea5cbff6d04eee7ce4aa15deab6f00dac1cc", - "adminx/modules/Console/Runner.php": "bd65deeeaea97f4a7ee47826148775e9488a2edac80f5f8640ada6ac96f6b26b", + "adminx/modules/Console/Runner.php": "6c8c216003d8228b091683acaae99791f612a93d4150542c2ff52c67dd029a33", "adminx/modules/Console/assets/console.css": "e2645bf876bfeafc15e6f4e5ad9889adb773aea35ddc42ff151d8ed1f8866eef", "adminx/modules/Console/assets/console.js": "f0f2cec892c4b5cdeb11730bf42e9cf68ff779974338086cdf92618ebcacdb10", "adminx/modules/Console/language/en/client.xml": "02bef78a30b83340eb8968297639b6283dc642236e853bef413e5b7d2ef70315", @@ -212,12 +225,12 @@ "adminx/modules/Console/language/ru/interface.xml": "55f68a6bb3fea3fbaf953482b36593a1f3e585d3a99b6e2da30d6c097915ed08", "adminx/modules/Console/language/ru/runtime.xml": "441ebbfbd7f0e21cef5d298ec5fe314d3236cddd57e05f05fe60eb1ac3d1b8b0", "adminx/modules/Console/migrations/001_create_console_snippets.sql": "70272e04d744eca3d48416d9ffc83d78ad00a02d2a6f68bc388618e350c427e5", - "adminx/modules/Console/module.php": "cb95fd418b5f7970dea53d5bcdd0478bb0560c8b2688a95d5492ab053f324de8", - "adminx/modules/Console/view/index.twig": "c64fe9632db751357ed98cf2ddca524001e71200297e1b50f0d5c827483933af", - "adminx/modules/Customers/Controller.php": "8f64a15c6bb87eb6ef3535bac2c5364a3bb64ad6d1575eb0ae82c56a0bd04462", - "adminx/modules/Customers/CustomerCenter.php": "02eca82e2d1e2d7998fe6203458ee535421913197e76fb28a6e674231fb989fd", - "adminx/modules/Customers/GlobalSearchProvider.php": "6be85ba385ade05cef1167a6acf146a7e078d176b72db773e0d26d2ab8ea65de", - "adminx/modules/Customers/Model.php": "fb84df79d1ef9b403f16e44100325d26db5c144b6cbc6a69e3425a9a3f29a043", + "adminx/modules/Console/module.php": "c6c883cc51ee1a27460f71eca88edd44726f2be493934b84adeee1b7d8ccf8dc", + "adminx/modules/Console/view/index.twig": "54f877d5feea19bc4f4b618eea7cf7523a2266bb2e3909f3fb723edd16d4a326", + "adminx/modules/Customers/Controller.php": "0d9b8dfaf794c378ef8da5ddf2084e461b54ce0ed7eb490bda62c30574f05763", + "adminx/modules/Customers/CustomerCenter.php": "af34954e6330bc9036c04fb56b439b143f591e95b83124d95a034d87d6870bdf", + "adminx/modules/Customers/GlobalSearchProvider.php": "d76002b2baefa8395d2da3e603b547938b9ead8e692775562cc7d7a65e9601fa", + "adminx/modules/Customers/Model.php": "f9ec0574ae68a3cdc120cd5be9444067e205d650608f64e071f199190c876792", "adminx/modules/Customers/assets/customers.css": "2542f8ba075ce20f4fa48a966eda5de82b5e574a17f5bebc88727b608eb47bb8", "adminx/modules/Customers/assets/customers.js": "7ef3b863eff78c83bce7d78a7cdd67fc715d26fe8d9d9239af8d43f27bbb45c9", "adminx/modules/Customers/language/en/center.xml": "0ebf643e8a2e00fdf07a290adfd108d0d7e53781d15c37f44d55564c7476b738", @@ -235,10 +248,10 @@ "adminx/modules/Customers/migrations/005_checkout_registration.sql": "4e5ac6d4d5868f9e1c91b896fc03ad0982d5716287101e721a3820d21abe1744", "adminx/modules/Customers/migrations/006_phone_identity.php": "97e8aa457fd8f8ade781a03a98bdc7ee03cac5734782f9e72fb0b76bdbd2cdbb", "adminx/modules/Customers/migrations/007_customer_center.php": "7adde1ce2b92f24b7f1d1d483aca4304bad918d06ae2f213226eca96dcff344e", - "adminx/modules/Customers/module.php": "ef1055dbaf8d23748b492f0fcd8a7ce184b534d72714744d684875735f4b7f73", + "adminx/modules/Customers/module.php": "85424c3ed22c4def6b79af1454ce79ceae4987fd0212b53451e4d95f43af354c", "adminx/modules/Customers/view/customer-center-detail.twig": "2239602955041c4eebe704c70e835fd45272f721abd28bf254904901bd77a4eb", "adminx/modules/Customers/view/customer-center.twig": "223e657861275fbfe482ce624a6216a91e8df893cd1c5fe58a2999606468addf", - "adminx/modules/Customers/view/index.twig": "61b25d07799e50b3a06c5ab91b4895dd685b54a29085f896f9c51aebbfdb3d7b", + "adminx/modules/Customers/view/index.twig": "20868f4edf7f9ca7770a733d4593eb56ffb225199940212d60aa709f2c760070", "adminx/modules/Dashboard/Controller.php": "7e6ad0610df7581bb010cee574d8d093c4737af439bd2b0763142f50ddee1c54", "adminx/modules/Dashboard/Widgets.php": "56fc7ba7c1f9e30d7eae8c61524b6441b1c706a2cd56b1db883bb8bff7186eed", "adminx/modules/Dashboard/assets/dashboard.css": "8e28b56cac2e55d9b57304ea6e265fd94a52400674c082fbdd78f1eed1872424", @@ -283,23 +296,23 @@ "adminx/modules/Directories/migrations/001_create_directories.sql": "d1f84fccf75892fc1dcfe6b770112101e233d96d9f4866a08d1977646a690530", "adminx/modules/Directories/module.php": "f6b7f0bfb96d1c2c7aa52c51e60abaa09cdba7a4309d8363394a3a02a8df96a0", "adminx/modules/Directories/view/index.twig": "0bde23bc578b9ffad3c47534c304c0016b661a36588d6d3c9ffb92e2daa388a0", - "adminx/modules/Documents/BulkEditor.php": "6ca33e4ee73f83e043b13363bddb391199a1da902e74dfa7709a10ac32be574b", + "adminx/modules/Documents/BulkEditor.php": "24a2c33a50e5025e4f8a15426bf8d4f6ac21e6574ef636f30fb5c6fad0249133", "adminx/modules/Documents/Controller.php": "283aec04707c7b7fdd01e2ba69c1961b925f259655e9a455889bdd5dfc66f4fe", - "adminx/modules/Documents/DocumentHookRunner.php": "7e0cd24ed5791b6d5010e4a05351b709be09b6df1e700a3ff06685b9078b2b1d", + "adminx/modules/Documents/DocumentHookRunner.php": "eb915de91391858987386925da37d943318fea1f46893de1b3fcc0957ce43e07", "adminx/modules/Documents/DocumentMediaReplacement.php": "e97d960c852162e655a4389cc5f477f0178597da50e733b00bdf5bcab076c65d", "adminx/modules/Documents/EditConflict.php": "ebc3ebb92c3fc3ccf3909d2ff517014fc888ee20576a520b4979f68bde632b97", - "adminx/modules/Documents/Model.php": "5287c9adc603c942d1ffae135df360717ee18b1d1b2b9c29bb35d183b477b54a", - "adminx/modules/Documents/Revisions.php": "8caef88f7456000b001757cff13393ae36991b33c4c3070c27f3eaa4e8bf6492", - "adminx/modules/Documents/assets/documents.css": "396d4cc7625b3331a41d94f040ac3fabafd9c0b993181b490ceea8f365e1a5ad", - "adminx/modules/Documents/assets/documents.js": "dce151189d8244be41fb6b1bbb85a26241c788d91111ccb5314c361150024c3d", + "adminx/modules/Documents/Model.php": "ff03bcfe54618d1b83a6780c57cb86816ea25879351f1beb4e847cbb1ae4c9a5", + "adminx/modules/Documents/Revisions.php": "f17f0f7ebb4fc3ccc44695e4a1ca7093dfa3f4fe3f9c895e17fa8631567769fe", + "adminx/modules/Documents/assets/documents.css": "88c0911854e90ed0d71b005ec95560520ba8db2f44c8e3a457c074a3b5aa323d", + "adminx/modules/Documents/assets/documents.js": "9d0abe758f84ed6fe4f262c335628ffc07678a012b579aacb5787a250cac8065", "adminx/modules/Documents/assets/redirects.js": "95ca395c6b4fd3814da37450c27da351e75f509e26421c975dd8e0ee5f657efd", "adminx/modules/Documents/language/en/bulk-editor.xml": "b7af92bd151c7899a56cc461f4a09edaa842213cb6e6b4b4363af31aee228e50", - "adminx/modules/Documents/language/en/client.xml": "9ccb6f2aa9a83862abff2c1fa1c6b10760600c3202293fd7e42073c9cfb5fae4", + "adminx/modules/Documents/language/en/client.xml": "a139276798beac506ad56539ca0ead06e7cd7dc69149c99e04d6cac12d72bde6", "adminx/modules/Documents/language/en/interface.xml": "87b858717419a48ed12e1181183b8de0285483df4c9de37403730f8a7c034b52", "adminx/modules/Documents/language/en/runtime.xml": "7009fa27d67ab168fecd9d9bf182216dfb0397d9c006eb6563ee3c464daa5b5e", "adminx/modules/Documents/language/en/zz-interface.xml": "839ea37d585ff10d7b819a4fcd4bda876c94d51019489cc8d35af15fbc905f2b", "adminx/modules/Documents/language/ru/bulk-editor.xml": "29500cbda63189ed9a0afdb57b20f998241594b254d0225a776a8be32d108834", - "adminx/modules/Documents/language/ru/client.xml": "3e14cb51de149aca6c3f309ffb3973a47f80f638805325ff0a5cc23dd1025302", + "adminx/modules/Documents/language/ru/client.xml": "e265a5a3c8f6bf973dbde8f3f8970ab9cc88557cd71df6449f3b4a93dbbaffec", "adminx/modules/Documents/language/ru/interface.xml": "f8d60edba8ac5b6d0eb6deaa3b9b89733cd0b8d3eb951c105a9874ea6769c305", "adminx/modules/Documents/language/ru/runtime.xml": "6c3e9a2dbf862bef5a76ff0bb600084fcd9d5d1a7f0cbfd02377242b74fcd759", "adminx/modules/Documents/language/ru/zz-interface.xml": "2d819845ff2ae5246203374a685f8a5ad881ba9067166161eb96d5c9271026dd", @@ -319,18 +332,19 @@ "adminx/modules/Documents/migrations/014_document_relation_edges.sql": "3491ade2987535f8db1b3b00ff27f5a2062b02a3a1cb2e707d2752ff446b566c", "adminx/modules/Documents/migrations/015_document_creation_presets.sql": "a123f8867ab6f6346002d08317acac37062c2792913ad14102c81812d45571bd", "adminx/modules/Documents/migrations/016_reconcile_document_content_columns.php": "06671bb26bf29479dce24888fb3712c0d20e2aac5b3d70196604ce4097c875fc", - "adminx/modules/Documents/module.php": "dc7642c19f3fabda2ce0577dbf2002de389ee2f5616c26a6f6cdd88c639f91c9", + "adminx/modules/Documents/migrations/017_document_public_visibility.sql": "2a8298ade4af686a4aba41bcda0aaff5504c40f3140bf3ae710e8be727f38e18", + "adminx/modules/Documents/module.php": "81a2c35cb77fc142972a40d33e3fba3f712a6af886e3b4538246f1058f8ec38b", "adminx/modules/Documents/view/_product_panels.twig": "d0e681a94f6e48c061dbc61cc47c384bd52c4eb66d648e36784b68d0c8ada603", "adminx/modules/Documents/view/api.twig": "a035a2d566298587ce8530c98915e80e4e7f9b0f41a60e0e55d4547b996a6c40", - "adminx/modules/Documents/view/bulk-editor.twig": "27d9baca8cf591dc2ee81b26fda3efc9017c6809c1d23168a749d01bddfebdfa", - "adminx/modules/Documents/view/edit.twig": "94eb351309fb3d6fdab844e253ab2d7e3b03e1d846b35da19b7e498733ad27a2", - "adminx/modules/Documents/view/index.twig": "8bb67e66a55007fda1374d83a3bc0b19fdc518a47d191a04913772d8c8ce132c", + "adminx/modules/Documents/view/bulk-editor.twig": "270d6a2e52aa6aa8efed73b4b5c311e8e8f7d8e0cf82ef1e510e7e6fb4d3f029", + "adminx/modules/Documents/view/edit.twig": "cb4b983e868912bd3d072e3cbc6b57f21616e7d35cabacec7dca8b9e40ce09ef", + "adminx/modules/Documents/view/index.twig": "ccf2bd0d384091f8a98ead4e93a8ad389a515176d17e5b5ee661e7e3ea0908de", "adminx/modules/Documents/view/redirects.twig": "4a4903edc81e4770220852d9be9f02a983ddde6f829a4360ea01d3b68456582b", "adminx/modules/Documents/view/views.twig": "821f19085ba3997b6ef8f005e838f6fd449f0b32bce0d2804271311856cce8f0", "adminx/modules/Events/Controller.php": "c57fc25e815c47c14626371d044079e6711f0a4bce2004d2b9256db62f0d0e85", "adminx/modules/Events/Model.php": "a3a09370aa47766c7c4e70ec002af98a13bfcad3151b9ecb96a6ffb6cddf7ae2", - "adminx/modules/Events/assets/events.css": "91457d384056a6ea96bdc6bf012c033c19e0cb21401460ae9b0f9398493258fc", - "adminx/modules/Events/assets/events.js": "d4c6cc636473ecd81bd1551c37df5b96434e7e35fbb251f93b3647a25f52d336", + "adminx/modules/Events/assets/events.css": "775a6dd0099f2846319d5d7d8f742d2a637e59d9b0cac86c0203f41b9684f63b", + "adminx/modules/Events/assets/events.js": "a9b8fe56cced63eb6778c883e1339a8e3615d7b5a717bd44528f15f43e1716e5", "adminx/modules/Events/language/en/client.xml": "ea0be743665d923eed666b97590a63ad078caa2d439f18ccf78c22f040ab9e24", "adminx/modules/Events/language/en/interface.xml": "8ba72b194174cff23b1678a34930d0fcc8fbb51465a6e0457cd503c2d0119dc1", "adminx/modules/Events/language/en/runtime.xml": "e6d14b92215da7d85bc8460f2e42410c0005f51c1245533b9d57c62fc211a457", @@ -338,8 +352,8 @@ "adminx/modules/Events/language/ru/interface.xml": "701b5862e628fd5e4d64ac4df5983d1f40b8aa1b2fc67b4eccde0a1143338e27", "adminx/modules/Events/language/ru/runtime.xml": "339a6e6c117e3e14e871acd38ab398ec4f54f0bee3785a9c3f5d90048899a9ec", "adminx/modules/Events/migrations/001_materialize_event_logs.sql": "59500ca428021c9dce543ff9a7711e2b729f7e9566ec6277be09939e78b9ddd0", - "adminx/modules/Events/module.php": "d506c314dfc71abe7216ec80243f46549a833a6cc3371333d1e7a6435312c46b", - "adminx/modules/Events/view/index.twig": "7057cee5abe6ace177509c54c2d1736ab88b96f9f17f6db2bec0553b4eb1ac5f", + "adminx/modules/Events/module.php": "6958762304e3d31b51fb68a670b87509a23242cd2c4c9f9cfe6ec6cccabde5d8", + "adminx/modules/Events/view/index.twig": "077d09060c5f1ff8abefb4bfd1532f0bee28fed275bd5aa7b06dc7077a142286", "adminx/modules/GlobalSearch/Controller.php": "6a109bbe2ca833df90138af056cb6cc32c313677d3bda071b3638448ca7e33fa", "adminx/modules/GlobalSearch/Model.php": "b81c82cc43c6f90e411645b3f934b42d7457eaa7eea1f5afd7c8efa362f82d97", "adminx/modules/GlobalSearch/language/en/interface.xml": "a025b7a82786864c5d74bea07880e3c7b3f0cd7e52d8142ea5be8db346ae1a3e", @@ -351,11 +365,11 @@ "adminx/modules/Groups/Setup.php": "b7140a87474e343455fa81809f8f8f2dfe7e1ee3640c5f024a1bb06ebd783865", "adminx/modules/Groups/assets/groups.css": "403f4555599cf1637ec2b5a2e6248a1fdde7b1bf599646c1c103e816fc3fa85f", "adminx/modules/Groups/assets/groups.js": "235a058028abd1d9559b915f8a36da9333bd7b381b0113f9c98b28305c593188", - "adminx/modules/Groups/language/en/client.xml": "dbceb0c80a844d05f3c370dbc495a71142818c0c9e3ff94c33586bea89f07831", + "adminx/modules/Groups/language/en/client.xml": "e633c67dddf6f8d1d07d8f4bff18d0b0a527dc074073a5b79d033cf10f24aa98", "adminx/modules/Groups/language/en/interface.xml": "ca7dfd681c7f1e8580914cafdfd259d5d3be84868e7ee97288fa36ab3f785de7", "adminx/modules/Groups/language/en/runtime.xml": "7ab07b963b2b29f046f6c363f04c1104de6d5914f5e70b6b8cd42f5770847168", "adminx/modules/Groups/language/en/simulator.xml": "de91b32f039df2dd87ecd47e27725a6dffe018867a53a72df2d21e3394e63146", - "adminx/modules/Groups/language/ru/client.xml": "320b9b55d6eaf6f4d3643f3fa4675544a779540ec34b3b94e03deac6f57579b0", + "adminx/modules/Groups/language/ru/client.xml": "f19cd9563c2f5cc46bab9468cce38e316d17d670e9f9e98817f593696eb3f8b8", "adminx/modules/Groups/language/ru/interface.xml": "6f9da63ae65ecc8890ea16dbe7e879bdf5520d5f74c0a6204285b19d580f15e3", "adminx/modules/Groups/language/ru/runtime.xml": "eea7d7d2b88befb79b50d00c89dc63d0bdfa95883483d9606ce0605c03c92402", "adminx/modules/Groups/language/ru/simulator.xml": "14d8db8fa0e8a71206ad93817fbf2fac71cc023003977e03ffb2b7e581d16659", @@ -398,7 +412,7 @@ "adminx/modules/Modules/Controller.php": "de3c800b32a05ab41dac31d4964242fb0635aef6ef32f69a332509159b8e755d", "adminx/modules/Modules/ModuleArchiveInstaller.php": "22f015a6695de9f4fea12a580adfb76837e59ee4e9de92c153782c5a40a97560", "adminx/modules/Modules/ModuleRepository.php": "29ced1e9d5c36dab4f0018c10fab1b80282b8d225805bf13f075e148016c45f9", - "adminx/modules/Modules/NativeModuleAdapter.php": "b753875997fcabd019e4dc7420f5b43f3b75b6c673c24ee98462e6809f00b665", + "adminx/modules/Modules/NativeModuleAdapter.php": "0bd4f968dc72afcf6342628901f0ee132127f5d4f945a5aea57284a133486248", "adminx/modules/Modules/NotificationProvider.php": "55571216fe84feca3484b06937f988748d00ec9bb68b45d0e85d16a3c3836b25", "adminx/modules/Modules/assets/modules.css": "ba3f1f6721dd1e529903254abe8fd24e20873b6c00e0a2d2d2d7bcc65170d884", "adminx/modules/Modules/assets/modules.js": "7ed1e03f0114ff942b4db057ca57586c2cc94ed55a2d10f5ca5dafa908acabb5", @@ -408,17 +422,17 @@ "adminx/modules/Modules/language/ru/client.xml": "176d4ebdae73e7533082676c2e56199d47a43336ace47fbeb31392adad26d212", "adminx/modules/Modules/language/ru/interface.xml": "ee33c8914bb44478a8801ae23522954c860a6ecb007b848bb956fc07d5be4ce7", "adminx/modules/Modules/language/ru/runtime.xml": "271e31a8ae1586ed045861933a6db150427c78be7560942185633f35cf90686a", - "adminx/modules/Modules/module.php": "aad8c5f3a11dec21eb14a5d9a83e30c88cb4ea1206e78bc758804d015a828456", + "adminx/modules/Modules/module.php": "710e4441a369d89c29e99a68f94b591dadb2b3df1d951ca6dcd16385b428941c", "adminx/modules/Modules/view/index.twig": "2ca402c0886f611129e884129f6e0b0022e14a8c385d5d4ad55abe49ee39f5c2", "adminx/modules/Navigation/Controller.php": "968da27af968f5f6e29b0e015f4c5df438c17735daf646a9d6343c055acfd48c", "adminx/modules/Navigation/Model.php": "58d08733e246f92abc02d9ccfa74b88627970588fd2223688a59d03a4d6c4b87", "adminx/modules/Navigation/Revisions.php": "165a2e2d8ad218c1437fffbed9297b7be25d0ae6567f0d72aa78da872cefa1b8", "adminx/modules/Navigation/assets/navigation.css": "e7ad029b253b19546ed2c0eb2a3527dbb3b03a56db13c6f3089c3da9c12933ca", "adminx/modules/Navigation/assets/navigation.js": "72d68867419fab1b3e7e822a42a7fbcd866ccdbef8fa3051a245e23f23f5ec30", - "adminx/modules/Navigation/language/en/client.xml": "9a0ad0248941cd6377ada049666387581bea0dd76353d34779b4c1343b74ba50", + "adminx/modules/Navigation/language/en/client.xml": "251de8adc3c264bd01b816ba8f9ff18d9266ccc061c0a996755a039782c4141b", "adminx/modules/Navigation/language/en/interface.xml": "2a6bfcf0fdfc36fd87d666842008635dc12b73c3038fa486e45a4d117956a53e", "adminx/modules/Navigation/language/en/runtime.xml": "341b984ede453a135b6e155fd29c08d9d5a2ab16e62046379f982d9a56dc4a81", - "adminx/modules/Navigation/language/ru/client.xml": "46f18714dea8e751f33baa049f3684d824109e49e23b1b325ef37c8a043f1c7e", + "adminx/modules/Navigation/language/ru/client.xml": "0cefd219fe8f8f3726a07e8e82c5d1a4fd07244f79c1b6d2ce16c12ed7962f9d", "adminx/modules/Navigation/language/ru/interface.xml": "6b460827a6f170057ca6c4310d747a73f67d8468eb98c12cf4238a54ab6c6918", "adminx/modules/Navigation/language/ru/runtime.xml": "45d994c39fa7fa092525ed4a61d6775008f3993f4cdcdd615d61e5f15a749cff", "adminx/modules/Navigation/migrations/001_create_navigation_tables.sql": "43c937b8d7d7f5b70569c591db58ee8592386b988e8cf148fae723b1f68b3012", @@ -432,14 +446,14 @@ "adminx/modules/PublicSite/PublicViewTemplates.php": "d1d7f7bff22703500f17807d5fe6b02d42a2fcc4cb6d7e7224fe0a47f7dc1ffa", "adminx/modules/PublicSite/TemplateMap.php": "22bc2649771e972ea23f2387c6bd9abb02ea3ee89054f38e5492e9401b400959", "adminx/modules/PublicSite/assets/presentations.css": "fddb0b4300129f7d7c33853b7e31bce131eb6f867e577443429dcafac8cc67d4", - "adminx/modules/PublicSite/assets/presentations.js": "3a74e7c7ae3269b98eca52d2d61db1f4f21871c6f4a5425cc7bab58d24efd963", + "adminx/modules/PublicSite/assets/presentations.js": "0e9f9c392360773f1220cd417d303e23a60e643092d60ea46563029543f30b0f", "adminx/modules/PublicSite/assets/public-site.css": "b61c2c699e85ef1ae36e306d83d7d9508bde41702f06037a7ec8c6e69744cb67", "adminx/modules/PublicSite/language/en/client.xml": "5a93fd6babc20670e54795211cf44ed155417aa933e5d4a50be21aebfa7bd510", "adminx/modules/PublicSite/language/en/interface.xml": "e5e868af542e9fe6c981dff18e31a0050d2d7ae2b8b217e7b71bd98b496b5f5b", "adminx/modules/PublicSite/language/ru/client.xml": "f49060544db65a516b2a643420871571b4da5911ae800075c01ae35e55a5d639", "adminx/modules/PublicSite/language/ru/interface.xml": "67a7d1a651d8dd4ca3edc6c281d68c15146a8e84cb18833f9f4041fbbd57a619", "adminx/modules/PublicSite/migrations/001_create_presentations.sql": "023d97f077af57107549dd9e420f5ed55a278fb8318caeb8d86f2dc7750dbfbb", - "adminx/modules/PublicSite/module.php": "46694327d2ac6315da3f9ae02b87a038cfea4da4314ef30c1bc451efe6b279b7", + "adminx/modules/PublicSite/module.php": "607b8329ba3cf5dabce53fcb3b0ac56e6cd3f1412a41214d1e410c4b6a99dc5d", "adminx/modules/PublicSite/view/_placement_pagination.twig": "b10413bdba07695500803625486b56ae5283c5a0a6dff83c668c8f93d1eab182", "adminx/modules/PublicSite/view/_tabs.twig": "92db2936b265d3b07056ea3aea8ab9dc30e05849b6b3f7d04bf86231fc273643", "adminx/modules/PublicSite/view/diagnostics.twig": "242682325bd7c99c75f9694d0e7ff6217124bf093364c027d13f609db660f4b7", @@ -453,10 +467,8 @@ "adminx/modules/Requests/Revisions.php": "78483f6667f25c050cbb2d224baa8ccf8d977b30eb84538c3cfb18c7af764bdc", "adminx/modules/Requests/assets/requests.css": "bf29e90ce2211bbd4ce3b61b29bd3e60d5e5d8f75c9e4ffd99402cb000f27685", "adminx/modules/Requests/assets/requests.js": "da981f0941d3008ceb4042540ecbfde1385431395f2f56eb94836b4139feaa0e", - "adminx/modules/Requests/language/en/client.xml": "544406edaf2b568062e697f7264b5c5fbe3fc45c27e019615601e831612db0bc", "adminx/modules/Requests/language/en/interface.xml": "766190d3f850f6fc414a4646d786f79f394ee8f03524b3a2ae820fbe792d4602", "adminx/modules/Requests/language/en/runtime.xml": "8f618793f78ea2941557d9eb9c253f8b12ac6b60b76e7158801461b30dbfea28", - "adminx/modules/Requests/language/ru/client.xml": "9dc1d047bd12190cce6197b20e4bc4b3a1597c387873ffd99d59710d7bbe60d9", "adminx/modules/Requests/language/ru/interface.xml": "2a5e72c60d70a0a7e7243dba02bab846807269da888a9468976719987b50284c", "adminx/modules/Requests/language/ru/runtime.xml": "6e5b2487c9b3aeefebcf1c93e4458b0f902d1a334117d935dd9a0631d3c4547e", "adminx/modules/Requests/migrations/001_native_database_calls.sql": "cdab530043491203e61b5e5ed325da707e99797d3737019b27f36f0495639344", @@ -490,7 +502,7 @@ "adminx/modules/Rubrics/FieldEditors/RelationFieldEditor.php": "9504549a3b81893ec8a114a286a960450bbd302cb2f8f9af97660c8332f91925", "adminx/modules/Rubrics/FieldEditors/SimpleFieldEditor.php": "e961e7ab2bf3a16d6d9131513f11d2d4b9520ecee5572ae7e1b7ae8475d1cacb", "adminx/modules/Rubrics/FieldTypes.php": "c6cc64b0e1b1fb29b65275d337e5747b9f9f68ca46171e7e7f07129cd27b8409", - "adminx/modules/Rubrics/Model.php": "21f19765b1ca8e4ca7bd43fa8bd9737e26008cc152e77737215bbcc0572855c7", + "adminx/modules/Rubrics/Model.php": "85aac47a064c1d2d95cc3e620e106c03989ebe9cf237cb4516253c338e2f49a9", "adminx/modules/Rubrics/RubricFieldPresets.php": "80109bd9f066f077ad19e48d7c7599e4dbf7b1d0c06c54db9fc921992bca2e77", "adminx/modules/Rubrics/RubricFieldSetLinks.php": "41cd27e5947680eb184dafba5fd95f707ccad0c20fab2ca948cf882ada0adefe", "adminx/modules/Rubrics/RubricRevisions.php": "83d3659ae0b1d2ce75fe02b0ebfc11d04857556ff352dc2b5408a1481291c658", @@ -499,7 +511,7 @@ "adminx/modules/Rubrics/assets/field-sets.css": "fe5253279caa882f9623b058c92ab461c476c080baf2399425e522a36b75336c", "adminx/modules/Rubrics/assets/field-sets.js": "efbb011014f756dd59ac1357441d06c9b69b278a9d934f7cd9d2d1d7398cd313", "adminx/modules/Rubrics/assets/rubrics.css": "72898787697fd0e26c61105c6f1fe5b509d731234e60b438742e30cda8fd6cc9", - "adminx/modules/Rubrics/assets/rubrics.js": "d0384f3fedd7a765a5d0006a1ca5080a9cc37fade556e95c2561927d197ea2ca", + "adminx/modules/Rubrics/assets/rubrics.js": "2d85e91a7426c17e2f79f9f71dbbe37318c9fb5f020be5bf3a7e53062c1e3dc0", "adminx/modules/Rubrics/language/en/interface.xml": "7edfdc6fe447448dbed987bfbfb8244557b2a7fab89a2ff3dd1e29eca57a6033", "adminx/modules/Rubrics/language/en/runtime.xml": "4cad3d97c6a2695d038286cedc2c4509230c3bc9fa789dba2da9db89e8d9c93d", "adminx/modules/Rubrics/language/ru/interface.xml": "7df4b3f148b367ca3185174c0feb3129f7ce28f99791a2f49de256d78bae7730", @@ -520,24 +532,26 @@ "adminx/modules/Rubrics/migrations/014_rubric_purpose.php": "2913a1a7bb6f4c5665ad05704dcbbde78c7ab6b75b0ad7643fd00e482af69411", "adminx/modules/Rubrics/migrations/015_merge_directory_permissions.php": "648b3e232e9426f84d69238295515342e42695e46288b901950cde2e8979b0e6", "adminx/modules/Rubrics/migrations/016_rubric_trash.sql": "09385e8d14d5d4d977db59c60035338404019783cc44c6c784cc2172faab08f0", - "adminx/modules/Rubrics/module.php": "ae3e957e9c3e099e66fcd14d9ddcf76a172207ff68d77d6279ca7a4c15c37c36", + "adminx/modules/Rubrics/migrations/017_rubric_public_visibility.sql": "2a6ef9686235b453807701c24d1cd8dc81ddfdbb80824a7ff10a70ca3f949827", + "adminx/modules/Rubrics/module.php": "a7ebe05a45eb4270f057f2d59f717b2f06df0350a0e86041f2319c70895b9a5b", "adminx/modules/Rubrics/view/_content_tabs.twig": "e4c0a9b899b5085aeaec2e1793b8eeac4a7619959fffc7ce39151af1250e0d9f", "adminx/modules/Rubrics/view/field-sets.twig": "911dd7cc9546411e1b29724466328c7d2db8db630b2b52d33e8443a066b16810", - "adminx/modules/Rubrics/view/index.twig": "4c01e7b020d16528b2a2df68e35bb2e4639efabe30adc4f19508d85701e788e3", - "adminx/modules/Security/Controller.php": "c7f2263f460057191402dacf8948b822f9b33bbacbe94e6e9ad871613147fc86", - "adminx/modules/Security/assets/security.css": "fd8c477d07c38450587153402380040b72fa9a92c60edd532c4b21046cfe375a", - "adminx/modules/Security/assets/security.js": "f270dd0b910b9aa4815680b8d80d66ffb31bc7862f74e628cc60efba4205a99b", - "adminx/modules/Security/language/en/client.xml": "f474d114b7a8dc3dd0b8438109e9ddb4dfb89717807c6733965f5b410270475c", + "adminx/modules/Rubrics/view/index.twig": "d25a68bbbde3bf2da618c465cc2ce6cdaa70b557388db6633c719e0d6deff163", + "adminx/modules/Security/Controller.php": "e5d785a8c907757c23785eac8d7ad90932424b9a6bdca86f197fdc6a2f6a6605", + "adminx/modules/Security/assets/security.css": "463dcdc2449fcbe569d5f189c95ea54bd0dd5d7bed08568f67f30fad1bcda6a3", + "adminx/modules/Security/assets/security.js": "a24fae3873ce9c41fca452981b9ecf60284eaefb0a5c1eba44ab4cbeefa71f5e", + "adminx/modules/Security/language/en/client.xml": "c43f9a474b5bc3d6a2c010416bb0dd74cfe70561db098c447a9fc81b905796f5", "adminx/modules/Security/language/en/interface.xml": "1a64dbe2ea97b26131f6d7237d9cc89d55cc236dbdfaeee125fe5ba359c35928", "adminx/modules/Security/language/en/runtime.xml": "968b9f0fe17b31d8ea7d3ef942aad19d123ae4abe19f111fc1efdd89249af7da", - "adminx/modules/Security/language/ru/client.xml": "77e5f3aeb0d673b31735840b5915bcd8508365f93bbb2337cfb5cc2fd06d6b94", + "adminx/modules/Security/language/ru/client.xml": "37088b5db55df21d618f241ee9106a1826ef5751b01123c808bfc4ee7c40136f", "adminx/modules/Security/language/ru/interface.xml": "934110ec57234db479a8764143250553a1b355d120fb4b7d9d866879e887ae61", "adminx/modules/Security/language/ru/runtime.xml": "cbeafc1cbc995206868fa3a3c3ee8d64d2dab21c1360c38ce6f69611d0d09a9e", "adminx/modules/Security/migrations/001_create_ip_blocks.sql": "832aff733e2c25a43164c60a601dba08c21fa29ac9e826568055dcdb65417a76", - "adminx/modules/Security/module.php": "557308dd9dbe114e5c7129e1cfbc06f25ded104e08c473513e2f388ba945198f", - "adminx/modules/Security/view/index.twig": "b70bb298a3e1aed377d31bc6dd6b7937cbdcb71cd7098113b8b026ba9dbfab84", + "adminx/modules/Security/migrations/002_create_user_agent_blocks.php": "1afce9594d058d8bc60532d09001c87213aa133eb9086624b80cff146bd53a62", + "adminx/modules/Security/module.php": "c8e42bac300bc4ae73cd7997a8d820bcd721291360d54bc28301caf5e142c1c6", + "adminx/modules/Security/view/index.twig": "3954407173cc5bedbf58b47dd527acc548e2138f838b4a9a2921e34cdd807c81", "adminx/modules/Settings/Constants.php": "5f8d15924c33f2e23e6f57f2934229e9c16333334f8ce3679793b35e5acbe0c2", - "adminx/modules/Settings/Controller.php": "90aff255d936753ab763ec534a097e9988f6c737bd5be2ba1194a7700a372e72", + "adminx/modules/Settings/Controller.php": "dadaa11752c61da85c099732491630e44c8a4dd6691e2ac4e91d996f3792d810", "adminx/modules/Settings/Model.php": "9e16004c78a03d39db1bdfd5e23b88b77e8afc7b8f57a2a21997602ace277440", "adminx/modules/Settings/ProductionDiagnostics.php": "b23d473a5f6e7c231c68abe4d48159b177699b5f0c23020e96252b240f86c87d", "adminx/modules/Settings/Schema.php": "7367374ace3e25fd86e5a829ebedb0358ba38bf5be0cfab9b859f1e5d3cf0525", @@ -556,7 +570,7 @@ "adminx/modules/Settings/migrations/005_order_system_navigation.php": "df82c4966f28ff3b7d9cf184013477c60a4ae635345f21809bd58b94310e8957", "adminx/modules/Settings/migrations/006_create_admin_saved_views.sql": "69cf1cc085a31b2310f82fe55ad6e0abbb6f14707715205e5a53f06334759a99", "adminx/modules/Settings/migrations/007_public_site_url.sql": "7aab89b5e7ea5f68b519322341c90d42b424b7f994d706f949aa326e571f6cbe", - "adminx/modules/Settings/module.php": "b5de078a2dd6e0b766ac0d5456f7fa60ded80741c29053e6689ee3e48b539593", + "adminx/modules/Settings/module.php": "5f9365f2bfb8d2b7553a8723b83a19936dfc51ad906c8f3e1e1b809ad1d1e913", "adminx/modules/Settings/view/index.twig": "7d64c286f8e580ae51b1529253fd816cdcabf8122a564fcc428ced98bd7b14d6", "adminx/modules/Templates/Controller.php": "00b1758860293b56b052a8c41f7174d0d97ba1d78e012b80f8e9f29194eb77d4", "adminx/modules/Templates/Model.php": "4f7a9082f462170533c0c140ef085f6a33111d20061b464864eb86066d50289f", @@ -603,17 +617,17 @@ "adminx/modules/Updates/language/ru/client.xml": "7dc664bbf759fe501c96a145dd6a052f2dd5541890a8aeb1d533fee378cde0f3", "adminx/modules/Updates/language/ru/interface.xml": "1d8a827ac996bf7da2d15f75b52d2205f0f36105cd73b50cebbbf1e99ea98bc7", "adminx/modules/Updates/language/ru/runtime.xml": "45d622ea8c6f772d5f53690bd7c08beb55efcbd51c7308f0336c995885b68258", - "adminx/modules/Updates/module.php": "13dcee85d0558141f4305c2b0dd37401706dc4cbf193564153a0eaecf5dbc681", + "adminx/modules/Updates/module.php": "d3507184cdf713263cbeb20c6ac8761c941ac021853dbb40f27a8a851e65cd3a", "adminx/modules/Updates/view/index.twig": "8d975051ca96fd99902714bd60b6a85131d6304578ea65a3dbf732523b638ece", "adminx/modules/Users/Controller.php": "dd8e11ec359064b37437057f0e1295dd1f20239186d5726119ee0764bce2fd10", "adminx/modules/Users/Model.php": "afd334340bbbeed9ac638421bc490262a21173007734740d2724d4ddc8cace63", "adminx/modules/Users/NotificationProvider.php": "460ab91a010e473937318542903a30053e08303b512b94ffe43559a85da8c57d", "adminx/modules/Users/assets/users.css": "1322f3db7bbfa13e548a8c019ac6580f947de5786c5f460435db2561dc7b1f28", "adminx/modules/Users/assets/users.js": "d7d2cdeda4a0bf569ef14eae2bdbb6feb593ea65951382cc2208052518fafd10", - "adminx/modules/Users/language/en/client.xml": "a84c325dcf979712bf3eb5cfe1cae2f2d4a9bf698702d003bfeb66954eaee159", + "adminx/modules/Users/language/en/client.xml": "4a8ba2493d2697518531b57fbe5a2bf9c304d1f114048a39539bc94e06a25d9e", "adminx/modules/Users/language/en/interface.xml": "4434abf4230c2a616004dbfcfd4d79f43ccc60f32030842d0eade5bc3bfc28eb", "adminx/modules/Users/language/en/runtime.xml": "2f4b8e5ae2ed591f03bc5d10e4e305b2404dee89f75ee4480f0daead10cc58f5", - "adminx/modules/Users/language/ru/client.xml": "64054dfdd9c2bd70084b283e776597c86b1899a603fb5a07935832ff5960a3ba", + "adminx/modules/Users/language/ru/client.xml": "7ba843fb0b54bb2245816291b83772bb404f4e47776e51ffad6ee4dac2a606d6", "adminx/modules/Users/language/ru/interface.xml": "9df72a9413ddbde9513a6ad9b5d04a0bd68a828fc328197bdd180a5a77d4b274", "adminx/modules/Users/language/ru/runtime.xml": "5bb9db970f2e2da5849e20272d700ec4c70de65443ab4bf10b960aea79195842", "adminx/modules/Users/migrations/001_normalize_public_session_activity.sql": "e72f1cacea2cff5f74f7baebe3cb1f154b87fd7368541b1a3a5d2300a0deae86", @@ -627,7 +641,7 @@ "adminx/view/_saved_views.twig": "51217212a3b0ef7437de4dd55a07fdf55aaa3138155b00f65fd85af686412da1", "adminx/view/auth.twig": "e44eb6337248470418be7015886e6e5d7bf5b0021fc917f4cc0dcd98c6492d85", "adminx/view/error.twig": "1187e6d0c325e0bbf6a7bd9836a5ef6d96cb8f72a0599050d3da7120683a6202", - "adminx/view/main.twig": "4aa00f5a2c50e3cc4d339c0ad9377526db6e8a65575843be59dfd20df5210344", + "adminx/view/main.twig": "6845837fe7c9adab56c611dcbdd713f910d65cbee0c7b62eff7ed7bcf571d6f5", "configs/db.config.example.php": "e42bdb47aa7d1bc9cd85cb36e05acf940cafd951d6220664c485d1b971f18809", "configs/index.php": "48e6b249e3d83530187c095ede0198af498c59f384bd458db74f9e0c2306eba9", "configs/public.config.php": "882ea6a03ee8df191be8ea68ba78d39ae600d918a0880ebdff158fc38f534aa6", @@ -650,7 +664,7 @@ "help/content/README.md": "273451d12b6360df56cc58323bfb8fd7521b4e3daf8f7ad93c874fe6877af048", "help/content/blocks.md": "1d55be2a9f1a48df426e5856a54307d77b352d0037d68f08ed7e16a322d9ef8c", "help/content/bulk-editor.md": "821107322b125c9344bd04346037fbd19f8ccd0ad6da822fe78af3299d01f746", - "help/content/catalog.md": "8ca86447bc45f8f14688d7f74521569b50d4176b9ec2ecca0bad25d91a0ef0b8", + "help/content/catalog.md": "12434610186c9d797270ee0bd802abe79b67e976a88a94dbb10f2ee1fb4dbdbf", "help/content/content-model-tools.md": "baaf738aec454b1855ea110b8f5e1ef3ceaaad51cb920d4551cd088daeab7e76", "help/content/content-studio.md": "91ef485e614a9a71a07d4e6c143377f7052ddf3b00d3f2f0d0a32258bdb14dbe", "help/content/directories.md": "ff6c5ea9bd7fe1fb9d0aa1875d03fc3bc00b2bd2b0ec83d6496cd55168db294c", @@ -699,7 +713,7 @@ "help/en/content/README.md": "7c871512fea7f1bf635d5a816e322161dedbd8c158d520cd0385467e8dadd1ff", "help/en/content/blocks.md": "34657f0a7ab977bf78d5b7645bf80a0755f40830bab4d4620ca7f39ea5992210", "help/en/content/bulk-editor.md": "c99d5eb5058d6714f0281b84bfdf9020fc63ef27519c095af5f92fa2d5e96b44", - "help/en/content/catalog.md": "8f12199f4d727e0ad715f91e980de638357fe1fa5af0702351fd3ee30afe29e1", + "help/en/content/catalog.md": "82769c6156f6dbe1bbe7cf5b3d0a8d4a55fc1be7389fe1e3945f0fe745200654", "help/en/content/content-model-tools.md": "fc50f8c334a129d3ead4a58503a7659abc2b1fdea7c9d277feff1e3ba4bb7b6b", "help/en/content/content-studio.md": "194fe41a1eefdf7da3c9a47b8539ed3ed9d785d2a75816a39da6d2784ec40925", "help/en/content/directories.md": "cfadf2361ead3bed9b63435041af8c91e24ec8b492906f0d9b976047edc123fb", @@ -780,7 +794,7 @@ "help/en/modules/benchmark.md": "e7b9d233abd386db41413f45c79639a3bebfc37241bdbab461028e87a5615f87", "help/en/modules/cache-warmup.md": "d92183948e03dd4f9a18308d36df7d5ba57e7e87f268b5f3f1de38ed99c518dc", "help/en/modules/comments.md": "55f45f748f32fbd36233c43574e212f3fa3fdd1ce8eb819f9d1cafb53215ed97", - "help/en/modules/commerce.md": "e31cbf7ead9d758401e35d7723f0189a354ce818a4fcf5a0118c73795088dcf0", + "help/en/modules/commerce.md": "e0a156c6ba634fe4c5beeae5eca0e0d592adddedbdab7e444da647609dbe01f7", "help/en/modules/commerceml.md": "610f6b2f9abbeeac604da0408192ae52dae7d6f3fb09e0d321e14d1cc190b808", "help/en/modules/contacts.md": "81c818e9cbcd1d61f34d9199219ea160a04bf88af110b32c3527c65fba65d39e", "help/en/modules/content-packages.md": "4b89df6d05fa75e484e38e404d53fa7ecc6b61aa6930fbf74cf9b6611bfbf532", @@ -879,7 +893,7 @@ "help/modules/benchmark.md": "6c31f16fc3f083f29c65398c38b78609a3d94da168d30a93825af8ee9650eb0d", "help/modules/cache-warmup.md": "56c05ec252b44a1a0d1c4b09efa90cf9d450b0b138787903c898f56b377b8749", "help/modules/comments.md": "04bcdea5aa7740e3749b51dcc9fc2954e98b22298d3e4fcdcc9865d94e1d0376", - "help/modules/commerce.md": "0255c08bdf10f7db8e530c15e987c5d137ee115434ca0a0d71a831cc9fbfad9f", + "help/modules/commerce.md": "d398d0dfc1edca5010d81ae660d412b4b1ed8d4df448fc78f237ad0ee2cc731a", "help/modules/commerceml.md": "60a0a5c541b560b5a6a4ed8213d9145caace68c15be534af13f5f817eb5251ba", "help/modules/contacts.md": "cb4284709f7b469164e3ef4c817893cdcaf4eba1b460164fb36f75a798bc457d", "help/modules/content-packages.md": "8dde0ebc897b7891a6afadb6491f4797bef26622918addab67f0f4ebfd719678", @@ -943,16 +957,16 @@ "setup/language/en.php": "963dd284ee244e6d006dede8522c1118dc0909974dcd24d477fd06b7e1965fa1", "setup/language/ru.php": "8522b13c4f147804538eaa857585296db811c736e1361e6b403daecea05f34f8", "setup/recovery.php": "359ae9b79508a382bdde175824b18a5bdb0fd69ddb7776c7d18ac9eb826d127a", - "setup/schema.sql": "f3a291d2dacebf1b1141254bd899bb522f20ff8c5c9ef5b76e04c324e0de6d73", + "setup/schema.sql": "4942144465f8854cce3c3e6f06bc9d2ebd11ad1239d4a086feda318a0ae70448", "storage/.htaccess": "ca761a92f2b6eb47feffcf200a8fd846e7fdd8abe065008d29beff0319ca3496", "system/App/Common/AdminAssets.php": "dcd58f19fda48263b2912a10dfdc24e3f12967683649c4f614a8d54c62d51b86", "system/App/Common/AdminLocation.php": "d2729b0d627ddbdeb7f0c4b110664ed949f373946f80929c51688391c64352de", "system/App/Common/ApiTokenRepository.php": "0795e1add3723708240949b7aeada364fbeadd74366884b57e9142efcfaf617c", - "system/App/Common/AuditLog.php": "5306d21085ff7ef7313f5798fbd416872825ff7d2831f94cd9744de010e1cc2b", + "system/App/Common/AuditLog.php": "9609b82e4d8b9ef2e1ea7268cc8b4636b1ff38080e63455777ea7d8329b9801f", "system/App/Common/Auth.php": "647421cae2ec8bbf273d687ac1fda134c7bcc28db4977e01f5896c79e7c2c83d", - "system/App/Common/Auth/IdentityLinker.php": "7da4d902405bb4ead2278e5bc3578cd0318422fe82318bec94257ed44d5ba98c", + "system/App/Common/Auth/IdentityLinker.php": "26c85deda5918b413f2ee4fc48ef00d5268aa6ef1081095b801b2428d61979f7", "system/App/Common/Auth/PublicUserNames.php": "0345d1f452e0a9accaa9b98eea71f27a428692a5b4f2ad225a10ad8bcf777684", - "system/App/Common/Auth/PublicUserProvider.php": "163449ae80d412a605fc60380e97107b8710d6e14d06baf042d9e63d35d75c29", + "system/App/Common/Auth/PublicUserProvider.php": "5d3690dcbc0e9cace7e09350d283cba541f15cf8a346c9bedcbd9f6cec58d65f", "system/App/Common/Cache.php": "bb322246cbfa12cbcdd67c16a24c55715ad67b31d356b4f18091c0aa1d00eb9a", "system/App/Common/CacheKey.php": "47400cd90d1880d061995bfea585b56210e18a870607d9b1cf7f0eeaccf15032", "system/App/Common/CompiledModuleRegistry.php": "9ab055fa759f4041944bb94bfc2e28361986a09b742210981928bd6ee15f758e", @@ -988,7 +1002,7 @@ "system/App/Common/FileCacheInvalidator.php": "24d34113c9909e3c56d74ea35ed61a1744c20f7fa7ecaef49761f2a28a774a18", "system/App/Common/Flash.php": "8fcdafb920b374a6f346c10a42e3f846003fccba97f4bb48fa7028aae2a0091b", "system/App/Common/HookCatalog.php": "52e151f1b41856436d13cb6fd6610ffdec264225a0ff7bd29761ad405ece92a8", - "system/App/Common/IpBlocker.php": "5536f4802233588671fa5aaace1c017ae8f52590b64213d5d9681a608ff3c3b9", + "system/App/Common/IpBlocker.php": "356afbf56805056a4e9ca3ccd614e4be1726995702b8659b5226104fcf497d2c", "system/App/Common/Language.php": "587672d76ade8b5e1b30e98796e3656a833515db4d09b44717da443e0fa7ac1f", "system/App/Common/Lifecycle.php": "4a59040f213232dfc9cc5c3a9bee491eab5916b65accb47c9972ecfb2573b68e", "system/App/Common/LifecycleEvent.php": "68a94bcf55a62dc88f0b464ae2ed13c4056f2722fa550cf31f4633f313c8e359", @@ -1004,8 +1018,8 @@ "system/App/Common/ModuleSettings.php": "b3280ad547e59bfcefb4a338339a36905fab5db1548345b0f00e257d275a6294", "system/App/Common/ModuleTagRegistry.php": "9e68b64303825a25ef4f71f31a7adedee9d2752e4d1d06162605da1dc510a6a5", "system/App/Common/Navigation.php": "1a1b0a3552d941dc0fc59c334ab705e86a73ee96ee42871dc958012bec1d81f6", - "system/App/Common/NotFoundLog.php": "3b1cdeefd05ae250e90d1a05dd9e1bc0ba2cb0c2cbb89d504fe009151f20452a", - "system/App/Common/OutboundHttpClient.php": "1be0216c2147b35a329e18d78911e7c5aa2cc225141b749ccba8c1bd9222a26d", + "system/App/Common/NotFoundLog.php": "f5630e839aee069c1122452da42f42ecae5d2082fe582dda7be6760059f3eb71", + "system/App/Common/OutboundHttpClient.php": "dc03f3093e70438ba92cd8d15ae690141186b6cf7916a863bd6f7de4c15be0bd", "system/App/Common/PackageModuleRuntime.php": "5ad492b9053bcea4d3bd4398b11fb2d976272e2036d379bdc24c19dd5fe7486f", "system/App/Common/Permission.php": "8e2241bca829de0104741dc2518860cd15489866dc310437e07aeab2bf0f0355", "system/App/Common/Proxy.php": "707564208ec8c24ac2ffb5bf41cba34f1b39059ce6bda31a69aaf4b116d864e2", @@ -1021,7 +1035,7 @@ "system/App/Common/Ref.php": "e5e55093871022e86a7c8e8841e2e150922a7454500668dfb6f30fd50aa6044b", "system/App/Common/ReferrerLog.php": "a2da447a548011c9ae6f8be2ef4aacac2a3d4c38b62a19d7ea5442589cb7febe", "system/App/Common/Registry.php": "7b4ddbb3f96a7ad54a8d43e151cb595e598d3620b368d21721927ce9d54e1ea3", - "system/App/Common/ResponseSecurityHeaders.php": "b159a7105d3c1fb3ffdefbbc5997741d7b0f83bfb26fabcac96f61b53969364f", + "system/App/Common/ResponseSecurityHeaders.php": "54d181e1ec49f328e63851d6d2d693d8318fdf82684efc8d3f4cd5d394c03e0d", "system/App/Common/Router.php": "4b0dd3fd9f732177070ce3326b691a8decfbe698aa0d8cf687a18f6c02b73d91", "system/App/Common/RuntimeConstantSchema.php": "e67a2de96a2496e0463a3b9e9d66fa6b2ef0e4b7dff768469ef472ba639797d7", "system/App/Common/RuntimeConstants.php": "decb1277dada75ee2b5c7d531f064db7ffecef5b3ad71d306111b2a45121d34f", @@ -1035,7 +1049,7 @@ "system/App/Common/SignedEventProof.php": "732c3b27f360dcbadcef405cac3d2cec5827e02cc789dc203001627750efcec8", "system/App/Common/SiteOrigin.php": "d0471ed37ff0c8e08b4ab9e12138e793996d6bfe0a467ab020e82e301c773d19", "system/App/Common/StoredPhpRuntime.php": "2bd4fa83c970cfa0b8002b11cf36bd0def7b37ea19c025f7c6b2f37f0de38286", - "system/App/Common/SystemTables.php": "85e95425b64d35bd8a3a5d4ddb14c1367ecbaa64ff6aaf4378055a1019895806", + "system/App/Common/SystemTables.php": "3c95b8902bb344f1f7fb50ab4ff355cb17791f4ac4a716b40eec0bb279fcfe74", "system/App/Common/Template.php": "5a0cacde5059fa9918321e6baa71a09a754a024055dedffae161486c51902f45", "system/App/Common/TemporaryDirectory.php": "4aad5e498b0ef5483427c18ec6eaaf9db64ffd1348a0ee3f445da080987e4bd1", "system/App/Common/TrafficAttribution.php": "cef0bce9cbd2ca3d8dd297cf1b969669ed1cb996cfa175841ecc6836ca7eb8e0", @@ -1056,7 +1070,7 @@ "system/App/Content/Documents/ContentCacheInvalidator.php": "d2bfd72d4df1a05e281b88543fabb8356ffc9799d37286c3767c6a0173ea91b1", "system/App/Content/Documents/DocumentAliasRegistry.php": "803ccfa70c3648e2e4c625aaa827597431d4ea54dbf83618b904f699f9560846", "system/App/Content/Documents/DocumentAliasTemplate.php": "47bbcead460d5d4160daea31ebfd36ecc3657f74f905162f20ee0d1eae78e3db", - "system/App/Content/Documents/DocumentCreationPresetRepository.php": "a58de0560e194d8386932a4e2e0e807a097ba1bad1f56cc8d11587f6c20e54c3", + "system/App/Content/Documents/DocumentCreationPresetRepository.php": "840ac8e512260323394ecf648f67d6c0aeace68810f989601f7532b3f7c9a708", "system/App/Content/Documents/DocumentExcerpt.php": "8c5742f976a492531af95f2291e8c824c3a9804a54a27997f9f51a716a57f19d", "system/App/Content/Documents/DocumentHookContext.php": "021d6e15aad1bcfb9168149189ff7cae13b1f88020a76c07d27f22ecd7d6b5a7", "system/App/Content/Documents/DocumentHookException.php": "a2158338b544e5ba49226efa3a80c52b4f60c88d1112a3ca1a72f50043abe0a0", @@ -1065,17 +1079,18 @@ "system/App/Content/Documents/DocumentMediaFinalization.php": "5ea218f79f835a4fc25da0199aab87137e620b60c31d8e2f587a1b911b3af6b1", "system/App/Content/Documents/DocumentMediaFinalizer.php": "785487028e221726c6bc15662f83ed985701a8e5d21e9fc9605e816ef4c835b7", "system/App/Content/Documents/DocumentMediaPath.php": "23b40c98d3cf9a73df3b7f7083b58b60dd80b8bda71e3e3a6354342cc72322a8", - "system/App/Content/Documents/DocumentMutationService.php": "868e1301cf822516debe1f441f80eb1897d0a1617f8d27a6a2a1dced8bafa6f1", + "system/App/Content/Documents/DocumentMutationService.php": "84a05df5bd0aa77e65896a4d74be4b1a83e3fd3719c758039218aa18018a2ca8", "system/App/Content/Documents/DocumentPickerRepository.php": "0e8efb5b42aadd74725213b8302b53b5147d7e0eb2e83d301a5f073bb0c13142", - "system/App/Content/Documents/DocumentRevisionPayload.php": "c7f83350de98b6d98531508eafecc4a2b5a0551c7f2ba77a9e5b954fd1287ea0", + "system/App/Content/Documents/DocumentRevisionPayload.php": "84cbd5fa39bd5b2beee18018e75361288a6db42920371f1d6aa37ab57cecc93c", "system/App/Content/Documents/DocumentRubricCodeRunner.php": "a9c04196b76ab91c7b1f8b53afbd45fc6727805c953fb5fe5ff36b69846d389f", "system/App/Content/Documents/DocumentSaveEvent.php": "ad30286d10b734e090732f84f0e6a77bff050acf653949140049418dda8663bf", "system/App/Content/Documents/DocumentSaveEvents.php": "79a9fa78819938f179bc24121992e7d44306c51612734d9ef10839b764a5d394", "system/App/Content/Documents/DocumentSaveRejected.php": "4ebdc1c5aa622f6389b107320a7786b8acde89438f6c56a660317deb623cec5f", "system/App/Content/Documents/DocumentSearch.php": "ba8d1c7a28878e3b7e2c96aff07c59c2ca9b5b5eabb4eda99de6362b4274d54e", - "system/App/Content/Documents/DocumentSnapshotBuilder.php": "1d9437433d721198fbd18c6cb141657da93c569fabba46bc59466981b68b5ee3", + "system/App/Content/Documents/DocumentSnapshotBuilder.php": "cb741de6e24d3ffae47d1dd1c5a202afa8454858ef8deddda1cbb1e66ae1fc34", "system/App/Content/Documents/DocumentSnapshotRepository.php": "54faed521c064f84cc4017e65ea0dba8b9bb34571240a90c083eb076c9a11b1a", "system/App/Content/Documents/DocumentSnapshotStore.php": "e61a7873204ba76b50d2ae7f087a59442cb93fa10ad5c3f0ac0087d941dd2075", + "system/App/Content/Documents/DocumentVisibility.php": "9a8b17058d47f40d4eb5e7c6a96f15d7f234c02b275ba1f6a46aadf58251324f", "system/App/Content/Documents/FieldTemplateManifest.php": "8eca7f9c9056ad5492b2b22cc55b407d8a65b32f5889775a5ecf6b4caae7ab8b", "system/App/Content/Documents/RubricSchemaBuilder.php": "a391211bbdbde83a6b8f4ce2ccf98bad2ad2f5d39f35edb5e262782729dd86f2", "system/App/Content/ExtensionTables.php": "7e59c929815afc570dbd6e0af754339ee94a1cb32b928edbc6d50cf34bcbe085", @@ -1161,7 +1176,7 @@ "system/App/Content/Packages/ContentPackageWriter.php": "dea33706a29e11d2a04ca6904bad49f8ff6c062b2cc1eeef1bf9ba18f49adb00", "system/App/Content/Packages/PackageDependencyContext.php": "8a1f8ca6bbb7add9d8af11d28cdf7137c9a9eaf25b528b0259cf9bd0be00c8b7", "system/App/Content/Packages/PackageModuleProvisioner.php": "28a922ac5471d86ea0c467de11164c97fecd0ee432814e79927bb875995629cc", - "system/App/Content/PaymentPrograms.php": "d233564780b9ab9655f8c41c490aad207c95731f36e6967fb52ac54b9c89e52e", + "system/App/Content/PaymentPrograms.php": "a34c9ed42bfc0d465ac4f37fad28d02ab0145164702e2cbed35991da501f9e3a", "system/App/Content/Presentation/DocumentPresentationBridge.php": "be90d44accf2f75435a62e8be5e79184e095708a3601784daaf87aa4e05ba501", "system/App/Content/Presentation/PresentationAssignmentRepository.php": "a24059e979dfacf7d06f72a46a0322f50b921d0361ed8a426a64b393ca387cca", "system/App/Content/Presentation/PresentationDataRegistry.php": "7159542177443ab7e84ce85e52764bfd7df4448320059175dcbb151ef40eb38c", @@ -1170,7 +1185,8 @@ "system/App/Content/Presentation/PresentationRepository.php": "bc5fb81cb6068ac54551463e563637ad9d75fc5a571d27e8642ed19e7a43b118", "system/App/Content/Presentation/PresentationRevisionRepository.php": "061af7ebd841a44a24be91e6ae3d3b497347bb5333819c74712348b81c40c2df", "system/App/Content/Presentation/PresentationSyntax.php": "aa15536fc06559d34b9aa6b248136e8314f75997b467083dcfff4f1445fe44f9", - "system/App/Content/ProductShipping.php": "8a92b23991b502a97ee3d718add3e87e05382ff80aa83e3c657f54400c3745bc", + "system/App/Content/ProductDerivedCacheInvalidator.php": "6a15cec08f3de799ec9826034fdb2260dcddbfee1987141f5e670235495e6a48", + "system/App/Content/ProductShipping.php": "54f8add77a6cb75515a6eb8191893888da43aaf36a167a699f3d7e946a858349", "system/App/Content/PublicShellTables.php": "f89cd224436da9d1122c988a3a0ac19e5bef85351c51649506fe16a05c514cd9", "system/App/Content/PublicUserTables.php": "f44a184075c9f978564f4b413cb3e89cda5f17528459d52258f458cef87941ab", "system/App/Content/Requests/NativeRequestExecutor.php": "988a1635bb4602b55edb89a1fb8dd26ee0537c074c77fc86b00ecbd8800517ba", @@ -1192,8 +1208,8 @@ "system/App/Content/Rubrics/RubricMutationService.php": "e3190ef1ddd94bdaf077148ce84c5758b2a32683369730a2b847dbbd0d72a74c", "system/App/Content/Templates/TemplateMutationService.php": "8c7e52202454e35eb0c8176325cc23ba760ac40ff0430cbba5e1446c83cf7a87", "system/App/Frontend/Auth/AuthService.php": "1cef24cffd7773f3783ec96479b8e7daa1808b03a34dfc8da37b84e6e09144b8", - "system/App/Frontend/Auth/Controller.php": "8002201011ff8e1c477dc879ec57d2138bf325621978ccbc9a56921c4eff7f9d", - "system/App/Frontend/Auth/Feature.php": "7910b5f938854a4a32feed637b757182b12f4a7178cad97a1498509173571776", + "system/App/Frontend/Auth/Controller.php": "248d7c8018c643622fe41cc679f186d2915017da5250d343415e7a223803c496", + "system/App/Frontend/Auth/Feature.php": "b8ff790cce56875274c372095b80d52c0d54fe12c518573e7a02c583744a3389", "system/App/Frontend/Auth/FormTemplateRepository.php": "bcd8797e63cb67803aef2267a2c62e3ad368b5fd947057a768adbb1288ee9281", "system/App/Frontend/Auth/OAuth/HttpClient.php": "b09acf92b312bd05ca78500fccaac3b77d36a1046d64d3615a00d60af4e3594f", "system/App/Frontend/Auth/OAuth/IdentityRepository.php": "35ab143e0d536185125c685f17f5f4bb0ba0cc72daba62552ef6f82370e38ec1", @@ -1203,32 +1219,33 @@ "system/App/Frontend/Auth/OAuth/ProviderRegistry.php": "e0ff4bc12ef2349d4f914299c78715438d9fbd44ab98e00937534786baa07800", "system/App/Frontend/Auth/Phone/ChallengeRepository.php": "2bfd7ad7b4640e653d3ad0e1ed5f1779ad8df2b569bee071c040d44996c15ea8", "system/App/Frontend/Auth/Phone/Controller.php": "bb07bfb74a89742c2dbeeb7edf79f02ae43ef6a252d02a3bb7c3c78144cf280d", - "system/App/Frontend/Auth/Phone/PhoneAuthService.php": "4f31afa6d600a754cf76805b2a475bf47ffbf7a540b65fb99df098f227211a5a", + "system/App/Frontend/Auth/Phone/PhoneAuthService.php": "b06155f0cfd10b006e26f181300500c19553dfb9ef2482af8674b24f1c4457e2", "system/App/Frontend/Auth/Phone/ProviderInterface.php": "55107c8f6a89bea01b91314dfe523b684bccf5bf9f1a3c8f047fd3a509aa6d00", "system/App/Frontend/Auth/Phone/ProviderRegistry.php": "fbeb488c9b63843085b680500ecafd61389a29f55e0d76692550939e378d2f16", - "system/App/Frontend/Auth/ProfileRepository.php": "9cd7be35b048faf407a925e6369a5895b8bfafe6cf756eb3c4321ea4979a9af6", + "system/App/Frontend/Auth/ProfileRepository.php": "7d18e219821994cff6fe9b4c5339a8ff206a08b478e99338d0804688663d4a31", "system/App/Frontend/Auth/Registration/CheckoutRegistrationGateInterface.php": "6ce038a47e21b594a47bee16075dbf102775b493efd5f2304fa8f80341d34bec", "system/App/Frontend/Auth/Registration/EmailRegistrationGate.php": "ce6dcad98a944aade0fd9a80444c183b4f29bc50e1f119aa3d1d72bdfc3fa732", "system/App/Frontend/Auth/Registration/RegistrationGateInterface.php": "99e73d0d6db972bb5b2c2a7a21dd50823a9bcef7d05937c525d0e7096775aedd", "system/App/Frontend/Auth/Registration/RegistrationGateRegistry.php": "bc7c8546780daacd23243d16778062384f043661674956d1a5a47a8b6440eb44", - "system/App/Frontend/Auth/Renderer.php": "502cfc65cc32e653bae3be85a0a93299365fcec56143cc6e1c7f2bb4d53023ab", + "system/App/Frontend/Auth/Renderer.php": "b814406bf6935064761de83c83bccc35f653e4f2361c8eb1b507d21345adbe26", "system/App/Frontend/Auth/Tags.php": "92fc1c466e720be36a0adce5a735e47c94bf88d9a3e70ba9d62196ef63e639f5", "system/App/Frontend/Auth/TokenRepository.php": "5e09b154d4747bfb3d2795a9b449ae4aefffe47508edef92d1a7b392c0371207", - "system/App/Frontend/Auth/UserRepository.php": "07a35cb3a82201465360b7f75e4be1534c88d53789ad2a5b1ef617be97a060e3", - "system/App/Frontend/Auth/assets/auth.css": "a16949511a17462d9157945f2ceccb445254b34243e9e368796dd3137aac49c8", - "system/App/Frontend/Auth/assets/auth.js": "a619f46dfbdf5dbe783a316ac5964a6ed40b89f872b3bd7a5dd88d5825c1acfe", - "system/App/Frontend/Auth/view/login.twig": "87ca03cc43b778ded40a8a6c8481d0463977219c1b48424896a18fd6f0ec84cd", + "system/App/Frontend/Auth/UserRepository.php": "ad924aa73f6c78de471ac3f4be5e2a7f0348a6b5834042475df616c1fb1a14d4", + "system/App/Frontend/Auth/assets/auth.css": "4b9ecaf9050577c8c9d412f436a22e3f6aa89bc596fc0b1395dea06a6a48e33f", + "system/App/Frontend/Auth/assets/auth.js": "840c7322fc21f9a8dd61af8cd16e0ceed36d6beaeac45d948bedbcb87b6012c4", + "system/App/Frontend/Auth/assets/phone-mask.js": "b9001aeeeda7f2c290bcc24b3453ef828e43c54ec0db9c891b6ffc1a555d2878", + "system/App/Frontend/Auth/view/login.twig": "6d0435f45cdc75ca3c1454bd1c6f0286d6227ac4dd943715927906623e01efd8", "system/App/Frontend/Auth/view/message.twig": "4e9a660af7d0deae9424a07550816c401902e75279414b11657e0eddf573deea", "system/App/Frontend/Auth/view/oauth.twig": "7ccb2c7ae36a378586334887c32fda96909cb6fa235601f1f697d4c8049af648", "system/App/Frontend/Auth/view/oauth_connections.twig": "e2d845ce9d906ca12d3f642e92ede1aa415203f7dc2ac472006f40e80eb8bf27", "system/App/Frontend/Auth/view/overview.twig": "97ad0cd5f72f63b6c92139f7cba54041840131975fdca417352dacf7f64ddb68", - "system/App/Frontend/Auth/view/panel.twig": "de638d18587b6c6671d1ba9db4f9eb3c8cc7765be560aee55ceda4625cd9eb50", + "system/App/Frontend/Auth/view/panel.twig": "5c32483dff6f349ac0a2b8f88c4ff1da6d89cf5e191b5cbf07e15999b843c672", "system/App/Frontend/Auth/view/password.twig": "099d7c1e48649faba57eb50df9bd9a091c79dad12e9c1fee9b38c3f2e025ed80", - "system/App/Frontend/Auth/view/phone.twig": "f84dc89e54a9d2197cb6a9f8b3c4b838f008805e109061add189a43232dc427e", - "system/App/Frontend/Auth/view/profile.twig": "71032aeeebc80036301181932c90ee004d9440bbc64b1b3f79ba03c0e3037a87", - "system/App/Frontend/Auth/view/register.twig": "92f78ab66ba8c16e0b7c3727ac80ae239e01f696399b72049f19a1087d3f3cc9", - "system/App/Frontend/Auth/view/remember.twig": "fb170509256e4ecf13ebe5f64cae8cbd6ad8ef143abe645f4bc1f269e0d8a0fe", - "system/App/Frontend/Auth/view/reset.twig": "5dff5378aa0e2203fd67fe9f1dd61586ee51e3d3f6138011b9d2b5dec4da31aa", + "system/App/Frontend/Auth/view/phone.twig": "b0803ff6b00d4f63ab056a2129d2cf3a2a870bdada106e7d706ba5427cd9a6a8", + "system/App/Frontend/Auth/view/profile.twig": "a2523111d171324a2a116ab4200ade94299af08de45639a0a5d36c9df05eb5dd", + "system/App/Frontend/Auth/view/register.twig": "513dd690a902a51752b7239b113ff6b612d754c238718e6ccc1f5698dfa42c52", + "system/App/Frontend/Auth/view/remember.twig": "9872a6f4812aeb7d8f04870156e0427e9a73be4277c59b1480dfb81d9dc0dc5d", + "system/App/Frontend/Auth/view/reset.twig": "a8d13ea135ca16cc20af3d1940cd8ed5cb13143043992e2ec73b5c7d2c60c8cf", "system/App/Frontend/BlockRenderer.php": "1d23d7ff85ba1052f896e204d1885f99ec2c0f6382e01d5d1903d69fe661305b", "system/App/Frontend/Bootstrap.php": "7cdbfc2855a819b80e742eaa15dbbbf9d1f29ef47fda18860c82ea4541fdb8a4", "system/App/Frontend/BreadcrumbRenderer.php": "6db4cbc31bc4e887a8ab4222c0359008dcc8d08bca969e9e3c3a098db12794b4", @@ -1239,14 +1256,14 @@ "system/App/Frontend/Debug/PublicDebugCollector.php": "6755ad710e9d045b2bfa74745354f717c101236b5d64ad5530fd779468969a00", "system/App/Frontend/Debug/PublicDebugToolbar.php": "7a92630b59483e63d623fa769e13c246e6bd657c6d027a5d14ab8f95986da0de", "system/App/Frontend/Debug/PublicLayoutInspector.php": "8a7f5ea4cff9834ad13cac7e0d2380d878978e406e71860e1ad9ca183e427dab", - "system/App/Frontend/DocumentAccessPolicy.php": "13102e35dada9eb0e174189b976605257b48a32756e801d1ec5c358d0c0912b8", + "system/App/Frontend/DocumentAccessPolicy.php": "4a0d67a678708904694b957d553a6e454ea0fcf9aa62075760d5cc754bda6f07", "system/App/Frontend/DocumentApi/Controller.php": "7c5ffecf7e1515710931d7c31b5b2c5f555f8d35778a1fc202a8136b07c2490c", "system/App/Frontend/DocumentApi/Feature.php": "afe7143ed3739b704064927d08fa9b2c79e7050e19e08aca5e4e6638ba5c6994", "system/App/Frontend/DocumentFieldRenderer.php": "1b9eeeb27e40274f8f73bac1a34da993b799731575fef36e790a7f5d87fa7e65", "system/App/Frontend/DocumentFieldRepository.php": "fc8e80455feca50e4bb5bff3566eb6814fcca1994f1167bd8dd7972f1c87ac69", "system/App/Frontend/DocumentPaginationRenderer.php": "560c9441fb758bc760b14f37bd88c39c36207c1717a18f43b5c57599a61231ed", "system/App/Frontend/DocumentRenderCache.php": "9dd0e9e666021e00eda8c4d09cf8f197f17468b8ad545b4d357654bf646bfc15", - "system/App/Frontend/DocumentRepository.php": "f5388b21eee81b3536ef32f239a521dd3187410c9874adbdbdc416949409c27a", + "system/App/Frontend/DocumentRepository.php": "bef6b4c740d5b4071433d39aada6564e39107510c90dd82fd112f751f7d71caa", "system/App/Frontend/DocumentRevisionPreview.php": "372b751f673411722c2de5f04ee1fac57b745e327a2259916313781a3f175e89", "system/App/Frontend/DocumentRevisionRepository.php": "bbfb1c5a77c979b9998cc647a0e410150ca33727f6caf46f7c49f6ed01774f45", "system/App/Frontend/DocumentRouteRepository.php": "68b7a9c4972613c7fae886c74273cced26d5749d9242f0cf7463176c1ac0d854", @@ -1256,11 +1273,11 @@ "system/App/Frontend/Feeds/Controller.php": "2d446f4bb973c781ce01bf4831721b3e5fac1f6e1642f1862d86da8e8f4c271d", "system/App/Frontend/Feeds/Feature.php": "1322bf61d4296c79ad8825671ed556cca79bc9b2eed46cbee8b4f70d19f6ca6b", "system/App/Frontend/Feeds/FeedPresets.php": "bff8edea9f095a63fa2c127ee6afb35c5a3ce4434ca309e9a8a7dd047ba16428", - "system/App/Frontend/Feeds/Generator.php": "4234369a293bad237fe0d36dd3c78da616d03ce37070ac72988893f796773da9", + "system/App/Frontend/Feeds/Generator.php": "a2147e1443ececc8e2fe4ec3ddfb37bead2021198b779d2b09c4437f35768a94", "system/App/Frontend/Feeds/PathController.php": "faf72ea5ba08256c8566a5ee9a73adfcabd59ef0f510c108a6003b659659635b", - "system/App/Frontend/Feeds/Repository.php": "3d36c493b3714780ed078537ecc8788c7f403a6a6eebb022128bd3e9b3e6dfa1", + "system/App/Frontend/Feeds/Repository.php": "78576a9bb53eb1e982ee29f2ebd755ce882c24bf6e8204512adeba4db7e197b1", "system/App/Frontend/Feeds/Schema.php": "a4603347d631e6a748e0b245901f952736754e4e5e73c96289ed17a1998f5a31", - "system/App/Frontend/Feeds/Service.php": "3d0f3badbc07a9d47e5902c106be270a24a71e2d64a636ca909b2393bdeb59f7", + "system/App/Frontend/Feeds/Service.php": "2188a059be3b46131010bc68af39b74da59d135a4b7c8af2bf6d5bcbe6816cba", "system/App/Frontend/FieldEndpoint.php": "0bb0200a52169ecb77498df32d6ee3053324844cfa2e9200d6b3b819dd4c6aa8", "system/App/Frontend/HiddenContentRenderer.php": "62d34471f729ee37811b17f3eb80a4c6031dddc04f7b7bcc377cb79cc353649d", "system/App/Frontend/Media/Controller.php": "1a6102b612e14f7f595a0279d1ec47e1fbd828cb87f0bee0f619e6ca2213845e", @@ -1276,7 +1293,7 @@ "system/App/Frontend/ModuleTagRenderer.php": "0b41289126478a1aeda6cf7119f8f3d5f89b43bb7684c92ab5447d5dab408795", "system/App/Frontend/NavigationRenderer.php": "d176186b06d56832e640a8b3d53ccad4aff04e3834b261d83670d675271e4d00", "system/App/Frontend/NavigationRepository.php": "3714ca40102621191837fbed7511873327250ecb186c54bb0cce89781bdb0db1", - "system/App/Frontend/PageComponentRenderer.php": "4176d2748b73b1c7cb72963cf67cfafd66246d93119728d9d688e6a48b882485", + "system/App/Frontend/PageComponentRenderer.php": "83260a9d4d83603a99bf7aa8b620e06b78911f994b807b5fc179d3fd939b0812", "system/App/Frontend/PageShellRenderer.php": "e87c2ca686fbf6fd721a35656e752aaf590633a196c93715f75d36ddd78245fb", "system/App/Frontend/PageTemplateRepository.php": "58f73d71e6bb394f725aee0201f436236214acca343dc75bacdcfa752f3e8caf", "system/App/Frontend/PageTemplateResolver.php": "1af594b212cd4e30d5de361415a8e238493995d8bf955df9291e40169afe621c", @@ -1293,14 +1310,14 @@ "system/App/Frontend/PublicNotFoundResponder.php": "52f165cd61251cea4189f6fa876e77b4934cf25cd973e7be14eb592f0999a390", "system/App/Frontend/PublicNotice.php": "b0eca5f46e64d4b0f33feadd126c81c8072a7ca959c13c2194defb8a3a01f027", "system/App/Frontend/PublicPageContext.php": "cc41038f45307b8abbd18b843cd0d82e1f77e8bc36d2807fef17c5a49b4f5c49", - "system/App/Frontend/PublicPageLifecycle.php": "e973e9648354eb36c8bd00c84c6a124910c82f5eba9c456c042960927e1b485f", + "system/App/Frontend/PublicPageLifecycle.php": "74f9b301d6d7bdc336aa490f3080705c5098ae155b99d164ea5df3708fa9e4eb", "system/App/Frontend/PublicPageRenderer.php": "10cb6a6b8f6e00998619ca4e92b4c85324cf19ccf955806647ccd09e94971a5a", "system/App/Frontend/PublicProfiler.php": "81d276cae6c51474987d9099e1f916a7e24b0ed03abf0819e3a9f08152196c52", "system/App/Frontend/PublicRedirectResponder.php": "67b0915e9820fb5d0f4270d86ea39d99502acc49c86997aef7fe0d9ef3fe7b60", "system/App/Frontend/PublicRequestApi.php": "803113eaeaf272d423ba39331e0dfa25a540251aab4961d2b796a6734215ed37", "system/App/Frontend/PublicSettings.php": "e70b9a478cefe4eb9f55ba0cde37ed14e270be345d08773e8a6b736be3bef321", "system/App/Frontend/QuickEdit/PublicQuickEdit.php": "11e2c721e676d3ddba73c6f8cbe11ac9a9ad15acfd4e97ddfd88ba7f8f43dfd7", - "system/App/Frontend/RequestFieldRenderer.php": "01d6e96d630727f740874334e7ffbdfbdc076d896a6d8a390723c8aaaec41c97", + "system/App/Frontend/RequestFieldRenderer.php": "5065a2bb8b630eeffb4e3ac978e6abf15f296e523b6a0a3421c4f3a870258cdf", "system/App/Frontend/RequestItemContext.php": "28f70292d7feeb77b88246d1d3299ff54447afc1aa6c525211835a4cfd51ebb4", "system/App/Frontend/RequestItemRenderer.php": "6426d63609073d85938dbaef0f77935d856a57eca225503d812e31f07ab98790", "system/App/Frontend/RequestListReadModel.php": "7eedb83e42382cada17e60484108702a52f8a890b59d93cd921e194025af6064", @@ -1313,10 +1330,10 @@ "system/App/Frontend/SiteAccess.php": "a1cc5a86ab9f88c22214b9cb642556d7e6ffe73b00084de988ddd730805410fc", "system/App/Frontend/Sitemap/Controller.php": "7a1504cb00897bba10ba0d6d297c7338f8c9eda8782577ef1ef8f8316a35846a", "system/App/Frontend/Sitemap/Feature.php": "7ff7afcc57c8533acdd1050a40a0c4ca65815668f5e8c72d4b02bdd6c92ae8fd", - "system/App/Frontend/Sitemap/Repository.php": "f7d645987328e97bd01b8204cea3f40fe22921c7f07806ebeba2fe53be80f02f", + "system/App/Frontend/Sitemap/Repository.php": "2dff32e665493d6cd7e6946952541ad414d460d8c1b0d873161b134e24ed1ffe", "system/App/Frontend/StoredTemplateApi.php": "042e21a93a6c914ac6cf53465b5292833a8240645870ac1bcf29e6fed0a2288f", "system/App/Frontend/ThemeAssets.php": "5870bace8823561820b64752e5dba16e434449e12de317ad4085087b31232396", - "system/App/Frontend/ThemePageShellRenderer.php": "483e2cf8ef91099b5ff3b3c1d70aa3a4d2ddf354add2bff650ec523150384ea9", + "system/App/Frontend/ThemePageShellRenderer.php": "86638d6559c6b57f540ff2705ea3ba7607001ee46c94e9398ece474a34b05f52", "system/App/Frontend/ThemeSettings.php": "77509ac00183373005f2456a58abf2c8608d6677751f7540d8cb8f5db36284a4", "system/App/Frontend/ThemeSettingsTags.php": "13fa6b59c8518dca33f6b3482e0f9a00c14a248d430756f62a9b0c5175346fab", "system/App/Frontend/view/content/list.twig": "60db68391fa334c46694d3d646a0458e661e147770feb86e7be174b62e1f1f65", @@ -1354,7 +1371,7 @@ "system/language/ru/lang.xml": "7d46cc9822f064a1af4d6528ccfe2dd4a9cc318912e327fd807befd2ca3dfd11", "system/preload.php": "66bfb1aaf36ba85945b729b09a37e96ead0be1cc2749ebd9abd4b9da4f25ba0e", "system/public.php": "3f72b5bf44bc0ba5ba6fb6b3adc2e2f9d5e9902d9cdfa3d7b748f4aebd179b87", - "system/release.php": "313f3e82832ba2ed320647a9a3eee63b54cda5b390b8b1916fb9ceb7d2344764", + "system/release.php": "96de420b08cdefac694c48399015fdbccc9bc81c5992d1052254934c8e0d2f2e", "system/vendor/Captcha/fonts/palatino_linotype_bold.png": "38b6a1d4a4bd1d0c056205119c5e24176913ac0d898371f53a0339952a319365", "system/vendor/Captcha/fonts/perpetua_bold.png": "55dbbe97dae128dea47b5e8d640414b3d813119c854c688037a20a581ec8fdc4", "system/vendor/Captcha/fonts/times_bold.png": "d3d32ce1f418e3c3c3b1afe3b6acabadb36207bc12368f5e86420cf1a529ff8f", @@ -2118,18 +2135,31 @@ "adminx/modules/Blocks/module.php": "core", "adminx/modules/Blocks/view/index.twig": "core", "adminx/modules/Catalog/Controller.php": "core", + "adminx/modules/Catalog/EditorContextLinks.php": "core", "adminx/modules/Catalog/Model.php": "core", "adminx/modules/Catalog/assets/catalog-drawer.css": "core", "adminx/modules/Catalog/assets/catalog.css": "core", "adminx/modules/Catalog/assets/catalog.js": "core", + "adminx/modules/Catalog/assets/context-links.css": "core", "adminx/modules/Catalog/language/en/client.xml": "core", + "adminx/modules/Catalog/language/en/context-links.xml": "core", "adminx/modules/Catalog/language/en/interface.xml": "core", + "adminx/modules/Catalog/language/en/product-roles.xml": "core", + "adminx/modules/Catalog/language/en/product-selection.xml": "core", "adminx/modules/Catalog/language/en/runtime.xml": "core", "adminx/modules/Catalog/language/ru/client.xml": "core", + "adminx/modules/Catalog/language/ru/context-links.xml": "core", "adminx/modules/Catalog/language/ru/interface.xml": "core", + "adminx/modules/Catalog/language/ru/product-roles.xml": "core", + "adminx/modules/Catalog/language/ru/product-selection.xml": "core", "adminx/modules/Catalog/language/ru/runtime.xml": "core", "adminx/modules/Catalog/migrations/005_add_catalog_purpose.sql": "core", + "adminx/modules/Catalog/migrations/006_add_item_sources.php": "core", "adminx/modules/Catalog/module.php": "core", + "adminx/modules/Catalog/view/_context-links.twig": "core", + "adminx/modules/Catalog/view/_product-role-preview.twig": "core", + "adminx/modules/Catalog/view/_product-roles.twig": "core", + "adminx/modules/Catalog/view/_product-selection-preview.twig": "core", "adminx/modules/Catalog/view/edit.twig": "core", "adminx/modules/Catalog/view/index.twig": "core", "adminx/modules/Console/Controller.php": "core", @@ -2251,6 +2281,7 @@ "adminx/modules/Documents/migrations/014_document_relation_edges.sql": "core", "adminx/modules/Documents/migrations/015_document_creation_presets.sql": "core", "adminx/modules/Documents/migrations/016_reconcile_document_content_columns.php": "core", + "adminx/modules/Documents/migrations/017_document_public_visibility.sql": "core", "adminx/modules/Documents/module.php": "core", "adminx/modules/Documents/view/_product_panels.twig": "core", "adminx/modules/Documents/view/api.twig": "core", @@ -2385,10 +2416,8 @@ "adminx/modules/Requests/Revisions.php": "core", "adminx/modules/Requests/assets/requests.css": "core", "adminx/modules/Requests/assets/requests.js": "core", - "adminx/modules/Requests/language/en/client.xml": "core", "adminx/modules/Requests/language/en/interface.xml": "core", "adminx/modules/Requests/language/en/runtime.xml": "core", - "adminx/modules/Requests/language/ru/client.xml": "core", "adminx/modules/Requests/language/ru/interface.xml": "core", "adminx/modules/Requests/language/ru/runtime.xml": "core", "adminx/modules/Requests/migrations/001_native_database_calls.sql": "core", @@ -2452,6 +2481,7 @@ "adminx/modules/Rubrics/migrations/014_rubric_purpose.php": "core", "adminx/modules/Rubrics/migrations/015_merge_directory_permissions.php": "core", "adminx/modules/Rubrics/migrations/016_rubric_trash.sql": "core", + "adminx/modules/Rubrics/migrations/017_rubric_public_visibility.sql": "core", "adminx/modules/Rubrics/module.php": "core", "adminx/modules/Rubrics/view/_content_tabs.twig": "core", "adminx/modules/Rubrics/view/field-sets.twig": "core", @@ -2466,6 +2496,7 @@ "adminx/modules/Security/language/ru/interface.xml": "core", "adminx/modules/Security/language/ru/runtime.xml": "core", "adminx/modules/Security/migrations/001_create_ip_blocks.sql": "core", + "adminx/modules/Security/migrations/002_create_user_agent_blocks.php": "core", "adminx/modules/Security/module.php": "core", "adminx/modules/Security/view/index.twig": "core", "adminx/modules/Settings/Constants.php": "core", @@ -3008,6 +3039,7 @@ "system/App/Content/Documents/DocumentSnapshotBuilder.php": "core", "system/App/Content/Documents/DocumentSnapshotRepository.php": "core", "system/App/Content/Documents/DocumentSnapshotStore.php": "core", + "system/App/Content/Documents/DocumentVisibility.php": "core", "system/App/Content/Documents/FieldTemplateManifest.php": "core", "system/App/Content/Documents/RubricSchemaBuilder.php": "core", "system/App/Content/ExtensionTables.php": "core", @@ -3102,6 +3134,7 @@ "system/App/Content/Presentation/PresentationRepository.php": "core", "system/App/Content/Presentation/PresentationRevisionRepository.php": "core", "system/App/Content/Presentation/PresentationSyntax.php": "core", + "system/App/Content/ProductDerivedCacheInvalidator.php": "core", "system/App/Content/ProductShipping.php": "core", "system/App/Content/PublicShellTables.php": "core", "system/App/Content/PublicUserTables.php": "core", @@ -3149,6 +3182,7 @@ "system/App/Frontend/Auth/UserRepository.php": "core", "system/App/Frontend/Auth/assets/auth.css": "core", "system/App/Frontend/Auth/assets/auth.js": "core", + "system/App/Frontend/Auth/assets/phone-mask.js": "core", "system/App/Frontend/Auth/view/login.twig": "core", "system/App/Frontend/Auth/view/message.twig": "core", "system/App/Frontend/Auth/view/oauth.twig": "core", diff --git a/adminx/Support/InterfaceSettings.php b/adminx/Support/InterfaceSettings.php index ee395d7..6203153 100644 --- a/adminx/Support/InterfaceSettings.php +++ b/adminx/Support/InterfaceSettings.php @@ -75,6 +75,46 @@ return $saved; } + /** Синхронизировать переключатель из карточки модуля с общей раскладкой главной. */ + public static function setModuleDashboardVisibility($moduleCode, $visible, array $definitions) + { + $moduleCode = (string) $moduleCode; + $targetCodes = array(); + foreach ($definitions as $definition) { + if (!is_array($definition) || !isset($definition['module_code'], $definition['layout_code'])) { + continue; + } + + if ((string) $definition['module_code'] === $moduleCode) { + $targetCodes[(string) $definition['layout_code']] = true; + } + } + + if (!$targetCodes) { + return self::config('dashboard'); + } + + $config = self::config('dashboard'); + $seen = array(); + foreach ($config as &$row) { + $code = (string) $row['code']; + if (isset($targetCodes[$code])) { + $row['visible'] = (bool) $visible; + $seen[$code] = true; + } + } + + unset($row); + foreach (array_keys($targetCodes) as $code) { + if (!isset($seen[$code])) { + $config[] = array('code' => $code, 'visible' => (bool) $visible); + } + } + + Settings::set(self::key('dashboard'), $config, 'json'); + return $config; + } + protected static function prepare(array $items, $codeKey) { $prepared = array(); diff --git a/adminx/Support/ModuleExtensions.php b/adminx/Support/ModuleExtensions.php index 5f69fc7..e28e492 100644 --- a/adminx/Support/ModuleExtensions.php +++ b/adminx/Support/ModuleExtensions.php @@ -106,6 +106,7 @@ public static function notifications($base, array $summary) { foreach (self::contributions('notifications') as $contribution) { + $moduleCode = isset($contribution['module_code']) ? (string) $contribution['module_code'] : ''; $data = isset($contribution['data']) ? $contribution['data'] : array(); $items = isset($data['items']) && is_array($data['items']) ? $data['items'] : (self::isList($data) ? $data : array()); foreach ($items as $item) { @@ -113,6 +114,11 @@ continue; } + $key = isset($item['key']) ? trim((string) $item['key']) : ''; + if ($key !== '' && !self::notificationEnabled($moduleCode, $key)) { + continue; + } + $count = isset($item['count']) ? max(0, (int) $item['count']) : 1; if ($count < 1) { continue; @@ -137,8 +143,7 @@ 'title' => AdminLocale::translateMarkup(isset($item['title']) ? (string) $item['title'] : ''), )); $summary['total'] += $count; - if (!empty($item['key'])) { - $key = (string) $item['key']; + if ($key !== '') { $summary[$key] = isset($summary[$key]) ? (int) $summary[$key] + $count : $count; } } @@ -147,6 +152,80 @@ return $summary; } + /** Настраиваемые элементы колокольчика без запуска их провайдеров. */ + public static function notificationPreferences() + { + self::boot(); + $out = array(); + foreach (self::$extensions as $code => $extension) { + $module = self::currentModule($code, $extension['module']); + if (empty($module['installed']) || empty($module['enabled'])) { + continue; + } + + $config = $extension['config']; + if (empty($config['notifications'])) { + continue; + } + + foreach (self::normalizeItems($config['notifications']) as $item) { + if (!is_array($item) || !self::allowed($item) || empty($item['preferences']) || !is_array($item['preferences'])) { + continue; + } + + foreach ($item['preferences'] as $preference) { + if (!is_array($preference)) { + continue; + } + + $key = isset($preference['key']) ? trim((string) $preference['key']) : ''; + $label = isset($preference['label']) ? trim((string) $preference['label']) : ''; + if (!preg_match('/^[a-z][a-z0-9_]{0,63}$/', $key) || $label === '') { + continue; + } + + $default = !empty($preference['default']); + $out[] = array( + 'module_code' => (string) $code, + 'key' => $key, + 'label' => AdminLocale::translateMarkup($label), + 'description' => AdminLocale::translateMarkup(isset($preference['description']) ? (string) $preference['description'] : ''), + 'enabled' => (bool) ModuleSettings::get(self::notificationSettingKey($key), $default, $code), + 'default' => $default, + 'sort_order' => isset($preference['sort_order']) ? (int) $preference['sort_order'] : 100, + ); + } + } + } + + usort($out, function ($left, $right) { + if ($left['sort_order'] === $right['sort_order']) { + return strcmp($left['label'], $right['label']); + } + + return $left['sort_order'] <=> $right['sort_order']; + }); + return $out; + } + + /** Сохранить один объявленный модулем переключатель уведомлений. */ + public static function setNotificationPreference($moduleCode, $key, $enabled) + { + $moduleCode = strtolower(trim((string) $moduleCode)); + $key = strtolower(trim((string) $key)); + foreach (self::notificationPreferences() as $preference) { + if ($preference['module_code'] !== $moduleCode || $preference['key'] !== $key) { + continue; + } + + ModuleSettings::set(self::notificationSettingKey($key), (bool) $enabled, $moduleCode, 'bool'); + $preference['enabled'] = (bool) $enabled; + return $preference; + } + + throw new \InvalidArgumentException('Настройка уведомления не найдена'); + } + public static function catalog() { self::boot(); @@ -410,4 +489,20 @@ { return (bool) ModuleSettings::get('adminx_' . $type . '_enabled', true, $code); } + + protected static function notificationEnabled($moduleCode, $key) + { + foreach (self::notificationPreferences() as $preference) { + if ($preference['module_code'] === (string) $moduleCode && $preference['key'] === (string) $key) { + return !empty($preference['enabled']); + } + } + + return true; + } + + protected static function notificationSettingKey($key) + { + return 'adminx_notification_' . preg_replace('/[^a-z0-9_]+/', '_', strtolower((string) $key)) . '_enabled'; + } } diff --git a/adminx/Support/PhpCode.php b/adminx/Support/PhpCode.php index 07293af..d554900 100644 --- a/adminx/Support/PhpCode.php +++ b/adminx/Support/PhpCode.php @@ -16,8 +16,12 @@ defined('BASEPATH') || die('Direct access to this location is not allowed.'); + use App\Common\PublicConfiguration; + class PhpCode { + const LINT_TIMEOUT = 3; + public static function source($code) { $code = trim((string) $code); @@ -41,12 +45,27 @@ return array('ok' => false, 'message' => 'Не удалось записать временный файл.', 'output' => '', 'line' => 0); } - $php = defined('PHP_BINARY') && PHP_BINARY ? PHP_BINARY : 'php'; - $output = array(); - $status = 0; - @exec(escapeshellarg($php) . ' -l ' . escapeshellarg($file) . ' 2>&1', $output, $status); + if (!function_exists('proc_open')) { + @unlink($file); + return array('ok' => false, 'message' => 'Проверка PHP недоступна на сервере.', 'output' => 'Функция proc_open отключена хостингом.', 'line' => 0); + } + + $php = self::resolvePhpBinary(); + if ($php === '') { + @unlink($file); + return array('ok' => false, 'message' => 'PHP CLI не найден.', 'output' => 'Укажите путь к PHP CLI в ADMINX_PHP_BINARY.', 'line' => 0); + } + + $result = self::runProcess( + escapeshellarg($php) . ' -l ' . escapeshellarg($file), + self::LINT_TIMEOUT + ); @unlink($file); - $text = trim(implode("\n", $output)); + if ($result['timed_out']) { + return array('ok' => false, 'message' => 'Проверка PHP превысила лимит времени.', 'output' => 'Проверьте ADMINX_PHP_BINARY: требуется консольный PHP, а не php-fpm.', 'line' => 0); + } + + $text = trim($result['output']); $text = str_replace($file, 'код', $text); $line = 0; if (preg_match('/ on line ([0-9]+)/i', $text, $match)) { @@ -54,10 +73,101 @@ } return array( - 'ok' => $status === 0 && $text !== '', - 'message' => $status === 0 ? 'Синтаксис PHP без ошибок.' : 'В PHP-коде есть синтаксическая ошибка.', + 'ok' => $result['exit_code'] === 0 && $text !== '', + 'message' => $result['exit_code'] === 0 ? 'Синтаксис PHP без ошибок.' : 'В PHP-коде есть синтаксическая ошибка.', 'output' => $text, 'line' => $line, ); } + + protected static function resolvePhpBinary() + { + $candidates = array(); + $configured = trim((string) getenv('ADMINX_PHP_BINARY')); + if ($configured !== '') { $candidates[] = $configured; } + $config = PublicConfiguration::value('admin_console', array()); + if (is_array($config) && !empty($config['php_binary'])) { + $candidates[] = trim((string) $config['php_binary']); + } + + if (defined('PHP_BINDIR') && PHP_BINDIR !== '') { + $candidates[] = rtrim(PHP_BINDIR, '/\\') . DIRECTORY_SEPARATOR . 'php'; + } + + if (defined('PHP_BINARY') && PHP_BINARY !== '') { + $candidates[] = dirname(PHP_BINARY) . DIRECTORY_SEPARATOR . 'php'; + $candidates[] = PHP_BINARY; + } + + $candidates[] = 'php'; + + foreach (array_unique($candidates) as $candidate) { + $result = self::runProcess( + escapeshellarg((string) $candidate) . ' -r ' . escapeshellarg('echo PHP_VERSION_ID;'), + 1 + ); + if (!$result['timed_out'] && $result['exit_code'] === 0 && preg_match('/^\d{5,6}$/', trim($result['output']))) { + return (string) $candidate; + } + } + + return ''; + } + + protected static function runProcess($command, $timeout) + { + $pipes = array(); + try { + $process = @proc_open($command, array( + 0 => array('pipe', 'r'), + 1 => array('pipe', 'w'), + 2 => array('pipe', 'w'), + ), $pipes); + } catch (\Throwable $e) { + return array('exit_code' => 1, 'timed_out' => false, 'output' => $e->getMessage()); + } + + if (!is_resource($process)) { + return array('exit_code' => 1, 'timed_out' => false, 'output' => 'Не удалось запустить PHP CLI.'); + } + + fclose($pipes[0]); + stream_set_blocking($pipes[1], false); + stream_set_blocking($pipes[2], false); + $started = microtime(true); + $output = ''; + $error = ''; + $timedOut = false; + $knownExitCode = null; + do { + $output .= (string) stream_get_contents($pipes[1]); + $error .= (string) stream_get_contents($pipes[2]); + $status = proc_get_status($process); + if (!$status['running']) { + $knownExitCode = isset($status['exitcode']) ? (int) $status['exitcode'] : null; + break; + } + + if (microtime(true) - $started >= max(1, (int) $timeout)) { + $timedOut = true; + proc_terminate($process, 9); + break; + } + + usleep(20000); + } while (true); + + $output .= (string) stream_get_contents($pipes[1]); + $error .= (string) stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + $closedExitCode = proc_close($process); + $exitCode = $knownExitCode !== null && $knownExitCode >= 0 ? $knownExitCode : $closedExitCode; + + return array( + 'exit_code' => $exitCode, + 'timed_out' => $timedOut, + 'output' => $output . ($error !== '' ? ($output !== '' ? "\n" : '') . $error : ''), + ); + } } diff --git a/adminx/Support/SectionHelp.php b/adminx/Support/SectionHelp.php index 59aab58..6f02118 100644 --- a/adminx/Support/SectionHelp.php +++ b/adminx/Support/SectionHelp.php @@ -76,6 +76,7 @@ protected static function definitions() { return array( + self::item('#^/documents/bulk-editor$#', 'Массовый редактор', 'Меняет одинаковые настройки сразу у выбранной группы документов и сначала показывает точный предпросмотр.', array('Служебный документ остаётся доступен полям, запросам и связям, но его публичный адрес возвращает 404.', 'Служебные документы автоматически исключаются из публичного поиска, sitemap, SEO-задач и проверок качества.', 'Внутренний поиск, robots и участие в sitemap можно менять отдельными действиями, не скрывая саму страницу.'), 'Сначала выберите рубрику или другой узкий фильтр, проверьте примеры и только затем запускайте изменение.'), self::item('#^/documents/redirects$#', 'Редиректы документов', 'Управляет старыми адресами страниц и перенаправляет посетителей на актуальные документы.', array('Ищите редирект по старому URL, названию или ID документа.', 'Коды 301/308 подходят для постоянного переноса, 302/307 — для временного.', 'Перед удалением убедитесь, что старый адрес больше не используется во внешних ссылках.'), 'Каждый старый URL должен быть уникальным и вести на существующий документ.'), self::item('#^/documents/views$#', 'Статистика просмотров', 'Показывает посещаемость документов по дням и помогает находить наиболее востребованные страницы.', array('Период влияет только на подневную статистику.', 'Общий счётчик документа хранится отдельно и не очищается вместе с дневными данными.', 'Очистка статистики необратима и предназначена для обслуживания.'), 'Для бизнес-аналитики используйте внешнюю систему аналитики; здесь хранится внутренняя статистика CMS.'), self::item('#^/documents/(?:create|\d+/edit)$#', 'Редактор документа', 'Создаёт и изменяет страницу сайта: её адрес, публикацию, SEO и значения полей выбранной рубрики.', array('Набор полей и их группы задаются рубрикой.', 'Изменение alias может автоматически создать редирект со старого URL.', 'Ctrl+S сохраняет изменения и оставляет редактор открытым; ревизии позволяют вернуться к прошлой версии.'), 'Смена рубрики существующего документа запрещена без отдельной миграции полей.'), @@ -133,12 +134,12 @@ self::item('#^/settings/benchmark$#', 'Производительность', 'Измеряет скорость PHP, базы данных, файловых операций и кеша на текущем сервере.', array('Результаты нужны для сравнения одного окружения во времени, а не разных проектов.', 'Повторяйте тест после обновления PHP, БД или инфраструктуры.', 'Тест создаёт кратковременную нагрузку и сохраняет историю запусков.'), 'Оценивайте не только общий балл, но и конкретный медленный этап.'), self::item('#^/settings#', 'Настройки системы', 'Собирает общие параметры AVE.cms, интерфейс, константы, пагинацию, обслуживание и диагностику.', array('Каждый подраздел имеет собственный URL и назначение.', 'Типизированные значения проверяются перед сохранением.', 'Настройки устанавливаемых модулей хранятся вместе с соответствующим модулем.'), 'Меняйте только параметры, назначение которых понятно и проверено на текущем сайте.', 'administration/settings'), self::item('#^/events#', 'Системные события', 'Объединяет аудит действий, ошибки, SQL-журнал, 404 и статистику внешних переходов.', array('Цвет и состояние помогают отличать успешные операции, предупреждения и ошибки.', 'Переходы группируются по посетителю, странице входа и источнику; открытые IP и произвольные query-параметры не сохраняются.', 'Фильтры и CSV-экспорт работают отдельно для каждого источника.'), 'Очистка журнала необратима и не исправляет причину ошибки.'), - self::item('#^/security/ip-blocks#', 'IP-блокировки', 'Управляет ручными блокировками адресов, а автоматические ограничения выполняет RateLimiter.', array('Блокировка может быть постоянной или иметь дату окончания.', 'Причина видна администраторам и записывается в аудит.', 'Разблокировка возвращает доступ сразу.'), 'Не блокируйте адрес reverse proxy без проверки реального клиентского IP.'), + self::item('#^/security/ip-blocks#', 'Блокировки запросов', 'Ограничивает доступ к публичному сайту по IP-адресу или характерной строке User-Agent.', array('IP и User-Agent находятся на отдельных вкладках.', 'Правило может быть постоянным или иметь дату окончания.', 'Для робота используйте его уникальное имя, а не общие слова Mozilla, Chrome или Safari.', 'Причина и действия блокировки записываются в аудит.'), 'Перед блокировкой User-Agent проверьте предложенный фрагмент: слишком общее правило может затронуть обычных посетителей.'), self::item('#^/database#', 'База данных', 'Показывает рабочие таблицы AVE.cms, выполняет обслуживание, применяет миграции ядра и управляет полными резервными копиями.', array('OPTIMIZE применяется только к выбранной таблице или ко всей рабочей схеме после подтверждения.', 'Применение миграций не восстанавливает данные: это отдельная операция развития структуры ядра.', 'Новые дампы используют {{prefix}}, поэтому могут восстанавливаться с другим dbpref.', 'Перед восстановлением система проверяет файл и автоматически создаёт страховочную копию.'), 'Восстановление полностью заменяет таблицы текущей установки; рабочую копию дополнительно храните вне сервера.', 'database'), self::item('#^/kanban$#', 'Канбан', 'Организует рабочие карточки по колонкам и показывает доску на дашборде.', array('Карточки и порядок сохраняются без перезагрузки.', 'Колонки можно настроить под собственный процесс.', 'Карточке можно назначить ответственного, срок и связанный документ.', 'Командная карточка видна коллегам, а назначенная отдельно появляется у ответственного.'), 'Колонки принадлежат владельцу доски: назначенный сотрудник видит карточку в общем списке, но не перестраивает чужую доску.'), self::item('#^/notes$#', 'Заметки', 'Хранит личные текстовые заметки сотрудника и закрепляет важные записи на дашборде.', array('Заметки видны только их владельцу.', 'Закрепление влияет на порядок выдачи.', 'Быстрое создание доступно из шапки, если вклад модуля включён.'), 'Не храните в заметках пароли, токены и другие секреты.'), self::item('#^/reminders$#', 'Напоминания', 'Хранит личные задачи с датой и выводит просроченные пункты в общем колокольчике.', array('Срок определяет состояние напоминания.', 'Выполнение сохраняется сразу.', 'Просроченный счётчик формируется отдельно для текущего сотрудника.'), 'Удаление напоминания необратимо; выполненный пункт можно вернуть в работу.'), - self::item('#^/notfound$#', 'Ошибки 404', 'Показывает несуществующие публичные адреса и помогает создать документ или редирект.', array('Сначала проверьте источник переходов и частоту запроса.', 'Для старого адреса существующей страницы создайте постоянный редирект.', 'Случайные сканирующие URL можно удалить из журнала без создания страницы.'), 'Редирект должен вести на действительно соответствующий документ, а не всегда на главную.'), + self::item('#^/notfound$#', 'Ошибки 404', 'Показывает несуществующие публичные адреса, источник перехода, IP и программу или робота, который сделал запрос.', array('Кнопка «Подробнее» раскрывает полный реферер, User-Agent и время первого и последнего обращения.', 'Для старого адреса существующей страницы создайте постоянный редирект.', 'Нежелательного робота можно отправить в блокировки прямо из записи.', 'CSV учитывает текущий поиск и переключатель нерешённых записей.', 'Очистка всего журнала необратима, но новые ошибки начнут собираться снова.'), 'Не блокируйте обычный браузер только из-за одного ошибочного адреса; сначала оцените источник и частоту обращений.'), self::item('#^/seo-audit$#', 'SEO-аудит', 'Проверяет документы на технические и редакционные проблемы, влияющие на поисковое представление.', array('Фильтры помогают сначала разобрать критичные и массовые проблемы.', 'Оценка документа складывается из нескольких независимых проверок.', 'Исправления выполняются в редакторе документа и проявляются после повторного аудита.'), 'Автоматическая оценка является подсказкой и не заменяет проверку содержания страницы.', 'modules/seo-audit'), self::item('#^/todo$#', 'Todo', 'Хранит рабочие задачи сотрудников со сроками и связями с документами.', array('Задачу можно создать из выпадающего списка в шапке на любой странице панели управления.', 'Ответственный видит назначенную задачу и может отметить её выполненной.', 'Режим «Команда» открывает задачу всем сотрудникам с правом просмотра Todo.', 'Связанный документ открывается прямо из строки задачи.'), 'Изменять и удалять задачу может её создатель; выполнение доступно создателю и ответственному.'), self::item('#^/modules/search#', 'Поиск по сайту', 'Индексирует опубликованные документы и формирует обычную страницу результатов и JSON для живого поиска.', array('Области позволяют отдельно искать по товарам, новостям, статьям и другим наборам рубрик.', 'Алгоритм определяет разбор строки и вес точного совпадения, заголовка и содержимого.', 'Штатный HTML можно заменить шаблонами модуля или существующим запросом документов.'), 'После изменения рубрик или индексируемых полей выполните полную переиндексацию.', 'modules/search'), diff --git a/adminx/Support/SensitiveRoutes.php b/adminx/Support/SensitiveRoutes.php index 96decf8..a33cae3 100644 --- a/adminx/Support/SensitiveRoutes.php +++ b/adminx/Support/SensitiveRoutes.php @@ -31,6 +31,7 @@ 'legacy_migration.write' => 'Изменение реквизитов исходной системы', 'legacy_migration.execute' => 'Очистка стартовых данных и миграция старой AVE.cms', 'database.restore' => 'Полная замена рабочей схемы данными из резервной копии', + 'external_connection.write' => 'Изменение адреса или секрета внешнего подключения', ); protected static $required = array( 'POST /modules/archive/install' => array('module.code', 'install_module_code'), diff --git a/adminx/assets/css/adminkit.css b/adminx/assets/css/adminkit.css index 67c68ce..be85561 100644 --- a/adminx/assets/css/adminkit.css +++ b/adminx/assets/css/adminkit.css @@ -537,6 +537,16 @@ hr { .notif-head b { font-size: 13px; } +.notif-head-actions { + display: flex; + align-items: center; + gap: 6px; +} +.notif-head-actions .btn { + width: 32px; + height: 32px; + min-height: 32px; +} .notif-list { max-height: 60vh; overflow-y: auto; @@ -591,6 +601,66 @@ hr { margin-bottom: 6px; color: var(--color-success); } +.notif-settings { + max-height: 60vh; + overflow-y: auto; + padding: 6px; + background: var(--background-subtle); + border-top: 1px solid var(--border-default); +} +.notif-settings-copy { + padding: 8px 8px 6px; +} +.notif-settings-copy b, +.notif-settings-copy small { + display: block; +} +.notif-settings-copy b { + font-size: 12px; +} +.notif-settings-copy small { + margin-top: 2px; + color: var(--text-muted); + font-size: 11px; + line-height: 1.35; + text-wrap: pretty; +} +.notif-setting-row { + display: grid; + grid-template-columns: minmax(0, 1fr) 44px; + align-items: center; + gap: 10px; + min-height: 56px; + padding: 8px; + border-radius: var(--radius-sm); + background: var(--surface-card); + cursor: pointer; +} +.notif-setting-row + .notif-setting-row { + margin-top: 5px; +} +.notif-setting-row > span:first-child { + min-width: 0; +} +.notif-setting-row b, +.notif-setting-row small { + display: block; +} +.notif-setting-row b { + font-size: 12.5px; +} +.notif-setting-row small { + margin-top: 2px; + color: var(--text-muted); + font-size: 10.5px; + line-height: 1.3; + text-wrap: pretty; +} +.notif-setting-row .switch { + min-width: 40px; + min-height: 40px; + justify-content: center; +} /* Заголовок в меню пользователя */ #userMenu > .dropdown-menu { width: 280px; @@ -3137,18 +3207,27 @@ table.table { gap: 12px; padding: var(--space-5) var(--space-5) 0; } +.modal-header > div { + min-width: 0; + flex: 1 1 auto; +} .modal-header h3 { flex: 1; } .modal-close { + display: grid; + flex: 0 0 32px; + width: 32px; + height: 32px; + place-items: center; border: none; background: none; cursor: pointer; color: var(--text-muted); font-size: 18px; - padding: 4px; + padding: 0; border-radius: var(--radius-xs); - margin: -4px -4px 0 0; + margin: -4px -4px 0 auto; } .modal-close:hover { color: var(--text-primary); diff --git a/adminx/assets/js/adminx.js b/adminx/assets/js/adminx.js index f2de9b9..dc06f16 100644 --- a/adminx/assets/js/adminx.js +++ b/adminx/assets/js/adminx.js @@ -488,6 +488,53 @@ } }; + // ------------------------------------------------------------------ // + // Видимость отдельных элементов в колокольчике. + // ------------------------------------------------------------------ // + Adminx.NotificationPreferences = { + init: function () { + document.addEventListener('click', function (event) { + var toggle = event.target.closest('[data-notification-settings-toggle]'); + if (!toggle) { return; } + event.preventDefault(); + event.stopPropagation(); + var menu = toggle.closest('.notif-menu'); + var panel = menu ? menu.querySelector('[data-notification-settings]') : null; + var list = menu ? menu.querySelector('.notif-list') : null; + if (!panel) { return; } + var opening = panel.hidden; + panel.hidden = !opening; + if (list) { list.hidden = opening; } + toggle.setAttribute('aria-expanded', opening ? 'true' : 'false'); + }); + + document.addEventListener('change', function (event) { + var input = event.target.closest('[data-notification-preference]'); + if (!input) { return; } + var previous = !input.checked; + var data = new FormData(); + data.append('module_code', input.getAttribute('data-module-code') || ''); + data.append('key', input.getAttribute('data-key') || ''); + data.append('enabled', input.checked ? '1' : '0'); + input.disabled = true; + Adminx.Ajax.post(Adminx.base() + '/settings/notifications', data).then(function (payload) { + var response = Adminx.Ajax.handle(payload); + if (!payload.ok || response.success === false) { + input.checked = previous; + return; + } + + window.location.reload(); + }).catch(function () { + input.checked = previous; + Adminx.Toast.show(Adminx.t('server_unavailable', 'Сервер недоступен. Повторите попытку.'), 'error'); + }).then(function () { + input.disabled = false; + }); + }); + } + }; + // ------------------------------------------------------------------ // // Меню пользователя в подвале сайдбара. Сайдбар имеет overflow:hidden, // поэтому всплывающее меню позиционируем fixed вручную (над триггером), @@ -977,6 +1024,7 @@ Adminx.Sidebar.init(); Adminx.Theme.init(); Adminx.Dropdown.init(); + Adminx.NotificationPreferences.init(); Adminx.SidebarUser.init(); Adminx.Tabs.init(); Adminx.Drawer.init(); diff --git a/adminx/index.php b/adminx/index.php index ac24ebc..6629334 100644 --- a/adminx/index.php +++ b/adminx/index.php @@ -62,6 +62,7 @@ //-- Ядро: автозагрузчик, БД, сессии, настройки, Twig, хуки. include BASEPATH . DS . 'system' . DS . 'bootstrap.php'; App::init(); + \App\Common\ResponseSecurityHeaders::apply(array('http://127.0.0.1:*', 'http://localhost:*')); use App\Common\AdminAssets; use App\Common\Auth; @@ -207,6 +208,20 @@ } } + if (isset($navFlat['modules']['children'])) { + usort($navFlat['modules']['children'], function ($left, $right) { + $leftRegistry = isset($left['code']) && (string) $left['code'] === 'modules_registry'; + $rightRegistry = isset($right['code']) && (string) $right['code'] === 'modules_registry'; + if ($leftRegistry !== $rightRegistry) { return $leftRegistry ? -1 : 1; } + $leftLabel = isset($left['label']) ? (string) $left['label'] : ''; + $rightLabel = isset($right['label']) ? (string) $right['label'] : ''; + $leftCyrillic = preg_match('/^\p{Cyrillic}/u', $leftLabel) === 1; + $rightCyrillic = preg_match('/^\p{Cyrillic}/u', $rightLabel) === 1; + if ($leftCyrillic !== $rightCyrillic) { return $leftCyrillic ? -1 : 1; } + return strnatcasecmp($leftLabel, $rightLabel); + }); + } + $commandPaletteItems = array(); foreach ($navigationItems as $navItem) { if (!isset($navItem['url']) || (string) $navItem['url'] === '#') { continue; } @@ -249,7 +264,9 @@ //-- Сводка уведомлений в шапке (новые заказы/письма) — только для админа. $notifications = array('total' => 0, 'orders' => 0, 'messages' => 0, 'items' => array()); + $notificationPreferences = array(); if ($canAdmin) { + $notificationPreferences = \App\Adminx\Support\ModuleExtensions::notificationPreferences(); $notificationKey = 'adminx.notifications.' . (int) Auth::id() . '.' . md5((string) ADMINX_BASE); $notifications = Cache::remember($notificationKey, 15, function () { $summary = \App\Adminx\Support\Notifications::summary(ADMINX_BASE); @@ -257,7 +274,11 @@ }); } - Twig::addGlobal('notifications', $notifications); + Twig::addGlobals(array( + 'notifications' => $notifications, + 'notification_preferences' => $notificationPreferences, + 'can_manage_notification_preferences' => Permission::check('manage_settings'), + )); if (!$isPublic && !$canAdmin) { if (Request::isAjax()) { diff --git a/adminx/language/en/client.xml b/adminx/language/en/client.xml index 3efc7c8..192be64 100644 --- a/adminx/language/en/client.xml +++ b/adminx/language/en/client.xml @@ -7,6 +7,7 @@ Cancel Close Confirm + Create Delete Save Show @@ -34,6 +35,7 @@ Theme Loading... General + Name Nothing found Remove Search diff --git a/adminx/language/ru/client.xml b/adminx/language/ru/client.xml index 39dcd6e..2baf20c 100644 --- a/adminx/language/ru/client.xml +++ b/adminx/language/ru/client.xml @@ -7,6 +7,7 @@ Отмена Закрыть Подтвердить + Создать Удалить Сохранить Показать @@ -34,6 +35,7 @@ Тема Загрузка... Основное + Название Ничего не найдено Убрать Поиск diff --git a/adminx/modules/Catalog/Controller.php b/adminx/modules/Catalog/Controller.php index 9eaa334..eb67b11 100644 --- a/adminx/modules/Catalog/Controller.php +++ b/adminx/modules/Catalog/Controller.php @@ -121,6 +121,8 @@ if (!Permission::check('view_products')) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403); } if (!Model::hasCommerceCatalogs()) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Товарные каталоги не настроены'), 404); } $this->assets(); + $state = Request::getStr('state', 'active'); + if (!in_array($state, array('active', 'inactive', 'all'), true)) { $state = 'active'; } $stats = Model::productQualityStats(); $issues = Model::productQualityIssues($stats); $allowed = array(); @@ -130,11 +132,15 @@ $filters = array( 'q' => Request::getStr('q', ''), 'issue' => $issue, + 'state' => $state, 'page' => Request::getInt('page', 1), 'per_page' => Request::getInt('per_page', 25), 'include_quality' => true, - 'quality_scope' => true, + 'quality_scope' => $state === 'active', ); + $aiModule = ModuleManager::get('ai_assistant'); + $aiAvailable = $aiModule && !empty($aiModule['installed']) && !empty($aiModule['enabled']) + && class_exists('\App\Modules\AiAssistant\Repository') && Permission::check('manage_ai_assistant'); return $this->render('@products/product-quality.twig', array( 'result' => Model::products($filters), 'stats' => $stats, @@ -142,6 +148,8 @@ 'filters' => $filters, 'saved_views' => SavedViews::all('catalog_quality', Auth::id(), $this->savedViewFields('quality')), 'can_manage' => Permission::check('manage_products'), + 'can_toggle' => Permission::check('manage_products') && Permission::check('manage_documents'), + 'ai_available' => (bool) $aiAvailable, )); } @@ -1367,6 +1375,7 @@ 'shipping_profile' => $shippingProfile, 'package_templates' => ShippingPackageTemplates::all(), 'native_attributes' => $nativeAttributes, + 'product_context_links' => EditorContextLinks::product($document, $nativeAttributes), 'product_readiness' => ProductReadiness::build($product, $nativeAttributes, $shippingProfile, $variantGroup), 'product_promotions_available' => Promotions::available(), 'product_promotions' => Promotions::forProduct($id), @@ -1692,9 +1701,12 @@ $settings['filters_default_ids'] = $this->ids(isset($settings['filters_default']) ? $settings['filters_default'] : ''); $styles = @unserialize(isset($settings['filters_default_settings']) ? (string) $settings['filters_default_settings'] : '', array('allowed_classes' => false)); $settings['filter_styles'] = is_array($styles) ? $styles : array(); + $flat = Model::items($rubricId, $fieldId); return $this->render('@catalog/edit.twig', array( 'catalog' => $catalog, 'settings' => $settings, - 'tree' => Model::tree($rubricId, $fieldId), 'flat' => Model::items($rubricId, $fieldId), + 'tree' => Model::tree($rubricId, $fieldId), 'flat' => $flat, + 'source_options' => Model::sourceOptions($rubricId, $fieldId), + 'source_schema_available' => Model::sourceItemsAvailable(), 'fields' => $fields, 'field_groups' => array_values($fieldGroups), 'request_options' => Model::requestOptions(), @@ -1704,15 +1716,44 @@ 'filter_template_options' => $filterTemplatesAvailable ? FilterTemplates::options() : array(), 'filter_templates_available' => $filterTemplatesAvailable, 'products_available' => $productsAvailable, + 'product_roles' => $productsAvailable ? \App\Adminx\Packages\Products\ProductRoleSettings::view($rubricId, $settings, Request::getInt('product_preview', 0)) : array(), 'can_manage' => Permission::check('manage_catalog'), )); } + public function productPreview(array $params = array()) + { + $rubricId = isset($params['rubric']) ? (int) $params['rubric'] : 0; + $fieldId = isset($params['field']) ? (int) $params['field'] : 0; + if (!Permission::check('view_catalog') || !$this->productsAvailable() + || !\App\Adminx\Packages\Products\ProductRoleSettings::canReadRubric($rubricId)) { + return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403); + } + + $catalog = Model::catalog($rubricId, $fieldId); + if (!$catalog) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Каталог не найден'), 404); } + $settings = Model::settings($rubricId, $fieldId); + $selection = array(); + foreach (array('context', 'category', 'feed', 'scope', 'query', 'source', 'limit') as $key) { + $selection[$key] = Request::getStr('selection_' . $key, $key === 'context' ? 'catalog' : ($key === 'limit' ? '8' : '')); + } + + return $this->partial('@catalog/_product-role-preview.twig', array( + 'catalog' => $catalog, 'settings' => $settings, + 'product_roles' => \App\Adminx\Packages\Products\ProductRoleSettings::view($rubricId, $settings, + Request::getInt('product_preview', 0), $selection, Request::getInt('selection_check', 0) === 1), + )); + } + public function item(array $params = array()) { if (!Permission::check('view_catalog')) { return $this->error('Недостаточно прав', array(), 403); } $item = Model::item(isset($params['id']) ? (int) $params['id'] : 0); - if ($item) { $item['condition_context'] = Model::conditionContext($item['id']); } + if ($item) { + $item['condition_context'] = Model::conditionContext($item['id']); + $item['context_links'] = EditorContextLinks::section($item, $this->productsAvailable()); + } + return $item ? $this->success('', array('data' => $item)) : $this->error('Раздел не найден', array(), 404); } @@ -1818,6 +1859,7 @@ return $this->success($id > 0 ? 'Раздел сохранён' : 'Раздел создан', array('data' => array( 'id' => $saved, 'condition_context' => Model::conditionContext($saved), + 'context_links' => EditorContextLinks::section(Model::item($saved), $this->productsAvailable()), ))); } @@ -1893,7 +1935,7 @@ protected function savedViewFields($scope) { return $scope === 'quality' - ? array('q', 'issue', 'per_page') + ? array('q', 'issue', 'state', 'per_page') : array('q', 'state', 'issue', 'payment', 'per_page', 'view'); } @@ -1934,6 +1976,7 @@ { AdminAssets::addStyle($this->base() . '/modules/Catalog/assets/catalog.css', 50); AdminAssets::addStyle($this->base() . '/modules/Catalog/assets/catalog-drawer.css', 51); + AdminAssets::addStyle($this->base() . '/modules/Catalog/assets/context-links.css', 52); AdminAssets::addScript($this->base() . '/modules/Catalog/assets/catalog.js', 50); if ($this->productsAvailable()) { if (is_file(BASEPATH . '/modules/products/admin/assets/products.css')) { diff --git a/adminx/modules/Catalog/EditorContextLinks.php b/adminx/modules/Catalog/EditorContextLinks.php new file mode 100644 index 0000000..779a2c0 --- /dev/null +++ b/adminx/modules/Catalog/EditorContextLinks.php @@ -0,0 +1,159 @@ + + | @copyright 2007-2026 (c) AVE.cms + | @link https://ave-cms.ru + | @version 3.3 + */ + + namespace App\Adminx\Catalog; + + defined('BASEPATH') || die('Direct access to this location is not allowed.'); + + use App\Adminx\Support\AdminLocale; + use App\Common\AdminLocation; + use App\Common\Permission; + use App\Content\CatalogTables; + use App\Content\ContentTables; + use App\Content\Presentation\PresentationRenderer; + use App\Helpers\Html; + use App\Modules\Products\CardSettings; + use App\Modules\Products\CardTemplateRepository; + use DB; + + /** Saved editor context, linking to HTML parent pages rather than JSON drawer endpoints. */ + class EditorContextLinks + { + public static function product(array $document, array $attributes) + { + $links = static::page($document); + $sections = DB::query( + 'SELECT DISTINCT i.id,i.rubric_id,i.field_id,i.name,i.filter_runtime FROM %b cp JOIN %b i ON i.id=cp.catalog_item_id WHERE cp.product_id=%i ORDER BY i.name,i.id', + CatalogTables::table('catalog_category_products'), CatalogTables::table('module_catalog_items'), (int) $document['Id'] + )->getAll() ?: array(); + $sets = array(); + foreach ($attributes['groups'] as $group) { + $id = (int) $group['set_id']; + if (isset($sets[$id])) { continue; } + $sets[$id] = true; + static::add($links, 'attribute_set', '/catalog/attribute-sets/' . $id, array(), 'view_products', $group['set_name']); + } + + if (!$sections) { static::card($links, (int) $document['rubric_id'], array(), array()); } + $catalogSettings = array(); + foreach ($sections as $section) { + $key = (int) $section['rubric_id'] . ':' . (int) $section['field_id']; + if (!isset($catalogSettings[$key])) { + $catalogSettings[$key] = Model::settings((int) $section['rubric_id'], (int) $section['field_id']); + } + + static::filters($links, $section); + static::card($links, (int) $document['rubric_id'], $section, $catalogSettings[$key]); + } + + return $links; + } + + public static function section(array $section, $productsAvailable = true) + { + $document = !empty($section['document_id']) ? DB::query( + 'SELECT Id,rubric_id,rubric_tmpl_id FROM %b WHERE Id=%i LIMIT 1', + ContentTables::table('documents'), (int) $section['document_id'] + )->getAssoc() : array(); + $links = $document ? static::page($document) : array(); + static::filters($links, $section, $productsAvailable); + if (!$productsAvailable) { return $links; } + $settings = Model::settings((int) $section['rubric_id'], (int) $section['field_id']); + if (!empty($section['attribute_set_id'])) { + static::add($links, 'attribute_set', '/catalog/attribute-sets/' . (int) $section['attribute_set_id'], array(), 'view_products', '#' . (int) $section['attribute_set_id']); + } + + if (isset($settings['purpose']) && $settings['purpose'] === 'commerce') { + static::card($links, (int) $section['rubric_id'], $section, $settings); + } + + return $links; + } + + protected static function filters(array &$links, array $section, $productsAvailable = true) + { + $native = isset($section['filter_runtime']) && $section['filter_runtime'] === 'native'; + if ($native && !$productsAvailable) { return; } + if ($native) { + static::add($links, 'filters', '/catalog/attributes', array('view' => 'filters', 'section_id' => (int) $section['id']), 'view_products', $section['name']); + } elseif (static::allowed('manage_catalog')) { + static::add($links, 'filters', '/catalog/' . (int) $section['rubric_id'] . '/' . (int) $section['field_id'], array('item' => (int) $section['id'], 'tab' => 'filters'), 'view_catalog', $section['name']); + } + } + + protected static function card(array &$links, $rubricId, array $section, array $settings) + { + $targets = array('rubric' => (string) $rubricId, 'module' => 'products'); + if (!empty($section['rubric_id'])) { $targets['rubric'] = (string) $section['rubric_id']; } + if (!empty($section['id'])) { $targets['catalog'] = (string) $section['id']; } + if (!empty($settings['request_id'])) { $targets['request'] = (string) $settings['request_id']; } + $scope = isset($section['name']) ? $section['name'] : ''; + $presentation = static::presentation($targets); + if ($presentation && trim((string) $presentation['published_item_markup']) !== '') { + if (static::allowed('manage_public_presentations')) { + static::add($links, 'card', '/public-site/presentations', array('edit' => (int) $presentation['presentation_id']), 'view_public_site', $scope); + } + + return; + } + + $template = static::cardTemplate($rubricId); + if ($template && trim((string) $template['published_markup']) !== '') { + if (static::allowed('manage_products')) { + static::add($links, 'card', '/catalog/card-templates', array('edit' => (int) $template['id']), 'view_products', $scope); + } + } else { + static::add($links, 'card', '/catalog/public-templates', array('template' => 'product-card'), 'view_products', $scope); + } + } + + protected static function page(array $document) + { + $links = array(); + if (!static::allowed('view_rubrics') || !static::allowed('manage_rubrics')) { return $links; } + $query = array('templates' => (int) $document['rubric_id']); + $alternate = !empty($document['rubric_tmpl_id']) ? static::alternate($document) : null; + // DocumentRepository falls back to the rubric template when the alternate is empty. + if ($alternate && trim((string) $alternate['template']) !== '') { $query['template'] = (int) $alternate['id']; } + static::add($links, 'page', '/rubrics', $query, 'view_rubrics'); + return $links; + } + + protected static function alternate(array $document) + { + return DB::query('SELECT id,template FROM %b WHERE id=%i AND rubric_id=%i LIMIT 1', ContentTables::table('rubric_templates'), (int) $document['rubric_tmpl_id'], (int) $document['rubric_id'])->getAssoc(); + } + + protected static function presentation(array $targets) { return PresentationRenderer::resolve('content_list', $targets); } + + protected static function cardTemplate($rubricId) + { + $settings = CardSettings::forRubric($rubricId); + return CardTemplateRepository::published((int) $settings['template_id']); + } + + protected static function allowed($permission) { return Permission::check($permission); } + + protected static function add(array &$links, $kind, $path, array $query, $permission, $scope = '') + { + if (!static::allowed($permission)) { return; } + $url = AdminLocation::url($path); + $links[] = array( + 'kind' => $kind, + 'url' => $query ? Html::appendQuery($url, $query) : $url, + 'label' => AdminLocale::text('catalog_context_' . $kind), + 'scope' => (string) $scope, + ); + } + } diff --git a/adminx/modules/Catalog/Model.php b/adminx/modules/Catalog/Model.php index 795f55b..0247f2a 100644 --- a/adminx/modules/Catalog/Model.php +++ b/adminx/modules/Catalog/Model.php @@ -23,10 +23,14 @@ use App\Common\DatabaseSchema; use App\Common\FileCacheInvalidator; use App\Common\ModuleManager; + use App\Common\ModuleSettings; + use App\Common\Registry; + use App\Modules\Products\ProductRoles; use App\Content\CatalogTables; use App\Content\ContentTables; use App\Content\Fields\FieldValueCodec; use App\Content\Requests\RequestConditionValue; + use App\Frontend\Feeds\Service as FeedService; use App\Helpers\Json; use App\Adminx\Rubrics\Model as RubricsModel; use App\Adminx\Documents\Model as DocumentsModel; @@ -146,8 +150,21 @@ } public static function saveSettings($rubricId, $fieldId, array $input) + { + if (DB::$transaction_in_progress) { + throw new \LogicException('Catalog settings must be saved outside an existing transaction'); + } + + $throwOnError = DB::$throw_exception_on_error; + DB::$throw_exception_on_error = true; + try { return self::saveSettingsOwned($rubricId, $fieldId, $input); } + finally { DB::$throw_exception_on_error = $throwOnError; } + } + + protected static function saveSettingsOwned($rubricId, $fieldId, array $input) { self::requireCatalog($rubricId, $fieldId); + $existing = self::settings($rubricId, $fieldId); $data = array(); $data['purpose'] = isset($input['purpose']) && $input['purpose'] === 'commerce' ? 'commerce' : 'content'; if ($data['purpose'] === 'commerce' && !self::productsAvailable()) { @@ -162,7 +179,24 @@ throw new \InvalidArgumentException('У рубрики уже есть товарный каталог'); } - foreach(array('product_title_field_id','product_article_field_id','product_price_field_id','product_old_price_field_id','product_stock_field_id','product_images_field_id') as $key){$data[$key]=isset($input[$key])?max(0,(int)$input[$key]):0;if($data[$key]&&!DB::query('SELECT Id FROM '.self::fieldsTable().' WHERE Id=%i AND rubric_id=%i LIMIT 1',$data[$key],(int)$rubricId)->getValue()){throw new \InvalidArgumentException('Коммерческое поле не принадлежит рубрике');}} + foreach (array('product_title_field_id','product_article_field_id','product_price_field_id','product_old_price_field_id','product_stock_field_id','product_images_field_id') as $key) { + $data[$key] = isset($input[$key]) ? max(0, (int) $input[$key]) : (int) $existing[$key]; + if (isset($input[$key]) && $data[$key] && !DB::query('SELECT Id FROM ' . self::fieldsTable() . ' WHERE Id=%i AND rubric_id=%i LIMIT 1', $data[$key], (int) $rubricId)->getValue()) { + throw new \InvalidArgumentException('Коммерческое поле не принадлежит рубрике'); + } + } + + $roleInput = null; + if ($data['purpose'] === 'commerce' && isset($input['product_roles']) && is_array($input['product_roles'])) { + $roleFields = ProductRoles::fields($rubricId); + $roleInput = array_replace(ProductRoles::configured($rubricId), ProductRoles::validate($input['product_roles'], $roleFields)); + $roleSettings = self::settings($rubricId, $fieldId); + $resolved = ProductRoles::resolve($roleFields, $roleSettings, $roleInput); + foreach (ProductRoles::definitions() as $role => $definition) { + if (isset($definition['column'])) { $data[$definition['column']] = $resolved[$role]; } + } + } + $data['product_card_template_id'] = isset($input['product_card_template_id']) ? max(0, (int) $input['product_card_template_id']) : 0; if ($data['product_card_template_id'] > 0 && !DB::query( 'SELECT id FROM ' . CatalogTables::table('catalog_card_templates') . ' WHERE id=%i LIMIT 1', @@ -215,30 +249,41 @@ ); } - $existing = self::settings($rubricId, $fieldId); - if ((int) $existing['id'] > 0) { - DB::Update(self::settingsTable(), $data, 'id = %i', (int) $existing['id']); - if ($data['purpose'] !== $existing['purpose'] && self::productsAvailable()) { - try { ProductIndexer::rebuild(); } - catch (\Throwable $e) { DB::Update(self::settingsTable(), array('purpose'=>$existing['purpose']), 'id=%i', (int) $existing['id']); throw $e; } + $reindex = $data['purpose'] !== $existing['purpose']; + foreach ($data as $key => $value) { + if (preg_match('/^product_.*_field_id$/', $key) && (int) $value !== (int) $existing[$key]) { $reindex = true; } + } + + if ($roleInput !== null && $resolved !== ProductRoles::resolve($roleFields, $existing, ProductRoles::configured($rubricId))) { $reindex = true; } + $registrySettings = Registry::get('settings'); + DB::startTransaction(); + try { + $settingsId = (int) $existing['id']; + if ($settingsId > 0) { + DB::Update(self::settingsTable(), $data, 'id=%i', $settingsId); + } else { + $data['rubric_id'] = (int) $rubricId; + $data['field_id'] = (int) $fieldId; + DB::Insert(self::settingsTable(), $data); + $settingsId = (int) DB::insertId(); } - Cache::forgetTag(CacheKey::tag('catalog')); - FileCacheInvalidator::publicPresentation(); - return (int) $existing['id']; + if ($roleInput !== null) { ModuleSettings::set('field_roles_' . (int) $rubricId, $roleInput, 'products', 'json'); } + if (self::productsAvailable()) { ProductRoles::reset(); } + if ($reindex && self::productsAvailable()) { ProductIndexer::reindexRubric($rubricId, $existing['purpose'] === 'commerce' && $data['purpose'] === 'commerce'); } + DB::afterCommit(function () { + DB::clearTags(array('settings')); + Cache::forgetTag(CacheKey::tag('catalog')); + FileCacheInvalidator::publicPresentation(); + }); + DB::commit(); + } catch (\Throwable $e) { + DB::rollback(); + Registry::set('settings', $registrySettings); + if (self::productsAvailable()) { ProductRoles::reset(); } + throw $e; } - $data['rubric_id'] = (int) $rubricId; - $data['field_id'] = (int) $fieldId; - DB::Insert(self::settingsTable(), $data); - $settingsId = (int) DB::insertId(); - if ($data['purpose'] === 'commerce') { - try { ProductIndexer::rebuild(); } - catch (\Throwable $e) { DB::Delete(self::settingsTable(), 'id=%i', $settingsId); throw $e; } - } - - Cache::forgetTag(CacheKey::tag('catalog')); - FileCacheInvalidator::publicPresentation(); return $settingsId; } @@ -358,6 +403,33 @@ return $tree; } + public static function sourceOptions($rubricId, $fieldId) + { + $items = self::items($rubricId, $fieldId); + $map = array(); + foreach ($items as $item) { $map[(int) $item['id']] = $item; } + $out = array(); + foreach ($items as $item) { + $parts = array((string) $item['name']); + $parentId = (int) $item['parent_id']; + $visited = array((int) $item['id'] => true); + while ($parentId > 0 && isset($map[$parentId]) && empty($visited[$parentId])) { + $visited[$parentId] = true; + array_unshift($parts, (string) $map[$parentId]['name']); + $parentId = (int) $map[$parentId]['parent_id']; + } + + $out[] = array( + 'id' => (int) $item['id'], + 'name' => (string) $item['name'], + 'path' => implode(' / ', $parts), + 'status' => (int) $item['status'], + ); + } + + return $out; + } + public static function item($id) { $row = DB::query( @@ -385,6 +457,12 @@ if ($documentId > 0 && !self::documentExists($documentId)) { throw new \RuntimeException('Выбранный документ не найден'); } $filterIds = self::ids(isset($input['filters_use']) ? $input['filters_use'] : array()); $filterIds = self::orderedIds(isset($input['filters_order']) ? $input['filters_order'] : array(), $filterIds); + $sourceIds = self::validSourceItemIds( + isset($input['source_item_ids']) ? $input['source_item_ids'] : array(), + $id, + $rubricId, + $fieldId + ); $data = array( 'rubric_id' => (int) $rubricId, 'field_id' => (int) $fieldId, 'name' => $name, 'parent_id' => $parentId, @@ -396,6 +474,12 @@ 'filters_use' => implode(',', $filterIds), 'filters_settings' => serialize(self::filterStyles(isset($input['filter_style']) ? $input['filter_style'] : array(), $filterIds)), ); + if (self::sourceItemsAvailable()) { + $data['source_item_ids'] = implode(',', $sourceIds); + } elseif ($sourceIds) { + throw new \RuntimeException('Сначала примените миграцию каталога для источников товаров'); + } + if ($id > 0) { DB::Update(self::itemsTable(), $data, 'id = %i', $id); } else { @@ -608,7 +692,7 @@ return array( 'total'=>0,'quality_total'=>0,'active'=>0,'inactive'=>0,'without_category'=>0, 'without_image'=>0,'without_article'=>0,'without_price'=>0,'without_stock'=>0, - 'without_excerpt'=>0,'without_seo'=>0,'stale'=>0,'shipping_incomplete'=>0, + 'without_description'=>0,'without_seo'=>0,'stale'=>0,'shipping_incomplete'=>0, 'shipping_disabled'=>0,'duplicate_articles'=>0,'legacy_attributes'=>0, 'filter_index_errors'=>0,'registration_incomplete'=>0,'without_video'=>0, 'variant_errors'=>0,'description_markup'=>0,'sfr'=>0,'issues'=>0,'recommendations'=>0,'indexed_at'=>0, @@ -1084,6 +1168,7 @@ protected static function clearCache() { DB::clearTags(array('documents', 'requests', 'modules', 'catalog')); + (new FeedService())->clearAll(); Cache::forgetTag(CacheKey::tag('catalog')); } @@ -1124,6 +1209,28 @@ protected static function idsString($value) { return implode(',', self::ids($value)); } + public static function sourceItemsAvailable() + { + return DatabaseSchema::columnExists(self::itemsTable(), 'source_item_ids'); + } + + protected static function validSourceItemIds($value, $itemId, $rubricId, $fieldId) + { + $ids = array_values(array_diff(self::ids($value), array((int) $itemId))); + if (!$ids) { return array(); } + $rows = DB::query( + 'SELECT id FROM ' . self::itemsTable() + . ' WHERE rubric_id=%i AND field_id=%i AND id IN (' . implode(',', $ids) . ')', + (int) $rubricId, + (int) $fieldId + )->getAll() ?: array(); + $allowed = array(); + foreach ($rows as $row) { $allowed[(int) $row['id']] = true; } + return array_values(array_filter($ids, function ($id) use ($allowed) { + return isset($allowed[(int) $id]); + })); + } + protected static function orderedIds($order, array $selected) { $selectedMap = array_fill_keys($selected, true); $out = array(); @@ -1164,6 +1271,7 @@ 'level' => (int) $row['level'], 'position' => (int) $row['position'], 'fields_use' => self::ids(isset($row['fields_use']) ? $row['fields_use'] : ''), 'filters_use' => self::ids(isset($row['filters_use']) ? $row['filters_use'] : ''), + 'source_item_ids' => self::ids(isset($row['source_item_ids']) ? $row['source_item_ids'] : ''), 'filter_styles' => is_array($styles) ? $styles : array(), 'attribute_set_id' => isset($row['attribute_set_id']) ? (int) $row['attribute_set_id'] : 0, 'attributes_runtime' => isset($row['attributes_runtime']) ? (string) $row['attributes_runtime'] : 'legacy', diff --git a/adminx/modules/Catalog/assets/catalog-drawer.css b/adminx/modules/Catalog/assets/catalog-drawer.css index dfedb84..33147b7 100644 --- a/adminx/modules/Catalog/assets/catalog-drawer.css +++ b/adminx/modules/Catalog/assets/catalog-drawer.css @@ -739,6 +739,45 @@ grid-template-columns: minmax(0, 1fr) 40px; gap: 6px; } +.catalog-source-picker { + display: grid; + grid-template-columns: minmax(0, 1fr) 40px; + gap: 6px; +} +.catalog-source-picker [data-catalog-source-inputs] { + display: none; +} +.catalog-source-value { + display: flex; + align-items: center; + gap: 9px; + min-width: 0; + min-height: 40px; + padding: 8px 11px; + border: 1px solid var(--border-default); + border-radius: var(--radius-md); + background: var(--background-card); + color: var(--text-primary); + text-align: left; + cursor: pointer; +} +.catalog-source-value:hover:not(:disabled) { + border-color: var(--blue-300); + background: var(--blue-50); +} +.catalog-source-value:disabled { + opacity: 0.6; + cursor: not-allowed; +} +.catalog-source-value i { + flex: 0 0 auto; + color: var(--blue-600); +} +.catalog-source-value span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} .catalog-document-value { display: flex; align-items: center; @@ -832,6 +871,74 @@ font-size: 11px; font-style: normal; } +.catalog-source-modal { + width: min(66.666vw, 1000px); + height: min(72vh, 760px); + max-height: calc(100vh - 40px); +} +.catalog-source-modal .modal-body { + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; +} +.catalog-source-modal .modal-header > div { + flex: 1 1 auto; + min-width: 0; +} +.catalog-source-modal .modal-header p { + margin: 3px 0 0; +} +.catalog-source-options { + display: grid; + flex: 1 1 auto; + align-content: start; + gap: 6px; + min-height: 0; + margin-top: 12px; + overflow-y: auto; +} +.catalog-source-option { + display: grid; + grid-template-columns: 20px minmax(0, 1fr) auto; + align-items: center; + gap: 10px; + min-height: 54px; + padding: 8px 11px; + border: 1px solid var(--border-default); + border-radius: var(--radius-md); + background: var(--background-card); + cursor: pointer; +} +.catalog-source-option:hover { + border-color: var(--blue-300); + background: var(--blue-50); +} +.catalog-source-option[hidden] { + display: none; +} +.catalog-source-option > span { + display: grid; + min-width: 0; +} +.catalog-source-option b, +.catalog-source-option small { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.catalog-source-option small { + color: var(--text-secondary); + font-size: 11px; +} +.catalog-source-option em { + font-style: normal; +} +.catalog-source-empty { + padding: 28px 0; + color: var(--text-secondary); + text-align: center; +} @media (max-width: 640px) { .catalog-condition-toolbar { align-items: stretch; @@ -885,6 +992,16 @@ .catalog-document-result em { display: none; } + .catalog-source-modal { + width: calc(100vw - 20px); + height: calc(100vh - 20px); + } + .catalog-source-option { + grid-template-columns: 20px minmax(0, 1fr); + } + .catalog-source-option em { + display: none; + } } @media (max-width: 1100px) { .catalog-behavior-grid { @@ -956,3 +1073,123 @@ height: 40px; } } +.catalog-role-preview { + margin-top: 24px; + min-width: 0; +} +.catalog-role-preview .catalog-selection-preview { + margin: 20px 0; + padding: 16px 0; + border-top: 1px solid var(--border-color, #e3e5e8); + border-bottom: 1px solid var(--border-color, #e3e5e8); +} +.catalog-role-preview .catalog-selection-preview h4 { + margin: 0 0 12px; + font-size: 15px; +} +.catalog-role-preview .catalog-selection-preview [hidden] { + display: none !important; +} +.catalog-role-preview .catalog-selection-controls { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr)); + align-items: end; + gap: 12px; +} +.catalog-role-preview .catalog-selection-controls .field { + min-width: 0; + margin: 0; +} +.catalog-role-preview .catalog-selection-controls .select, +.catalog-role-preview .catalog-selection-controls .input { + width: 100%; + min-width: 0; + min-height: 40px; +} +.catalog-role-preview .catalog-selection-controls .btn { + min-height: 40px; +} +.catalog-role-preview .catalog-selection-result { + margin-top: 16px; +} +.catalog-role-preview .catalog-selection-result ul { + list-style: none; + padding: 0; + margin: 0 0 12px; +} +.catalog-role-preview .catalog-selection-result li { + display: flex; + align-items: baseline; + gap: 8px; + margin: 6px 0; + overflow-wrap: anywhere; +} +.catalog-role-preview .catalog-selection-result li .ti { + flex: 0 0 16px; +} +.catalog-role-preview .catalog-selection-result li span { + min-width: 0; +} +.catalog-role-preview .catalog-selection-result b { + font-variant-numeric: tabular-nums; +} +.catalog-role-preview h3, +.catalog-role-preview p { + margin-bottom: 12px; +} +.catalog-role-preview .table { + table-layout: fixed; + width: 100%; + min-width: 0; +} +.catalog-role-preview th:first-child { + width: 24%; +} +.catalog-role-preview th:nth-child(2) { + width: 32%; +} +.catalog-role-preview td { + white-space: normal; + overflow-wrap: anywhere; + font-variant-numeric: tabular-nums; +} +.catalog-role-preview .btn-group { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; +} +.catalog-role-preview .btn { + white-space: normal; + min-height: 40px; +} +@media (max-width: 760px) { + .catalog-role-preview .table-wrap { + overflow: visible; + } + .catalog-role-preview .table, + .catalog-role-preview tbody, + .catalog-role-preview tr, + .catalog-role-preview td { + display: block; + width: 100%; + } + .catalog-role-preview thead { + display: none; + } + .catalog-role-preview tr { + padding: 12px 0; + border-bottom: 1px solid var(--border); + } + .catalog-role-preview td { + padding: 4px 8px; + border: 0; + } + .catalog-role-preview td:first-child { + font-weight: 600; + } + .catalog-role-preview td[data-label]::before { + content: attr(data-label) ': '; + color: var(--text-secondary); + } +} diff --git a/adminx/modules/Catalog/assets/catalog.js b/adminx/modules/Catalog/assets/catalog.js index 1c3ea6b..766d720 100644 --- a/adminx/modules/Catalog/assets/catalog.js +++ b/adminx/modules/Catalog/assets/catalog.js @@ -2,12 +2,14 @@ 'use strict'; var Adminx = window.Adminx || (window.Adminx = {}); Adminx.Catalog = { - itemForm: null, settingsForm: null, dragItem: null, dragGroup: [], dragPlaceholder: null, dragGhost: null, dragPointerId: null, dragStartX: 0, dragStartY: 0, dragStarted: false, dragOrderSnapshot: '', orderSaving: false, filterOrder: [], itemSaveTimer: null, settingsSaveTimer: null, conditionContext: null, + itemForm: null, settingsForm: null, dragItem: null, dragGroup: [], dragPlaceholder: null, dragGhost: null, dragPointerId: null, dragStartX: 0, dragStartY: 0, dragStarted: false, dragOrderSnapshot: '', orderSaving: false, filterOrder: [], sourceItemIds: [], sourceOptionsData: null, itemSaveTimer: null, settingsSaveTimer: null, conditionContext: null, init: function () { this.itemForm = document.getElementById('catalogItemForm'); this.settingsForm = document.getElementById('catalogSettingsForm'); var self = this; document.addEventListener('click', function (e) { + if (e.target.closest('[data-product-role-refresh]')) { self.refreshRolePreview(false); return; } + if (e.target.closest('[data-product-selection-check]')) { self.refreshRolePreview(true); return; } var pageTab = e.target.closest('[data-catalog-page-tab]'); if (pageTab) { self.tab('[data-catalog-page-tab]', '[data-catalog-page-panel]', pageTab.getAttribute('data-catalog-page-tab')); } var drawerTab = e.target.closest('[data-catalog-drawer-tab]'); @@ -27,6 +29,8 @@ if (treeRow && !e.target.closest('button, input, label, a, select')) { self.editItem(treeRow.getAttribute('data-catalog-builder-item')); return; } if (e.target.closest('[data-catalog-document-pick]')) { self.openDocumentPicker(); } if (e.target.closest('[data-catalog-document-clear]')) { self.setDocument(null); self.scheduleItemSave(); } + if (e.target.closest('[data-catalog-source-open]')) { self.openSourcePicker(); } + if (e.target.closest('[data-catalog-source-clear]')) { self.setSources([]); self.scheduleItemSave(); } var conditionView = e.target.closest('[data-catalog-condition-view]'); if (conditionView) { self.showCondition(conditionView.getAttribute('data-catalog-condition-view')); } var conditionSync = e.target.closest('[data-catalog-condition-sync]'); @@ -37,9 +41,11 @@ if (e.target.closest('[data-catalog-recompile-all]')) { self.recompileAll(); } }); document.addEventListener('input', function (e) { + if (e.target.closest('[data-product-role-preview]')) { self.selectionChanged(); return; } if (e.target.matches('[data-catalog-tree-search]')) { self.search(e.target.value); } }); document.addEventListener('change', function (e) { + if (e.target.closest('[data-product-role-preview]')) { self.selectionChanged(); return; } if (e.target.matches('[data-catalog-item-status]')) { self.setTreeStatus(e.target); return; } if (self.itemForm && e.target.closest('#catalogItemForm')) { if (e.target.matches('[name="filters_use[]"]')) { self.syncFilterOrderSelection(); } @@ -59,6 +65,44 @@ document.addEventListener('pointercancel', function () { self.treeDragEnd(); }); this.normalizeTreeHierarchy(); this.openRequestedItem(); + if (window.location.hash === '#settings') { this.tab('[data-catalog-page-tab]', '[data-catalog-page-panel]', 'settings'); } + }, + selectionChanged: function () { + this.rolePreviewSequence = (this.rolePreviewSequence || 0) + 1; + var root = document.querySelector('[data-product-role-preview]'); + if (!root) { return; } + var context = root.querySelector('[data-selection="context"]'); + root.querySelectorAll('[data-selection-context]').forEach(function (el) { el.hidden = !context || el.getAttribute('data-selection-context') !== context.value; }); + var result = root.querySelector('[data-selection-result]'), stale = root.querySelector('[data-selection-stale]'); + if (result) { result.hidden = true; if (stale) { stale.hidden = false; } } + }, + refreshRolePreview: function (check) { + var input = document.querySelector('[data-product-role-document]'); + if (!input || !input.checkValidity()) { if (input) { input.reportValidity(); } return; } + var root = input.closest('[data-product-role-preview]'); + var invalid = Array.prototype.find.call(root.querySelectorAll('[data-selection-context]:not([hidden]) input'), function (el) { return !el.checkValidity(); }); + if (invalid) { invalid.reportValidity(); return; } + var self = this, sequence = (this.rolePreviewSequence || 0) + 1; + var errorMessage = root.getAttribute('data-error'); + this.rolePreviewSequence = sequence; + var url = new URL(this.url() + '/product-preview', window.location.origin); + url.searchParams.set('product_preview', input.value || '0'); + url.searchParams.set('selection_check', check === true ? '1' : '0'); + root.querySelectorAll('[data-selection]').forEach(function (el) { url.searchParams.set('selection_' + el.getAttribute('data-selection'), el.value); }); + root.setAttribute('aria-busy', 'true'); + root.querySelectorAll('button').forEach(function (el) { el.disabled = true; }); + fetch(url.toString(), { credentials: 'same-origin' }).then(function (response) { + if (!response.ok) { throw new Error(errorMessage); } + return response.text(); + }).then(function (html) { + if (sequence !== self.rolePreviewSequence) { return; } + var preview = new DOMParser().parseFromString(html, 'text/html').querySelector('[data-product-role-preview]'); + var current = document.querySelector('[data-product-role-preview]'); + if (!preview || !current) { throw new Error(errorMessage); } + current.replaceWith(preview); + }).catch(function (error) { if (sequence === self.rolePreviewSequence) { Adminx.Toast.show(error.message, 'error'); } }).finally(function () { + root.removeAttribute('aria-busy'); root.querySelectorAll('button').forEach(function (el) { el.disabled = false; }); + }); }, base: function () { return (this.itemForm || this.settingsForm).getAttribute('data-base'); }, rubric: function () { return (this.itemForm || this.settingsForm).getAttribute('data-rubric'); }, @@ -76,6 +120,7 @@ document.querySelectorAll(panels).forEach(function (el) { el.hidden = el.getAttribute(panels.indexOf('drawer') >= 0 ? 'data-catalog-drawer-panel' : 'data-catalog-page-panel') !== value; }); }, newItem: function (parentId) { + this.renderContextLinks([]); this.itemForm.reset(); this.itemForm.querySelector('[name="id"]').value = ''; this.itemForm.querySelector('[name="status"]').checked = true; this.itemForm.querySelectorAll('[name="parent_id"] option').forEach(function (option) { option.disabled = false; }); var parent = this.itemForm.querySelector('[name="parent_id"]'); @@ -84,7 +129,7 @@ this.checkValues('fields_use[]', defaultFields); this.checkValues('filters_use[]', defaultFilters); defaultFilters.forEach(function (id) { var source = this.settingsForm.querySelector('[name="filter_style[' + id + ']"]'), target = this.itemForm.querySelector('[name="filter_style[' + id + ']"]'); if (source && target) { target.value = source.value; } }, this); this.filterOrder = defaultFilters.slice(); this.syncFilterOrderInput(); - this.setDocument(null); this.setState('item', ''); this.renderConditionContext(null); + this.setDocument(null); this.setSources([]); this.setState('item', ''); this.renderConditionContext(null); document.querySelector('[data-catalog-drawer-title]').textContent = 'Новый раздел'; this.tab('[data-catalog-drawer-tab]', '[data-catalog-drawer-panel]', 'main'); this.updateCounts(); Adminx.Drawer.open('catalogItemDrawer'); }, @@ -105,10 +150,12 @@ this.editItem(itemId, params.get('tab') || 'main'); }, fill: function (item) { + this.renderContextLinks(item.context_links || []); this.itemForm.reset(); this.itemForm.querySelector('[name="id"]').value = item.id || ''; this.itemForm.querySelector('[name="name"]').value = item.name || ''; this.itemForm.querySelectorAll('[name="parent_id"] option').forEach(function (option) { option.disabled = false; }); this.itemForm.querySelector('[name="parent_id"]').value = item.parent_id || 0; this.itemForm.querySelector('[name="status"]').checked = Number(item.status) === 1; this.setDocument(item.document_id ? { id: item.document_id, title: item.document_title || '', alias: item.document_alias || '' } : null); + this.setSources(item.source_item_ids || []); this.filterOrder = (item.filters_use || []).map(function (id) { return String(id); }); this.syncFilterOrderInput(); this.checkValues('fields_use[]', item.fields_use || []); this.checkValues('filters_use[]', item.filters_use || []); Object.keys(item.filter_styles || {}).forEach(function (id) { var select = this.itemForm.querySelector('[name="filter_style[' + id + ']"]'); if (select) { select.value = item.filter_styles[id]; } }, this); var own = this.itemForm.querySelector('[name="parent_id"] option[value="' + item.id + '"]'); if (own) { own.disabled = true; } @@ -125,6 +172,20 @@ syncFilterOrderSelection: function () { var selected = {}, next = []; this.itemForm.querySelectorAll('[name="filters_use[]"]:checked').forEach(function (input) { selected[input.value] = true; }); this.filterOrder.forEach(function (id) { if (selected[id]) { next.push(id); delete selected[id]; } }); this.itemForm.querySelectorAll('[name="filters_use[]"]:checked').forEach(function (input) { if (selected[input.value]) { next.push(input.value); delete selected[input.value]; } }); this.filterOrder = next; this.syncFilterOrderInput(); }, syncFilterOrderInput: function () { var input = this.itemForm && this.itemForm.querySelector('[name="filters_order"]'); if (input) { input.value = this.filterOrder.join(','); } }, openFilterOrder: function () { var self = this; this.syncFilterOrderSelection(); if (this.filterOrder.length < 2) { return; } var overlay = document.createElement('div'), rows = ''; this.filterOrder.forEach(function (id) { var input = self.itemForm.querySelector('[name="filters_use[]"][value="' + id + '"]'), row = input.closest('.catalog-filter-row'), title = row.querySelector('.catalog-choice-copy b').textContent.trim(), type = row.querySelector('.catalog-field-type b').textContent.trim(); rows += '
' + self.esc(title) + '' + self.esc(type) + ' · #' + self.esc(id) + '
'; }); overlay.className = 'overlay catalog-filter-order-overlay'; overlay.innerHTML = ''; document.body.appendChild(overlay); requestAnimationFrame(function () { overlay.classList.add('show'); }); var dragged = null, close = function () { overlay.classList.remove('show'); setTimeout(function () { overlay.remove(); }, 160); }; overlay.addEventListener('dragstart', function (e) { var handle = e.target.closest('.catalog-filter-order-handle'), row = handle ? handle.closest('[data-filter-order-id]') : null; if (!row) { e.preventDefault(); return; } dragged = row; row.classList.add('is-dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', row.getAttribute('data-filter-order-id')); }); overlay.addEventListener('dragover', function (e) { var target = e.target.closest('[data-filter-order-id]'); if (!dragged || !target || target === dragged) { return; } e.preventDefault(); var rect = target.getBoundingClientRect(), reference = e.clientY < rect.top + rect.height / 2 ? target : target.nextSibling; if (reference !== dragged && dragged.nextSibling !== reference) { target.parentNode.insertBefore(dragged, reference); } }); overlay.addEventListener('dragend', function () { if (dragged) { dragged.classList.remove('is-dragging'); dragged = null; } }); overlay.addEventListener('click', function (e) { if (e.target === overlay || e.target.closest('[data-filter-order-close]')) { close(); return; } if (e.target.closest('[data-filter-order-save]')) { self.filterOrder = Array.prototype.map.call(overlay.querySelectorAll('[data-filter-order-id]'), function (row) { return row.getAttribute('data-filter-order-id'); }); self.syncFilterOrderInput(); self.scheduleItemSave(); close(); } }); }, + renderContextLinks: function (links) { + var panel = this.itemForm && this.itemForm.querySelector('[data-catalog-context-links]'); + if (!panel) { return; } + var list = panel.querySelector('nav'); + list.textContent = ''; + (links || []).forEach(function (link) { + var anchor = document.createElement('a'), icon = document.createElement('i'), label = document.createElement('span'); + anchor.href = link.url; anchor.target = '_blank'; anchor.rel = 'noopener'; anchor.setAttribute('data-context-kind', link.kind); + icon.className = 'ti ti-external-link'; icon.setAttribute('aria-hidden', 'true'); label.textContent = link.label; + if (link.scope) { var scope = document.createElement('small'); scope.textContent = link.scope; label.appendChild(scope); } + anchor.appendChild(icon); anchor.appendChild(label); list.appendChild(anchor); + }); + panel.hidden = !list.children.length; + }, renderConditionContext: function (context) { this.conditionContext = context || { request: { id: 0, title: '' }, states: {}, summary: { synced: 0, staged: 0, missing: 0, different: 0, unsupported: 0 } }; var request = this.conditionContext.request || {}, summary = this.conditionContext.summary || {}, requestEl = document.querySelector('[data-catalog-condition-request]'), summaryEl = document.querySelector('[data-catalog-condition-summary]'), bulk = document.querySelector('[data-catalog-conditions-sync]'); @@ -172,16 +233,61 @@ saveItem: function (auto) { var id = this.itemForm.querySelector('[name="id"]').value; var self = this; this.ajax(this.url() + '/items' + (id ? '/' + id : ''), new FormData(this.itemForm), function (payload) { + if (payload.data && payload.data.context_links) { self.renderContextLinks(payload.data.context_links); } if (auto) { self.setState('item', 'Сохранено', 'ok'); self.syncTreeRow(id); if (payload.data && payload.data.condition_context) { self.renderConditionContext(payload.data.condition_context); } return; } Adminx.Toast.show(payload.message, 'success'); if (Adminx.Drawer) { Adminx.Drawer.close('catalogItemDrawer'); } window.location.reload(); }, { quiet: !!auto, fail: function () { self.setState('item', 'Не сохранено', 'error'); } }); }, - saveSettings: function (auto) { var self = this; this.ajax(this.url() + '/settings', new FormData(this.settingsForm), function (payload) { if (auto) { self.setState('settings', 'Сохранено', 'ok'); } else { self.setState('settings', 'Сохранено', 'ok'); Adminx.Toast.show(payload.message, 'success'); } }, { quiet: !!auto, fail: function () { self.setState('settings', 'Не сохранено', 'error'); } }); }, + saveSettings: function (auto) { var self = this; this.ajax(this.url() + '/settings', new FormData(this.settingsForm), function (payload) { self.refreshRolePreview(); if (auto) { self.setState('settings', 'Сохранено', 'ok'); } else { self.setState('settings', 'Сохранено', 'ok'); Adminx.Toast.show(payload.message, 'success'); } }, { quiet: !!auto, fail: function () { self.setState('settings', 'Не сохранено', 'error'); } }); }, setState: function (type, message, state) { var el = document.querySelector('[data-catalog-' + type + '-state]'); if (!el) { return; } el.textContent = message || ''; el.classList.toggle('is-ok', state === 'ok'); el.classList.toggle('is-error', state === 'error'); }, syncTreeRow: function (id) { var row = document.querySelector('[data-catalog-item][data-id="' + id + '"]'); if (!row) { return; } var toggle = row.querySelector('[data-catalog-item-status]'), active = this.itemForm.querySelector('[name="status"]').checked, filterCount = this.itemForm.querySelectorAll('[name="filters_use[]"]:checked').length, name = row.querySelector('.catalog-tree-name b'); if (toggle) { toggle.checked = active; this.updateTreeStatus(toggle); } if (name) { name.textContent = this.itemForm.querySelector('[name="name"]').value || 'Без названия'; } row.setAttribute('data-filter-count', String(filterCount)); var count = row.querySelector('.catalog-tree-count'); if (count) { count.textContent = this.itemForm.querySelectorAll('[name="fields_use[]"]:checked').length + ' / ' + filterCount; } }, updateTreeStatus: function (input) { var item = input.closest('[data-catalog-item]'), active = input.checked, label = item.querySelector('[data-catalog-item-status-label]'), control = input.closest('.switch'); item.classList.toggle('is-inactive', !active); if (label) { label.textContent = active ? 'активен' : 'скрыт'; } input.setAttribute('aria-label', active ? 'Скрыть раздел' : 'Включить раздел'); if (control) { control.setAttribute('data-tooltip', active ? 'Скрыть раздел' : 'Включить раздел'); } }, setTreeStatus: function (input) { var self = this, id = input.getAttribute('data-catalog-item-status'), previous = !input.checked, data = new FormData(); this.updateTreeStatus(input); input.disabled = true; data.append('_csrf', this.csrf()); data.append('status', input.checked ? '1' : '0'); this.ajax(this.url() + '/items/' + encodeURIComponent(id) + '/status', data, function (payload) { input.checked = Number(payload.data.status) === 1; input.disabled = false; self.updateTreeStatus(input); }, { quiet: true, fail: function () { input.checked = previous; input.disabled = false; self.updateTreeStatus(input); } }); }, setDocument: function (item) { if (!this.itemForm) { return; } var input = this.itemForm.querySelector('[name="document_id"]'); var label = this.itemForm.querySelector('[data-catalog-document-label]'); var clear = this.itemForm.querySelector('[data-catalog-document-clear]'); var id = item && item.id ? Number(item.id) : 0; input.value = id || ''; if (label) { label.textContent = id ? ('#' + id + ' · ' + (item.title || item.alias || 'Без названия')) : 'Документ не выбран'; } if (clear) { clear.disabled = !id; } }, + sourceOptions: function () { + if (this.sourceOptionsData !== null) { return this.sourceOptionsData; } + var node = document.querySelector('[data-catalog-source-options]'); + try { this.sourceOptionsData = node ? JSON.parse(node.textContent || '[]') : []; } + catch (e) { this.sourceOptionsData = []; } + return this.sourceOptionsData; + }, + setSources: function (values) { + if (!this.itemForm) { return; } + var ownId = Number(this.itemForm.querySelector('[name="id"]').value) || 0, allowed = {}, selected = []; + this.sourceOptions().forEach(function (item) { if (Number(item.id) !== ownId) { allowed[String(item.id)] = item; } }); + (values || []).forEach(function (id) { id = String(Number(id) || 0); if (allowed[id] && selected.indexOf(id) === -1) { selected.push(id); } }); + this.sourceItemIds = selected; + var inputs = this.itemForm.querySelector('[data-catalog-source-inputs]'), label = this.itemForm.querySelector('[data-catalog-source-label]'), clear = this.itemForm.querySelector('[data-catalog-source-clear]'); + if (inputs) { + inputs.innerHTML = ''; + selected.forEach(function (id) { var input = document.createElement('input'); input.type = 'hidden'; input.name = 'source_item_ids[]'; input.value = id; inputs.appendChild(input); }); + } + if (label) { + var names = selected.map(function (id) { return allowed[id].name; }); + label.textContent = names.length === 0 ? 'Товары только из этого раздела' : (names.slice(0, 2).join(', ') + (names.length > 2 ? ' · ещё ' + (names.length - 2) : '')); + } + if (clear) { clear.disabled = selected.length === 0; } + }, + openSourcePicker: function () { + var self = this, ownId = Number(this.itemForm.querySelector('[name="id"]').value) || 0; + var options = this.sourceOptions().filter(function (item) { return Number(item.id) !== ownId; }); + var selected = {}; this.sourceItemIds.forEach(function (id) { selected[String(id)] = true; }); + var overlay = document.createElement('div'), rows = ''; + options.forEach(function (item) { + var id = String(item.id), search = (id + ' ' + (item.path || item.name || '')).toLowerCase(); + rows += ''; + }); + overlay.className = 'overlay catalog-source-overlay'; + overlay.innerHTML = ''; + document.body.appendChild(overlay); requestAnimationFrame(function () { overlay.classList.add('show'); }); + var search = overlay.querySelector('[data-catalog-source-search]'), count = overlay.querySelector('[data-catalog-source-count]'), empty = overlay.querySelector('[data-catalog-source-empty]'); + var update = function () { var checked = overlay.querySelectorAll('[data-catalog-source-option] input:checked').length; count.textContent = checked ? ('Выбрано: ' + checked) : 'Источники не выбраны'; }; + var filter = function () { var query = (search.value || '').trim().toLowerCase(), visible = 0; overlay.querySelectorAll('[data-catalog-source-option]').forEach(function (row) { var show = !query || row.getAttribute('data-search').indexOf(query) !== -1; row.hidden = !show; if (show) { visible++; } }); empty.hidden = visible > 0; }; + var close = function () { overlay.classList.remove('show'); setTimeout(function () { overlay.remove(); }, 160); }; + overlay.addEventListener('change', update); + overlay.addEventListener('click', function (e) { if (e.target === overlay || e.target.closest('[data-catalog-source-close]')) { close(); return; } if (e.target.closest('[data-catalog-source-save]')) { self.setSources(Array.prototype.map.call(overlay.querySelectorAll('[data-catalog-source-option] input:checked'), function (input) { return input.value; })); self.scheduleItemSave(); close(); } }); + search.addEventListener('input', filter); update(); search.focus(); + }, openDocumentPicker: function () { var self = this, overlay = document.createElement('div'); overlay.className = 'overlay catalog-document-overlay'; diff --git a/adminx/modules/Catalog/assets/context-links.css b/adminx/modules/Catalog/assets/context-links.css new file mode 100644 index 0000000..efda7b6 --- /dev/null +++ b/adminx/modules/Catalog/assets/context-links.css @@ -0,0 +1,37 @@ +.catalog-context-links { + padding: 16px 0; + border-bottom: 1px solid var(--border-default); + min-width: 0; +} +.catalog-context-links h3 { + margin: 0 0 10px; + font-size: 14px; +} +.catalog-context-links nav { + display: flex; + flex-wrap: wrap; + gap: 10px 24px; +} +.catalog-context-links a { + display: inline-flex; + align-items: flex-start; + gap: 6px; + max-width: 100%; + min-width: 0; + font-size: 13px; +} +.catalog-context-links a > i { + flex: none; + margin-top: 2px; +} +.catalog-context-links a > span { + min-width: 0; + overflow-wrap: anywhere; +} +.catalog-context-links small { + display: block; + color: var(--text-secondary); +} +.catalog-item-form > .catalog-context-links { + padding: 16px 20px; +} diff --git a/adminx/modules/Catalog/language/en/client.xml b/adminx/modules/Catalog/language/en/client.xml index b4b82d3..8942ac5 100644 --- a/adminx/modules/Catalog/language/en/client.xml +++ b/adminx/modules/Catalog/language/en/client.xml @@ -20,6 +20,7 @@ hidden Request Error Section + Find section <div class="alert alert-error"><i class="ti ti-alert-triangle"></i><div>Errors: </p></div><button class="modal-close" type="button" data-catalog-recompile-close aria-label="Close" disabled><i class="ti ti-x"></i></button></div><div class="modal-body"><div class="catalog-recompile-status" data-catalog-recompile-status>Preparing...</div><div class="progress"><div class="progress-bar" data-catalog-recompile-progress style="width:0%;--progress-color:var(--blue-500)"></div></div><div class="catalog-recompile-errors" data-catalog-recompile-errors></div></div><div class="modal-footer"><button class="btn btn-ghost" type="button" data-catalog-recompile-stop>Stop</button><button class="btn btn-primary" type="button" data-catalog-recompile-close disabled>Close</button></div></div> Hide section @@ -36,6 +37,11 @@ No document selected Not included Replace condition? + more + No + Catalog + Category + Search Catalog created different Catalog diff --git a/adminx/modules/Catalog/language/en/context-links.xml b/adminx/modules/Catalog/language/en/context-links.xml new file mode 100644 index 0000000..0982c75 --- /dev/null +++ b/adminx/modules/Catalog/language/en/context-links.xml @@ -0,0 +1,8 @@ + + + Related Settings + Section Filters + Attribute Set + Catalog Card Template + Page Template + diff --git a/adminx/modules/Catalog/language/en/product-roles.xml b/adminx/modules/Catalog/language/en/product-roles.xml new file mode 100644 index 0000000..b1d1bc9 --- /dev/null +++ b/adminx/modules/Catalog/language/en/product-roles.xml @@ -0,0 +1,63 @@ + + + Product Description + Warranty + Registration Document Text + Registration Document Number + Product Field Roles + Saved Mapping + Document # + Sample Product + Check + Optional Project Fields + Automatic: + Not Assigned + Name + SKU + Price + Old Price + Stock Quantity + Images + Manufacturer + Card Caption + Hide Price + Exclude From Price List + New Product + Discount + Gift + Video + Video Instructions + Square Image + Cover Image + Registration Document + Payment Program Eligibility + Payment Program Code + Published + Not Publicly Available + Available to Order + Not Available to Order + Not Published + Deleted + Technical Document or Rubric + Publication Has Not Started + Publication Has Ended + No Positive Price + Price Is Hidden + Price List Exclusion Is Enabled. + Role + Field + Value + Yes + No + Not Filled + more + No Document Found in This Rubric. + Request Conditions + Page Template + Product Card + filters + Attribute Set + Could Not Check the Mapping + Invalid Field Role + Role Field Does Not Belong to the Product Rubric + diff --git a/adminx/modules/Catalog/language/en/product-selection.xml b/adminx/modules/Catalog/language/en/product-selection.xml new file mode 100644 index 0000000..179e13b --- /dev/null +++ b/adminx/modules/Catalog/language/en/product-selection.xml @@ -0,0 +1,52 @@ + + + Selection Check + Selection + Catalog + Recommendations + Search + Feed + Category + Not Selected + Search Scope + Everywhere + Search Query + Source Product # + Limit + Check Selection + Parameters Changed; Result Is Outdated + Included in Selection + Not Included in Selection + Check Incomplete + Insufficient Permissions + Unknown Selection + Check Unavailable: Review Settings and Index Health + Eligibility Before Grouping and Pagination, Without Visitor Filters + Category Not Found + Published Category Page + Insufficient Permissions for Request Conditions + Index, Publication, Category Sources and Variant Status + Saved Request Conditions + Product or Its Group Is Eligible + Search Before Pagination Using the Effective Query, Including Synonyms and Typo Correction + Effective Search Query + Ambiguous Correction: No Replacement Selected + Search Handler Did Not Return a Verification Plan + Search Pipeline Returned Results + Search Unavailable + Search Scope Not Found + Search Query Is Too Short + Search Index, Text and Public Visibility + Search Scope Rubrics + Search Scope Conditions + Export Eligibility Under Saved Settings, Without File Generation + Feed Not Found + Feed Is Enabled + Allowed Feed Categories + Index, Publication, Price Visibility and Feed Categories + Price and Price List Inclusion Under Feed Settings + Actual Recommendations Including Limit, Ranking and Cache + Source Product Unavailable + Native Recommendation Result + Source Categories, Active Status, Primary Variant, Price, Stock and Limit Apply + diff --git a/adminx/modules/Catalog/language/ru/client.xml b/adminx/modules/Catalog/language/ru/client.xml index 9f4c36e..6430866 100644 --- a/adminx/modules/Catalog/language/ru/client.xml +++ b/adminx/modules/Catalog/language/ru/client.xml @@ -20,6 +20,7 @@ скрыт Ошибка запроса Раздел + Найти раздел <div class="alert alert-error"><i class="ti ti-alert-triangle"></i><div>Ошибки: </p></div><button class="modal-close" type="button" data-catalog-recompile-close aria-label="Закрыть" disabled><i class="ti ti-x"></i></button></div><div class="modal-body"><div class="catalog-recompile-status" data-catalog-recompile-status>Подготовка...</div><div class="progress"><div class="progress-bar" data-catalog-recompile-progress style="width:0%;--progress-color:var(--blue-500)"></div></div><div class="catalog-recompile-errors" data-catalog-recompile-errors></div></div><div class="modal-footer"><button class="btn btn-ghost" type="button" data-catalog-recompile-stop>Остановить</button><button class="btn btn-primary" type="button" data-catalog-recompile-close disabled>Закрыть</button></div></div> Скрыть раздел @@ -36,6 +37,11 @@ Документ не выбран Не включён Заменить условие? + ещё + Нет + Каталог + Раздел + Поиск Каталог создан отличается Каталог diff --git a/adminx/modules/Catalog/language/ru/context-links.xml b/adminx/modules/Catalog/language/ru/context-links.xml new file mode 100644 index 0000000..6bb5963 --- /dev/null +++ b/adminx/modules/Catalog/language/ru/context-links.xml @@ -0,0 +1,8 @@ + + + Связанные настройки + Фильтры раздела + Набор характеристик + Шаблон карточки в каталоге + Шаблон страницы + diff --git a/adminx/modules/Catalog/language/ru/product-roles.xml b/adminx/modules/Catalog/language/ru/product-roles.xml new file mode 100644 index 0000000..825fa50 --- /dev/null +++ b/adminx/modules/Catalog/language/ru/product-roles.xml @@ -0,0 +1,63 @@ + + + Описание товара + Гарантия + Текст регистрационного документа + Номер регистрационного документа + Назначение полей товара + Сохранённое сопоставление + Документ # + Образец товара + Проверить + Дополнительные поля проекта + Автоматически: + Не назначено + Название + Артикул + Цена + Старая цена + Остаток + Изображения + Производитель + Подпись карточки + Скрывать цену + Исключить из прайса + Новинка + Скидка + Подарок + Видео + Видеоинструкция + Квадратное изображение + Обложка + Регистрационный документ + Участие в программе оплаты + Код программы оплаты + Опубликован + Не доступен публично + Доступен к покупке + Покупка недоступна + Не опубликован + Удалён + Служебный документ или рубрика + Публикация ещё не началась + Публикация завершена + Нет положительной цены + Цена скрыта + Исключение из прайса включено. + Назначение + Поле + Значение + Да + Нет + Не заполнено + ещё + Документ этой рубрики не найден. + Условия запроса + Шаблон страницы + Карточка товара + фильтры + Набор характеристик + Не удалось проверить сопоставление + Некорректное назначение поля + Поле роли не принадлежит рубрике товара + diff --git a/adminx/modules/Catalog/language/ru/product-selection.xml b/adminx/modules/Catalog/language/ru/product-selection.xml new file mode 100644 index 0000000..203ec7e --- /dev/null +++ b/adminx/modules/Catalog/language/ru/product-selection.xml @@ -0,0 +1,52 @@ + + + Проверка выборки + Выборка + Каталог + Рекомендации + Поиск + Фид + Раздел + Не выбран + Область поиска + Везде + Поисковая строка + Исходный товар # + Лимит + Проверить выборку + Параметры изменены, результат не актуален + Входит в выборку + Не входит в выборку + Проверка не завершена + Недостаточно прав + Неизвестная выборка + Проверка недоступна: проверьте настройки и состояние индексов + Допуск до группировки и страниц, без выбранных посетителем фильтров + Раздел не найден + Опубликованная страница раздела + Недостаточно прав для условий запроса + Индекс, публикация, источники раздела и активность варианта + Сохранённые условия запроса + Допуск товара или его группы в выдачу + Поиск до пагинации по фактической строке, с синонимами и исправлением опечаток + Фактическая поисковая строка + Неоднозначное исправление: поиск не выбрал замену + Поисковый обработчик не вернул план проверки + Поисковый pipeline вернул результаты + Поиск недоступен + Область поиска не найдена + Поисковый запрос слишком короткий + Поисковый индекс, текст и публичная доступность + Рубрики области поиска + Условия области поиска + Допуск в экспорт по сохранённым настройкам, без генерации файла + Фид не найден + Фид включён + Разрешённые разделы фида + Индекс, публикация, видимость цены и разделы фида + Цена и включение в прайс по настройкам фида + Фактическая выдача рекомендаций с учётом лимита, ранжирования и кеша + Исходный товар недоступен + Результат штатных рекомендаций + Учитываются категории исходного товара, активность, основной вариант, цена, остаток и лимит + diff --git a/adminx/modules/Catalog/migrations/006_add_item_sources.php b/adminx/modules/Catalog/migrations/006_add_item_sources.php new file mode 100644 index 0000000..d1eae11 --- /dev/null +++ b/adminx/modules/Catalog/migrations/006_add_item_sources.php @@ -0,0 +1,28 @@ + + | @copyright 2007-2026 (c) AVE.cms + | @link https://ave-cms.ru + | @version 3.3 + */ + + defined('BASEPATH') || die('Direct access to this location is not allowed.'); + + use App\Common\DatabaseSchema; + use App\Content\CatalogTables; + + return function () { + $table = CatalogTables::table('module_catalog_items'); + if (!DatabaseSchema::tableExists($table) || DatabaseSchema::columnExists($table, 'source_item_ids')) { + return 0; + } + + DB::query('ALTER TABLE %b ADD COLUMN `source_item_ids` TEXT NULL AFTER `filters_settings`', $table); + return 1; + }; diff --git a/adminx/modules/Catalog/module.php b/adminx/modules/Catalog/module.php index 86a4106..9f80110 100644 --- a/adminx/modules/Catalog/module.php +++ b/adminx/modules/Catalog/module.php @@ -14,7 +14,7 @@ defined('BASEPATH') || die('Direct access to this location is not allowed.'); return array( - 'code' => 'catalog', 'name' => 'Каталог', 'version' => '0.3.5', + 'code' => 'catalog', 'name' => 'Каталог', 'version' => '0.3.9', 'permissions' => array('key' => 'catalog', 'items' => array( array( 'code' => 'view_catalog', @@ -51,6 +51,7 @@ array('GET', '/catalog/documents', array(\App\Adminx\Catalog\Controller::class, 'documents')), array('GET', '/catalog/items/{id}', array(\App\Adminx\Catalog\Controller::class, 'item')), array('GET', '/catalog/{rubric}/{field}', array(\App\Adminx\Catalog\Controller::class, 'edit')), + array('GET', '/catalog/{rubric}/{field}/product-preview', array(\App\Adminx\Catalog\Controller::class, 'productPreview'), array('permission' => 'view_products')), array('POST', '/catalog/{rubric}/{field}/items', array(\App\Adminx\Catalog\Controller::class, 'storeItem')), array('POST', '/catalog/{rubric}/{field}/items/{id}', array(\App\Adminx\Catalog\Controller::class, 'updateItem')), array('POST', '/catalog/{rubric}/{field}/items/{id}/conditions/sync', array(\App\Adminx\Catalog\Controller::class, 'syncMissingConditions')), @@ -63,6 +64,7 @@ ), 'migrations' => array( array('id' => '005_add_catalog_purpose', 'file' => 'migrations/005_add_catalog_purpose.sql'), + array('id' => '006_add_item_sources', 'file' => 'migrations/006_add_item_sources.php'), ), 'view_globals' => array('module_code' => 'catalog'), ); diff --git a/adminx/modules/Catalog/view/_context-links.twig b/adminx/modules/Catalog/view/_context-links.twig new file mode 100644 index 0000000..9673412 --- /dev/null +++ b/adminx/modules/Catalog/view/_context-links.twig @@ -0,0 +1,4 @@ + diff --git a/adminx/modules/Catalog/view/_product-role-preview.twig b/adminx/modules/Catalog/view/_product-role-preview.twig new file mode 100644 index 0000000..9d94ab5 --- /dev/null +++ b/adminx/modules/Catalog/view/_product-role-preview.twig @@ -0,0 +1,28 @@ +
+

{{ 'Сохранённое сопоставление'|admin_trans }}

+ {% if product_roles.denied %}

{{ 'Недостаточно прав'|admin_trans }}

{% else %} +
+ +
{{ 'Образец товара'|admin_trans }}
+
+ {% include '@catalog/_product-selection-preview.twig' %} + {% if product_roles.snapshot %} + {% set sample = product_roles.snapshot %} +

{% if product_roles.can_edit_product %}{{ sample.title }} · #{{ sample.id }}{% else %}{{ sample.title }} · #{{ sample.id }}{% endif %}

+

{{ (sample.published ? 'Опубликован' : 'Не доступен публично')|admin_trans }} {{ (sample.orderable ? 'Доступен к покупке' : 'Покупка недоступна')|admin_trans }}

+ {% for reason in product_roles.reasons %}

{{ reason|admin_trans }}

{% endfor %} + {% if sample.exclude_from_price %}

{{ 'Исключение из прайса включено.'|admin_trans }}

{% endif %} +
+ {% for role,definition in product_roles.definitions %}{% if not definition.optional or definition.field_id %} + + {% endif %}{% endfor %} +
{{ 'Назначение'|admin_trans }}{{ 'Поле'|admin_trans }}{{ 'Значение'|admin_trans }}
{{ definition.label|admin_trans }}{% if definition.field %}{{ definition.field.title ?: definition.field.alias }} · #{{ definition.field_id }}{% else %}{{ 'Не назначено'|admin_trans }}{% endif %}{% set value = attribute(sample,role) %}{% if value is same as(true) %}{{ 'Да'|admin_trans }}{% elseif value is same as(false) %}{{ 'Нет'|admin_trans }}{% elseif value is iterable %}{% if value is empty %}{{ 'Не заполнено'|admin_trans }}{% else %}{{ value|slice(0, 3)|join(', ') }}{% if value|length > 3 %} · {{ 'ещё'|admin_trans }} {{ value|length - 3 }}{% endif %}{% endif %}{% elseif value is same as('') %}{{ 'Не заполнено'|admin_trans }}{% else %}{{ value }}{% endif %}
+ {% else %}

{{ 'Документ этой рубрики не найден.'|admin_trans }}

{% endif %} +
+ {% if settings.request_id and product_roles.can_requests %}{{ 'Условия запроса'|admin_trans }}{% endif %} + {% if product_roles.can_rubrics %}{{ 'Шаблон страницы'|admin_trans }}{% endif %} + {{ 'Карточка товара'|admin_trans }} +
+ {% for section in product_roles.sections %}

{{ section.name }}: {{ 'фильтры'|admin_trans }}{% if section.attribute_set_id %} · {{ 'Набор характеристик'|admin_trans }} #{{ section.attribute_set_id }}{% endif %}

{% endfor %} + {% endif %} +
diff --git a/adminx/modules/Catalog/view/_product-roles.twig b/adminx/modules/Catalog/view/_product-roles.twig new file mode 100644 index 0000000..2b65888 --- /dev/null +++ b/adminx/modules/Catalog/view/_product-roles.twig @@ -0,0 +1,20 @@ +
+

{{ 'Назначение полей товара'|admin_trans }}

+
+ {% for role,definition in product_roles.definitions %}{% if not definition.optional %} + + {% endif %}{% endfor %} +
+
{{ 'Дополнительные поля проекта'|admin_trans }}
+ {% for role,definition in product_roles.definitions %}{% if definition.optional %} + + {% endif %}{% endfor %} +
+ {% include '@catalog/_product-role-preview.twig' %} +
diff --git a/adminx/modules/Catalog/view/_product-selection-preview.twig b/adminx/modules/Catalog/view/_product-selection-preview.twig new file mode 100644 index 0000000..6c0aefa --- /dev/null +++ b/adminx/modules/Catalog/view/_product-selection-preview.twig @@ -0,0 +1,24 @@ +{% set selection = product_roles.selection %} +{% set options = product_roles.selection_options %} +
+

{{ 'Проверка выборки'|admin_trans }}

+
+ + + + + + + + +
+ + {% if product_roles.selection_result %} + {% set result = product_roles.selection_result %} +
+

{{ (result.matched is same as(true) ? 'Входит в выборку' : (result.matched is same as(false) ? 'Не входит в выборку' : 'Проверка не завершена'))|admin_trans }}

+ + {% if result.note %}

{{ result.note|admin_trans }}

{% endif %} +
+ {% endif %} +
diff --git a/adminx/modules/Catalog/view/edit.twig b/adminx/modules/Catalog/view/edit.twig index 812591a..77a4da3 100644 --- a/adminx/modules/Catalog/view/edit.twig +++ b/adminx/modules/Catalog/view/edit.twig @@ -42,7 +42,7 @@ - {% if products_available %}

Поля товарного представления

{% set commerce_fields={'product_title_field_id':'Название товара','product_article_field_id':'Артикул','product_price_field_id':'Цена','product_old_price_field_id':'Старая цена','product_stock_field_id':'Остаток','product_images_field_id':'Изображения'} %}{% for key,label in commerce_fields %}{% endfor %}
+ {% if products_available %}{% include '@catalog/_product-roles.twig' %}

Карточка товара

Единый вид для главной страницы и разделов товарного каталога.

@@ -95,11 +95,12 @@
-{% if can_manage %}