release: AVE.cms 3.3 build 0.39

This commit is contained in:
2026-08-25 12:03:26 +03:00
parent 33fe774e1a
commit b21858a4e5
90 changed files with 3127 additions and 285 deletions
+46 -1
View File
@@ -11,7 +11,52 @@
## Не выпущено
Новые изменения будут добавлены после build 0.38.
Новые изменения будут добавлены после build 0.39.
## 3.3 build 0.39 — 25 августа 2026
### Документы и публичные шаблоны
- Карта публичных шаблонов показывает, из каких штатных компонентов AVE.cms
собираются основные типы страниц и где каждый компонент редактируется.
- Поля оформленных информационных страниц снова доступны в редакторе документа,
включая подписи, адреса и содержимое карт на странице контактов.
- Исправлено сохранение документа из режима исходного HTML-кода редактора.
- Публичный адрес под названием документа в таблице стал активной ссылкой и
открывается в новой вкладке.
- Существующий документ можно сохранять со своим URL, даже если рядом с ним
позднее появился системный маршрут установленного модуля.
### Рубрики и темы
- Дополнительные шаблоны рубрики открываются в полноразмерной рабочей панели,
поддерживают проверку и корректно возвращаются из полноэкранного режима.
- Из редактора дополнительного шаблона можно перейти к связанному документу.
- Улучшено отображение представлений и публичных шаблонов в разделе темы.
### Поиск
- Поиск распознаёт артикулы без разделителя между буквами и цифрами, сохраняет
правила русской и английской раскладки и учитывает управляемые синонимы.
- Добавлена осторожная коррекция опечаток с подсказкой «Возможно, вы искали»;
исходный запрос пользователя не заменяется без объяснения.
- Переиндексация получила более устойчивый пошаговый прогресс и словарь
поисковых вариантов.
### Каталог модулей
- Модуль слайдеров добавлен в публичный каталог: слайды, ссылки, навигация,
адаптивные изображения и внешний вид управляются из панели.
- Галереи получили обновлённый публичный просмотр изображений.
- Модуль SMS-входа получил журнал событий и расширенную диагностику отправки.
- Обновлены модули поиска и связанных материалов.
- Торговые, проектные и служебные модули по-прежнему исключены из публичной
core-сборки и каталога.
### Выпуск
- Для build 0.39 подготовлены чистая core-сборка, компактный установщик и
накопительное обновление с build 0.20.
## 3.3 build 0.38 — 5 августа 2026
+96 -88
View File
@@ -2,18 +2,19 @@
"format": "ave-core-package-v2",
"product": "AVE.cms",
"version": "3.3",
"build": "0.38",
"build": "0.39",
"package": "core",
"built_at": "2026-08-05T18:33:56+00:00",
"built_at": "2026-08-25T09:00:39+00:00",
"installed_modules": 0,
"bundled_optional_modules": [],
"optional_after_install": [
"setup/"
],
"core_fingerprint": "a8ac3d2be6a3c4b43c7d871c4e5ece44b1641f1fdace69329e296468e1d192d2",
"core_fingerprint": "cd348589924dbf95a20d6e41994c39f036bd55372250ad6caa8c3484158bd6a5",
"files": {
".gitignore": "20d4fb177d47da6944a7a1c2dbf5918fd62766881aa52b00901f9054b1db4b49",
".htaccess": "71c06b7bc41b09c347a562ca72768acaa0ee1ef8388c749fc67956c793d8a826",
"CHANGELOG.md": "58ab5f834aad74ac1f8101983102fce3f0708d720999efadcdff9efdefa2ed64",
"README.md": "19b820f1b092780039aae011e133dc6a1e5f1cf213b435e0ff1962b592b9095b",
"adminx/.htaccess": "d03ea2d2972af8b7545a2d57c6253f303d15adeb30b775e161d887ca5ff73378",
"adminx/Support/AdminLocale.php": "a7fe84d86d65c9f021106172b1789c8bc6221c36b2fe39cf883ca9a505e3e8ad",
@@ -28,7 +29,7 @@
"adminx/Support/Roles.php": "6be335dd643317f77bd7c2db92f25895e601fe72818befd62f6aeb3ecac9fce7",
"adminx/Support/RouteGuard.php": "e9e20c097c0db354851546585be72bfc71a5d14652871f8bd1160e845d4dab1b",
"adminx/Support/SavedViews.php": "4548ca129f270184e61cb670e7f44a1c103099a6a6c2820bed1a11bc18f227c4",
"adminx/Support/SectionHelp.php": "6a5101c1f7b61ea1b94aa4a37985d3a5955aaed8b73537f918f90f7749dfa08b",
"adminx/Support/SectionHelp.php": "4ce72b8895ad4f813d3a49e56a895c20f2821ad7f546c87f6e891793592dee4c",
"adminx/Support/SensitiveRoutes.php": "d14d809310b43fdb66ec141b0573ec1020aea3720d20269d39f201f411d9f9c5",
"adminx/Support/SystemFeatures.php": "f18ada623710f8c3efc6588c037227f6074e75bad9a3820aeef60b4d8d4b20a4",
"adminx/Support/SystemHealth.php": "286aee452e90f71ad64437bd39d6dd985d312eb85bbb711490d3a0b631a16aea",
@@ -36,9 +37,9 @@
"adminx/Support/Twig/AdminLocaleExtension.php": "1fb6c1d1c35d74fdb4a578cd15b3f19441725db6f11b336dc6948ed982842f3a",
"adminx/Support/Twig/AdminLocaleNodeVisitor.php": "ccf387680fe8b0e11bd0fe0d23769be8b7ed9afc363cb9a802db5c4aeb7d405b",
"adminx/Support/Twig/AdminTextNode.php": "e51d8d3ee49411f42c8a874cd283f60c1fa0af6506b34568a119ebe2d18d7268",
"adminx/Support/ViewOverrideEditor.php": "c3804e0b399cacd604ad2765885366a4baefe0ee7a441475276fe7d3074dab36",
"adminx/Support/ViewOverrideEditor.php": "9704c974f36b6f9e204b6c609582d61b0a0f65aa182a3f1381fe7a7d1c0b15e7",
"adminx/Support/WorkItemContext.php": "d4b87b78ff1f2bdd89d77bc0483ebb46e5c11616635211ead9a43b34568e3a59",
"adminx/assets/css/adminkit.css": "15409efc9387120866479c3351a4a291ee7d8207694a84b228fc5d35fc184cbe",
"adminx/assets/css/adminkit.css": "0883ec02682b3069765388c61e0d9f8262786599fbd8bec51701affbab585a5e",
"adminx/assets/fonts/NunitoSans-Bold.woff2": "2d48580b7e1be60fa94b8092ebe86471fe0adfe5ca75242a7af2042ac9da825f",
"adminx/assets/fonts/NunitoSans-ExtraBold.woff2": "66612ee9d1cdc341c9eec8a43104bbf515e7cc52821558bfc7dfcf33c8cb0cc1",
"adminx/assets/fonts/NunitoSans-Regular.woff2": "878adc25278872933f004783f0ebbe092cebb3f9161aa30cffb2b9a8a07db040",
@@ -46,16 +47,16 @@
"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": "4303a0a0c89c8e339143e6d1e0269a51b02b1fe72383f83af6a2a8d3b4ee1cf0",
"adminx/assets/js/adminx.js": "34d38f1291372e5ce10d051d9cbc144f61020f833fce263f5041fdc6926c20d1",
"adminx/assets/js/bulk-actions.js": "d7ee80678e6703aff2418a4679ba8e5027e7220cb089848bcced6d9f100b3aae",
"adminx/assets/js/command-palette.js": "f62f0bad5ddf2b6e813e7eda094d73df63dce09a6e581025971e8f10430e8d9f",
"adminx/assets/js/editor-codemirror.js": "90a04c219e84f5391a40b0d1fc807ecfe4ca582a198bc202bdc5aa6dccec51fa",
"adminx/assets/js/editor-tiptap.js": "b924d1deccda0e85d947ec936ead0d32f85aa5517893d197316f75a614ac5092",
"adminx/assets/js/media-picker.js": "eec036a03bd1d0d7995708a15f3337cc6e5d2e8ede7d5be3b0c5eeaeed782ebc",
"adminx/assets/js/editor-tiptap.js": "d5c7596bb17de87cb061c18daf629f322d03e2c81be19dab78897e93691d52de",
"adminx/assets/js/media-picker.js": "74481279a7d352c7c72893b4043e02454ff3cc28b6582c5463dfa712b17e23a2",
"adminx/assets/js/saved-views.js": "30c02121634f6f035dcff6a730e6fe300a1892fc9321971f142dcff600802f30",
"adminx/assets/src/bulk-actions.js": "29bcc568366ce4a86a71b12a0f8be050bd71a9eabb0f65af6d9ca57281db9201",
"adminx/assets/src/command-palette.js": "ff86b0dc0f1ad59d551af772643dbe094cdfb2367d402eca49fd1a4c5b1bb9db",
"adminx/assets/src/editor-tiptap.js": "e4cf6d82b1759708e9e2df5bb54136570e572a99138cd57f6eee7f91257ed7c4",
"adminx/assets/src/editor-tiptap.js": "1cb23f5262b50aa8ac0fbf19d98b77ce063af966f47d9b2ab6b0e5e3fadf28ae",
"adminx/assets/vendor/codemirror/addon/dialog/dialog.css": "5df690d771f12118348562dd2a90060433ae2edeccd0908abcaa4480b1e3e468",
"adminx/assets/vendor/codemirror/addon/dialog/dialog.js": "1eea0e500dce10d8594d8d681a91d1b71a4209355c9c29885e86e26b2d76685e",
"adminx/assets/vendor/codemirror/addon/edit/closetag.js": "700b8aa7c46e040287e32ffd1edb86a42d09edc5b2a9594241590b154c0232bd",
@@ -184,8 +185,8 @@
"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": "425ebd7b8e54ef659879e7bf8212120feda968a7560e2a614b505be882fed410",
"adminx/modules/Catalog/Model.php": "e7c7cffd91754e04dac322c31a64b420739c671079a7f8e7effb550f5019ee13",
"adminx/modules/Catalog/Controller.php": "22a19db35e22824abf3bea9053050c2cdd74c937022de905684c5b627d52de57",
"adminx/modules/Catalog/Model.php": "d524730981ad388928cfc29208a821ed1d474bd01d6db1749fd37fdfb4e4958e",
"adminx/modules/Catalog/assets/catalog-drawer.css": "b3e4f798d7bdf329aa91987ae5be435910ddccfeb8a32211059846ab9dc6b7c4",
"adminx/modules/Catalog/assets/catalog.css": "49e7a33fc444f8330806e1cc5cbb0d523b673c64faafd25dbf0fe33067643efd",
"adminx/modules/Catalog/assets/catalog.js": "1fc4acc30b05fd702dee9ff62958141aeab7593ca38d6f6360567af7c3dc9742",
@@ -196,8 +197,8 @@
"adminx/modules/Catalog/language/ru/interface.xml": "15d8de452eef296375ba40d557ce15ac0363881a7b763b7111c8cc221e9861ff",
"adminx/modules/Catalog/language/ru/runtime.xml": "fe59945f4ceb2bf2d85699623369e7e5889613b8cecd8551a390c78023531d14",
"adminx/modules/Catalog/migrations/005_add_catalog_purpose.sql": "006ff237e25042a4fa9b9f74d5079741eeee1aa52db0cd996ea4c8b817e6700f",
"adminx/modules/Catalog/module.php": "69f6f46206bec880a6f08329e0b44e35bcf3c7e57ad5e85e3b720b7b4adb7f38",
"adminx/modules/Catalog/view/edit.twig": "7fa6f39eec641b23f2191a9df2186a4d4855e50d4b1cc506118a94313cf481a7",
"adminx/modules/Catalog/module.php": "c36086c3162d1eba433af05c879dcfb9cff830ee3c53da38705a72cc19b3777b",
"adminx/modules/Catalog/view/edit.twig": "13bd9ebabdc8bb470a6c8ceb813b122d33da5c5604a23bb7ad3a840cc061cde2",
"adminx/modules/Catalog/view/index.twig": "754c7c4dc81176e8c99b3d9e8c1fda2f62f03df341c3df1f7393f72d43c6f8e2",
"adminx/modules/Console/Controller.php": "af0309a50d7f4366279be8ea12959cf7718a3c68b895a65ec5846554fd6a4857",
"adminx/modules/Console/Model.php": "53aad480b8d4e5d68c4e9345d219ea5cbff6d04eee7ce4aa15deab6f00dac1cc",
@@ -253,11 +254,11 @@
"adminx/modules/Dashboard/view/widgets/recent-documents.twig": "d03f36f047574f8ce57fdcfd79b26d9b20ca01dba69ce7c71a3e9a277f0105d0",
"adminx/modules/Dashboard/view/widgets/shortcuts.twig": "af165425ea7889cdc3edbd72c7f0dd9dc5c4c06486bc4ab37816112cfb696718",
"adminx/modules/Dashboard/view/widgets/system-kpis.twig": "52dab540a189443b76811ac4b457a566cf90134366749aa9c7d82e2d23ffdaa3",
"adminx/modules/Database/Backup.php": "08192cdadd31b0d304393cec761ca78d696d10e7b9585389662fa71f53e8e4c0",
"adminx/modules/Database/Backup.php": "131bcff544bf3d2093ae5d14d984070fe5819bf0ae1aa9ec02a48d42b85d1e1b",
"adminx/modules/Database/BackupRestore.php": "1fb34abdec27ed24665ec0749f4ed44167f3881d3477129d5d4de6c7b36d6653",
"adminx/modules/Database/Controller.php": "87577df32e3cacc36edb57747ac9169d0e32a30b2b2f11dbbc4db18be486e3e6",
"adminx/modules/Database/Model.php": "52c23131d07bc798fb2a3489f91ae909985e60d2965e3cd5fbaf5ab441d85477",
"adminx/modules/Database/SqlDumpReader.php": "f0b3a501d0afea0ae50d35b43e901844e14d9faec286738956dc6f2b1c5ea4b3",
"adminx/modules/Database/SqlDumpReader.php": "e71f81a1fd4859fddced14da210f1530eee41131287e84d82d43d3033dca5ae2",
"adminx/modules/Database/assets/database.css": "9bbcaefb3c3f3e6230250cf90cb20ffe933f86812ddb8c49d17a3f65097aef5d",
"adminx/modules/Database/assets/database.js": "191c97d3d7586440ca736c5cffc228e4ea9f8915ba61e5a55146f849df7e9d12",
"adminx/modules/Database/language/en/client.xml": "4464d65adfd150e597436cade1d36b0c0436654fb4c710218fe2fad117b12369",
@@ -269,7 +270,7 @@
"adminx/modules/Database/migrations/001_mysql_legacy_index_compatibility.php": "e9a021b5643933d6ca000d54bd1a12e339080939aa84fd3ec28d0093a5b8222c",
"adminx/modules/Database/migrations/002_module_lifecycle_recovery.php": "779b04c8b35f3d81657848016536a9e3ef70fb76dd3ce5b1ba4b4a72166f7a2d",
"adminx/modules/Database/migrations/003_document_fields_text_composite_index.php": "541a8df87dd5fec6946f26dcca88f78a2bf5e09608f9300baa8532202020ffbf",
"adminx/modules/Database/module.php": "550471695f5bf7ab5045f0b22c891f65b398346fb03f5f0a0cc3bcf3fed379a7",
"adminx/modules/Database/module.php": "1592d603f73695883643df7aa3fe90c257cd120f99e272b4759b0cdf5c3d3da3",
"adminx/modules/Database/view/index.twig": "4e3d3a8ccdeeb2812d4b6d3ae471c8f162d8ac0e30c5dfcc4847356b4cc063a9",
"adminx/modules/Directories/Controller.php": "bae664ccff5a63a215fa1b59b721d0b0a4055ac3633319476d09889ead0d7d27",
"adminx/modules/Directories/Model.php": "ead423e3d0a07badf5a773780ee37d92394240fc753c10fad274a3131db7c2ce",
@@ -283,23 +284,23 @@
"adminx/modules/Directories/module.php": "f6b7f0bfb96d1c2c7aa52c51e60abaa09cdba7a4309d8363394a3a02a8df96a0",
"adminx/modules/Directories/view/index.twig": "0bde23bc578b9ffad3c47534c304c0016b661a36588d6d3c9ffb92e2daa388a0",
"adminx/modules/Documents/BulkEditor.php": "6ca33e4ee73f83e043b13363bddb391199a1da902e74dfa7709a10ac32be574b",
"adminx/modules/Documents/Controller.php": "e461ac2766b15194741b0a4ca2c2aafac9edab9b9f602516011dd5168269104e",
"adminx/modules/Documents/Controller.php": "283aec04707c7b7fdd01e2ba69c1961b925f259655e9a455889bdd5dfc66f4fe",
"adminx/modules/Documents/DocumentHookRunner.php": "7e0cd24ed5791b6d5010e4a05351b709be09b6df1e700a3ff06685b9078b2b1d",
"adminx/modules/Documents/DocumentMediaReplacement.php": "e97d960c852162e655a4389cc5f477f0178597da50e733b00bdf5bcab076c65d",
"adminx/modules/Documents/EditConflict.php": "ebc3ebb92c3fc3ccf3909d2ff517014fc888ee20576a520b4979f68bde632b97",
"adminx/modules/Documents/Model.php": "6ac1f3e85c73e5e7c8a531b31f9b72ea1a78ba561355102301e386baf0eafdac",
"adminx/modules/Documents/Model.php": "5287c9adc603c942d1ffae135df360717ee18b1d1b2b9c29bb35d183b477b54a",
"adminx/modules/Documents/Revisions.php": "8caef88f7456000b001757cff13393ae36991b33c4c3070c27f3eaa4e8bf6492",
"adminx/modules/Documents/assets/documents.css": "5e9dbbbfbf276ad94a32657dac8a8cf0a5c184e77fb6b1d40e0261876dbe84eb",
"adminx/modules/Documents/assets/documents.js": "b65c2396c0f866e1895d84637875a935ca4a0886378c65068e5b4215b6dd94c8",
"adminx/modules/Documents/assets/documents.css": "396d4cc7625b3331a41d94f040ac3fabafd9c0b993181b490ceea8f365e1a5ad",
"adminx/modules/Documents/assets/documents.js": "dce151189d8244be41fb6b1bbb85a26241c788d91111ccb5314c361150024c3d",
"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/interface.xml": "f3a30b3c565d2e01c841c843dc529fed9844dc2e095f90b7902008ded7214ed2",
"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/interface.xml": "718386366f913c590c2bb881f49ad3b7d436de82faeec30655a7c2f9b14d58e0",
"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",
"adminx/modules/Documents/migrations/001_correct_breadcrumb_title.sql": "e2b612f19cbc2c419d1a3d2c684909b4055811a7121f4debdf6b83e99bd214cf",
@@ -318,12 +319,12 @@
"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": "36069766b3397735293f9f7d491c6f874eb8fc3c12c7d5d983ec8eee6b4655a8",
"adminx/modules/Documents/view/_product_panels.twig": "e07898ea6a7cc0004ac7545b689afdc6b474e6d8b24357f5c5190b4ca5589142",
"adminx/modules/Documents/module.php": "dc7642c19f3fabda2ce0577dbf2002de389ee2f5616c26a6f6cdd88c639f91c9",
"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": "a7934bde27ca84a47b9a5865b8bedb3e28756b91337e479592e1fefba93ec6aa",
"adminx/modules/Documents/view/index.twig": "004594c77dba92188e58d73228af397ac825e723c3c1aba9ee7b94072f828ba1",
"adminx/modules/Documents/view/edit.twig": "94eb351309fb3d6fdab844e253ab2d7e3b03e1d846b35da19b7e498733ad27a2",
"adminx/modules/Documents/view/index.twig": "8bb67e66a55007fda1374d83a3bc0b19fdc518a47d191a04913772d8c8ce132c",
"adminx/modules/Documents/view/redirects.twig": "4a4903edc81e4770220852d9be9f02a983ddde6f829a4360ea01d3b68456582b",
"adminx/modules/Documents/view/views.twig": "821f19085ba3997b6ef8f005e838f6fd449f0b32bce0d2804271311856cce8f0",
"adminx/modules/Events/Controller.php": "c57fc25e815c47c14626371d044079e6711f0a4bce2004d2b9256db62f0d0e85",
@@ -367,7 +368,7 @@
"adminx/modules/Groups/module.php": "fcc5b6212c5d6b77d9a17c9a4878c50233a5b48b373cb6a113825a82c82b248a",
"adminx/modules/Groups/view/list.twig": "15af0a1544c7933f57e6bb45ec2a3302d12a025b671c98e1009bae01f5bce605",
"adminx/modules/Groups/view/simulator.twig": "82f0c5aba5acc6e7a079c03e355edefe4061efca60d34b3195ee16ffdc951306",
"adminx/modules/Media/Controller.php": "cf853d384714160ef9fdb8d4ad108fefe331572fa2cd43e3d05faf67c3e0d368",
"adminx/modules/Media/Controller.php": "fa78cff18ddba8d3f3c4db74caf65039e5144d982031e71c394425d31fce2514",
"adminx/modules/Media/GlobalSearchProvider.php": "799e9b9f1455ad1070bb388245ea6adc5c76b9bf5756bfe0d46b96f3378be92d",
"adminx/modules/Media/ImagePresets.php": "8874cbf436a1a33f655c9a395b5f16e143c04c7df1d957c43e3d4ce3ae0645d5",
"adminx/modules/Media/MediaAudit.php": "2deeacd899b4410cc620b40b6e448c388f54eb553b5a57a23475ac0b5cae02f3",
@@ -376,7 +377,7 @@
"adminx/modules/Media/MediaUsageIndex.php": "d39d232bc56352c9cc2335a2e3bd2a7b7f415709c70ece4b38f1a462c2969415",
"adminx/modules/Media/Model.php": "1ac78ca828c6f492857e731a715d54939525dd29545bf81fb4065fcc02f0bc50",
"adminx/modules/Media/assets/media.css": "e7db9f602bc1a46644ccf172f977ebe0798c572c73d33f64584c9569d531611f",
"adminx/modules/Media/assets/media.js": "1ce96968f03008c9e0d536070579962e80070b946920ee03d9f4e965664044e3",
"adminx/modules/Media/assets/media.js": "2fdd0c2541150e5c2bbb4ac3ef54f1f40edc029d48383297d432af0431607111",
"adminx/modules/Media/language/en/analyzer.xml": "5c6e8bec29fe73d0403cf041c7580800ecc2677d94f2148ffaf44c04d337c6d4",
"adminx/modules/Media/language/en/client.xml": "06cecdb6cacdd0871334a3da0a3c7be27d7f835e2a048d37ea31ae4f37469270",
"adminx/modules/Media/language/en/improvements.xml": "5873d8eb9c43db0a584161ae3f086dfd101a8df25bbd4d518a2fd283f4f8cb3d",
@@ -389,7 +390,7 @@
"adminx/modules/Media/language/ru/runtime.xml": "538db360abf2d61e32d6476846173ea4f5115703021362314a9100df3854d6f7",
"adminx/modules/Media/migrations/001_image_presets.sql": "303dec5f331135ab3fda9d264b7a7b674b987cdefa0a7904c544a5d63554f15a",
"adminx/modules/Media/migrations/002_media_search_index.sql": "e84168429acdd900d494c91e398a8974ae97023d8bee3385321c2947dd627378",
"adminx/modules/Media/module.php": "af83896b4cef13943f197b4c121a534a7ebea86a771ae12714a2c8c4d51e4495",
"adminx/modules/Media/module.php": "45634e3fc5bd65c1bf545d1aa79912692a38297d390288d956af0f646dbc8c1c",
"adminx/modules/Media/view/audit.twig": "06936cd87c80b5c7f6ecec8729f227c2ab3fd492e61b11ae7040e6423939b518",
"adminx/modules/Media/view/file.twig": "b317dacbc61411831a9029e30fb1ff3457fa1d34164df3b891b6a9a2fdbe32ce",
"adminx/modules/Media/view/index.twig": "c3a4c42477df3a10fcc8ee558bb03fe25782a58f6f47734cb078686798a2d06a",
@@ -409,42 +410,44 @@
"adminx/modules/Modules/language/ru/runtime.xml": "271e31a8ae1586ed045861933a6db150427c78be7560942185633f35cf90686a",
"adminx/modules/Modules/module.php": "aad8c5f3a11dec21eb14a5d9a83e30c88cb4ea1206e78bc758804d015a828456",
"adminx/modules/Modules/view/index.twig": "2ca402c0886f611129e884129f6e0b0022e14a8c385d5d4ad55abe49ee39f5c2",
"adminx/modules/Navigation/Controller.php": "99dca17606ed02b4554db50bb99c0414efac51f871c7fab9113f0c6ce77df4fc",
"adminx/modules/Navigation/Model.php": "f9e59561d2c1b918af994e827d206ea7f0683dff7aa342a6de9193bd877348ba",
"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/interface.xml": "5ec5a64240356c4b87f65baea911762e64e88dd652c1fd7c801079247d697b79",
"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/interface.xml": "cd5ac333b85534be6a81377a1b40fbd449174cf2d1f019d25b3bb3f5ba0713a5",
"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",
"adminx/modules/Navigation/migrations/002_navigation_revisions.sql": "cf24c9e9cdb95c2ba4ccea63ec4375d62f29370d5ef909a82a0c990624b8ad02",
"adminx/modules/Navigation/module.php": "8420e2f45024b5a1b076cd86a6658d19fe04156d337f87bcbe597b30a56dfce0",
"adminx/modules/Navigation/view/index.twig": "39092089d109c382f1714e5d724d1e857a8e579ca44b929b40708c9a3f6697e8",
"adminx/modules/PublicSite/Controller.php": "5805f86bed300709232e0a45fedf6950857dc8f23cb499f9c17786b61a67c722",
"adminx/modules/Navigation/module.php": "5eb07314b6c156893288f1616e248ac316ae3a3b561c996d63e772e4ea4e8b6f",
"adminx/modules/Navigation/view/index.twig": "b7dd1784238fc53456b1676a7bfd4d14c2d1efdd7f15cee34c2c4613bd0e7590",
"adminx/modules/PublicSite/Controller.php": "8c00240eca08149df4572732d1e8b03eeca5910d40f03c2742021035f16aa621",
"adminx/modules/PublicSite/Model.php": "912fcd71f2ba7215fc9a411882c1eb5e0953b6ebaedfbb92f01b7d3de477a015",
"adminx/modules/PublicSite/PresentationModel.php": "3407e5e29165c264c65527433cb94d7d13a80f724dc4d1bf23a49cd2de88b125",
"adminx/modules/PublicSite/PresentationsController.php": "e2c7cc517f1a38d6b5e6828b1d652caa6b63622532d2f1f3c5f176e164ba6533",
"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/public-site.css": "69a17727b3a18a88d103886c2d7f79f2c59539ac70d735379e591657e7f04489",
"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": "10fc91443e4a98b3f529d07fe1cde7bbced8f4f84335fb125d4b2bf509edc2f0",
"adminx/modules/PublicSite/module.php": "46694327d2ac6315da3f9ae02b87a038cfea4da4314ef30c1bc451efe6b279b7",
"adminx/modules/PublicSite/view/_placement_pagination.twig": "b10413bdba07695500803625486b56ae5283c5a0a6dff83c668c8f93d1eab182",
"adminx/modules/PublicSite/view/_tabs.twig": "16229bf10dcfc7fa2da84a7041912f52fe597ea4209088826236c454d98bd52c",
"adminx/modules/PublicSite/view/_tabs.twig": "92db2936b265d3b07056ea3aea8ab9dc30e05849b6b3f7d04bf86231fc273643",
"adminx/modules/PublicSite/view/diagnostics.twig": "242682325bd7c99c75f9694d0e7ff6217124bf093364c027d13f609db660f4b7",
"adminx/modules/PublicSite/view/index.twig": "54ea9422b90235e2926924f11c43b56d90ca8e6c4f255501fcc9c52068cb6f92",
"adminx/modules/PublicSite/view/map.twig": "11fba1f54578b4908c669a7dae128949f53a7f1f03746f45a5059c92f473385d",
"adminx/modules/PublicSite/view/placements.twig": "9e1ef173a28ed883ea236da0611a4c2e74de09937c5ca4b5a28ff8caa98c9776",
"adminx/modules/PublicSite/view/presentations.twig": "d0b245362001ef2a1508fc5c4fa462895b4bbff104025544656828c4513d4cc1",
"adminx/modules/PublicSite/view/template-map.twig": "274f8754d8276caa92ff56678b7309dc9d3cf6ff672c96cf3dab78318f7bb3c5",
"adminx/modules/Requests/Controller.php": "b0171c1bfcbf1674b9ba0f8e9461dfa7b3ba5d7bce368b8a0e0dcc2659d32a67",
"adminx/modules/Requests/Model.php": "3ab6c5b11cccb31512e61026ae650c414e4bc30debaac3c2b306e9116e3fc49d",
"adminx/modules/Requests/Revisions.php": "78483f6667f25c050cbb2d224baa8ccf8d977b30eb84538c3cfb18c7af764bdc",
@@ -475,7 +478,7 @@
"adminx/modules/Requests/view/index.twig": "164490c16e041b928e1ecf604915656399690055d1f7de4bc88caf5f5abd485d",
"adminx/modules/Requests/view/native-audit.twig": "42120fd41aa4afa39560fea3f2625a4e78a61cb4f9ccc0c0388ca319d4f39c06",
"adminx/modules/Rubrics/AdminView.php": "a866300dda84253046f88d19a4467ae1a545c8b1dcac7672d35be986bb38a180",
"adminx/modules/Rubrics/Controller.php": "f3895259809f9b536fbe8d827dec5c3ea2909133ea7b98391bdde6b999c223bf",
"adminx/modules/Rubrics/Controller.php": "d60d21f8d90048a59eb97e924d7f7539f4ea3979aade0883109118b05b0fc1d4",
"adminx/modules/Rubrics/FieldAdapter.php": "edc4b730499111917360670632573b8d28cb7c192f71e88e8c60c7c8c3e981d1",
"adminx/modules/Rubrics/FieldAdminEditors.php": "d3bb0a8cd7e4b44db93b780ec95c49d3974bb3fc3167110a2f8ef044e921c348",
"adminx/modules/Rubrics/FieldConditionImpact.php": "77600152ef7e6a98cbfcdff1bc638aa6b1a664048bc2b839188b56a1ebf0a735",
@@ -487,7 +490,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": "97007d19f651bf8fce9599d542bace4ed4408bc0f65407383ba5589b8b7eaa48",
"adminx/modules/Rubrics/Model.php": "21f19765b1ca8e4ca7bd43fa8bd9737e26008cc152e77737215bbcc0572855c7",
"adminx/modules/Rubrics/RubricFieldPresets.php": "80109bd9f066f077ad19e48d7c7599e4dbf7b1d0c06c54db9fc921992bca2e77",
"adminx/modules/Rubrics/RubricFieldSetLinks.php": "41cd27e5947680eb184dafba5fd95f707ccad0c20fab2ca948cf882ada0adefe",
"adminx/modules/Rubrics/RubricRevisions.php": "83d3659ae0b1d2ce75fe02b0ebfc11d04857556ff352dc2b5408a1481291c658",
@@ -495,11 +498,11 @@
"adminx/modules/Rubrics/RubricTrash.php": "0496ca44ac1ba27cc7af1fe9673ee27c7fc4b174507b0038549b90733d22570b",
"adminx/modules/Rubrics/assets/field-sets.css": "fe5253279caa882f9623b058c92ab461c476c080baf2399425e522a36b75336c",
"adminx/modules/Rubrics/assets/field-sets.js": "efbb011014f756dd59ac1357441d06c9b69b278a9d934f7cd9d2d1d7398cd313",
"adminx/modules/Rubrics/assets/rubrics.css": "98ec76c4301d02bdfef75de9af85e3ae493bfb3c491827b2a9c6e2552fdcae10",
"adminx/modules/Rubrics/assets/rubrics.js": "38ddc91374036bf9aa12cc55abd5ee412b829a729379ed7f9727d84bc12be84d",
"adminx/modules/Rubrics/language/en/interface.xml": "b13fb75bc6770cc6970ee314b2f3d8c492d6227532533d46381c8919fce8bc78",
"adminx/modules/Rubrics/assets/rubrics.css": "72898787697fd0e26c61105c6f1fe5b509d731234e60b438742e30cda8fd6cc9",
"adminx/modules/Rubrics/assets/rubrics.js": "d0384f3fedd7a765a5d0006a1ca5080a9cc37fade556e95c2561927d197ea2ca",
"adminx/modules/Rubrics/language/en/interface.xml": "7edfdc6fe447448dbed987bfbfb8244557b2a7fab89a2ff3dd1e29eca57a6033",
"adminx/modules/Rubrics/language/en/runtime.xml": "4cad3d97c6a2695d038286cedc2c4509230c3bc9fa789dba2da9db89e8d9c93d",
"adminx/modules/Rubrics/language/ru/interface.xml": "3654fb74ac081dbb291f5ad13f0c83b2a83b9ce9bc2ebdfa06f9e0a49417b2b0",
"adminx/modules/Rubrics/language/ru/interface.xml": "7df4b3f148b367ca3185174c0feb3129f7ce28f99791a2f49de256d78bae7730",
"adminx/modules/Rubrics/language/ru/runtime.xml": "1bf3f8341af4561a4b6b1da98a28b689105285758da0fed959a6222fd83757d1",
"adminx/modules/Rubrics/migrations/001_create_rubrics_tables.sql": "a827a92e9d83361569b7e1e7c65b65cb019d1675a9078e05002de25b3e62c72c",
"adminx/modules/Rubrics/migrations/002_rubric_field_settings.sql": "4406f7ce1a9e9b0a105c4a33588791d41f12a55449baeca2565d5573e79f798d",
@@ -517,10 +520,10 @@
"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": "4aec29585bc8cb20be30b4a741eeb124f5836bd714593ff717f0ce59a889ebb1",
"adminx/modules/Rubrics/module.php": "ae3e957e9c3e099e66fcd14d9ddcf76a172207ff68d77d6279ca7a4c15c37c36",
"adminx/modules/Rubrics/view/_content_tabs.twig": "e4c0a9b899b5085aeaec2e1793b8eeac4a7619959fffc7ce39151af1250e0d9f",
"adminx/modules/Rubrics/view/field-sets.twig": "911dd7cc9546411e1b29724466328c7d2db8db630b2b52d33e8443a066b16810",
"adminx/modules/Rubrics/view/index.twig": "896fb1f4b8e56803f857857b288f78207efe60cd659277f257abf85b2e8e1cc8",
"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",
@@ -533,7 +536,7 @@
"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/Settings/Constants.php": "03c0af9ffe9c17fe879974c49396567ac0584f2474101715baf6ada847ac3ff4",
"adminx/modules/Settings/Constants.php": "5f8d15924c33f2e23e6f57f2934229e9c16333334f8ce3679793b35e5acbe0c2",
"adminx/modules/Settings/Controller.php": "90aff255d936753ab763ec534a097e9988f6c737bd5be2ba1194a7700a372e72",
"adminx/modules/Settings/Model.php": "9e16004c78a03d39db1bdfd5e23b88b77e8afc7b8f57a2a21997602ace277440",
"adminx/modules/Settings/ProductionDiagnostics.php": "b23d473a5f6e7c231c68abe4d48159b177699b5f0c23020e96252b240f86c87d",
@@ -577,8 +580,8 @@
"adminx/modules/Themes/Revisions.php": "f3927eadf841873b7a64976e28f2752b6d1be839327922720a0cd33e30d0884d",
"adminx/modules/Themes/assets/themes.css": "c54dd5451693f5117a22c9b7d7a742847e6c6d97a7700d38dd28111337392d38",
"adminx/modules/Themes/assets/themes.js": "1ab50ed9c1ff67ad4e4b8b295c245ad5f040c7b11bab102d007953ba1f79042e",
"adminx/modules/Themes/assets/view-overrides.css": "62e6d1a90c8819f50318240e93941054aaeb1b15442d3cebb954a4e839075699",
"adminx/modules/Themes/assets/view-overrides.js": "e531167445281fe7beb93110d6e089ab258a4d110108c3fd44fd4ff981dc7c6d",
"adminx/modules/Themes/assets/view-overrides.css": "0edd417ee79470e3a650a232b859fb68390b38dfd0b401a5367326a6dd416ca8",
"adminx/modules/Themes/assets/view-overrides.js": "480b8a128320f633d68fc439065fd6706c4208ac07828f438019563d783a6aae",
"adminx/modules/Themes/language/en/client.xml": "5f9d0a1ca7320e8590468e626433fff92b55cb9ca38d8723d4a363feb77dc3b5",
"adminx/modules/Themes/language/en/interface.xml": "30365f6c02c048fa2d0e94feb82bda45bdf8cc9fa665a7943593bb60636f368a",
"adminx/modules/Themes/language/en/runtime.xml": "66f4efbea3acf5be49cda9931841873e7c8bd68e263ef7a247233b4d762e9887",
@@ -587,7 +590,7 @@
"adminx/modules/Themes/language/ru/runtime.xml": "f8046256e75de65f6f20c8d5b5bd6a927653ca4dc8b6b8f205756b255b624c51",
"adminx/modules/Themes/migrations/001_create_theme_asset_revisions.sql": "21a2ce27d0b97be6493cdb86bffbfb0cb8b16762cc7b0025d83bec71d56a760c",
"adminx/modules/Themes/migrations/002_place_theme_navigation.php": "62a2f2953f3d5bb8df7e39c83a1de83a56008952540c8c28c6a12f33a8fd7f95",
"adminx/modules/Themes/module.php": "7f961194564ff6b2d7a6c450fad2f966118d71589a6e2d9f1f183d4726f35c7b",
"adminx/modules/Themes/module.php": "5f84abd13c0d03f6be58f7c0f4693bc9ba9486401baf957cf620c680fda562f3",
"adminx/modules/Themes/view/index.twig": "68a06ac9ee43b0052c877e89c411a1d149f212418def78e44a1f4a73d80344a9",
"adminx/modules/Themes/view/view-overrides.twig": "876f8f47ff598962614198fd89e5f112f04294215d1ffbe5f6ea3bb294af60ea",
"adminx/modules/Updates/Controller.php": "1aef3e6cec043e4e66ab862b4f0c7081d452bc8a1295fc125860eb0b02fa89ce",
@@ -627,7 +630,7 @@
"adminx/view/main.twig": "4aa00f5a2c50e3cc4d339c0ad9377526db6e8a65575843be59dfd20df5210344",
"configs/db.config.example.php": "e42bdb47aa7d1bc9cd85cb36e05acf940cafd951d6220664c485d1b971f18809",
"configs/index.php": "48e6b249e3d83530187c095ede0198af498c59f384bd458db74f9e0c2306eba9",
"configs/public.config.php": "54ad564eeb68979b3040fc3cddda8e9b5607c016fd91a045bdb2ef3d0a1afa03",
"configs/public.config.php": "882ea6a03ee8df191be8ea68ba78d39ae600d918a0880ebdff158fc38f534aa6",
"help/README.md": "d12965e7851dec5241dd74db731c110a7e0f4acfd7139ae28ce21d53193b1123",
"help/administration/README.md": "af3efa4bcd54eb23236735d461484e13b6319751904ca8856412b931e2ae032d",
"help/administration/console.md": "5efb2adbee857eaf0b8c2816ef1556addb2c9b21420bf972c6bdf62911fa9ad4",
@@ -647,14 +650,14 @@
"help/content/README.md": "273451d12b6360df56cc58323bfb8fd7521b4e3daf8f7ad93c874fe6877af048",
"help/content/blocks.md": "1d55be2a9f1a48df426e5856a54307d77b352d0037d68f08ed7e16a322d9ef8c",
"help/content/bulk-editor.md": "821107322b125c9344bd04346037fbd19f8ccd0ad6da822fe78af3299d01f746",
"help/content/catalog.md": "2e278644df3a40d05fad7b2a753a90398667b4bb6cbd88dcee8177e3a930b3b8",
"help/content/catalog.md": "8ca86447bc45f8f14688d7f74521569b50d4176b9ec2ecca0bad25d91a0ef0b8",
"help/content/content-model-tools.md": "baaf738aec454b1855ea110b8f5e1ef3ceaaad51cb920d4551cd088daeab7e76",
"help/content/content-studio.md": "91ef485e614a9a71a07d4e6c143377f7052ddf3b00d3f2f0d0a32258bdb14dbe",
"help/content/directories.md": "ff6c5ea9bd7fe1fb9d0aa1875d03fc3bc00b2bd2b0ec83d6496cd55168db294c",
"help/content/documents.md": "273851452c0fb8fddd47019fea600da187025dcca271bdd91d59acae15d6b4c5",
"help/content/navigation.md": "a02d0f88b5a3aa67fd14a2c26a48c9c3c4bb73f4824fa453c20f3e2ef4ef24b8",
"help/content/navigation.md": "50425db83ae70b8be72c69ecc6c54f77a15f029a244185021de1cb56efbb344f",
"help/content/presentations.md": "6e349bb4d1d1577617e6bf3d987b86ca6b255f64a47cd1ddb0e38c5242b788cb",
"help/content/public-site.md": "ea0edea4b3afda64987e5643397237fab227f5fdfa9d9551b73b93b32558f818",
"help/content/public-site.md": "9b7cd51d6d36d21daa73b47cb243b84eeb79f0ff48e9eb61c5951e82c24f7c73",
"help/content/requests.md": "c008b1026520c85400fb9240cb117fcaf384531c06e859c1e36ccbb3a79a8590",
"help/content/rubrics.md": "2c9842ce5647fc85663b02cef14f141c673694f0a20856c9262b6ccbea436c3c",
"help/content/templates.md": "c6fae2ef91c741479d1f34fe66d00e83f2d2ec1aba1a4099773a04a6e1b6552c",
@@ -676,7 +679,7 @@
"help/database/security.md": "581d5a89b5ea7cc01f7800878dd879b48e48a912f4a4c81c5f5dadd9b990e556",
"help/database/tables.md": "a027eb9c5e3c65bce645e6a7afd8ad9e37adfcf2e514926836d8a14cb0258075",
"help/database/transactions.md": "da679da1e7656be20048566f25b51c6018a0d7eff1f2e5d54228fb3d54071aff",
"help/debug/README.md": "41b7954c443330efe28cd3a14519c7c8076165a9d96c0dc3d3781a111e757085",
"help/debug/README.md": "665915c103ebb1bba4360508d2bd111b2bc093fbd7989f3ab7adafd01e8d145d",
"help/debug/custom-data.md": "445ed27dbda5b0ca035504e2f0cda023746def5edd3aa1f62189e133946e293b",
"help/en/README.md": "cdf84634ac731f1f434ec496b171307c38fccd6e6df335dd4dbf074e3e2cc12e",
"help/en/administration/README.md": "478a59925324a3dfb3da193b8e83b9a45e38fe77cf41644efbc35cb0b65e0298",
@@ -696,14 +699,14 @@
"help/en/content/README.md": "7c871512fea7f1bf635d5a816e322161dedbd8c158d520cd0385467e8dadd1ff",
"help/en/content/blocks.md": "34657f0a7ab977bf78d5b7645bf80a0755f40830bab4d4620ca7f39ea5992210",
"help/en/content/bulk-editor.md": "c99d5eb5058d6714f0281b84bfdf9020fc63ef27519c095af5f92fa2d5e96b44",
"help/en/content/catalog.md": "99a6e5c3e17ac0c7f2e04cbde4f790334f687b3aebed5815be5006ab361e530a",
"help/en/content/catalog.md": "8f12199f4d727e0ad715f91e980de638357fe1fa5af0702351fd3ee30afe29e1",
"help/en/content/content-model-tools.md": "fc50f8c334a129d3ead4a58503a7659abc2b1fdea7c9d277feff1e3ba4bb7b6b",
"help/en/content/content-studio.md": "194fe41a1eefdf7da3c9a47b8539ed3ed9d785d2a75816a39da6d2784ec40925",
"help/en/content/directories.md": "cfadf2361ead3bed9b63435041af8c91e24ec8b492906f0d9b976047edc123fb",
"help/en/content/documents.md": "2831a54d7c9e02b89231c48c406475dff64721e48ce76d3bd92b768043422333",
"help/en/content/navigation.md": "18f6e033c5c01eecbd7a3bc4b7c557f272f0039c811ea6f4ec2354206b0a5f21",
"help/en/content/navigation.md": "585f6212c72faf32fd67bca6f03715b5e5754ae9d6b162174cf40e76cd9c2454",
"help/en/content/presentations.md": "f255470ae47453d2e458b1829316c0e1e8e7fae73c9a1c1190d89f522b78d871",
"help/en/content/public-site.md": "619cf5d38d49f6dc1c54836d4e27853da03a82f3404b69ce7e0c395a3222ec45",
"help/en/content/public-site.md": "17352cd32290a5172ce7ec149b53de83a63f7ac60d0e42e7cbbbc17363a74c76",
"help/en/content/requests.md": "71aea0d1fa6dc9b6f6dd41f1c7a4d80af93c0b5e18341dc26f59d1f0c156604b",
"help/en/content/rubrics.md": "bd69370ac19ab0aaf6d4c1cba615ae21f57cc3e241ecb6706f23a69a794fb3c0",
"help/en/content/templates.md": "f91c141dfee841a1af8321625788cecb9dabaef75901e188127815b6d5b5ce6b",
@@ -725,7 +728,7 @@
"help/en/database/security.md": "f2d0b218fe9ba8c8c5fcef2b6a832cebc58e0af26400a13f0c4a32d7d2ce1d93",
"help/en/database/tables.md": "d117e9b6ecf04f8b8cbcc99e81874a09b84871dc8273a248ba676304f37e8c67",
"help/en/database/transactions.md": "9cd1b15425647281d40d11d7d0453f8758a1c41859ce3bf1de5d091ccbf2246d",
"help/en/debug/README.md": "4000fae72a034ec3be9d1059fd0e0a850efa8911dbd4b8498d8cb0b23ddfa4a0",
"help/en/debug/README.md": "e148c32ba9c0c3231c02094263410c19519b81b38f5e74564dfaec4f91dc6f7b",
"help/en/debug/custom-data.md": "0cd068b8d74bd98240bb86d9d4bc01d2630129eaeff8eec88b1630a094016f32",
"help/en/fields/README.md": "46c4fb2b44f1fef2f25ed2be3c730dd1d4e1aa0f48950e6069c3772acdcc5b17",
"help/en/fields/development.md": "4a953779ec8f08a79e235bd658d58ebc69f5758e2d8c55d49d5bc074e5aa0a41",
@@ -753,7 +756,7 @@
"help/en/helpers/Request.md": "3e919b34a2559e818938895a6e92d3ca87550774712d03913044e58a4d304d99",
"help/en/helpers/Response.md": "ca68c1d5cdd133daaa78077e00a15fd702bc47890ad33c224656ab4c33c17e52",
"help/en/helpers/RussianStemmer.md": "c55946a78392ac8c0534187cb2b4232f5f0aa6276114e0c8e31aeb75beaf9fa0",
"help/en/helpers/SearchText.md": "42213dae0a4634936c01a67a4beff51167921b5e2e7f71c62179acc1eb2ddc1e",
"help/en/helpers/SearchText.md": "7962291f5e82ae3ce3a5c6749b479e2e5a02d83202910f57d4d9bef03791e657",
"help/en/helpers/Secure.md": "c95d170799ea7bae219b16f55385db17fd5f52db2898b59fb949fd87fa0a8e50",
"help/en/helpers/SoftDelete.md": "34912f62fa21bff29d5124280f11ce911f74f2a9fe836ac230879d0f992094a8",
"help/en/helpers/Str.md": "8c850a76b9932046bdf0232735a0050bbaa8ba4dcc2526d819862adfe2af368f",
@@ -769,7 +772,7 @@
"help/en/hooks/lifecycle.md": "ec82cd9ccb2bc48e9054ec1876c40552859da4ead45129f920026f821f84e87f",
"help/en/installation/README.md": "a122a89fc4a910064f02fc906a0fdf3a9098147f9ed2f98a75185ed66d190f29",
"help/en/installation/admin-directory.md": "26a7b2198078f8ea11dba7fa6a0699cf35c66e78fc88279c82012e6a01f59a38",
"help/en/media/README.md": "1f346b26bd6cb0f7238204ed0cb1d56cea9f6bac7f96e06d062fc8d01226d702",
"help/en/media/README.md": "3e6f40e20bc3e7e5c359600b0047534690e9e6f61b119aae91b282f797b68bda",
"help/en/media/analyzer.md": "d49175369396da1cc3d50ec4cbc7684f36971021532c648375999e37f90a4f20",
"help/en/modules/README.md": "fac8da620ec1f087bef3c7cc3a4178d95747ccece3524ee6c53ba6c3a65b4a21",
"help/en/modules/antispam.md": "1808f910d530f7068e60e2a9306c15ae5b69cccc30ee3e492f35cd535004f65d",
@@ -777,9 +780,9 @@
"help/en/modules/benchmark.md": "e7b9d233abd386db41413f45c79639a3bebfc37241bdbab461028e87a5615f87",
"help/en/modules/cache-warmup.md": "d92183948e03dd4f9a18308d36df7d5ba57e7e87f268b5f3f1de38ed99c518dc",
"help/en/modules/comments.md": "55f45f748f32fbd36233c43574e212f3fa3fdd1ce8eb819f9d1cafb53215ed97",
"help/en/modules/commerce.md": "d916b293c7bfb3567fee02f5d9f15de0d5329dde60d5c5d43f24bce660860156",
"help/en/modules/commerce.md": "e31cbf7ead9d758401e35d7723f0189a354ce818a4fcf5a0118c73795088dcf0",
"help/en/modules/commerceml.md": "610f6b2f9abbeeac604da0408192ae52dae7d6f3fb09e0d321e14d1cc190b808",
"help/en/modules/contacts.md": "3d4d7b109a7a7bf20ceda0cb0da65634131a837c7d39c4a1433f317c3bad3512",
"help/en/modules/contacts.md": "81c818e9cbcd1d61f34d9199219ea160a04bf88af110b32c3527c65fba65d39e",
"help/en/modules/content-packages.md": "4b89df6d05fa75e484e38e404d53fa7ecc6b61aa6930fbf74cf9b6611bfbf532",
"help/en/modules/contributions.md": "ec283e61f09d6008a7208fced495a2748685c50bbfe22b384f458cf338e63125",
"help/en/modules/crossposting.md": "7b8fc0b89c90e27c8e90431221cfbf7eca4c3cd2b5495a0c8ed5d76906dc1daa",
@@ -818,10 +821,10 @@
"help/en/modules/rss.md": "22958cb500ddfcd83201086b36e43365d51f9f125b8663ec38e2e30eded4d0ae",
"help/en/modules/scheduler.md": "fc0c34af054e0081c6ad8cb1abddcff014c0a6eb77178c111fb0b15e0585c75f",
"help/en/modules/search-analytics.md": "3e21c935d76171a5a74cba9fc2904092d2d30062918c2f7ff1988aae0e499264",
"help/en/modules/search.md": "6bd582bc687436d99d292655ab4f9c3e3726c53a5dcc7d9dd94744545d22daf0",
"help/en/modules/search.md": "10cec34b6a4eb452b514f224e59e9f600904548887c63f19a2cb61e6ed6bbe62",
"help/en/modules/seo-audit.md": "e18adcb79847fb5a1cedc8cfff03b5938eac1c2e2d22816ecc5e5b02b48b95d3",
"help/en/modules/site-readiness.md": "dbccccf0633f0e72d32cc8a2b0de319dea576f28b67f0d9c2907106c97fef1c1",
"help/en/modules/smsc-auth.md": "d66bdd5cb748db83b565e7cbc8150bd38099de778e94c9c4cb0ff1d065703479",
"help/en/modules/smsc-auth.md": "1c7a3337404e92da4e7f60f53d11fb6e791c18ee8e1533590deec111e3efe1ec",
"help/en/modules/system-health.md": "f43c3745ee1437f392724d35299d208dd73ff344a3efff60273cc7b23c6fa731",
"help/en/modules/traffic-analytics.md": "5ed8a48a4d05350d8dced3db8fadcedbf32a2bf80147da39872a0f591ca8ce84",
"help/en/modules/tutorial.md": "2e6bcf3e7de1799fadddbe5b5dd56de613c91baf2a4d6c45232eb2144279780c",
@@ -852,7 +855,7 @@
"help/helpers/Request.md": "93b98e898273dc5a1e0cc77babd14487181464bff2c30cb8e08f5425c520ae61",
"help/helpers/Response.md": "d96965599876b9244a6a2deaee2f18f9d033959a0f0f7b942c33e3aa1fcf143f",
"help/helpers/RussianStemmer.md": "10a849612da6b01d3fba062550a7765b1bfd6f558c2751e5ceb67887924bd2c3",
"help/helpers/SearchText.md": "314fcbffaa95ba52840bf07d0a0070372b51f0db62dcea4ff770184a467d6152",
"help/helpers/SearchText.md": "cb28d568ffefe43002611300c8273f2e8a489742c3123a3a4ca0795324e88d56",
"help/helpers/Secure.md": "17ffdeb9c226be816e0c00dc92c90bd86643261709bc889fd238afef5963b61a",
"help/helpers/SoftDelete.md": "787d30d2880a304d22b3086a3a5f8deb5d282b5617bb0c74e0b642295f54c39d",
"help/helpers/Str.md": "34b54c40ba492a5899e32fcd695380ec458518ca287ccaafbda4c0f38d9bfcf7",
@@ -868,17 +871,17 @@
"help/hooks/lifecycle.md": "0d73b7eea62dbc1cc5f3a07449f465105ad8a477adf85b22d2adbc53529b81d2",
"help/installation/README.md": "6a9c434bbe81f493ad3b07ad83fd8c8a8739423f67f3e657b5a9a3dba0d2738a",
"help/installation/admin-directory.md": "fcb31d8bad1c18dd7e5c6843dc4f7b8c4139dbceaa670a7c4e0c060479f5ef9d",
"help/media/README.md": "9a8027ffa29ad342e194868890174fbdf59b5dbbbd7ef39db9f1cf79888b2070",
"help/media/README.md": "517d9f25119bcac6910f49a9a4c9876fca9dcf6f128e0ce8eb4160648e3bc20d",
"help/media/analyzer.md": "a057f3e95842d8d5168f9d456be656c91dc2382c26ce9f205bb620a902132ad4",
"help/modules/README.md": "01f2d804dbc841e849e41fc07e9d86d5118adc30a3da98a39672200c2ae17d65",
"help/modules/README.md": "7baf0060effebd1f0f6870b75e617eb3aeabc8bd36bf174de790cd23717e69d6",
"help/modules/antispam.md": "ddcb858c6c80ee140addbdcd0e27ae3f7900100f1745ee006edabf52d0955fff",
"help/modules/banners.md": "e8faa2a923c5f5c39a0b7caf80a511d03fb680bd2604ddc59ec0362a244ee77f",
"help/modules/benchmark.md": "6c31f16fc3f083f29c65398c38b78609a3d94da168d30a93825af8ee9650eb0d",
"help/modules/cache-warmup.md": "56c05ec252b44a1a0d1c4b09efa90cf9d450b0b138787903c898f56b377b8749",
"help/modules/comments.md": "04bcdea5aa7740e3749b51dcc9fc2954e98b22298d3e4fcdcc9865d94e1d0376",
"help/modules/commerce.md": "7fac85f0cea873bc57baa79c5bc57bd4d5bdb11af291d105e01785ab84f3cedc",
"help/modules/commerce.md": "0255c08bdf10f7db8e530c15e987c5d137ee115434ca0a0d71a831cc9fbfad9f",
"help/modules/commerceml.md": "60a0a5c541b560b5a6a4ed8213d9145caace68c15be534af13f5f817eb5251ba",
"help/modules/contacts.md": "8df21307a300a335aaf243cb42a107829114ac031da128cfdfc6f4b94452dd3a",
"help/modules/contacts.md": "cb4284709f7b469164e3ef4c817893cdcaf4eba1b460164fb36f75a798bc457d",
"help/modules/content-packages.md": "8dde0ebc897b7891a6afadb6491f4797bef26622918addab67f0f4ebfd719678",
"help/modules/contributions.md": "3213f68af10e1b6a200aa61a3aaa9dad270da7ccd675b43bf7cc1ded90b55e60",
"help/modules/crossposting.md": "d6d0922c8d48d3f35418a00227a96d9343de024ecbc8affc1712c0a223f4547d",
@@ -917,10 +920,11 @@
"help/modules/rss.md": "0f8fe5fa5bd4991b432068a5a520d4a45d41dd6a2c6011f7f293f74c8d9c1588",
"help/modules/scheduler.md": "a39cf392ce9cf5af63e92063b95aea63979a24b27218a4b090b94834ef90b536",
"help/modules/search-analytics.md": "0444e0dacabe408358bcdfda4b6b2a475095e8b1c0ffc1361ff58c4db0576d35",
"help/modules/search.md": "3dd37c32598c12654dd4ab97ec0c50d42ffc5083485ed37be0f135296af23afe",
"help/modules/search.md": "a39ffbd45e7d5786cd16d2678f5386c55c3081063177f6525b12849da1c251d3",
"help/modules/seo-audit.md": "b0fb78eda0e83c0711b7717fcc730e7bc0cb86c6d692f21aa749cc4975f0ee27",
"help/modules/site-readiness.md": "2c7f845ece693ef14f23ea2b3b226f38ebfed305fc67bf842fae971f0e12255a",
"help/modules/smsc-auth.md": "13b844d102adb451d154ee9aff9b44d8b5faaac67c4491d73c832962f9a194d2",
"help/modules/sliders.md": "f3022a36b5c483f49d63b86b4651989e98636018fc8a7449ca45b2875277a3fe",
"help/modules/smsc-auth.md": "361b46fbc4812513f59e8ba15d095607851f835d7765ac396c3efd79ecde10e3",
"help/modules/system-health.md": "671d44d89f0d3ada4fcaa4ef3d8a7662a6c77dea6c0cd567d8f3e923d29e12a5",
"help/modules/traffic-analytics.md": "12f966acc6ffdf73e2d40682c6db203f8dc3918ba5461a78607c1d783ed54d02",
"help/modules/tutorial.md": "4239eef8d793b7bb799c901c821b229e456c7602ac694c0e3ffc5028b4eb57c8",
@@ -944,7 +948,7 @@
"system/App/Common/AdminAssets.php": "dcd58f19fda48263b2912a10dfdc24e3f12967683649c4f614a8d54c62d51b86",
"system/App/Common/AdminLocation.php": "d2729b0d627ddbdeb7f0c4b110664ed949f373946f80929c51688391c64352de",
"system/App/Common/ApiTokenRepository.php": "0795e1add3723708240949b7aeada364fbeadd74366884b57e9142efcfaf617c",
"system/App/Common/AuditLog.php": "4e2dfceba55fd653d071261766c9051322a03a994034677463782ca0d09f3f87",
"system/App/Common/AuditLog.php": "5306d21085ff7ef7313f5798fbd416872825ff7d2831f94cd9744de010e1cc2b",
"system/App/Common/Auth.php": "647421cae2ec8bbf273d687ac1fda134c7bcc28db4977e01f5896c79e7c2c83d",
"system/App/Common/Auth/IdentityLinker.php": "7da4d902405bb4ead2278e5bc3578cd0318422fe82318bec94257ed44d5ba98c",
"system/App/Common/Auth/PublicUserNames.php": "0345d1f452e0a9accaa9b98eea71f27a428692a5b4f2ad225a10ad8bcf777684",
@@ -1019,7 +1023,7 @@
"system/App/Common/Registry.php": "7b4ddbb3f96a7ad54a8d43e151cb595e598d3620b368d21721927ce9d54e1ea3",
"system/App/Common/ResponseSecurityHeaders.php": "b159a7105d3c1fb3ffdefbbc5997741d7b0f83bfb26fabcac96f61b53969364f",
"system/App/Common/Router.php": "4b0dd3fd9f732177070ce3326b691a8decfbe698aa0d8cf687a18f6c02b73d91",
"system/App/Common/RuntimeConstantSchema.php": "1ea1a029a29e4b0284d047f6ee6ec97dd50f4b42556e909a1a03481a4a380105",
"system/App/Common/RuntimeConstantSchema.php": "e67a2de96a2496e0463a3b9e9d66fa6b2ef0e4b7dff768469ef472ba639797d7",
"system/App/Common/RuntimeConstants.php": "decb1277dada75ee2b5c7d531f064db7ffecef5b3ad71d306111b2a45121d34f",
"system/App/Common/RuntimeDirectoryGuard.php": "2263f100530f7b0a7790f698581dff14181d4e2a917c12ad1ea888e5df1f59b2",
"system/App/Common/Session.php": "4a870a109c75ca941bebfd798d64b1396dde7d56ec060d9491f7d71b70a13deb",
@@ -1041,7 +1045,7 @@
"system/App/Common/WebMaintenance.php": "a91f9ee8371da6c6a020d9ce39e1f9f7b517b2644ea51ada65f1f487ff337e6b",
"system/App/Content/BasketTables.php": "1c4d1d9a5ff894c6ebb0edbcfd7bb6f791124e46dd8bed3b70fb0ef2c15069a0",
"system/App/Content/Blocks/ContentBlockRegistry.php": "53bc204e7dd1f9d61781049627162341a4d7e656783915161fa5c4eae4cfa228",
"system/App/Content/CatalogMenuRepository.php": "d2b384255fb9e67264f071064dafd645913a73ee640e559d600b1671455443eb",
"system/App/Content/CatalogMenuRepository.php": "6ee0890c5407fa37f5f4a88c5e27ca58b27407a8362e012320bc26fd072cf523",
"system/App/Content/CatalogTables.php": "5abd93b17142ea5c57a0726c8c6e3dcaf896790cf4ba31ea69be19a828ac9517",
"system/App/Content/ContactsTables.php": "d0042c761b854903b483e1b3a30d6014c596eef1a03c40cdbedae4fecb3c25d8",
"system/App/Content/ContentPlacementMap.php": "55950a599eb5385e2d81833815f0e2c08bdd06c61450565052ec1c19d38e00fe",
@@ -1050,7 +1054,7 @@
"system/App/Content/Directories/DirectoryRepository.php": "47d4f7a91841ff437c4448aa71f6a2b38c621dcf5a1204bd2ff4948f3ad671c1",
"system/App/Content/DocumentTerms.php": "353d0a36e0cef77d4016f9edf9c2a2a3a27db953a3eb4cb91d7ec49af18b0609",
"system/App/Content/Documents/ContentCacheInvalidator.php": "d2bfd72d4df1a05e281b88543fabb8356ffc9799d37286c3767c6a0173ea91b1",
"system/App/Content/Documents/DocumentAliasRegistry.php": "aa87cb8b65990f480f5b4f5dea83948dec9758fb4069b43eeba4be7f7752be51",
"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/DocumentExcerpt.php": "8c5742f976a492531af95f2291e8c824c3a9804a54a27997f9f51a716a57f19d",
@@ -1061,7 +1065,7 @@
"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": "5ad32bacb4ad5c2b2760db613be291f154ea3882695c2aa05607b1c5bc80f25f",
"system/App/Content/Documents/DocumentMutationService.php": "868e1301cf822516debe1f441f80eb1897d0a1617f8d27a6a2a1dced8bafa6f1",
"system/App/Content/Documents/DocumentPickerRepository.php": "0e8efb5b42aadd74725213b8302b53b5147d7e0eb2e83d301a5f073bb0c13142",
"system/App/Content/Documents/DocumentRevisionPayload.php": "c7f83350de98b6d98531508eafecc4a2b5a0551c7f2ba77a9e5b954fd1287ea0",
"system/App/Content/Documents/DocumentRubricCodeRunner.php": "a9c04196b76ab91c7b1f8b53afbd45fc6727805c953fb5fe5ff36b69846d389f",
@@ -1091,10 +1095,10 @@
"system/App/Content/Fields/FieldSettingsForm.php": "e436743e17fab7aaf14dab6ce034fa2ece2f5d165a4aa04450d034606c8e2a73",
"system/App/Content/Fields/FieldType.php": "47979aaa49b773e2a00288ffccb746218b58ece8fa6d178a90d0cc9c853055d9",
"system/App/Content/Fields/FieldValidationException.php": "1a1d6d5278d6f8ff697e5afe75e0a6f59db2b12121c93b786fd87f8b81287717",
"system/App/Content/Fields/FieldValidator.php": "5408849670818b379a66d5db9b7aa0d94ebbd8438d7788124f62cd221dae5561",
"system/App/Content/Fields/FieldValidator.php": "49a2342783f994d054ca037907cc7101a9cf3a4ad5f288804673c609eb2b0d63",
"system/App/Content/Fields/FieldValueCodec.php": "5868e9aaa11a8aa2c4dde33e8b358861b4a5a49f6ff235c1d0e22b836dc3df55",
"system/App/Content/Fields/HtmlSanitizer.php": "003ddd83bc6b3c89e1ccdbfe4abf94e013c4a6b1272cf29c1ed36790f3ac7919",
"system/App/Content/Fields/MediaFieldValue.php": "5821060785f859b414a448d4998a9e7b40df69d536574f0fbe600f1d7611ea35",
"system/App/Content/Fields/MediaFieldValue.php": "2cc5db5159f68eb0132937d63641eced0b79fd1465c69cdb1340afadfe88381e",
"system/App/Content/Fields/PublicFieldRuntime.php": "9abd8e5dd43c56b27dcbf87d258cd9764ae1d67e95a397a63a6c6516f7a417f8",
"system/App/Content/Fields/PublicFieldTemplateContext.php": "785d6757710dee59d8d30756917235bb4f49eb5bd743c01d2365c05d74bf0a5a",
"system/App/Content/Fields/PublicFieldTemplateResolver.php": "72c0750c50129cbabc0a97147fe2f2f4b2b3811cd77f4de197dc3753db46903a",
@@ -1188,7 +1192,7 @@
"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": "04793123f0d4bef77141e3f8471d454ac5b4d2b17dce4630eca40da0e2f0b576",
"system/App/Frontend/Auth/Controller.php": "14a425c24653fdfda4225550635130bcdf67a6d63a1eb3817ad86a41d292efe9",
"system/App/Frontend/Auth/Feature.php": "dbaeac564886c7949e0e47c85fc5496c25221d880c4f03cee40cc5e15d042a1c",
"system/App/Frontend/Auth/FormTemplateRepository.php": "bcd8797e63cb67803aef2267a2c62e3ad368b5fd947057a768adbb1288ee9281",
"system/App/Frontend/Auth/OAuth/HttpClient.php": "b09acf92b312bd05ca78500fccaac3b77d36a1046d64d3615a00d60af4e3594f",
@@ -1197,9 +1201,9 @@
"system/App/Frontend/Auth/OAuth/OAuthService.php": "c310a2a6756425a4b2875b12c048b0476dd5b424e89561ac1fa73aebfb3cf045",
"system/App/Frontend/Auth/OAuth/ProviderInterface.php": "c99525fd51a1bebc24c189f52f1a08bcef087b98ffa960d1b378deb68022d9ba",
"system/App/Frontend/Auth/OAuth/ProviderRegistry.php": "e0ff4bc12ef2349d4f914299c78715438d9fbd44ab98e00937534786baa07800",
"system/App/Frontend/Auth/Phone/ChallengeRepository.php": "1e53d355aaf0adcc8e10a2b1b50e638916fd688c62a7ed6a4bd9c161c14a6390",
"system/App/Frontend/Auth/Phone/ChallengeRepository.php": "2bfd7ad7b4640e653d3ad0e1ed5f1779ad8df2b569bee071c040d44996c15ea8",
"system/App/Frontend/Auth/Phone/Controller.php": "bb07bfb74a89742c2dbeeb7edf79f02ae43ef6a252d02a3bb7c3c78144cf280d",
"system/App/Frontend/Auth/Phone/PhoneAuthService.php": "783b671cb6b6f80eb4ecbd7bec86d417dd7030c7768ba01f742600e4a6b55a89",
"system/App/Frontend/Auth/Phone/PhoneAuthService.php": "4f31afa6d600a754cf76805b2a475bf47ffbf7a540b65fb99df098f227211a5a",
"system/App/Frontend/Auth/Phone/ProviderInterface.php": "55107c8f6a89bea01b91314dfe523b684bccf5bf9f1a3c8f047fd3a509aa6d00",
"system/App/Frontend/Auth/Phone/ProviderRegistry.php": "fbeb488c9b63843085b680500ecafd61389a29f55e0d76692550939e378d2f16",
"system/App/Frontend/Auth/ProfileRepository.php": "9cd7be35b048faf407a925e6369a5895b8bfafe6cf756eb3c4321ea4979a9af6",
@@ -1233,7 +1237,7 @@
"system/App/Frontend/ConfiguredPaginationRenderer.php": "df4d28a386e4180fd0dbddbc30f08ca8c6345e2f3b0d196482fa9d2a5d4f4d32",
"system/App/Frontend/ContentCollectionTags.php": "337085d73f0ad609b5a37a4fa72537459f4ae2e5f503e39a8750be95127d89b5",
"system/App/Frontend/Debug/PublicDebugCollector.php": "6755ad710e9d045b2bfa74745354f717c101236b5d64ad5530fd779468969a00",
"system/App/Frontend/Debug/PublicDebugToolbar.php": "89a0fd4a1cab3ac8e3576cd8116332edbaaf6e09d3e05b0daa5798cd8f59c6ff",
"system/App/Frontend/Debug/PublicDebugToolbar.php": "7a92630b59483e63d623fa769e13c246e6bd657c6d027a5d14ab8f95986da0de",
"system/App/Frontend/Debug/PublicLayoutInspector.php": "8a7f5ea4cff9834ad13cac7e0d2380d878978e406e71860e1ad9ca183e427dab",
"system/App/Frontend/DocumentAccessPolicy.php": "13102e35dada9eb0e174189b976605257b48a32756e801d1ec5c358d0c0912b8",
"system/App/Frontend/DocumentApi/Controller.php": "7c5ffecf7e1515710931d7c31b5b2c5f555f8d35778a1fc202a8136b07c2490c",
@@ -1336,7 +1340,7 @@
"system/App/Helpers/Request.php": "29cc2cf20e1e4acd6e2f23c3de9b9606aec3f44d0b3577613b4fde376f7ae858",
"system/App/Helpers/Response.php": "beea1596703526b7a2424459e392487e8bc8a9ce216f8d70bd2b09564709e0b9",
"system/App/Helpers/RussianStemmer.php": "60a896c8254290ae554273132c103c1c209a14944f52cfd631e7a3ae13145529",
"system/App/Helpers/SearchText.php": "1747521adb640161c6f4df648134c8349769e3cd7ddbc726118af81f1a4dcbf0",
"system/App/Helpers/SearchText.php": "12e2fa63a99dd47dd00316eba33f9e162cbfb90e5a5fb9c816fc9c21de0c6ec1",
"system/App/Helpers/Secure.php": "99a5a170cd574e5b6a6c3b433c5b8bac92407fba62088c93e893e215f18162a3",
"system/App/Helpers/SoftDelete.php": "9b85f227bbbc12e4bddd9bcf211b477044041b87bf94d1a422f80d46a10c3ccd",
"system/App/Helpers/SplitTest.php": "d0c875c264df66509b53693dccae8d9f482b069dfe98eb5820481077f3c4508c",
@@ -1350,7 +1354,7 @@
"system/language/ru/lang.xml": "7d46cc9822f064a1af4d6528ccfe2dd4a9cc318912e327fd807befd2ca3dfd11",
"system/preload.php": "66bfb1aaf36ba85945b729b09a37e96ead0be1cc2749ebd9abd4b9da4f25ba0e",
"system/public.php": "3f72b5bf44bc0ba5ba6fb6b3adc2e2f9d5e9902d9cdfa3d7b748f4aebd179b87",
"system/release.php": "e9aeb913c895912f8dae625b0311fb38cd871c20232eede2948e934ba4bff656",
"system/release.php": "73af5bab9ab6ad78aa6c4fc4509dd4419cf68e66142381c1500c01a72b655c8e",
"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",
@@ -1932,7 +1936,7 @@
"system/vendor/Twig/Util/TemplateDirIterator.php": "b52ad2c2cda8efdb9b6b3719fdd64c5a3e38bba1ca440c1941132ac60d756ec7",
"templates/index.php": "1a0b826446e8c8211372f1f45a6ace298395116cd80bc1cbb2cdef7762038a95",
"templates/public/index.php": "c57a4bb3eb5ae6c0394cc748a14dc7f703a9d819333f181f0edffffa680100bc",
"templates/public/templates/.htaccess": "ef94d4c2ac2a038b6c2ae673d9573333a0a20f7fac9e2d75af11c6c387a157a3",
"templates/public/templates/.htaccess": "ca761a92f2b6eb47feffcf200a8fd846e7fdd8abe065008d29beff0319ca3496",
"templates/public/theme.json": "4fb596d58dbda7f58c26c4f3640c56d6f6981f402ffbd368229adddc23683690",
"tmp/.htaccess": "ca761a92f2b6eb47feffcf200a8fd846e7fdd8abe065008d29beff0319ca3496",
"uploads/.htaccess": "90cd571b4c9f09a63732254fbcdca92cc7da8560a6e5b4d9ec7e58ac40e528bf",
@@ -1942,6 +1946,7 @@
"file_owners": {
".gitignore": "core",
".htaccess": "core",
"CHANGELOG.md": "core",
"README.md": "core",
"adminx/.htaccess": "core",
"adminx/Support/AdminLocale.php": "core",
@@ -2357,6 +2362,7 @@
"adminx/modules/PublicSite/PresentationModel.php": "core",
"adminx/modules/PublicSite/PresentationsController.php": "core",
"adminx/modules/PublicSite/PublicViewTemplates.php": "core",
"adminx/modules/PublicSite/TemplateMap.php": "core",
"adminx/modules/PublicSite/assets/presentations.css": "core",
"adminx/modules/PublicSite/assets/presentations.js": "core",
"adminx/modules/PublicSite/assets/public-site.css": "core",
@@ -2373,6 +2379,7 @@
"adminx/modules/PublicSite/view/map.twig": "core",
"adminx/modules/PublicSite/view/placements.twig": "core",
"adminx/modules/PublicSite/view/presentations.twig": "core",
"adminx/modules/PublicSite/view/template-map.twig": "core",
"adminx/modules/Requests/Controller.php": "core",
"adminx/modules/Requests/Model.php": "core",
"adminx/modules/Requests/Revisions.php": "core",
@@ -2848,6 +2855,7 @@
"help/modules/search.md": "core",
"help/modules/seo-audit.md": "core",
"help/modules/site-readiness.md": "core",
"help/modules/sliders.md": "core",
"help/modules/smsc-auth.md": "core",
"help/modules/system-health.md": "core",
"help/modules/traffic-analytics.md": "core",
+1
View File
@@ -81,6 +81,7 @@
self::item('#^/documents/(?:create|\d+/edit)$#', 'Редактор документа', 'Создаёт и изменяет страницу сайта: её адрес, публикацию, SEO и значения полей выбранной рубрики.', array('Набор полей и их группы задаются рубрикой.', 'Изменение alias может автоматически создать редирект со старого URL.', 'Ctrl+S сохраняет изменения и оставляет редактор открытым; ревизии позволяют вернуться к прошлой версии.'), 'Смена рубрики существующего документа запрещена без отдельной миграции полей.'),
self::item('#^/documents#', 'Документы', 'Основной реестр страниц и товарных карточек сайта, построенных на рубриках и их полях.', array('Фильтруйте документы по рубрике, состоянию и названию.', 'Публикацию, копирование и удаление можно выполнять из списка или пакетно.', 'Удалённый документ сначала попадает в корзину и только затем может быть удалён окончательно.'), 'Документ №1 и системная страница 404 защищены от окончательного удаления.', 'content/documents'),
self::item('#^/public-site/presentations$#', 'Представления', 'Создаёт повторно используемое оформление карточек, списков, слайдеров и других результатов.', array('Черновик проверяется на реальном материале и не меняет публичный сайт.', 'Публикация фиксирует версию, а назначение выбирает место и режим применения.', 'Представление меняет внешний вид готовых данных, но не заменяет рубрику, запрос, блок или модуль.'), 'Для совместимости каждое новое назначение начинает работу в режиме «Текущий вывод».', 'content/public-site'),
self::item('#^/public-site/template-map$#', 'Карта шаблонов', 'Показывает фактическую цепочку сборки обычных страниц, списков, каталога и товаров.', array('Общий шаблон создаёт внешний каркас страницы.', 'Шаблон рубрики размещает поля, блоки, запросы и модульные теги.', 'Тема оформляет только подключённые компоненты, если не включён отдельный режим оболочки темы.'), 'Карта работает только на чтение; каждая кнопка открывает штатный редактор выбранного слоя.', 'content/public-site'),
self::item('#^/public-site/map$#', 'Визуальная карта сайта', 'Показывает деревья меню, связанные страницы и опубликованные документы вне навигации.', array('Уровни строятся из действующих пунктов навигации.', 'Ссылка документа открывает его штатный редактор.', 'Документ вне меню может использоваться запросом, каталогом или прямой ссылкой.'), 'Карта является обзором и ничего не публикует.', 'content/public-site'),
self::item('#^/public-site/placements$#', 'Размещения компонентов', 'Показывает, в каких сохранённых шаблонах используются блоки, подборки, навигации и модульные теги.', array('Вкладка «Размещения» показывает каждое найденное место отдельно.', 'Вкладка «Где используется» объединяет одинаковые компоненты и показывает число связей.', 'Кнопки справа открывают конкретный исходный шаблон и конкретный подключённый компонент.'), 'Карта предназначена для поиска связей и ничего не меняет на публичном сайте.', 'content/public-site'),
self::item('#^/public-site/templates$#', 'Шаблоны публичного сайта', 'Редактирует общие файловые Twig-компоненты активной темы.', array('Список материалов имеет системный резерв.', 'Оболочка страницы и остальные компоненты темы редактируются без поиска файла вручную.', 'Сохранение проверяет Twig, создаёт ревизию и очищает публичный кеш.'), 'Шаблоны рубрик, запросов и блоков остаются в собственных разделах.', 'content/public-site'),
+1
View File
@@ -100,6 +100,7 @@
}
try {
ThemeAssets::registerViewOverrides();
Twig::twig()->createTemplate($content, 'public_view_' . preg_replace('/[^a-z0-9_]/i', '_', (string) $code));
} catch (\Throwable $e) {
return array('ok' => false, 'message' => 'Ошибка Twig: ' . $e->getMessage());
+38
View File
@@ -5088,6 +5088,30 @@ ul.sortable {
flex: 0 0 auto;
white-space: nowrap;
}
.media-picker-manage {
display: flex;
align-items: center;
gap: 8px;
}
.media-picker-manage[hidden] {
display: none;
}
.media-picker-folder-form {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.media-picker-folder-form[hidden] {
display: none;
}
.media-picker-folder-form .input-wrap {
min-width: 0;
flex: 1 1 auto;
}
.media-picker-folder-form .btn-icon {
min-width: 40px;
}
.media-picker-url {
display: flex;
gap: 8px;
@@ -5279,6 +5303,20 @@ ul.sortable {
.media-picker-tools .media-picker-search {
flex-basis: calc(100% - 50px);
}
.media-picker-manage {
width: 100%;
}
.media-picker-manage .btn-primary {
min-width: 0;
flex: 1 1 auto;
}
.media-picker-folder-form {
align-items: stretch;
flex-wrap: wrap;
}
.media-picker-folder-form .input-wrap {
flex-basis: 100%;
}
.media-picker-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
+82
View File
@@ -221,6 +221,87 @@
}
};
// ------------------------------------------------------------------ //
// Простые формы модулей: стандартный Ajax-контракт без отдельного JS.
// ------------------------------------------------------------------ //
Adminx.initAjaxForms = function () {
document.addEventListener('submit', function (event) {
var form = event.target.closest ? event.target.closest('form[data-ax-ajax-form]') : null;
if (!form || form.getAttribute('data-ax-submitting') === '1') { return; }
event.preventDefault();
var submit = event.submitter || form.querySelector('button[type="submit"],input[type="submit"]');
var data = new FormData(form);
if (submit && submit.name && !data.has(submit.name)) { data.append(submit.name, submit.value || '1'); }
form.setAttribute('data-ax-submitting', '1');
form.setAttribute('aria-busy', 'true');
if (submit) { submit.disabled = true; }
Adminx.Loader.show();
Adminx.Ajax.request(form.getAttribute('action') || window.location.href, {
method: (form.getAttribute('method') || 'POST').toUpperCase(),
body: data
}).then(function (payload) {
var response = Adminx.Ajax.handle(payload);
if (payload.ok && response.success !== false) {
form.dispatchEvent(new CustomEvent('adminx:form-saved', { bubbles: true, detail: response }));
if (form.hasAttribute('data-ax-reset-on-success')) { form.reset(); }
}
}).catch(function (error) {
Adminx.Toast.show(error && error.message ? error.message : Adminx.t('server_unavailable', 'Сервер недоступен. Повторите попытку.'), 'error');
}).then(function () {
Adminx.Loader.hide();
form.removeAttribute('data-ax-submitting');
form.removeAttribute('aria-busy');
if (submit) { submit.disabled = false; }
});
});
};
// ------------------------------------------------------------------ //
// Пакетная загрузка обходит серверный max_file_uploads без изменения php.ini.
// ------------------------------------------------------------------ //
Adminx.Upload = {
BATCH_SIZE: 20,
files: function (url, files, makeData, onProgress) {
files = Array.prototype.slice.call(files || []);
if (!files.length) { return Promise.resolve({ files: [], batches: 0, total: 0 }); }
var self = this;
var uploaded = [];
var batches = Math.ceil(files.length / self.BATCH_SIZE);
var run = function (batch) {
if (batch >= batches) {
return Promise.resolve({ files: uploaded, batches: batches, total: files.length });
}
var start = batch * self.BATCH_SIZE;
var chunk = files.slice(start, start + self.BATCH_SIZE);
var data = makeData(chunk, batch, batches);
return Adminx.Ajax.post(url, data).then(function (payload) {
var response = payload.data || {};
if (!payload.ok || response.success === false) {
var error = new Error(response.message || 'Не удалось загрузить файлы');
error.uploaded = uploaded.slice();
error.batch = batch;
throw error;
}
var result = response.data || {};
uploaded = uploaded.concat(result.files || []);
if (typeof onProgress === 'function') {
onProgress(Math.min(start + chunk.length, files.length), files.length, uploaded.length, response);
}
return run(batch + 1);
});
};
return run(0);
}
};
// ------------------------------------------------------------------ //
Adminx.Sidebar = {
KEY: 'adminx-sidebar',
@@ -900,6 +981,7 @@
Adminx.Tabs.init();
Adminx.Drawer.init();
Adminx.Tooltip.init();
Adminx.initAjaxForms();
Adminx.initLogout();
Adminx.initCopyTag();
Adminx.initSectionHelp();
File diff suppressed because one or more lines are too long
+91 -2
View File
@@ -47,8 +47,14 @@
+ '<div class="media-picker-tools">'
+ '<div class="input-wrap media-picker-search"><i class="ti ti-search"></i><input class="input" type="search" placeholder="' + placeholder + '" data-mp-search></div>'
+ '<button class="btn btn-ghost btn-icon" type="button" data-mp-up data-tooltip="На уровень выше" aria-label="На уровень выше"><i class="ti ti-arrow-up"></i></button>'
+ '<div class="media-picker-manage" data-mp-manage hidden>'
+ '<button class="btn btn-secondary btn-icon" type="button" data-mp-folder-toggle data-tooltip="Создать папку" aria-label="Создать папку"><i class="ti ti-folder-plus"></i></button>'
+ '<button class="btn btn-primary" type="button" data-mp-upload><i class="ti ti-upload"></i>' + (type === 'image' ? 'Загрузить изображения' : 'Загрузить файлы') + '</button>'
+ '<input type="file" data-mp-upload-input multiple hidden' + (type === 'image' ? ' accept="image/*"' : '') + '>'
+ '</div>'
+ (onUrl ? '<button class="btn btn-ghost btn-sm" type="button" data-mp-url-toggle><i class="ti ti-link"></i>По ссылке</button>' : '')
+ '</div>'
+ '<div class="media-picker-folder-form" data-mp-folder-form hidden><span class="input-wrap"><i class="ti ti-folder"></i><input class="input" type="text" maxlength="120" placeholder="Название новой папки" data-mp-folder-name></span><button class="btn btn-primary btn-sm" type="button" data-mp-folder-create><i class="ti ti-check"></i>Создать</button><button class="btn btn-ghost btn-icon btn-sm" type="button" data-mp-folder-cancel aria-label="Отмена" data-tooltip="Отмена"><i class="ti ti-x"></i></button></div>'
+ (onUrl ? '<div class="media-picker-url" data-mp-url hidden><input class="input" type="url" placeholder="https://…" data-mp-url-input><button class="btn btn-primary btn-sm" type="button" data-mp-url-apply><i class="ti ti-check"></i>Вставить</button></div>' : '')
+ '<div class="media-picker-crumbs" data-mp-crumbs></div>'
+ '<div class="media-picker-status" data-mp-status>Загрузка…</div>'
@@ -63,12 +69,16 @@
document.body.appendChild(overlay);
window.requestAnimationFrame(function () { overlay.classList.add('show'); });
var state = { dir: options.dir || '/uploads', parent: '', page: 1, pages: 1, q: '', type: type };
var state = { dir: options.dir || '/uploads', parent: '', page: 1, pages: 1, q: '', type: type, canManage: false, busy: false };
var grid = overlay.querySelector('[data-mp-grid]');
var crumbs = overlay.querySelector('[data-mp-crumbs]');
var status = overlay.querySelector('[data-mp-status]');
var count = overlay.querySelector('[data-mp-count]');
var search = overlay.querySelector('[data-mp-search]');
var manage = overlay.querySelector('[data-mp-manage]');
var uploadInput = overlay.querySelector('[data-mp-upload-input]');
var folderForm = overlay.querySelector('[data-mp-folder-form]');
var folderName = overlay.querySelector('[data-mp-folder-name]');
var timer = null;
function close() {
@@ -89,6 +99,8 @@
var folders = data.folders || [];
var files = data.files || [];
renderCrumbs(data.breadcrumbs || []);
state.canManage = !!data.can_manage;
manage.hidden = !state.canManage;
grid.innerHTML = '';
folders.forEach(function (folder) {
var folderMeta = state.q
@@ -111,6 +123,74 @@
overlay.querySelector('[data-mp-up]').disabled = !state.parent;
}
function responseData(payload) {
var response = payload && payload.data ? payload.data : {};
if (!payload || !payload.ok || response.success === false) {
throw new Error(response.message || (response.error && response.error.message) || 'Не удалось выполнить действие');
}
return response;
}
function setBusy(value, message) {
state.busy = !!value;
manage.querySelectorAll('button').forEach(function (button) { button.disabled = state.busy; });
status.hidden = !message;
if (message) { status.textContent = message; }
}
function clearSearch() {
state.q = '';
search.value = '';
}
function createFolder() {
var name = folderName.value.trim();
if (!name || state.busy) { folderName.focus(); return; }
var data = new FormData();
data.set('_csrf', Adminx.csrf());
data.set('dir', state.dir);
data.set('name', name);
setBusy(true, 'Создаём папку…');
Adminx.Ajax.post(base + '/media/folders', data).then(responseData).then(function (response) {
var result = response.data || {};
folderForm.hidden = true;
folderName.value = '';
clearSearch();
Adminx.Toast.show(response.message || 'Папка создана', 'success');
load(result.path || state.dir, 1);
}).catch(function (error) {
setBusy(false, '');
Adminx.Toast.show(error.message || 'Не удалось создать папку', 'error');
folderName.focus();
});
}
function upload(files) {
if (!files || !files.length || state.busy) { return; }
setBusy(true, 'Подготовка к загрузке: ' + files.length + '…');
Adminx.Upload.files(base + '/media/upload', files, function (chunk) {
var data = new FormData();
data.set('_csrf', Adminx.csrf());
data.set('dir', state.dir);
chunk.forEach(function (file) { data.append('files[]', file); });
return data;
}, function (processed, total) {
status.textContent = 'Загружено: ' + processed + ' из ' + total + '…';
}).then(function (result) {
uploadInput.value = '';
clearSearch();
Adminx.Toast.show('Загружено файлов: ' + result.files.length, 'success');
load(state.dir, 1);
}).catch(function (error) {
uploadInput.value = '';
setBusy(false, '');
var uploaded = error && error.uploaded ? error.uploaded.length : 0;
var message = error.message || 'Не удалось загрузить файлы';
if (uploaded) { message += '. До ошибки загружено: ' + uploaded; }
Adminx.Toast.show(message, 'error');
});
}
function load(dir, page) {
state.dir = dir || state.dir;
state.page = page || 1;
@@ -131,9 +211,10 @@
state.parent = data.parent_dir || '';
state.page = data.page || 1;
state.pages = data.pages || 1;
setBusy(false, '');
render(data);
})
.catch(function () { status.textContent = 'Не удалось загрузить медиа'; status.hidden = false; });
.catch(function () { setBusy(false, ''); status.textContent = 'Не удалось загрузить медиа'; status.hidden = false; });
}
overlay.addEventListener('click', function (e) {
@@ -146,6 +227,10 @@
if (e.target.closest('[data-mp-prev]') && state.page > 1) { load(state.dir, state.page - 1); }
if (e.target.closest('[data-mp-next]') && state.page < state.pages) { load(state.dir, state.page + 1); }
if (e.target.closest('[data-mp-up]') && state.parent) { load(state.parent, 1); }
if (e.target.closest('[data-mp-folder-toggle]')) { folderForm.hidden = !folderForm.hidden; if (!folderForm.hidden) { folderName.focus(); } return; }
if (e.target.closest('[data-mp-folder-cancel]')) { folderForm.hidden = true; folderName.value = ''; return; }
if (e.target.closest('[data-mp-folder-create]')) { createFolder(); return; }
if (e.target.closest('[data-mp-upload]')) { uploadInput.click(); return; }
if (onUrl) {
if (e.target.closest('[data-mp-url-toggle]')) {
var box = overlay.querySelector('[data-mp-url]');
@@ -163,6 +248,10 @@
window.clearTimeout(timer);
timer = window.setTimeout(function () { state.q = search.value.trim(); load(state.dir, 1); }, 220);
});
folderName.addEventListener('keydown', function (e) {
if (e.key === 'Enter') { e.preventDefault(); createFolder(); }
});
uploadInput.addEventListener('change', function () { upload(uploadInput.files); });
document.addEventListener('keydown', onKey);
load(state.dir, 1);
search.focus();
+18 -9
View File
@@ -290,20 +290,29 @@ var EnhancedImage = Image.extend({
bindForm: function (textarea, editor) {
var form = textarea.closest('form');
var self = this;
if (!form) { return; }
form.addEventListener('submit', function () {
var root = textarea.closest('.rich-editor');
if (root && root.classList.contains('rich-editor-source-open') && root._richSourceEditor) {
root._richSourceEditor.save();
textarea.value = root._richSourceEditor.getValue();
return;
}
if (textarea.getAttribute('data-rich-editor-dirty') === '1') {
textarea.value = editor.getHTML();
}
self.syncTextarea(textarea, editor);
});
},
syncTextarea: function (textarea, editor) {
var root;
if (!textarea) { return ''; }
root = textarea.closest('.rich-editor');
if (root && root.classList.contains('rich-editor-source-open') && root._richSourceEditor) {
root._richSourceEditor.save();
textarea.value = root._richSourceEditor.getValue();
textarea.setAttribute('data-rich-editor-dirty', '1');
return textarea.value;
}
if (editor && textarea.getAttribute('data-rich-editor-dirty') === '1') {
textarea.value = editor.getHTML();
}
return textarea.value;
},
openSource: function (editor) {
var root = editor.view.dom.closest('.rich-editor');
if (!root || !window.CodeMirror) { return; }
+129 -13
View File
@@ -90,12 +90,15 @@
'state' => Request::getStr('state', ''),
'issue' => Request::getStr('issue', ''),
'payment' => Request::getStr('payment', ''),
'view' => Request::getStr('view', 'table') === 'cards' ? 'cards' : 'table',
'page' => Request::getInt('page', 1),
'per_page' => Request::getInt('per_page', 25),
);
$statsSnapshot = Model::productStatsSnapshot();
return $this->render('@products/products.twig', array(
'result' => Model::products($filters),
'stats' => Model::productStats(),
'stats' => $statsSnapshot['stats'],
'stats_loaded' => $statsSnapshot['loaded'],
'filters' => $filters,
'saved_views' => SavedViews::all('catalog_products', Auth::id(), $this->savedViewFields('products')),
'can_manage' => Permission::check('manage_products'),
@@ -104,26 +107,32 @@
));
}
public function productStats(array $params = array())
{
if (!Permission::check('view_products')) { return $this->error('Недостаточно прав', array(), 403); }
$stats = Model::productStats();
$stats['indexed_label'] = !empty($stats['indexed_at']) ? date('d.m H:i', (int) $stats['indexed_at']) : '—';
return $this->success('', array('data' => array('stats' => $stats)));
}
public function productQuality(array $params = array())
{
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();
$stats = Model::productStats();
$stats = Model::productQualityStats();
$issues = Model::productQualityIssues($stats);
$allowed = array();
foreach ($issues as $item) { $allowed[(string) $item['code']] = true; }
$issue = Request::getStr('issue', '');
if ($issue !== '' && !isset($allowed[$issue])) { $issue = ''; }
$state = Request::getStr('state', '');
if (!in_array($state, array('', 'active', 'inactive'), true)) { $state = ''; }
$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,
);
return $this->render('@products/product-quality.twig', array(
'result' => Model::products($filters),
@@ -1050,6 +1059,7 @@
$search = Request::getStr('q', '');
return $this->render('@products/variant-group.twig', array(
'group' => $group, 'search' => $search,
'focus_product_id' => Request::getInt('focus', 0),
'variant_fields' => VariantGroups::fields((int) $group['id']),
'variant_attributes' => VariantGroups::nativeAttributes(),
'package_templates' => ShippingPackageTemplates::all(),
@@ -1169,6 +1179,7 @@
public function copyVariantProduct(array $params = array())
{
if (($error = $this->productGuard()) !== null) { return $error; }
if (!Request::postBool('confirmed', false)) { return $this->error('Подтвердите создание варианта', array(), 409); }
$sourceId = (int) $params['product'];
$newId = 0;
try {
@@ -1181,12 +1192,15 @@
}
$this->audit('catalog.variant_copied', (int) $params['id'], array('source_id' => $sourceId, 'product_id' => $newId));
return $this->success('Вариант создан копированием', array('redirect' => $this->base() . '/catalog/products/' . $newId . '/edit'));
return $this->success('Вариант создан копированием', array(
'redirect' => $this->base() . '/catalog/products/' . $newId . '/edit?variant_group=' . (int) $params['id'],
));
}
public function copyProduct(array $params = array())
{
if (($error = $this->guardPermission('manage_products', 'manage_documents')) !== null) { return $error; }
if (!Request::postBool('confirmed', false)) { return $this->error('Подтвердите создание копии товара', array(), 409); }
$sourceId = isset($params['id']) ? (int) $params['id'] : 0;
try {
$copy = ProductDuplicator::copy($sourceId, Auth::id());
@@ -1269,7 +1283,23 @@
VariantGroups::add($groupId, $productId);
} catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.variant_group_created_from_product', $groupId, array('product_id' => $productId));
return $this->success('Группа вариантов создана', array('redirect' => $this->base() . '/catalog/variant-groups/' . $groupId));
return $this->success('Группа вариантов создана', array('redirect' => $this->variantGroupUrl($groupId, $productId)));
}
public function attachProductVariantGroup(array $params = array())
{
if (($error = $this->productGuard()) !== null) { return $error; }
$productId = isset($params['id']) ? (int) $params['id'] : 0;
$groupId = Request::postInt('group_id', 0);
if (!Model::product($productId)) { return $this->error('Товар не найден', array(), 404); }
if ($groupId <= 0) { return $this->error('Выберите группу вариантов', array(), 422); }
if (VariantGroups::membership($productId)) { return $this->error('Товар уже состоит в группе', array(), 422); }
try { VariantGroups::add($groupId, $productId); }
catch (\Throwable $e) { return $this->error($e->getMessage(), array(), 422); }
$this->audit('catalog.variant_added_from_product', $groupId, array('product_id' => $productId));
return $this->success('Товар добавлен в группу вариантов', array(
'redirect' => $this->variantGroupUrl($groupId, $productId),
));
}
public function editProduct(array $params = array())
@@ -1287,8 +1317,17 @@
CodeEditor::useRichEditor();
$this->attributeAssets();
$variantGroup = VariantGroups::membership($id);
$returnUrl = $this->base() . '/catalog/products';
$returnLabel = 'Товары';
$returnGroupId = Request::getInt('variant_group', 0);
if ($variantGroup && $returnGroupId === (int) $variantGroup['id']) {
$returnUrl = $this->variantGroupUrl((int) $variantGroup['id'], $id);
$returnLabel = 'Группа вариантов';
}
$shippingProfile = ProductShipping::profile($id);
$nativeAttributes = Attributes::productEditor($id);
$searchAliasesAvailable = $this->productSearchAliasesAvailable();
$mediaDraftToken = DocumentMediaDraft::issue(Auth::id(), $id);
return $this->render('@documents/edit.twig', array(
'document' => $document,
@@ -1301,6 +1340,7 @@
'is_new' => false, 'can_manage' => true, 'quick_edit' => Request::getBool('quick_edit', false),
'actor_id' => Auth::id(),
'catalog_mode' => true, 'product' => $product, 'variant_group' => $variantGroup,
'variant_groups' => $variantGroup ? array() : VariantGroups::all(),
'shipping_profile' => $shippingProfile,
'package_templates' => ShippingPackageTemplates::all(),
'native_attributes' => $nativeAttributes,
@@ -1308,7 +1348,12 @@
'product_promotions_available' => Promotions::available(),
'product_promotions' => Promotions::forProduct($id),
'can_manage_product_promotions' => Permission::check('manage_orders'),
'return_url' => $this->base() . '/catalog/products',
'product_search_aliases_available' => $searchAliasesAvailable,
'product_search_aliases' => $searchAliasesAvailable
? \App\Modules\Search\DocumentAliases::forDocument($id)
: array(),
'return_url' => $returnUrl,
'return_label' => $returnLabel,
'product_copy_url' => $this->base() . '/catalog/products/' . $id . '/copy',
'submit_url' => $this->base() . '/documents/' . $id,
));
@@ -1419,6 +1464,47 @@
if(($error=$this->productGuard())!==null){return $error;}$productId=isset($params['id'])?(int)$params['id']:0;if(!Model::product($productId)){return $this->error('Товар не найден',array(),404);}try{$data=Attributes::saveProductValues($productId,Request::postAll(),Auth::id());}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}$this->audit('catalog.product_attributes_saved',$productId,$data);return $this->success('Характеристики товара сохранены',array('data'=>$data));
}
public function saveProductSearchAliases(array $params = array())
{
if (($error = $this->productGuard()) !== null) { return $error; }
$productId = isset($params['id']) ? (int) $params['id'] : 0;
if (!Model::product($productId)) { return $this->error('Товар не найден', array(), 404); }
if (!$this->productSearchAliasesAvailable()) {
return $this->error('Обновите и включите модуль «Поиск по сайту»', array(), 409);
}
try {
$aliases = \App\Modules\Search\DocumentAliases::saveDocument(
$productId,
(array) Request::post('aliases', array()),
Auth::id()
);
} catch (\Throwable $e) {
return $this->error($e->getMessage(), array(), 422);
}
$this->audit('catalog.product_search_aliases_saved', $productId, array('count' => count($aliases)));
return $this->success('Поисковые названия сохранены', array('data' => array('aliases' => $aliases)));
}
public function productSearchAliasSuggestions(array $params = array())
{
if (!Permission::check('manage_products')) {
return $this->error('Недостаточно прав', array(), 403);
}
if (!$this->productSearchAliasesAvailable()) {
return $this->error('Обновите и включите модуль «Поиск по сайту»', array(), 409);
}
$query = Request::getStr('q', '');
$documentId = Request::getInt('document_id', 0);
$items = mb_strlen(trim($query), 'UTF-8') >= 2
? \App\Modules\Search\DocumentAliases::suggestions($query, $documentId, 12)
: array();
return $this->success('', array('data' => array('items' => $items)));
}
public function copyProductShippingToVariants(array $params = array())
{
if (($error = $this->productGuard()) !== null) { return $error; }
@@ -1521,14 +1607,24 @@
$allowed = Model::productRubrics();
$rubricId = Request::getInt('rubric_id', 0);
if (!in_array($rubricId, $allowed, true)) { $rubricId = !empty($allowed) ? (int) reset($allowed) : 0; }
if ($rubricId <= 0) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Товарная рубрика не настроена'), 404); }
if (!$allowed) { return $this->renderStatus('@adminx/404.twig', array('title' => 'Товарная рубрика не настроена'), 404); }
if (!in_array($rubricId, $allowed, true)) {
$this->assets();
return $this->render('@products/product-create.twig', array(
'rubrics' => Model::productRubricOptions(),
'return_url' => $this->base() . '/catalog/products',
));
}
$document = DocumentsModel::blank($rubricId);
$mediaDraftToken = DocumentMediaDraft::issue(Auth::id(), 0);
AdminAssets::addStyle($this->base() . '/modules/Documents/assets/documents.css', 50);
AdminAssets::addScript($this->base() . '/modules/Documents/assets/documents.js', 50);
CodeEditor::useRichEditor(); $this->assets();
return $this->render('@documents/edit.twig', array(
'document' => $document, 'field_groups' => DocumentsModel::fieldsForRubric($rubricId, 0),
'document' => $document,
'field_groups' => DocumentsModel::fieldsForRubric($rubricId, 0, $mediaDraftToken),
'media_draft_token' => $mediaDraftToken,
'rubrics' => array_values(array_filter(DocumentsModel::rubrics(), function ($rubric) use ($allowed) { return in_array((int) $rubric['Id'], $allowed, true); })),
'templates' => DocumentsModel::rubricTemplates($rubricId), 'navigation_items' => DocumentsModel::navigationItems(),
'authors' => DocumentsModel::authors(), 'is_new' => true, 'can_manage' => true,
@@ -1774,8 +1870,8 @@
protected function savedViewFields($scope)
{
return $scope === 'quality'
? array('q', 'issue', 'state', 'per_page')
: array('q', 'state', 'issue', 'payment', 'per_page');
? array('q', 'issue', 'per_page')
: array('q', 'state', 'issue', 'payment', 'per_page', 'view');
}
protected function quickEditData()
@@ -1801,6 +1897,16 @@
'quick_profiles_available' => QuickEditorProfiles::available());
}
protected function variantGroupUrl($groupId, $productId = 0)
{
$url = $this->base() . '/catalog/variant-groups/' . (int) $groupId;
if ((int) $productId > 0) {
$url .= '?focus=' . (int) $productId . '#variant-' . (int) $productId;
}
return $url;
}
protected function assets()
{
AdminAssets::addStyle($this->base() . '/modules/Catalog/assets/catalog.css', 50);
@@ -1827,6 +1933,16 @@
&& is_file(BASEPATH . '/modules/products/admin/module.php');
}
protected function productSearchAliasesAvailable()
{
$module = ModuleManager::get('search');
return is_array($module)
&& !empty($module['installed'])
&& !empty($module['enabled'])
&& class_exists('App\\Modules\\Search\\DocumentAliases')
&& \App\Modules\Search\DocumentAliases::available();
}
protected function attributeAssets()
{
$this->assets();
+80 -3
View File
@@ -257,8 +257,8 @@
protected static function filterTemplateSettings(array $input, $submitted = false)
{
$zero = isset($input['zero_behavior']) ? (string) $input['zero_behavior'] : 'disable';
if (!in_array($zero, array('show', 'disable', 'hide'), true)) { $zero = 'disable'; }
$zero = isset($input['zero_behavior']) ? (string) $input['zero_behavior'] : 'hide';
if (!in_array($zero, array('show', 'disable', 'hide'), true)) { $zero = 'hide'; }
return array(
'show_counts' => $submitted ? !empty($input['show_counts']) : (!array_key_exists('show_counts', $input) || !empty($input['show_counts'])),
'zero_behavior' => $zero,
@@ -519,6 +519,47 @@
return $out;
}
/** Разделы каталога, доступные как родители в редакторе документа. */
public static function searchParentDocuments($query, $limit = 30)
{
$query = trim((string) $query);
$limit = max(1, min(50, (int) $limit));
$sql = 'SELECT i.id AS catalog_item_id,i.name,i.status AS catalog_status,i.document_id,d.rubric_id,'
. ' d.document_title,d.document_alias,r.rubric_title'
. ' FROM ' . self::itemsTable() . ' i'
. ' INNER JOIN ' . self::documentsTable() . ' d ON d.Id=i.document_id'
. ' LEFT JOIN ' . self::rubricsTable() . ' r ON r.Id=d.rubric_id'
. " WHERE i.document_id>0 AND d.document_deleted!='1'";
$args = array();
if ($query !== '') {
$sql .= ' AND (i.name LIKE %ss OR d.document_title LIKE %ss'
. ' OR d.document_alias LIKE %ss OR d.Id=%i)';
$args[] = $query;
$args[] = $query;
$args[] = $query;
$args[] = (int) $query;
}
$sql .= ' ORDER BY i.level ASC,i.position ASC,i.name ASC LIMIT ' . $limit;
$rows = call_user_func_array(array('DB', 'query'), array_merge(array($sql), $args))->getAll();
$out = array();
foreach ($rows ?: array() as $row) {
$out[] = array(
'id' => (int) $row['document_id'],
'rubric_id' => (int) $row['rubric_id'],
'title' => self::decode($row['name']),
'alias' => (string) $row['document_alias'],
'status' => (int) $row['catalog_status'],
'rubric_title' => 'Раздел каталога'
. ((int) $row['catalog_status'] === 1 ? '' : ' · выключен')
. ' · ' . self::decode($row['rubric_title']),
'catalog_item_id' => (int) $row['catalog_item_id'],
);
}
return $out;
}
public static function parseValue($value)
{
$data = FieldValueCodec::decodeStructured($value, array());
@@ -543,7 +584,35 @@
{
return self::productsAvailable()
? ProductIndexer::stats()
: array('total' => 0, 'active' => 0, 'inactive' => 0, 'categories' => 0);
: self::emptyProductStats();
}
public static function productQualityStats()
{
return self::productsAvailable()
? ProductIndexer::qualityStats()
: self::emptyProductStats();
}
public static function productStatsSnapshot()
{
$stats = self::productsAvailable() ? ProductIndexer::cachedStats() : self::emptyProductStats();
return array(
'loaded' => is_array($stats),
'stats' => array_merge(self::emptyProductStats(), is_array($stats) ? $stats : array()),
);
}
protected static function emptyProductStats()
{
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,
'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,
);
}
public static function productQualityIssues(array $stats)
@@ -561,6 +630,14 @@
return self::productsAvailable() ? ProductIndexer::productRubrics() : array();
}
public static function productRubricOptions()
{
$allowed = array_flip(self::productRubrics());
return array_values(array_filter(self::rubricOptions(), function ($rubric) use ($allowed) {
return isset($allowed[(int) $rubric['id']]);
}));
}
public static function reindexProducts($limit = 0)
{
return self::productsAvailable()
+1 -1
View File
@@ -14,7 +14,7 @@
defined('BASEPATH') || die('Direct access to this location is not allowed.');
return array(
'code' => 'catalog', 'name' => 'Каталог', 'version' => '0.3.2',
'code' => 'catalog', 'name' => 'Каталог', 'version' => '0.3.5',
'permissions' => array('key' => 'catalog', 'items' => array(
array(
'code' => 'view_catalog',
+2 -2
View File
@@ -47,7 +47,7 @@
<div class="catalog-behavior-head"><span class="icon-tile catalog-behavior-icon is-blue"><i class="ti ti-layout-cards"></i></span><div><h3>Карточка товара</h3><p class="text-secondary text-sm">Единый вид для главной страницы и разделов товарного каталога.</p></div></div>
<div class="catalog-product-card-layout">
<div class="form-grid catalog-product-card-values">
<label class="field col-12"><span class="field-label">Представление</span><select class="select" name="product_card_template_id"><option value="0">Стандартное файловое</option>{% for template in card_template_options %}<option value="{{ template.id }}"{{ settings.product_card_template_id == template.id ? ' selected' : '' }}>{{ template.title }} · {{ template.code }}{% if not template.is_published %} · черновик{% else %} · v{{ template.version }}{% endif %}</option>{% endfor %}</select><span class="field-hint"><a href="{{ ADMINX_BASE }}/catalog/card-templates">Редактировать представления карточек</a>. На сайте используется только опубликованная версия.</span></label>
<label class="field col-12"><span class="field-label">Представление</span><select class="select" name="product_card_template_id"><option value="0">Общая «Стандартная карточка»</option>{% for template in card_template_options %}{% if template.id != 1 %}<option value="{{ template.id }}"{{ settings.product_card_template_id == template.id ? ' selected' : '' }}>{{ template.title }} · {{ template.code }}{% if not template.is_published %} · черновик{% else %} · v{{ template.version }}{% endif %}</option>{% endif %}{% endfor %}</select><span class="field-hint"><a href="{{ ADMINX_BASE }}/catalog/card-templates">Редактировать представления карточек</a>. Значение по умолчанию использует опубликованную «Стандартную карточку»; файловый шаблон остаётся резервом, пока её разметка пуста.</span></label>
<label class="field col-6"><span class="field-label">Фотографий в слайдере</span><select class="select" name="product_card[max_images]">{% for count in 1..5 %}<option value="{{ count }}"{{ settings.product_card.max_images == count ? ' selected' : '' }}>{{ count }}</option>{% endfor %}</select></label>
<label class="field col-6"><span class="field-label">Бесплатная доставка от</span><div class="catalog-product-card-price"><input class="input" type="number" min="0" step="1000" name="product_card[delivery_threshold]" value="{{ settings.product_card.delivery_threshold|number_format(0, '.', '') }}"><span>₽</span></div></label>
</div>
@@ -66,7 +66,7 @@
<div class="form-grid">
<label class="field col-5"><span class="field-label">Шаблон</span><select class="select" name="filter_template_id"><option value="0">Не выбран</option>{% for template in filter_template_options %}<option value="{{ template.id }}"{{ settings.filter_template_id == template.id ? ' selected' : '' }}>{{ template.title }} · {{ template.code }}{% if template.is_published %} · v{{ template.version }}{% else %} · черновик{% endif %}</option>{% endfor %}</select><span class="field-hint"><a href="{{ ADMINX_BASE }}/catalog/filter-templates">Редактировать шаблоны фильтров</a></span></label>
<label class="field col-4"><span class="field-label">Режим вывода</span><select class="select" name="filter_template_mode"><option value="legacy"{{ settings.filter_template_mode == 'legacy' ? ' selected' : '' }}>Текущий публичный вывод</option><option value="preview"{{ settings.filter_template_mode == 'preview' ? ' selected' : '' }}>Только предпросмотр</option><option value="native"{{ settings.filter_template_mode == 'native' ? ' selected' : '' }}>Опубликованный Twig</option></select><span class="field-hint">«Предпросмотр» не меняет сайт.</span></label>
<label class="field col-3"><span class="field-label">Нулевые значения</span><select class="select" name="filter_template[zero_behavior]"><option value="show"{{ settings.filter_template.zero_behavior == 'show' ? ' selected' : '' }}>Показывать</option><option value="disable"{{ settings.filter_template.zero_behavior == 'disable' ? ' selected' : '' }}>Отключать</option><option value="hide"{{ settings.filter_template.zero_behavior == 'hide' ? ' selected' : '' }}>Скрывать</option></select></label>
<label class="field col-3"><span class="field-label">Нулевые значения</span><select class="select" name="filter_template[zero_behavior]"><option value="hide"{{ settings.filter_template.zero_behavior == 'hide' ? ' selected' : '' }}>Скрывать</option><option value="disable"{{ settings.filter_template.zero_behavior == 'disable' ? ' selected' : '' }}>Показывать неактивными</option><option value="show"{{ settings.filter_template.zero_behavior == 'show' ? ' selected' : '' }}>Показывать активными</option></select><span class="field-hint">Выбранное значение не скрывается, чтобы фильтр можно было снять.</span></label>
<div class="field col-12 catalog-toggle-row"><span><b>Показывать количество товаров</b><small>В Twig значение доступно как <code>option.count</code>.</small></span><label class="switch"><input type="checkbox" name="filter_template[show_counts]" value="1"{{ settings.filter_template.show_counts ? ' checked' : '' }} aria-label="Показывать количество товаров"></label></div>
</div>
</section>{% endif %}
+5 -1
View File
@@ -337,9 +337,13 @@
/** Заменяет префикс только внутри SQL-идентификаторов, не затрагивая данные строк. */
protected static function portableDdl($ddl)
{
$ddl = preg_replace_callback('/\bCONSTRAINT\s+`([^`]+)`/i', function ($match) {
return 'CONSTRAINT `{{prefix}}_fk_' . substr(hash('sha256', (string) $match[1]), 0, 16) . '`';
}, (string) $ddl);
$ddl = preg_replace_callback('/`([^`]+)`/', function ($match) {
return '`' . Model::portableTableName($match[1]) . '`';
}, (string) $ddl);
}, $ddl);
//-- MariaDB выводит DEFAULT NULL у TEXT/BLOB, тогда как MySQL 5.7
//-- запрещает default для этих типов. NULL и без clause остаётся default.
+4 -3
View File
@@ -96,7 +96,7 @@
throw new \RuntimeException('SQL-дамп оборван или содержит незавершённую команду');
}
if ($meta['format'] !== 'adminx full dump') {
if ($meta['format'] !== 'ave.cms full dump') {
throw new \RuntimeException('Файл не является полным дампом AVE.cms');
}
@@ -309,8 +309,9 @@
protected static function readMetadata($line, array &$meta)
{
$value = trim(substr(ltrim((string) $line), 2));
if (strcasecmp($value, 'adminx full dump') === 0) {
$meta['format'] = 'adminx full dump';
if (strcasecmp($value, 'AVE.cms full dump') === 0
|| strcasecmp($value, 'adminx full dump') === 0) {
$meta['format'] = 'ave.cms full dump';
return;
}
+1 -1
View File
@@ -21,7 +21,7 @@
return [
'code' => 'database',
'name' => 'База данных',
'version' => '0.2.0',
'version' => '0.2.2',
'permissions' => [
'key' => 'database',
+14 -5
View File
@@ -361,13 +361,22 @@
return $this->error('Недостаточно прав', array(), 403);
}
$scope = Request::getStr('scope', 'document');
$items = $scope === 'parent'
? Model::parentDocumentPicker(
Request::getStr('q', ''),
Request::getInt('limit', 20),
Request::getInt('exclude_id', 0)
)
: Model::documentPicker(
Request::getStr('q', ''),
Request::getStr('rubric_id', ''),
Request::getInt('limit', 20)
);
return $this->success('', array(
'data' => array(
'items' => Model::documentPicker(
Request::getStr('q', ''),
Request::getStr('rubric_id', ''),
Request::getInt('limit', 20)
),
'items' => $items,
),
));
}
+142 -25
View File
@@ -304,10 +304,21 @@
if (!empty($allowed)) { $catalogAllowed = array_values(array_unique(array_merge($catalogAllowed, $allowed))); }
}
$conditionReferences = $conditionsEnabled ? self::conditionReferences($rows) : array();
$conditionSources = array();
$groups = array();
foreach ($rows as $row) {
$settings = FieldSettings::effective($row);
if (array_key_exists('placed', $settings) && empty($settings['placed'])) { continue; }
if (array_key_exists('placed', $settings) && empty($settings['placed'])) {
$idReference = 'id:' . (int) $row['Id'];
$aliasReference = 'alias:' . strtolower(trim((string) $row['rubric_field_alias']));
if (isset($conditionReferences[$idReference]) || isset($conditionReferences[$aliasReference])) {
$conditionSources[(int) $row['Id']] = self::conditionSourceRow($row);
}
continue;
}
$groupId = (int) $row['rubric_field_group'];
if (!isset($groups[$groupId])) {
$groups[$groupId] = array(
@@ -324,7 +335,12 @@
$groups[$groupId]['items'][] = $field;
}
return array_values($groups);
$groups = array_values($groups);
if (!empty($groups) && !empty($conditionSources)) {
$groups[0]['condition_sources'] = array_values($conditionSources);
}
return $groups;
}
/**
@@ -338,13 +354,11 @@
return array();
}
$fields = array();
$fieldGroups = self::resolveFieldGroups($rubricId, (int) $documentId, $fieldGroups);
$fields = self::conditionAwareFields($fieldGroups);
$effectiveValues = array();
foreach (self::resolveFieldGroups($rubricId, (int) $documentId, $fieldGroups) as $group) {
foreach ($group['items'] as $field) {
$fields[] = $field;
$effectiveValues[(int) $field['Id']] = isset($field['field_value']) ? $field['field_value'] : '';
}
foreach ($fields as $field) {
$effectiveValues[(int) $field['Id']] = isset($field['field_value']) ? $field['field_value'] : '';
}
foreach ($values as $id => $value) {
@@ -356,14 +370,14 @@
public static function computedFieldValues($rubricId, array $values, array $document = array(), $documentId = 0, $fieldGroups = null)
{
$fieldGroups = self::resolveFieldGroups((int) $rubricId, (int) $documentId, $fieldGroups);
$fields = self::conditionAwareFields($fieldGroups);
$definitions = array();
$effectiveValues = array();
foreach (self::resolveFieldGroups((int) $rubricId, (int) $documentId, $fieldGroups) as $group) {
foreach ($group['items'] as $field) {
$fieldId = (int) $field['Id'];
$definitions[$fieldId] = $field;
$effectiveValues[$fieldId] = isset($field['field_value']) ? $field['field_value'] : '';
}
foreach ($fields as $field) {
$fieldId = (int) $field['Id'];
$definitions[$fieldId] = $field;
$effectiveValues[$fieldId] = isset($field['field_value']) ? $field['field_value'] : '';
}
foreach ($values as $fieldId => $value) {
@@ -387,18 +401,19 @@
return $values;
}
$fields = array();
$fieldGroups = self::resolveFieldGroups((int) $rubricId, (int) $documentId, $fieldGroups);
$fields = self::conditionAwareFields($fieldGroups);
$fieldMap = array();
$storedValues = array();
$effectiveValues = array();
foreach (self::resolveFieldGroups((int) $rubricId, (int) $documentId, $fieldGroups) as $group) {
foreach ($group['items'] as $field) {
$fields[] = $field;
foreach ($fields as $field) {
$fieldId = (int) $field['Id'];
if (empty($field['_condition_source'])) {
$fieldMap[$fieldId] = $field;
}
$storedValues[$fieldId] = (int) $documentId > 0 && isset($field['field_value']) ? $field['field_value'] : '';
$effectiveValues[$fieldId] = isset($field['field_value']) ? $field['field_value'] : '';
}
}
foreach ($values as $id => $value) {
@@ -821,6 +836,29 @@
: self::fieldsForRubric((int) $rubricId, (int) $documentId);
}
protected static function conditionAwareFields(array $fieldGroups)
{
$fields = array();
$seen = array();
foreach ($fieldGroups as $group) {
foreach (isset($group['condition_sources']) && is_array($group['condition_sources']) ? $group['condition_sources'] : array() as $field) {
$fieldId = isset($field['Id']) ? (int) $field['Id'] : 0;
if ($fieldId <= 0 || isset($seen[$fieldId])) { continue; }
$seen[$fieldId] = true;
$fields[] = $field;
}
foreach (isset($group['items']) && is_array($group['items']) ? $group['items'] : array() as $field) {
$fieldId = isset($field['Id']) ? (int) $field['Id'] : 0;
if ($fieldId <= 0 || isset($seen[$fieldId])) { continue; }
$seen[$fieldId] = true;
$fields[] = $field;
}
}
return $fields;
}
public static function delete($id)
{
$id = (int) $id;
@@ -1331,6 +1369,33 @@
return (new \App\Content\Documents\DocumentPickerRepository())->search($q, $rubricId, $limit);
}
public static function parentDocumentPicker($q, $limit = 20, $excludeId = 0)
{
$limit = max(1, min(50, (int) $limit));
$excludeId = max(0, (int) $excludeId);
$items = CatalogModel::searchParentDocuments($q, $limit);
$seen = array();
$result = array();
foreach ($items as $item) {
$id = (int) $item['id'];
if ($id <= 0 || $id === $excludeId || isset($seen[$id])) { continue; }
$seen[$id] = true;
$result[] = $item;
if (count($result) >= $limit) { return $result; }
}
$documents = (new \App\Content\Documents\DocumentPickerRepository())->search($q, array(), $limit);
foreach ($documents as $item) {
$id = (int) $item['id'];
if ($id <= 0 || $id === $excludeId || isset($seen[$id])) { continue; }
$seen[$id] = true;
$result[] = $item;
if (count($result) >= $limit) { break; }
}
return $result;
}
/** Existing keywords/tags for the searchable tag inputs in document editor. */
public static function termSuggestions($kind, $q = '', $rubricId = 0, $limit = 12)
{
@@ -1673,11 +1738,7 @@
{
$type = (string) $row['rubric_field_type'];
$hasInitialValue = array_key_exists('__initial_value', $row);
$raw = $hasInitialValue
? (string) $row['__initial_value']
: ($row['document_field_id']
? (string) $row['field_value'] . (string) $row['field_value_more']
: self::initialFieldValue($type, (string) $row['rubric_field_default']));
$raw = self::rawFieldValue($row);
$editor = FieldAdminEditors::describe($type);
$kind = self::documentEditorKind($editor);
$settings = FieldSettings::effective($row);
@@ -1777,6 +1838,60 @@
return $field;
}
protected static function rawFieldValue(array $row)
{
if (array_key_exists('__initial_value', $row)) { return (string) $row['__initial_value']; }
if (!empty($row['document_field_id'])) {
return (string) $row['field_value'] . (string) $row['field_value_more'];
}
return self::initialFieldValue((string) $row['rubric_field_type'], (string) $row['rubric_field_default']);
}
protected static function conditionSourceRow(array $row)
{
return array(
'Id' => (int) $row['Id'],
'rubric_id' => (int) $row['rubric_id'],
'rubric_field_group' => (int) $row['rubric_field_group'],
'rubric_field_title' => self::decode($row['rubric_field_title']),
'rubric_field_alias' => (string) $row['rubric_field_alias'],
'rubric_field_type' => (string) $row['rubric_field_type'],
'rubric_field_settings' => isset($row['rubric_field_settings']) ? (string) $row['rubric_field_settings'] : '',
'group_settings' => isset($row['group_settings']) ? (string) $row['group_settings'] : '',
'field_value' => self::rawFieldValue($row),
'_condition_source' => true,
);
}
protected static function conditionReferences(array $rows)
{
$references = array();
foreach ($rows as $row) {
foreach (array(FieldConditionEvaluator::condition($row), FieldConditionEvaluator::groupCondition($row)) as $condition) {
if (!empty($condition['tree']) && is_array($condition['tree'])) {
self::collectConditionReferences($condition['tree'], $references);
}
}
}
return $references;
}
protected static function collectConditionReferences(array $node, array &$references)
{
if (isset($node['items']) && is_array($node['items'])) {
foreach ($node['items'] as $item) {
if (is_array($item)) { self::collectConditionReferences($item, $references); }
}
return;
}
$reference = isset($node['field']) ? strtolower(trim((string) $node['field'])) : '';
if ($reference !== '') { $references[$reference] = true; }
}
protected static function hasGroupSettingsColumn()
{
static $has = null;
@@ -1941,7 +2056,9 @@
}
if ($kind === 'relation') {
return isset($parsed['document_id']) ? (string) $parsed['document_id'] : (string) $raw;
return array(
'document_id' => isset($parsed['document_id']) ? (string) $parsed['document_id'] : (string) $raw,
);
}
if (in_array($kind, array('lines', 'relation_list'), true)) {
+38 -2
View File
@@ -571,6 +571,40 @@
text-overflow: ellipsis;
white-space: nowrap;
}
.documents-public-link {
display: inline-flex;
align-items: center;
gap: 5px;
min-width: 0;
max-width: 100%;
width: fit-content;
color: var(--blue-600);
font-size: 11.5px;
line-height: 1.35;
text-decoration: none;
}
.documents-public-link i {
flex: 0 0 auto;
color: var(--blue-600);
font-size: 13px;
}
.documents-public-link span {
overflow: hidden;
min-width: 0;
white-space: nowrap;
text-overflow: ellipsis;
}
.documents-public-link:hover,
.documents-public-link:focus-visible {
color: var(--blue-700);
}
.documents-public-alias-empty {
color: var(--text-muted);
font-size: 11.5px;
}
.documents-admin-card-title .documents-public-link {
margin-top: 3px;
}
.documents-search-flag {
display: inline-flex;
align-items: center;
@@ -1462,13 +1496,15 @@
.documents-editor-workspace[data-active-panel="attributes"] #documentForm,
.documents-editor-workspace[data-active-panel="promotions"] #documentForm,
.documents-editor-workspace[data-active-panel="variants"] #documentForm,
.documents-editor-workspace[data-active-panel="shipping"] #documentForm {
.documents-editor-workspace[data-active-panel="shipping"] #documentForm,
.documents-editor-workspace[data-active-panel="search"] #documentForm {
display: none;
}
.documents-editor-workspace[data-active-panel="attributes"] [data-document-workspace-panel="attributes"],
.documents-editor-workspace[data-active-panel="promotions"] [data-document-workspace-panel="promotions"],
.documents-editor-workspace[data-active-panel="variants"] [data-document-workspace-panel="variants"],
.documents-editor-workspace[data-active-panel="shipping"] [data-document-workspace-panel="shipping"] {
.documents-editor-workspace[data-active-panel="shipping"] [data-document-workspace-panel="shipping"],
.documents-editor-workspace[data-active-panel="search"] [data-document-workspace-panel="search"] {
display: block;
}
.documents-editor-workspace [data-document-workspace-panel] {
+105 -17
View File
@@ -1124,6 +1124,13 @@
documentFieldConditionContext: function () {
var context = {};
this.form.querySelectorAll('[data-document-condition-source]').forEach(function (source) {
var id = String(parseInt(source.getAttribute('data-field-id'), 10) || 0);
var alias = String(source.getAttribute('data-field-alias') || '').trim().toLowerCase();
var values = [String(source.value == null ? '' : source.value)];
context['id:' + id] = values;
if (alias) { context['alias:' + alias] = values; }
});
this.form.querySelectorAll('.ax-document-field[data-rubric-field-id]').forEach(function (field) {
var values = [];
var controls = Array.prototype.slice.call(field.querySelectorAll('input[name], select[name], textarea[name]'));
@@ -1349,9 +1356,24 @@
searchCatalog: function (catalog, value) {
if (!catalog) { return; }
value = String(value || '').trim().toLowerCase();
catalog.querySelectorAll('[data-document-catalog-node]').forEach(function (node) {
node.classList.toggle('is-filtered', value && String(node.getAttribute('data-search') || '').toLowerCase().indexOf(value) < 0);
var normalize = function (input) {
return String(input || '').toLowerCase().replace(/ё/g, 'е').replace(/[^0-9a-zа-я]+/gi, ' ').trim();
};
var query = normalize(value);
var tokens = query ? query.split(/\s+/) : [];
var nodes = Array.prototype.slice.call(catalog.querySelectorAll('[data-document-catalog-node]'));
nodes.forEach(function (node) {
var haystack = normalize(node.getAttribute('data-search'));
var matches = !tokens.length || tokens.every(function (token) { return haystack.indexOf(token) >= 0; });
node.classList.toggle('is-filtered', !matches);
});
nodes.forEach(function (node) {
if (node.classList.contains('is-filtered')) { return; }
var parent = node.parentElement ? node.parentElement.closest('[data-document-catalog-node]') : null;
while (parent) {
parent.classList.remove('is-filtered');
parent = parent.parentElement ? parent.parentElement.closest('[data-document-catalog-node]') : null;
}
});
},
@@ -1496,7 +1518,7 @@
saveActiveWorkspace: function () {
var workspace = document.querySelector('[data-document-workspace]');
var panel = workspace ? workspace.getAttribute('data-active-panel') || 'main' : 'main';
if (panel === 'attributes' || panel === 'shipping' || panel === 'promotions') {
if (panel === 'attributes' || panel === 'shipping' || panel === 'promotions' || panel === 'search') {
var form = workspace.querySelector('[data-document-workspace-panel="' + panel + '"] form');
if (form && typeof form.requestSubmit === 'function') { form.requestSubmit(); }
else if (form) { form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); }
@@ -1772,6 +1794,16 @@
if (textarea._adminxCodeMirror) { textarea._adminxCodeMirror.save(); }
});
this.form.querySelectorAll('textarea[data-rich-editor]').forEach(function (textarea) {
if (window.Adminx && Adminx.RichEditor && typeof Adminx.RichEditor.syncTextarea === 'function') {
Adminx.RichEditor.syncTextarea(textarea, textarea._adminxTiptap || null);
return;
}
var root = textarea.closest('.rich-editor');
if (root && root.classList.contains('rich-editor-source-open') && root._richSourceEditor) {
root._richSourceEditor.save();
textarea.value = root._richSourceEditor.getValue();
return;
}
if (textarea._adminxTiptap && textarea.getAttribute('data-rich-editor-dirty') === '1') {
textarea.value = textarea._adminxTiptap.getHTML();
}
@@ -2467,7 +2499,11 @@
applyPickedMedia: function (target, file) {
var input = target ? target.querySelector('[data-document-media-url]') : null;
if (input && file && file.url) { input.value = file.url; input.dispatchEvent(new Event('input', { bubbles: true })); }
if (input && file && file.url) {
input.value = file.url;
input.dispatchEvent(new Event('input', { bubbles: true }));
this.sortMediaRowsByName(target.closest('[data-document-media-list]'));
}
},
mediaPickerType: function (target) {
@@ -2507,25 +2543,40 @@
uploadMediaFiles: function (container, files, replace) {
if (!container || !files || !files.length) { return; }
var data = new FormData();
data.append('_csrf', this.csrf());
data.append('dir', container.getAttribute('data-upload-dir') || '/uploads');
data.append('field_id', container.getAttribute('data-field-id') || '0');
data.append('document_id', (this.field('id') || {}).value || '0');
data.append('media_draft_token', (this.field('media_draft_token') || {}).value || '');
Array.prototype.forEach.call(files, function (file) { data.append('files[]', file); });
var self = this;
this.ajax(container.getAttribute('data-upload-url') || (this.base() + '/media/upload'), data, function (payload) {
var uploaded = ((payload.data || {}).files) || [];
if (replace && uploaded.length) {
self.prepareMediaReplacement(container);
var buttons = container.querySelectorAll('[data-document-media-upload]');
buttons.forEach(function (button) { button.disabled = true; });
Adminx.Loader.show();
Adminx.Upload.files(
container.getAttribute('data-upload-url') || (this.base() + '/media/upload'),
files,
function (chunk) {
var data = new FormData();
data.append('_csrf', self.csrf());
data.append('dir', container.getAttribute('data-upload-dir') || '/uploads');
data.append('field_id', container.getAttribute('data-field-id') || '0');
data.append('document_id', (self.field('id') || {}).value || '0');
data.append('media_draft_token', (self.field('media_draft_token') || {}).value || '');
chunk.forEach(function (file) { data.append('files[]', file); });
return data;
}
).then(function (result) {
var uploaded = result.files || [];
if (replace && uploaded.length) { self.prepareMediaReplacement(container); }
if (container.matches('[data-document-media-single]')) {
self.applyUploadedSingle(container, uploaded[0] || null);
} else {
self.appendMediaFiles(container, uploaded);
}
Adminx.Toast.show(payload.message || 'Файлы загружены', 'success');
Adminx.Toast.show('Загружено файлов: ' + uploaded.length, 'success');
}).catch(function (error) {
var uploaded = error && error.uploaded ? error.uploaded.length : 0;
var message = (error && error.message) || 'Не удалось загрузить файлы';
if (uploaded) { message += '. До ошибки загружено: ' + uploaded; }
Adminx.Toast.show(message, 'error');
}).finally(function () {
Adminx.Loader.hide();
buttons.forEach(function (button) { button.disabled = false; });
});
},
@@ -2606,10 +2657,43 @@
seen[url] = true;
added++;
});
if (added) { this.sortMediaRowsByName(list); }
this.updateMediaListEmpty(list);
if (!added && files.length) { Adminx.Toast.show('Эти файлы уже есть в поле', 'info'); }
},
sortMediaRowsByName: function (list) {
if (!list || list.getAttribute('data-media-accept') !== 'image') { return; }
var box = list.querySelector('[data-document-media-items]');
if (!box) { return; }
var rows = Array.prototype.slice.call(box.querySelectorAll('[data-document-media-row]'));
if (rows.length < 2) { return; }
var self = this;
var collator = window.Intl && window.Intl.Collator
? new window.Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
: null;
rows.sort(function (left, right) {
var leftName = self.mediaRowFileName(left);
var rightName = self.mediaRowFileName(right);
if (!leftName && !rightName) { return 0; }
if (!leftName) { return 1; }
if (!rightName) { return -1; }
return collator ? collator.compare(leftName, rightName) : leftName.localeCompare(rightName);
});
rows.forEach(function (row) { box.appendChild(row); });
this.reindexMediaList(list);
},
mediaRowFileName: function (row) {
var input = row ? row.querySelector('[data-media-key="url"]') : null;
var value = input ? String(input.value || '').trim() : '';
if (!value) { return ''; }
value = value.split(/[?#]/, 1)[0].replace(/\\/g, '/');
var name = value.slice(value.lastIndexOf('/') + 1);
try { name = decodeURIComponent(name); } catch (e) {}
return name;
},
createMediaRow: function (list) {
this.addMediaRow(list);
var rows = list.querySelectorAll('[data-document-media-row]');
@@ -3016,11 +3100,15 @@
var rubricEl = target ? (target.querySelector && target.querySelector('[data-document-picker-rubric]')) : null;
if (!rubricEl && target && target.matches && target.matches('[data-document-picker-rubric]')) { rubricEl = target; }
var rubric = rubricEl ? (rubricEl.getAttribute('data-document-picker-rubric') || '') : '';
var scope = target && target.getAttribute ? (target.getAttribute('data-document-picker-scope') || '') : '';
var excludeId = target && target.getAttribute ? (target.getAttribute('data-document-picker-exclude') || '') : '';
var load = function () {
var params = new URLSearchParams();
params.set('q', search.value.trim());
params.set('limit', 20);
if (rubric) { params.set('rubric_id', rubric); }
if (scope) { params.set('scope', scope); }
if (excludeId) { params.set('exclude_id', excludeId); }
status.textContent = 'Загрузка...';
status.hidden = false;
fetch(self.base() + '/documents/picker?' + params.toString(), { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' })
@@ -493,6 +493,8 @@
<phrase data="auto_30278b05221dd957">Refresh page</phrase>
<phrase data="auto_8bf2a1644e73bfa4">Published</phrase>
<phrase data="auto_14c618ff49e965ac">Published</phrase>
<phrase data="auto_5677fc62ef7bda9c">Open on site</phrase>
<phrase data="auto_b41b9aaec38f5380">Open page in a new tab</phrase>
<phrase data="auto_80a2c74df6ffb08d">Open the desired folder and confirm your selection. All suitable files from it will be added to the field.</phrase>
<phrase data="auto_555ad1c0c33d26b3">Cancel</phrase>
<phrase data="auto_f6e2120e73d10a68">Cancel change</phrase>
@@ -493,6 +493,8 @@
<phrase data="auto_30278b05221dd957">Обновить страницу</phrase>
<phrase data="auto_8bf2a1644e73bfa4">Опубликован</phrase>
<phrase data="auto_14c618ff49e965ac">Опубликованные</phrase>
<phrase data="auto_5677fc62ef7bda9c">Открыть на сайте</phrase>
<phrase data="auto_b41b9aaec38f5380">Открыть страницу в новой вкладке</phrase>
<phrase data="auto_80a2c74df6ffb08d">Откройте нужную папку и подтвердите выбор. В поле добавятся все подходящие файлы из неё.</phrase>
<phrase data="auto_555ad1c0c33d26b3">Отменить</phrase>
<phrase data="auto_f6e2120e73d10a68">Отменить изменение</phrase>
+1 -1
View File
@@ -17,7 +17,7 @@
return array(
'code' => 'documents',
'name' => 'Документы',
'version' => '0.3.3',
'version' => '0.3.9',
'permissions' => array(
'key' => 'documents',
@@ -1,3 +1,26 @@
{% if catalog_mode and not is_new and product_search_aliases_available %}
<section class="card documents-card product-search-aliases" data-document-workspace-panel="search" data-product-search-aliases data-suggestions-url="{{ ADMINX_BASE }}/catalog/search-alias-suggestions" data-document-id="{{ document.Id }}">
<form method="post" action="{{ ADMINX_BASE }}/catalog/products/{{ document.Id }}/search-aliases" data-product-search-alias-form>
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
<div class="documents-section-head">
<div class="documents-head-left"><span class="icon-tile documents-head-icon" style="--tile-bg:var(--blue-100);--tile-fg:var(--blue-600)"><i class="ti ti-vocabulary"></i></span><div><h2>Поисковые названия</h2><p>Другие названия только этого товара: народные, прежние или разговорные.</p></div></div>
<span class="badge badge-blue" data-product-search-alias-count>{{ product_search_aliases|length }}</span>
</div>
<div class="documents-card-body product-search-aliases-body">
<div class="alert alert-info"><i class="ti ti-info-circle"></i><div><b>Название товара не изменится</b><p>Например, «кушетка» или «стол-чемодан» помогут найти товар, но не появятся в карточке на сайте.</p></div></div>
<div class="product-search-alias-list" data-product-search-alias-list>
{% for alias in product_search_aliases %}
<div class="product-search-alias-row" data-product-search-alias-row><div class="combobox product-search-alias-control"><span class="input-wrap"><i class="ti ti-search"></i><input class="input" name="aliases[]" value="{{ alias|e('html_attr') }}" maxlength="190" autocomplete="off" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-label="Поисковое название" data-product-search-alias-input></span><div class="combo-list product-search-alias-suggestions" role="listbox" hidden data-product-search-alias-suggestions></div></div><button class="btn btn-ghost btn-icon btn-sm ax-act ax-act-danger" type="button" data-product-search-alias-remove data-tooltip="Удалить название" aria-label="Удалить название"><i class="ti ti-trash"></i></button></div>
{% endfor %}
</div>
<div class="product-search-alias-empty" data-product-search-alias-empty{{ product_search_aliases ? ' hidden' : '' }}><i class="ti ti-vocabulary-off"></i><span>Дополнительных названий пока нет</span></div>
<div class="product-search-alias-actions"><button class="btn btn-secondary" type="button" data-product-search-alias-add><i class="ti ti-plus"></i>Добавить название</button><button class="btn btn-primary" type="submit"><i class="ti ti-device-floppy"></i>Сохранить</button></div>
</div>
</form>
<template data-product-search-alias-template><div class="product-search-alias-row" data-product-search-alias-row><div class="combobox product-search-alias-control"><span class="input-wrap"><i class="ti ti-search"></i><input class="input" name="aliases[]" value="" maxlength="190" autocomplete="off" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-label="Поисковое название" placeholder="Например, кушетка" data-product-search-alias-input></span><div class="combo-list product-search-alias-suggestions" role="listbox" hidden data-product-search-alias-suggestions></div></div><button class="btn btn-ghost btn-icon btn-sm ax-act ax-act-danger" type="button" data-product-search-alias-remove data-tooltip="Удалить название" aria-label="Удалить название"><i class="ti ti-trash"></i></button></div></template>
</section>
{% endif %}
{% if catalog_mode and not is_new %}
<section class="product-promotions-panel" data-document-workspace-panel="promotions" data-product-promotions data-base="{{ ADMINX_BASE }}" data-product-id="{{ document.Id }}" data-product-title="{{ document.document_title|e('html_attr') }}" data-search-url="{{ ADMINX_BASE }}/catalog/product-relations/products" data-csrf="{{ csrf_token }}">
<header class="section-header blocks-panel-header product-promotions-header">
@@ -88,18 +111,34 @@
{% if catalog_mode and not is_new %}
<section class="card documents-card catalog-product-variant-bridge" data-document-workspace-panel="variants">
<div class="documents-section-head"><h2>Варианты товара</h2>{% if variant_group %}<span class="badge badge-blue">Группа #{{ variant_group.id }}</span>{% endif %}</div>
<div class="documents-card-body between">
<div class="documents-section-head"><div class="documents-head-left"><span class="icon-tile documents-head-icon" style="--tile-bg:var(--violet-100);--tile-fg:var(--violet-600)"><i class="ti ti-box-multiple"></i></span><div><h2>Варианты товара</h2><p>Цвета, комплектации и другие исполнения одной модели.</p></div></div>{% if variant_group %}<span class="badge badge-blue">Группа #{{ variant_group.id }}</span>{% endif %}</div>
<div class="documents-card-body product-variant-bridge-body">
{% if variant_group %}
<div><b>{{ variant_group.title }}</b><p class="text-secondary">{{ variant_group.is_primary ? 'Основной вариант группы' : 'Вариант группы' }}</p></div>
<a class="btn btn-secondary" href="{{ ADMINX_BASE }}/catalog/variant-groups/{{ variant_group.id }}"><i class="ti ti-box-multiple"></i>Управление вариантами</a>
<div class="product-variant-current"><span class="icon-tile"><i class="ti ti-stack-2"></i></span><div><b>{{ variant_group.title }}</b><p class="text-secondary">{{ variant_group.is_primary ? 'Основной вариант группы' : 'Вариант группы' }}</p></div></div>
<div class="product-variant-bridge-actions">
<form method="post" action="{{ ADMINX_BASE }}/catalog/variant-groups/{{ variant_group.id }}/products/{{ document.Id }}/copy" data-variant-action data-confirm="Создать выключенную копию товара и сразу добавить её в эту группу?" data-confirm-kind="info" data-confirm-label="Создать вариант">
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
<button class="btn btn-secondary" type="submit"><i class="ti ti-copy"></i>Создать вариант копированием</button>
</form>
<a class="btn btn-primary" href="{{ ADMINX_BASE }}/catalog/variant-groups/{{ variant_group.id }}?focus={{ document.Id }}#variant-{{ document.Id }}"><i class="ti ti-adjustments-horizontal"></i>Открыть матрицу</a>
</div>
{% else %}
<div><b>Товар пока не объединён с вариантами</b><p class="text-secondary">Создайте группу на основе текущего товара.</p></div>
<form method="post" action="{{ ADMINX_BASE }}/catalog/products/{{ document.Id }}/variant-group" data-variant-action>
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
<input type="hidden" name="title" value="{{ document.document_title|e('html_attr') }}">
<button class="btn btn-secondary" type="submit"><i class="ti ti-plus"></i>Создать группу вариантов</button>
</form>
<div class="product-variant-start-copy"><b>Товар пока не входит в группу</b><p class="text-secondary">Создайте новую группу для этой модели или добавьте товар в уже существующую.</p></div>
<div class="product-variant-start-grid">
<form class="product-variant-start-option" method="post" action="{{ ADMINX_BASE }}/catalog/products/{{ document.Id }}/variant-group" data-variant-action>
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
<span class="product-variant-start-head"><i class="ti ti-library-plus"></i><span><b>Новая группа</b><small>Текущий товар станет первым и основным вариантом.</small></span></span>
<label class="field"><span class="field-label">Название группы</span><span class="input-wrap"><i class="ti ti-box-multiple"></i><input class="input" name="title" value="{{ document.document_title|e('html_attr') }}" required></span></label>
<button class="btn btn-primary" type="submit"><i class="ti ti-plus"></i>Создать группу</button>
</form>
<form class="product-variant-start-option" method="post" action="{{ ADMINX_BASE }}/catalog/products/{{ document.Id }}/variant-group/attach" data-variant-action>
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
<span class="product-variant-start-head"><i class="ti ti-link-plus"></i><span><b>Существующая группа</b><small>Подходит, если другие исполнения модели уже добавлены.</small></span></span>
<label class="field"><span class="field-label">Группа вариантов</span><select class="select" name="group_id" required><option value="">Выберите группу</option>{% for group in variant_groups %}<option value="{{ group.id }}">{{ group.title }} · {{ group.variants_count }} вариантов</option>{% endfor %}</select></label>
<button class="btn btn-secondary" type="submit"{{ variant_groups ? '' : ' disabled' }}><i class="ti ti-link"></i>Добавить в группу</button>
{% if not variant_groups %}<small class="field-hint">Других групп пока нет.</small>{% endif %}
</form>
</div>
{% endif %}
</div>
</section>
+12 -5
View File
@@ -4,7 +4,7 @@
<nav class="breadcrumbs" aria-label="Хлебные крошки">
<a href="{{ ADMINX_BASE }}/">Главная</a><i class="ti ti-chevron-right"></i>
{% if catalog_mode %}<a href="{{ ADMINX_BASE }}/catalog">Каталог</a><i class="ti ti-chevron-right"></i><a href="{{ return_url }}">Товары</a>{% else %}<a href="{{ ADMINX_BASE }}/documents">Документы</a>{% endif %}<i class="ti ti-chevron-right"></i>
{% if catalog_mode %}<a href="{{ ADMINX_BASE }}/catalog">Каталог</a><i class="ti ti-chevron-right"></i><a href="{{ return_url }}">{{ return_label|default('Товары') }}</a>{% else %}<a href="{{ ADMINX_BASE }}/documents">Документы</a>{% endif %}<i class="ti ti-chevron-right"></i>
<span>{{ is_new ? 'новый' : '#' ~ document.Id }}</span>
</nav>
@@ -15,7 +15,7 @@
<p class="text-secondary" style="margin-top:5px" data-document-edit-meta>{{ is_new ? (catalog_mode ? 'Заполните основные данные. Характеристики, варианты и доставка откроются после первого сохранения.' : 'Создание базовой карточки документа.') : 'ID ' ~ document.Id ~ ', ' ~ (document.state_label|admin_trans) ~ ', изменён: ' ~ document.changed_label }}</p>
</div>
<div class="cluster">
{% if catalog_mode and not is_new and can_manage and product_copy_url %}<button class="btn btn-secondary" type="button" data-catalog-product-copy data-url="{{ product_copy_url }}" data-csrf="{{ csrf_token }}"><i class="ti ti-copy"></i>Создать копию</button>{% endif %}
{% if catalog_mode and not is_new and can_manage and product_copy_url %}<button class="btn btn-secondary" type="button" data-catalog-product-copy data-url="{{ product_copy_url }}" data-csrf="{{ csrf_token }}" data-confirm-copy="1"><i class="ti ti-copy"></i>Создать копию</button>{% endif %}
<a class="btn btn-ghost" href="{{ return_url|default(ADMINX_BASE ~ '/documents') }}"><i class="ti ti-arrow-left"></i>К списку</a>
</div>
</div>
@@ -30,6 +30,7 @@
<button class="tab" type="button" role="tab" aria-selected="false" data-document-workspace-tab="attributes"{{ is_new ? ' aria-disabled="true" data-tooltip="Доступно после первого сохранения"' : '' }}><i class="ti ti-list-details"></i>Характеристики</button>
<button class="tab" type="button" role="tab" aria-selected="false" data-document-workspace-tab="variants"{{ is_new ? ' aria-disabled="true" data-tooltip="Доступно после первого сохранения"' : '' }}><i class="ti ti-box-multiple"></i>Варианты</button>
<button class="tab" type="button" role="tab" aria-selected="false" data-document-workspace-tab="shipping"{{ is_new ? ' aria-disabled="true" data-tooltip="Доступно после первого сохранения"' : '' }}><i class="ti ti-truck-delivery"></i>Доставка</button>
{% if product_search_aliases_available %}<button class="tab" type="button" role="tab" aria-selected="false" data-document-workspace-tab="search"><i class="ti ti-search"></i>Поиск</button>{% endif %}
{% endif %}
<button class="tab" type="button" role="tab" aria-selected="false" data-document-workspace-tab="additional"><i class="ti ti-adjustments-horizontal"></i>Дополнительно</button>
</div>
@@ -41,8 +42,9 @@
<option value="promotions"{{ is_new ? ' disabled' : '' }}>Акции{{ is_new ? ' · после сохранения' : '' }}</option>
<option value="attributes"{{ is_new ? ' disabled' : '' }}>Характеристики{{ is_new ? ' · после сохранения' : '' }}</option>
<option value="variants"{{ is_new ? ' disabled' : '' }}>Варианты{{ is_new ? ' · после сохранения' : '' }}</option>
<option value="shipping"{{ is_new ? ' disabled' : '' }}>Доставка{{ is_new ? ' · после сохранения' : '' }}</option>
{% endif %}
<option value="shipping"{{ is_new ? ' disabled' : '' }}>Доставка{{ is_new ? ' · после сохранения' : '' }}</option>
{% if product_search_aliases_available %}<option value="search">Поиск</option>{% endif %}
{% endif %}
<option value="additional">Дополнительно</option>
</select>
</label>
@@ -71,6 +73,11 @@
<input type="hidden" name="id" value="{{ document.Id }}">
<input type="hidden" name="document_version" value="{{ document.document_version|default(1) }}">
<input type="hidden" name="media_draft_token" value="{{ media_draft_token|default('')|e('html_attr') }}">
{% if field_groups %}
{% for source in field_groups[0].condition_sources|default([]) %}
<input type="hidden" value="{{ source.field_value|e('html_attr') }}" data-document-condition-source data-field-id="{{ source.Id }}" data-field-alias="{{ source.rubric_field_alias|e('html_attr') }}">
{% endfor %}
{% endif %}
<section class="documents-draft-recovery" data-document-draft-recovery hidden>
<span class="icon-tile" style="--tile-bg:var(--amber-100);--tile-fg:var(--amber-700)"><i class="ti ti-device-floppy"></i></span>
@@ -298,7 +305,7 @@
<label class="field col-12"><span class="field-label">Автор</span><select class="select" name="document_author_id">{% for author in authors %}<option value="{{ author.id }}"{{ document.document_author_id == author.id ? ' selected' : '' }}>{{ author.name }}{% if not author.active %} · отключён{% endif %}</option>{% endfor %}</select></label>
<div class="field col-12">
<span class="field-label">Родительский документ</span>
<div class="documents-picker-input">
<div class="documents-picker-input" data-document-picker-scope="parent" data-document-picker-exclude="{{ document.Id }}">
<input class="input" type="number" name="document_parent" value="{{ document.document_parent }}" placeholder="ID документа" min="0" data-document-parent-id>
<button class="btn btn-secondary btn-icon btn-sm" type="button" data-document-parent-pick data-tooltip="Выбрать документ" aria-label="Выбрать документ"><i class="ti ti-file-search"></i></button>
</div>
+11 -3
View File
@@ -16,6 +16,14 @@
{% endif %}
</div>
{% endmacro %}
{% macro public_link(item) %}
{% if item.document_alias %}
{% set public_url = item.document_alias == '/' ? '/' : '/' ~ item.document_alias|trim('/') %}
<a class="documents-public-link mono" href="{{ public_url }}" target="_blank" rel="noopener" aria-label="Открыть страницу в новой вкладке" data-tooltip="Открыть на сайте"><i class="ti ti-external-link"></i><span>{{ item.document_alias }}</span></a>
{% else %}
<span class="documents-public-alias-empty">без alias</span>
{% endif %}
{% endmacro %}
{% import _self as document_ui %}
{% block title %}Документы{% endblock %}
{% block content %}
@@ -136,7 +144,7 @@
<article class="documents-admin-card {{ item.document_deleted ? 'is-deleted' : (item.document_status ? 'is-published' : 'is-draft') }}" data-document-row data-id="{{ item.Id }}" data-title="{{ item.document_title|e('html_attr') }}">
<header>
{% if can_manage %}<input type="checkbox" value="{{ item.Id }}" data-document-check aria-label="Выбрать документ #{{ item.Id }}">{% endif %}
<div class="documents-admin-card-title"><a href="{{ ADMINX_BASE }}/documents/{{ item.Id }}/edit">{{ item.document_title }}</a><small>#{{ item.Id }} · {{ item.document_alias ? item.document_alias : 'без alias' }}</small></div>
<div class="documents-admin-card-title"><a href="{{ ADMINX_BASE }}/documents/{{ item.Id }}/edit">{{ item.document_title }}</a><small>#{{ item.Id }}</small>{{ document_ui.public_link(item) }}</div>
<span class="badge badge-{{ item.state_badge }}">{{ item.state_label }}</span>
</header>
<div class="documents-admin-card-body">
@@ -172,7 +180,7 @@
<tr class="documents-row {{ item.document_deleted ? 'is-deleted' : (item.document_status ? 'is-published' : 'is-draft') }}" data-document-row data-id="{{ item.Id }}" data-title="{{ item.document_title|e('html_attr') }}">
{% if can_manage %}<td><input type="checkbox" value="{{ item.Id }}" data-document-check aria-label="Выбрать документ #{{ item.Id }}"></td>{% endif %}
<td class="mono text-muted">{{ item.Id }}</td>
<td><div class="documents-name"><b title="{{ item.document_title|e('html_attr') }}">{{ item.document_title }}</b><small>{{ item.document_alias ? item.document_alias : 'без alias' }}</small></div></td>
<td><div class="documents-name"><b title="{{ item.document_title|e('html_attr') }}">{{ item.document_title }}</b>{{ document_ui.public_link(item) }}</div></td>
{% for display in item.admin_view_values %}
<td class="documents-admin-value is-{{ display.width }}">
{% if display.format == 'image' %}{% if display.value.image %}<img src="{{ display.value.image }}" alt="">{% else %}<span class="text-muted">—</span>{% endif %}
@@ -218,7 +226,7 @@
<td class="documents-cell-title">
<div class="documents-name">
<b title="{{ item.document_title|e('html_attr') }}">{{ item.document_title }}</b>
<span class="documents-title-meta"><span><i class="ti ti-eye"></i>{{ item.document_count_view|number_format(0, '.', ' ') }} просмотров</span><span class="mono"><i class="ti ti-link"></i>{{ item.document_alias ? item.document_alias : 'без alias' }}</span></span>
<span class="documents-title-meta"><span><i class="ti ti-eye"></i>{{ item.document_count_view|number_format(0, '.', ' ') }} просмотров</span><span>{{ document_ui.public_link(item) }}</span></span>
</div>
</td>
<td class="documents-cell-rubric">
+2
View File
@@ -197,6 +197,7 @@
'pages' => $listing['pages'],
'total' => $listing['total'],
'per_page' => $perPage,
'can_manage' => Permission::check('manage_media'),
),
));
}
@@ -278,6 +279,7 @@
}
return $this->success('Папка создана', array(
'data' => array('path' => $path),
'redirect' => $this->base() . '/media?dir=' . rawurlencode($path),
));
}
+19 -19
View File
@@ -446,32 +446,32 @@
}
function upload(files) {
var fd;
var i;
if (!files || !files.length) {
setStatus('Файлы не выбраны', 'error');
return;
}
fd = new FormData(form);
fd.delete('files[]');
for (i = 0; i < files.length; i++) {
fd.append('files[]', files[i]);
}
var total = files.length;
form.classList.add('is-loading');
setStatus('Загрузка: ' + files.length, 'progress');
Adminx.Ajax.post(self.base() + '/media/upload', fd).then(function (payload) {
setStatus('Подготовка к загрузке: ' + total, 'progress');
Adminx.Upload.files(self.base() + '/media/upload', files, function (chunk) {
var fd = new FormData(form);
fd.delete('files[]');
chunk.forEach(function (file) { fd.append('files[]', file); });
return fd;
}, function (processed, count) {
setStatus('Загружено: ' + processed + ' из ' + count, 'progress');
}).then(function (result) {
form.classList.remove('is-loading');
var d = payload.data || {};
if (d.success) {
setStatus(d.message || 'Готово', 'success');
Adminx.Ajax.handle(payload);
} else {
setStatus(d.message || 'Ошибка загрузки', 'error');
Adminx.Toast.show(d.message || 'Ошибка загрузки', 'error');
}
}).catch(function () {
setStatus('Загружено файлов: ' + result.files.length, 'success');
Adminx.Toast.show('Загружено файлов: ' + result.files.length, 'success');
window.location.href = self.base() + '/media?dir=' + encodeURIComponent(form.getAttribute('data-dir') || '/uploads');
}).catch(function (error) {
form.classList.remove('is-loading');
setStatus('Ошибка сети', 'error');
var uploaded = error && error.uploaded ? error.uploaded.length : 0;
var message = (error && error.message) || 'Ошибка сети';
if (uploaded) { message += '. До ошибки загружено: ' + uploaded; }
setStatus(message, 'error');
Adminx.Toast.show(message, 'error');
});
}
+1 -1
View File
@@ -19,7 +19,7 @@
return array(
'code' => 'media',
'name' => 'Медиа',
'version' => '0.6.2',
'version' => '0.6.4',
'permissions' => array(
'key' => 'media',
+12
View File
@@ -17,10 +17,12 @@
defined('BASEPATH') || die('Direct access to this location is not allowed.');
use App\Adminx\Support\CodeEditor;
use App\Adminx\Support\AdminLocale;
use App\Common\AdminAssets;
use App\Common\Auth;
use App\Common\Controller as BaseController;
use App\Common\Permission;
use App\Helpers\Hooks;
use App\Helpers\Request;
class Controller extends BaseController
@@ -32,12 +34,22 @@
CodeEditor::useCodeMirror('htmlmixed');
$q = Request::getStr('q', '');
$panelSources = Hooks::filter('navigation.item.panel_sources', array());
if (!is_array($panelSources)) { $panelSources = array(); }
$panelSources = array_values(array_filter(array_map(function ($source) {
if (!is_array($source)) { return null; }
$code = strtolower(trim(isset($source['code']) ? (string) $source['code'] : ''));
$title = trim(isset($source['title']) ? (string) $source['title'] : '');
if (!preg_match('/^[a-z][a-z0-9_-]{1,31}$/', $code) || $title === '') { return null; }
return array('code' => $code, 'title' => AdminLocale::translateMarkup($title));
}, $panelSources)));
return $this->render('@navigation/index.twig', array(
'navigation_items' => Model::all($q),
'stats' => Model::stats(),
'filters' => array('q' => $q),
'access_groups' => Model::accessGroups(),
'navigation_panel_sources' => $panelSources,
'can_manage' => Permission::check('manage_navigation'),
));
}
+44 -1
View File
@@ -16,6 +16,7 @@
defined('BASEPATH') || die('Direct access to this location is not allowed.');
use App\Common\FileCacheInvalidator;
use App\Content\ContentTables;
use App\Content\PublicUserTables;
use App\Helpers\Dir;
@@ -234,6 +235,10 @@
}
$navigationId = (int) $current['navigation_id'];
if (!array_key_exists('status', $input)) {
$data['status'] = (string) $current['status'];
}
self::assertItemParent($navigationId, (int) $id, (int) $data['parent_id']);
$data['navigation_id'] = $navigationId;
DB::startTransaction();
@@ -406,6 +411,7 @@
public static function clearCache($id, $alias = '')
{
$id = (int) $id;
$keys = array((string) (int) $id);
$alias = trim((string) $alias);
if ($alias !== '') {
@@ -417,6 +423,8 @@
self::removeFile(self::cacheDir($key) . '/items.cache');
self::removeDir(self::cacheDir($key));
}
FileCacheInvalidator::navigation($id, $alias);
}
protected static function input(array $input)
@@ -485,6 +493,11 @@
}
$parentId = isset($input['parent_id']) ? (int) $input['parent_id'] : 0;
$cssClass = isset($input['css_class']) ? (string) $input['css_class'] : '';
if (array_key_exists('panel_source', $input)) {
$cssClass = self::applyPanelSource($cssClass, (string) $input['panel_source']);
}
return array(
'document_id' => isset($input['document_id']) && (int) $input['document_id'] > 0 ? (int) $input['document_id'] : null,
'alias' => $alias,
@@ -494,7 +507,7 @@
'image' => isset($input['image']) ? (string) $input['image'] : '',
'css_style' => isset($input['css_style']) ? (string) $input['css_style'] : '',
'css_id' => isset($input['css_id']) ? (string) $input['css_id'] : '',
'css_class' => isset($input['css_class']) ? (string) $input['css_class'] : '',
'css_class' => $cssClass,
'parent_id' => $parentId,
'level' => '1',
'position' => isset($input['position']) ? (int) $input['position'] : 1,
@@ -502,6 +515,25 @@
);
}
protected static function applyPanelSource($cssClass, $source)
{
$classes = preg_split('/\s+/', trim((string) $cssClass)) ?: array();
$classes = array_values(array_filter($classes, function ($class) {
return $class !== 'catalog-menu-link'
&& $class !== 'catalog-menu-panel'
&& strpos($class, 'catalog-menu-source-') !== 0;
}));
$source = strtolower(trim((string) $source));
if ($source === 'children') {
$classes[] = 'catalog-menu-panel';
} elseif (preg_match('/^[a-z][a-z0-9_-]{1,31}$/', $source)) {
$classes[] = 'catalog-menu-source-' . $source;
}
return implode(' ', array_values(array_unique(array_filter($classes))));
}
protected static function row(array $row, $withCode = false)
{
$id = (int) $row['navigation_id'];
@@ -530,6 +562,7 @@
$row['id'] = $row['navigation_item_id'];
$row['navigation_id'] = isset($row['navigation_id']) ? (int) $row['navigation_id'] : 0;
$row['document_id'] = isset($row['document_id']) ? (int) $row['document_id'] : 0;
$row['panel_source'] = self::panelSource(isset($row['css_class']) ? $row['css_class'] : '');
$row['parent_id'] = isset($row['parent_id']) ? (int) $row['parent_id'] : 0;
$row['level'] = isset($row['level']) ? (string) $row['level'] : '1';
$row['position'] = isset($row['position']) ? (int) $row['position'] : 1;
@@ -541,6 +574,16 @@
return $row;
}
protected static function panelSource($cssClass)
{
$cssClass = trim((string) $cssClass);
if (preg_match('/(?:^|\s)catalog-menu-source-([a-z][a-z0-9_-]{1,31})(?:\s|$)/', $cssClass, $match)) {
return (string) $match[1];
}
return preg_match('/(?:^|\s)catalog-menu-panel(?:\s|$)/', $cssClass) ? 'children' : 'link';
}
protected static function decode($value)
{
return html_entity_decode((string) $value, ENT_QUOTES, 'UTF-8');
@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<language>
<phrase data="navigation_catalog_panel_behavior">Catalog menu behavior</phrase>
<phrase data="navigation_catalog_panel_link">Regular link</phrase>
<phrase data="navigation_catalog_panel_children">Show child items on the right</phrase>
<phrase data="navigation_catalog_panel_hint">Used only by navigation connected to the product catalog.</phrase>
<phrase data="navigation_parity_tag">[tag:if_every:2]even[tag:if_else]odd[tag:/if]</phrase>
<phrase data="auto_bf209b0159668b33">, and normal and active items are elements</phrase>
<phrase data="auto_3475d30e6543216b">, start and end of level - list</phrase>
@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<language>
<phrase data="navigation_catalog_panel_behavior">Поведение в меню каталога</phrase>
<phrase data="navigation_catalog_panel_link">Обычная ссылка</phrase>
<phrase data="navigation_catalog_panel_children">Показать справа дочерние пункты</phrase>
<phrase data="navigation_catalog_panel_hint">Используется только навигацией, подключённой к товарному каталогу.</phrase>
<phrase data="navigation_parity_tag">[tag:if_every:2]чётный[tag:if_else]нечётный[tag:/if]</phrase>
<phrase data="auto_bf209b0159668b33">, а обычный и активный пункты — элементы</phrase>
<phrase data="auto_3475d30e6543216b">, начало и конец уровня — список</phrase>
+8 -1
View File
@@ -17,7 +17,14 @@
return array(
'code' => 'navigation',
'name' => 'Навигация',
'version' => '0.2.0',
'version' => '0.2.2',
'hook_definitions' => array(
array(
'name' => 'navigation.item.panel_sources',
'description' => 'Дополнительные источники правой панели для пункта навигации товарного каталога.',
),
),
'permissions' => array(
'key' => 'navigation',
+11
View File
@@ -391,6 +391,17 @@
<label class="field col-12"><span class="field-label">CSS style</span><input class="input" type="text" name="css_style"></label>
<label class="field col-6"><span class="field-label">CSS class</span><input class="input" type="text" name="css_class"></label>
<label class="field col-6"><span class="field-label">CSS ID</span><input class="input" type="text" name="css_id"></label>
{% if navigation_panel_sources %}
<label class="field col-12">
<span class="field-label">Поведение в меню каталога</span>
<select class="select" name="panel_source">
<option value="link">Обычная ссылка</option>
<option value="children">Показать справа дочерние пункты</option>
{% for source in navigation_panel_sources %}<option value="{{ source.code }}">{{ source.title }}</option>{% endfor %}
</select>
<span class="field-hint">Используется только навигацией, подключённой к товарному каталогу.</span>
</label>
{% endif %}
</div>
</div>
</form>
+13
View File
@@ -46,6 +46,18 @@
));
}
public function templateMap(array $params = array())
{
if (!Permission::check('view_public_site')) {
return $this->renderStatus('@adminx/404.twig', array('title' => 'Недостаточно прав'), 403);
}
AdminAssets::addStyle($this->base() . '/modules/PublicSite/assets/public-site.css', 50);
return $this->render('@public_site/template-map.twig', array(
'template_map' => TemplateMap::build(),
));
}
public function placements(array $params = array())
{
if (!Permission::check('view_public_site')) {
@@ -217,6 +229,7 @@
protected function publicTemplateAssets()
{
CodeEditor::useCodeMirror('htmlmixed');
AdminAssets::addStyle($this->base() . '/modules/PublicSite/assets/public-site.css', 50);
AdminAssets::addStyle(ADMINX_BASE . '/modules/Themes/assets/view-overrides.css', 56);
AdminAssets::addScript(ADMINX_BASE . '/modules/Themes/assets/view-overrides.js', 56);
}
+317
View File
@@ -0,0 +1,317 @@
<?php
/*
|--------------------------------------------------------------------------------------
| AVE.cms
|--------------------------------------------------------------------------------------
| @package AVE.cms
| @file adminx/modules/PublicSite/TemplateMap.php
| @author AVE.cms <support@ave-cms.ru>
| @copyright 2007-2026 (c) AVE.cms
| @link https://ave-cms.ru
| @version 3.3
*/
namespace App\Adminx\PublicSite;
defined('BASEPATH') || die('Direct access to this location is not allowed.');
use App\Common\DatabaseSchema;
use App\Content\ContentTables;
use App\Frontend\ThemeAssets;
use DB;
/** Read-only description of the active public template chain. */
class TemplateMap
{
public static function build()
{
$theme = self::theme();
$siteTemplates = self::siteTemplates();
$rubrics = self::rubrics();
$components = self::components();
$product = self::productLayer();
return array(
'theme' => $theme,
'summary' => array(
'site_templates' => count($siteTemplates),
'rubrics' => count($rubrics),
'components' => $components['count'],
'active_overrides' => $components['active_count'],
),
'rules' => self::rules($theme),
'scenarios' => self::scenarios($theme, $components, $product),
'site_templates' => $siteTemplates,
'rubrics' => $rubrics,
'component_groups' => $components['groups'],
'product' => $product,
);
}
protected static function theme()
{
$code = ThemeAssets::currentTheme();
$manifest = ThemeAssets::manifest($code);
$mode = isset($manifest['presentation_mode']) && $manifest['presentation_mode'] === 'theme'
? 'theme' : 'native';
$pageShell = isset($manifest['page_shell']) ? trim((string) $manifest['page_shell']) : '';
return array(
'code' => $code,
'name' => isset($manifest['name']) && trim((string) $manifest['name']) !== ''
? (string) $manifest['name'] : $code,
'version' => isset($manifest['version']) ? (string) $manifest['version'] : '',
'mode' => $mode,
'mode_label' => $mode === 'theme' ? 'Оболочка темы' : 'Штатные шаблоны AVE.cms',
'mode_note' => $mode === 'theme'
? 'Тема формирует внешний каркас страницы; наследование шаблонов рубрики зависит от настроек темы.'
: 'Общий шаблон сайта и шаблон рубрики собирают страницу. Тема подключает стили и переоформляет отдельные компоненты.',
'page_shell' => $pageShell,
'page_shell_active' => $mode === 'theme' && $pageShell !== '',
'inherit_rubric_templates' => !empty($manifest['inherit_rubric_templates']),
'overrides' => isset($manifest['view_overrides']) && is_array($manifest['view_overrides'])
? array_values($manifest['view_overrides']) : array(),
'url' => '/themes?theme=' . rawurlencode($code),
);
}
protected static function siteTemplates()
{
$table = ContentTables::table('templates');
$rubrics = ContentTables::table('rubrics');
if (!self::tableExists($table) || !self::tableExists($rubrics)) { return array(); }
$rows = DB::query(
'SELECT t.Id id,t.template_title title,COUNT(r.Id) usage_count'
. ' FROM ' . $table . ' t LEFT JOIN ' . $rubrics . ' r ON r.rubric_template_id=t.Id'
. ' GROUP BY t.Id,t.template_title ORDER BY usage_count DESC,t.template_title,t.Id'
)->getAll() ?: array();
$out = array();
foreach ($rows as $row) {
$out[] = array(
'id' => (int) $row['id'],
'title' => self::decode($row['title']),
'usage_count' => (int) $row['usage_count'],
'url' => '/templates/' . (int) $row['id'],
);
}
return $out;
}
protected static function rubrics()
{
$out = array();
foreach (Model::structure() as $row) {
$purpose = 'Обычные документы';
if (!empty($row['catalogs_count'])) {
$purpose = 'Каталог';
} elseif (!empty($row['requests_count'])) {
$purpose = 'Документы и подборки';
}
$out[] = array(
'id' => (int) $row['id'],
'title' => (string) $row['title'],
'alias' => (string) $row['alias'],
'purpose' => $purpose,
'template_id' => (int) $row['template_id'],
'template_title' => (string) $row['template_title'],
'has_main_template' => (int) $row['template_length'] > 0,
'extra_templates_count' => (int) $row['rubric_templates_count'],
'documents_count' => (int) $row['documents_count'],
'url' => '/rubrics/' . (int) $row['id'] . '/templates',
);
}
return $out;
}
protected static function components()
{
$items = array();
try {
foreach (PublicViewTemplates::all() as $item) {
$items[] = self::component($item, 'site', '/public-site/templates');
}
} catch (\Throwable $e) {
// A broken optional theme must not make the read-only map unavailable.
}
$productClass = '\\App\\Adminx\\Packages\\Products\\PublicViewTemplates';
if (class_exists($productClass) && method_exists($productClass, 'all')) {
try {
foreach ($productClass::all() as $item) {
$items[] = self::component($item, 'products', '/catalog/public-templates');
}
} catch (\Throwable $e) {
// The products module can be installed without its latest schema during an update.
}
}
$groups = array();
$active = 0;
foreach ($items as $item) {
$group = $item['group'] !== '' ? $item['group'] : 'Другие компоненты';
if (!isset($groups[$group])) {
$groups[$group] = array('title' => $group, 'items' => array());
}
$groups[$group]['items'][] = $item;
$active += $item['active'] ? 1 : 0;
}
return array('count' => count($items), 'active_count' => $active, 'items' => $items, 'groups' => array_values($groups));
}
protected static function component(array $item, $owner, $route)
{
$source = isset($item['source']) ? (string) $item['source'] : 'missing';
return array(
'code' => isset($item['code']) ? (string) $item['code'] : '',
'namespace' => isset($item['namespace']) ? (string) $item['namespace'] : '',
'file' => isset($item['file']) ? (string) $item['file'] : '',
'path' => isset($item['theme_path']) ? (string) $item['theme_path'] : '',
'group' => isset($item['group']) ? (string) $item['group'] : '',
'title' => isset($item['title']) ? (string) $item['title'] : '',
'description' => isset($item['description']) ? (string) $item['description'] : '',
'icon' => isset($item['icon']) ? (string) $item['icon'] : 'ti ti-template',
'owner' => $owner,
'active' => !empty($item['active_override']) || $source === 'fallback',
'source' => $source,
'source_label' => isset($item['source_label']) ? (string) $item['source_label'] : 'Не определён',
'state_class' => $source === 'theme' ? 'badge-green' : ($source === 'fallback' ? 'badge-blue' : 'badge-amber'),
'url' => $route . '?template=' . rawurlencode(isset($item['code']) ? (string) $item['code'] : ''),
);
}
protected static function productLayer()
{
$result = array(
'available' => false,
'cards' => array('total' => 0, 'published' => 0, 'used' => 0, 'url' => '/catalog/card-templates'),
'filters' => array('total' => 0, 'published' => 0, 'used' => 0, 'url' => '/catalog/filter-templates'),
);
$classes = array(
'cards' => '\\App\\Adminx\\Packages\\Products\\CardTemplates',
'filters' => '\\App\\Adminx\\Packages\\Products\\FilterTemplates',
);
foreach ($classes as $key => $class) {
if (!class_exists($class) || !method_exists($class, 'stats')) { continue; }
try {
$stats = $class::stats();
$result[$key] = array_merge($result[$key], is_array($stats) ? $stats : array());
$result['available'] = true;
} catch (\Throwable $e) {
// Keep the map usable while an optional module waits for migrations.
}
}
return $result;
}
protected static function rules(array $theme)
{
return array(
array('icon' => 'ti ti-database', 'title' => 'Данные остаются в AVE.cms', 'text' => 'Документы, поля, рубрики и условия запросов не переносятся в файлы темы.'),
array('icon' => 'ti ti-template', 'title' => 'Шаблон задаёт место', 'text' => 'Общий шаблон создаёт каркас, а шаблон рубрики размещает содержимое конкретного типа документа.'),
array('icon' => 'ti ti-palette', 'title' => 'Тема оформляет результат', 'text' => $theme['mode'] === 'native'
? 'Сейчас тема подключает стили и заменяет только перечисленные публичные компоненты.'
: 'Сейчас активна оболочка темы; её файл формирует внешний каркас страницы.'),
);
}
protected static function scenarios(array $theme, array $components, array $product)
{
$componentItems = isset($components['items']) ? $components['items'] : array();
$productUrl = $product['available'] ? '/catalog/products' : '/modules';
$productCardsUrl = $product['available'] ? '/catalog/card-templates' : '/modules';
return array(
array(
'icon' => 'ti ti-browser', 'title' => 'Общий каркас сайта',
'description' => 'Шапка, подвал и место, куда AVE.cms вставляет содержимое текущего документа.',
'steps' => array(
self::step('Шаблон сайта', 'Хранит общий HTML и тег [tag:maincontent].', '/templates', 'Основной слой', 'blue'),
self::step('Назначение рубрике', 'Каждая рубрика выбирает нужный общий шаблон.', '/rubrics', 'Настройка', 'gray'),
self::step('Активная тема', $theme['mode_note'], $theme['url'], $theme['mode_label'], $theme['mode'] === 'native' ? 'green' : 'violet'),
),
),
array(
'icon' => 'ti ti-file-text', 'title' => 'Обычная текстовая страница',
'description' => 'Например: «О компании», «Доставка», статья или новость.',
'steps' => array(
self::step('Документ', 'Заголовок, текст, изображения и остальные значения полей.', '/documents', 'Данные', 'gray'),
self::step('Шаблон рубрики', 'Решает, какие поля и блоки вывести и в каком порядке.', '/rubrics', 'Сборка содержимого', 'blue'),
self::componentStep($componentItems, 'storefront', 'info.twig', 'Компонент темы', 'Дополнительное оформление, только если его вызывает шаблон.'),
self::step('Шаблон сайта', 'Оборачивает готовое содержимое общей шапкой и подвалом.', '/templates', 'Каркас', 'green'),
),
),
array(
'icon' => 'ti ti-list-search', 'title' => 'Список новостей или материалов',
'description' => 'Запрос выбирает документы, а его шаблоны или публичное представление оформляют список.',
'steps' => array(
self::step('Запрос', 'Условия, сортировка, количество и пагинация.', '/requests', 'Выбор данных', 'gray'),
self::step('Шаблоны запроса', 'Шаблон элемента и общая обёртка результата.', '/requests', 'Основной вывод', 'blue'),
self::componentStep($componentItems, 'content_public', 'list.twig', 'Резервный список', 'Используется для нативных коллекций без отдельного представления.'),
self::step('Рубрика и сайт', 'Определяют место списка на странице и общий каркас.', '/rubrics', 'Размещение', 'green'),
),
),
array(
'icon' => 'ti ti-category-2', 'title' => 'Раздел товарного каталога',
'description' => 'Категория остаётся документом, а каталог добавляет подразделы, фильтры, сортировку и выдачу.',
'steps' => array(
self::step('Раздел каталога', 'Связь с документом, полями, условиями и фильтрами.', '/catalog', 'Данные и правила', 'gray'),
self::step('Шаблон рубрики', 'Размещает теги подразделов, фильтров и товарной выдачи.', '/rubrics', 'Сборка страницы', 'blue'),
self::componentStep($componentItems, 'products_public', 'filter.twig', 'Фильтры', 'Оформляет готовые параметры фильтрации.'),
self::componentStep($componentItems, 'products_public', 'listing.twig', 'Товарная выдача', 'Оформляет список и пагинацию.'),
),
),
array(
'icon' => 'ti ti-package', 'title' => 'Карточка и страница товара',
'description' => 'Один товар может выглядеть по-разному в списке и на собственной странице.',
'steps' => array(
self::step('Товар', 'Документ, цены, изображения и нативные характеристики.', $productUrl, 'Данные', 'gray'),
self::step('Карточки товара', $product['available']
? 'Опубликованные карточки назначаются разделам и контекстам.'
: 'Слой появляется после установки модуля «Товары».', $productCardsUrl, 'Списки', 'blue'),
self::componentStep($componentItems, 'products_public', 'product-card.twig', 'Резервная карточка', 'Работает, если отдельная опубликованная карточка не назначена.'),
self::componentStep($componentItems, 'products_public', 'product-detail.twig', 'Страница товара', 'Галерея, цена, покупка, описание и характеристики.'),
),
),
);
}
protected static function componentStep(array $items, $namespace, $file, $title, $description)
{
foreach ($items as $item) {
if ($item['namespace'] !== $namespace || $item['file'] !== $file) { continue; }
return self::step($title, $description, $item['url'], $item['source_label'],
$item['source'] === 'theme' ? 'green' : ($item['source'] === 'fallback' ? 'blue' : 'amber'));
}
return self::step($title, $description, '', 'Не найден', 'amber');
}
protected static function step($title, $text, $url, $state, $tone)
{
return array(
'title' => (string) $title,
'text' => (string) $text,
'url' => (string) $url,
'state' => (string) $state,
'tone' => in_array($tone, array('gray', 'blue', 'green', 'violet', 'amber'), true) ? $tone : 'gray',
);
}
protected static function tableExists($table)
{
try { return DatabaseSchema::tableExists($table); }
catch (\Throwable $e) { return false; }
}
protected static function decode($value)
{
return html_entity_decode((string) $value, ENT_QUOTES, 'UTF-8');
}
}
@@ -837,3 +837,508 @@
margin-left: 0;
}
}
.public-template-map-notice {
margin-bottom: var(--space-4);
}
.public-template-map-summary {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.public-template-map-mode {
margin-top: var(--space-5);
}
.public-template-mode-row {
position: relative;
display: grid;
grid-template-columns: minmax(320px, 1.4fr) minmax(180px, 0.7fr) minmax(220px, 0.9fr) 40px;
align-items: center;
gap: var(--space-4);
min-height: 88px;
padding: var(--space-4) var(--space-5);
border-radius: var(--radius-sm);
background: var(--surface-muted);
}
.public-template-mode-main {
display: grid;
grid-template-columns: 42px minmax(0, 1fr);
align-items: center;
gap: var(--space-3);
}
.public-template-mode-main h3,
.public-template-mode-main p {
margin: 0;
}
.public-template-mode-main h3 {
margin-top: 2px;
font-size: 17px;
text-wrap: balance;
}
.public-template-mode-main p {
margin-top: 4px;
color: var(--text-secondary);
font-size: 12px;
line-height: 1.45;
text-wrap: pretty;
}
.public-template-mode-theme,
.public-template-mode-shell {
display: grid;
gap: 4px;
min-width: 0;
}
.public-template-mode-theme > span,
.public-template-mode-shell > span {
font-size: 12px;
}
.public-template-mode-theme > b,
.public-template-mode-shell > b,
.public-template-mode-theme > code,
.public-template-mode-shell > code {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.public-template-mode-theme > small,
.public-template-mode-shell > small {
color: var(--text-secondary);
}
.public-template-mode-shell > small {
color: var(--amber-700);
}
.public-template-mode-shell > small.is-active {
color: var(--green-700);
}
.public-template-rules {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1px;
margin-top: var(--space-3);
overflow: hidden;
border-radius: var(--radius-sm);
background: var(--border-default);
}
.public-template-rule {
display: grid;
grid-template-columns: 28px minmax(0, 1fr);
align-items: start;
gap: var(--space-3);
min-height: 72px;
padding: var(--space-4);
background: var(--surface);
}
.public-template-rule > i {
color: var(--blue-600);
font-size: 22px;
}
.public-template-rule > div {
display: grid;
gap: 4px;
}
.public-template-rule span {
color: var(--text-secondary);
font-size: 12px;
line-height: 1.45;
text-wrap: pretty;
}
.public-template-scenarios,
.public-template-inventory,
.public-template-components {
margin-top: var(--space-6);
}
.public-template-flow-list {
display: grid;
gap: var(--space-3);
}
.public-template-flow {
overflow: hidden;
border-radius: var(--radius-sm);
background: var(--surface);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.06);
}
.public-template-flow > header {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-4) var(--space-5);
border-bottom: 1px solid var(--border-default);
}
.public-template-flow > header h3,
.public-template-flow > header p {
margin: 0;
}
.public-template-flow > header h3 {
font-size: 15px;
text-wrap: balance;
}
.public-template-flow > header p {
margin-top: 3px;
color: var(--text-secondary);
font-size: 12px;
text-wrap: pretty;
}
[data-theme="dark"] .public-template-flow {
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.09);
}
.public-template-flow-steps {
display: flex;
align-items: stretch;
gap: 0;
padding: var(--space-3);
}
.public-template-flow-step {
position: relative;
display: grid;
grid-template-columns: 26px minmax(0, 1fr) 32px;
align-items: start;
gap: var(--space-2);
flex: 1 1 0;
min-width: 0;
padding: var(--space-3);
border-radius: var(--radius-sm);
background: var(--surface-muted);
}
.public-template-flow-step.is-blue .public-template-flow-number {
background: var(--blue-100);
color: var(--blue-700);
}
.public-template-flow-step.is-green .public-template-flow-number {
background: var(--green-100);
color: var(--green-700);
}
.public-template-flow-step.is-violet .public-template-flow-number {
background: var(--violet-100);
color: var(--violet-700);
}
.public-template-flow-step.is-amber .public-template-flow-number {
background: var(--amber-100);
color: var(--amber-700);
}
.public-template-flow-number {
display: grid;
place-items: center;
width: 26px;
height: 26px;
border-radius: 50%;
background: var(--gray-200);
color: var(--text-secondary);
font-size: 12px;
font-weight: 700;
}
.public-template-flow-copy {
display: grid;
gap: 6px;
min-width: 0;
}
.public-template-flow-copy p {
margin: 0;
color: var(--text-secondary);
font-size: 12px;
line-height: 1.4;
text-wrap: pretty;
}
.public-template-flow-title {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-2);
}
.public-template-flow-title b {
font-size: 13px;
line-height: 1.35;
}
.public-template-flow-title .badge {
flex: 0 0 auto;
max-width: 48%;
white-space: normal;
}
.public-template-flow-step > .btn {
width: 32px;
height: 32px;
min-height: 32px;
}
.public-template-flow-arrow {
align-self: center;
flex: 0 0 24px;
color: var(--text-secondary);
text-align: center;
}
.public-template-inventory-grid {
display: grid;
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
gap: var(--space-4);
}
.public-template-site-templates table th:nth-child(2),
.public-template-site-templates table td:nth-child(2) {
width: 120px;
}
.public-template-site-templates table th:last-child,
.public-template-site-templates table td:last-child {
width: 52px;
}
.public-template-product-links {
display: grid;
}
.public-template-product-links > a {
display: grid;
grid-template-columns: 40px minmax(0, 1fr) 20px;
align-items: center;
gap: var(--space-3);
min-height: 66px;
padding: var(--space-3) var(--space-4);
color: inherit;
text-decoration: none;
}
.public-template-product-links > a + a {
border-top: 1px solid var(--border-default);
}
.public-template-product-links > a:hover {
background: var(--surface-muted);
}
.public-template-product-links > a > span:nth-child(2) {
display: grid;
gap: 3px;
min-width: 0;
}
.public-template-product-links > a small {
color: var(--text-secondary);
font-size: 12px;
}
.public-template-product-links > a > i {
color: var(--text-secondary);
}
.public-template-product-empty {
min-height: 198px;
padding: var(--space-5);
}
.public-template-rubrics-card {
margin-top: var(--space-4);
}
.public-template-rubrics-table th:nth-child(1) {
width: 22%;
}
.public-template-rubrics-table th:nth-child(2) {
width: 15%;
}
.public-template-rubrics-table th:nth-child(3) {
width: 20%;
}
.public-template-rubrics-table th:nth-child(4) {
width: 23%;
}
.public-template-rubrics-table th:nth-child(5) {
width: 90px;
}
.public-template-rubrics-table th:last-child {
width: 52px;
}
.public-template-rubric-state {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 5px 8px;
}
.public-template-rubric-state small {
color: var(--text-secondary);
}
.public-template-component-groups {
column-count: 2;
column-gap: var(--space-4);
}
.public-template-component-group {
display: inline-block;
width: 100%;
margin-bottom: var(--space-4);
overflow: hidden;
padding: 0;
break-inside: avoid;
}
.public-template-component-group > summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
min-height: 58px;
padding: var(--space-3) var(--space-4);
cursor: pointer;
list-style: none;
}
.public-template-component-group > summary::-webkit-details-marker {
display: none;
}
.public-template-component-group > summary > span {
display: grid;
grid-template-columns: 24px minmax(0, 1fr) auto;
align-items: center;
gap: var(--space-2);
min-width: 0;
}
.public-template-component-group > summary > span > i {
color: var(--blue-600);
font-size: 20px;
}
.public-template-component-group > summary small {
color: var(--text-secondary);
font-size: 12px;
}
.public-template-component-group > summary > i {
transition: transform 0.18s cubic-bezier(0.2, 0, 0, 1);
}
.public-template-component-group[open] > summary {
border-bottom: 1px solid var(--border-default);
}
.public-template-component-group[open] > summary > i {
transform: rotate(180deg);
}
.public-template-component-list {
display: grid;
}
.public-template-component-row {
display: grid;
grid-template-columns: 40px minmax(0, 1fr) 40px;
align-items: center;
gap: var(--space-3);
min-height: 82px;
padding: var(--space-3) var(--space-4);
}
.public-template-component-row + .public-template-component-row {
border-top: 1px solid var(--border-default);
}
.public-template-component-copy {
display: grid;
gap: 4px;
min-width: 0;
}
.public-template-component-copy > div {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
}
.public-template-component-copy p {
margin: 0;
color: var(--text-secondary);
font-size: 12px;
line-height: 1.4;
text-wrap: pretty;
}
.public-template-component-copy code {
overflow: hidden;
color: var(--text-secondary);
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 1280px) {
.public-template-mode-row {
grid-template-columns: minmax(280px, 1.2fr) minmax(160px, 0.7fr) minmax(190px, 0.8fr) 40px;
}
.public-template-flow-steps {
align-items: stretch;
flex-wrap: wrap;
gap: var(--space-2);
}
.public-template-flow-step {
flex-basis: calc(50% - var(--space-2));
}
.public-template-flow-arrow {
display: none;
}
}
@media (max-width: 980px) {
.public-template-map-summary,
.public-template-rules,
.public-template-inventory-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.public-template-mode-row {
grid-template-columns: minmax(0, 1fr) minmax(160px, 0.6fr) 40px;
}
.public-template-mode-shell {
grid-column: 1 / -1;
grid-row: 2;
}
}
@media (max-width: 720px) {
.public-template-map-summary,
.public-template-rules,
.public-template-inventory-grid,
.public-template-mode-row {
grid-template-columns: 1fr;
}
.public-template-component-groups {
column-count: 1;
}
.public-template-mode-row {
gap: var(--space-3);
padding: var(--space-4);
}
.public-template-mode-shell {
grid-column: auto;
grid-row: auto;
}
.public-template-mode-row > .btn {
position: absolute;
right: var(--space-4);
}
.public-template-flow > header {
align-items: flex-start;
padding: var(--space-3) var(--space-4);
}
.public-template-flow-steps {
display: grid;
grid-template-columns: 1fr;
padding: var(--space-2);
}
.public-template-flow-step {
width: 100%;
}
.public-template-rubrics-card .table-scroll {
overflow: visible;
}
.public-template-rubrics-table {
display: block;
min-width: 0;
}
.public-template-rubrics-table thead,
.public-template-rubrics-table colgroup {
display: none;
}
.public-template-rubrics-table tbody,
.public-template-rubrics-table tr,
.public-template-rubrics-table td {
display: block;
width: 100%;
}
.public-template-rubrics-table tr {
box-sizing: border-box;
padding: var(--space-3) var(--space-4);
border-bottom: 1px solid var(--border-default);
}
.public-template-rubrics-table td {
box-sizing: border-box;
display: grid;
grid-template-columns: minmax(100px, 34%) minmax(0, 1fr);
gap: var(--space-3);
padding: 6px 0;
border: 0;
}
.public-template-rubrics-table td::before {
color: var(--text-secondary);
content: attr(data-label);
font-size: 12px;
}
.public-template-component-group > summary > span {
grid-template-columns: 24px minmax(0, 1fr);
}
.public-template-component-group > summary > span small {
grid-column: 2;
}
.public-template-component-row {
grid-template-columns: 36px minmax(0, 1fr) 40px;
align-items: start;
padding: var(--space-3);
}
.public-template-component-copy > div {
align-items: flex-start;
flex-direction: column;
}
}
+2 -1
View File
@@ -17,7 +17,7 @@
return array(
'code' => 'public_site',
'name' => 'Публичный сайт',
'version' => '0.5.0',
'version' => '0.5.2',
'permissions' => array(
'key' => 'public_site',
'items' => array(
@@ -56,6 +56,7 @@
),
'routes' => array(
array('GET', '/public-site', array(\App\Adminx\PublicSite\Controller::class, 'index')),
array('GET', '/public-site/template-map', array(\App\Adminx\PublicSite\Controller::class, 'templateMap')),
array('GET', '/public-site/map', array(\App\Adminx\PublicSite\Controller::class, 'siteMap')),
array('GET', '/public-site/placements', array(\App\Adminx\PublicSite\Controller::class, 'placements')),
array('GET', '/public-site/diagnostics', array(\App\Adminx\PublicSite\Controller::class, 'diagnostics')),
@@ -1,5 +1,6 @@
<nav class="tabs public-site-tabs" aria-label="Разделы публичного сайта">
<a class="tab {{ public_site_tab == 'structure' ? 'is-active' : '' }}" href="{{ ADMINX_BASE }}/public-site"><i class="ti ti-sitemap"></i>Структура</a>
<a class="tab {{ public_site_tab == 'template_map' ? 'is-active' : '' }}" href="{{ ADMINX_BASE }}/public-site/template-map"><i class="ti ti-hierarchy-2"></i>Карта шаблонов</a>
<a class="tab {{ public_site_tab == 'map' ? 'is-active' : '' }}" href="{{ ADMINX_BASE }}/public-site/map"><i class="ti ti-route"></i>Карта сайта</a>
<a class="tab {{ public_site_tab == 'presentations' ? 'is-active' : '' }}" href="{{ ADMINX_BASE }}/public-site/presentations"><i class="ti ti-layout-cards"></i>Представления</a>
<a class="tab {{ public_site_tab == 'templates' ? 'is-active' : '' }}" href="{{ ADMINX_BASE }}/public-site/templates"><i class="ti ti-template"></i>Шаблоны страницы</a>
@@ -0,0 +1,262 @@
{% extends '@adminx/main.twig' %}
{% block title %}Карта шаблонов{% endblock %}
{% block content %}
<nav class="breadcrumbs" aria-label="Хлебные крошки">
<a href="{{ ADMINX_BASE }}/">Главная</a><i class="ti ti-chevron-right"></i>
<a href="{{ ADMINX_BASE }}/public-site">Публичный сайт</a><i class="ti ti-chevron-right"></i>
<span>Карта шаблонов</span>
</nav>
<div class="page-header">
<div class="between public-site-page-head">
<div>
<h1>Карта шаблонов</h1>
<p class="text-secondary">Из чего собираются страницы и где редактируется каждый слой.</p>
</div>
<div class="cluster public-site-page-actions">
<a class="btn btn-secondary" href="{{ ADMINX_BASE }}/themes?theme={{ template_map.theme.code|url_encode }}"><i class="ti ti-palette"></i>Активная тема</a>
<a class="btn btn-primary" href="{{ ADMINX_BASE }}/templates"><i class="ti ti-template"></i>Шаблоны сайта</a>
</div>
</div>
</div>
{% set public_site_tab = 'template_map' %}
{% include '@public_site/_tabs.twig' %}
<div class="alert alert-info public-template-map-notice">
<i class="ti ti-info-circle"></i>
<div>
<b>Это навигатор, а не новый конструктор</b>
<span>Карта ничего не переключает и не сохраняет. Она показывает действующую цепочку AVE.cms и ведёт в штатный редактор нужного шаблона.</span>
</div>
</div>
<div class="public-site-summary public-template-map-summary">
<div class="card public-site-stat">
<span class="icon-tile" style="--tile-bg:var(--blue-100);--tile-fg:var(--blue-600)"><i class="ti ti-template"></i></span>
<div><b>{{ template_map.summary.site_templates }}</b><span>Общих шаблонов</span></div>
</div>
<div class="card public-site-stat">
<span class="icon-tile" style="--tile-bg:var(--green-100);--tile-fg:var(--green-600)"><i class="ti ti-folders"></i></span>
<div><b>{{ template_map.summary.rubrics }}</b><span>Рубрик</span></div>
</div>
<div class="card public-site-stat">
<span class="icon-tile" style="--tile-bg:var(--violet-100);--tile-fg:var(--violet-600)"><i class="ti ti-components"></i></span>
<div><b>{{ template_map.summary.components }}</b><span>Публичных компонентов</span></div>
</div>
<div class="card public-site-stat">
<span class="icon-tile" style="--tile-bg:var(--cyan-100);--tile-fg:var(--cyan-600)"><i class="ti ti-checks"></i></span>
<div><b>{{ template_map.summary.active_overrides }}</b><span>Действующих источников</span></div>
</div>
</div>
<section class="public-site-panel public-template-map-mode">
<div class="section-header public-site-panel-header">
<div class="section-icon"><i class="ti ti-adjustments-horizontal"></i></div>
<div>
<div class="section-eyebrow">Текущая установка</div>
<h2>Как сайт собирается сейчас</h2>
<p class="section-desc">Режим читается из активной темы, поэтому на карте показана фактическая, а не предполагаемая схема.</p>
</div>
</div>
<div class="public-template-mode-row">
<div class="public-template-mode-main">
<span class="icon-tile" style="--tile-bg:var(--blue-100);--tile-fg:var(--blue-600)"><i class="ti ti-stack-2"></i></span>
<div>
<span class="text-secondary">Способ сборки</span>
<h3>{{ template_map.theme.mode_label }}</h3>
<p>{{ template_map.theme.mode_note }}</p>
</div>
</div>
<div class="public-template-mode-theme">
<span class="text-secondary">Активная тема</span>
<b>{{ template_map.theme.name }}</b>
<small><code>{{ template_map.theme.code }}</code>{% if template_map.theme.version %} · версия {{ template_map.theme.version }}{% endif %}</small>
</div>
<div class="public-template-mode-shell">
<span class="text-secondary">Файловая оболочка</span>
{% if template_map.theme.page_shell %}<code>{{ template_map.theme.page_shell }}</code>{% else %}<b>Не задана</b>{% endif %}
<small class="{{ template_map.theme.page_shell_active ? 'is-active' : '' }}">{{ template_map.theme.page_shell_active ? 'Используется' : 'Не заменяет общий шаблон' }}</small>
</div>
<a class="btn btn-ghost btn-icon" href="{{ ADMINX_BASE }}{{ template_map.theme.url }}" data-tooltip-left="Открыть настройки темы" aria-label="Открыть настройки темы"><i class="ti ti-arrow-up-right"></i></a>
</div>
<div class="public-template-rules">
{% for rule in template_map.rules %}
<div class="public-template-rule">
<i class="{{ rule.icon }}"></i>
<div><b>{{ rule.title }}</b><span>{{ rule.text }}</span></div>
</div>
{% endfor %}
</div>
</section>
<section class="public-site-panel public-template-scenarios">
<div class="section-header public-site-panel-header">
<div class="section-icon"><i class="ti ti-route-square"></i></div>
<div>
<div class="section-eyebrow">Типовые страницы</div>
<h2>Цепочки сборки</h2>
<p class="section-desc">Читайте слева направо: данные выбираются, оформляются своим шаблоном и затем попадают в общий каркас сайта.</p>
</div>
</div>
<div class="public-template-flow-list">
{% for scenario in template_map.scenarios %}
<article class="public-template-flow">
<header>
<span class="icon-tile"><i class="{{ scenario.icon }}"></i></span>
<div><h3>{{ scenario.title }}</h3><p>{{ scenario.description }}</p></div>
</header>
<div class="public-template-flow-steps" style="--flow-columns:{{ scenario.steps|length }}">
{% for step in scenario.steps %}
<div class="public-template-flow-step is-{{ step.tone }}">
<div class="public-template-flow-number">{{ loop.index }}</div>
<div class="public-template-flow-copy">
<div class="public-template-flow-title"><b>{{ step.title }}</b><span class="badge badge-{{ step.tone == 'gray' ? 'gray' : step.tone }}">{{ step.state }}</span></div>
<p>{{ step.text }}</p>
</div>
{% if step.url %}<a class="btn btn-ghost btn-icon btn-sm" href="{{ ADMINX_BASE }}{{ step.url }}" data-tooltip-left="Открыть редактор" aria-label="Открыть редактор"><i class="ti ti-arrow-up-right"></i></a>{% endif %}
</div>
{% if not loop.last %}<i class="ti ti-chevron-right public-template-flow-arrow"></i>{% endif %}
{% endfor %}
</div>
</article>
{% endfor %}
</div>
</section>
<section class="public-site-panel public-template-inventory">
<div class="section-header public-site-panel-header">
<div class="section-icon"><i class="ti ti-link"></i></div>
<div>
<div class="section-eyebrow">Фактические назначения</div>
<h2>Общие шаблоны и рубрики</h2>
<p class="section-desc">Здесь видно, какой общий шаблон назначен каждому типу контента и есть ли у рубрики собственная разметка документа.</p>
</div>
</div>
<div class="public-template-inventory-grid">
<div class="card public-site-card public-template-site-templates">
<div class="public-site-card-head">
<div class="public-site-card-title"><span class="icon-tile"><i class="ti ti-browser"></i></span><div><h2>Общие шаблоны сайта</h2><p class="text-secondary">Каркас страницы и место [tag:maincontent]</p></div></div>
<a class="btn btn-ghost btn-icon" href="{{ ADMINX_BASE }}/templates" data-tooltip-left="Все шаблоны" aria-label="Все шаблоны"><i class="ti ti-arrow-up-right"></i></a>
</div>
<div class="table-scroll">
<table class="table table-compact">
<thead><tr><th>Шаблон</th><th>Назначен</th><th></th></tr></thead>
<tbody>
{% for item in template_map.site_templates %}
<tr>
<td><div class="public-site-name"><b>{{ item.title }}</b><span>ID {{ item.id }}</span></div></td>
<td><span class="badge {{ item.usage_count ? 'badge-green' : 'badge-gray' }}">{{ item.usage_count }} рубрик</span></td>
<td><a class="btn btn-ghost btn-icon btn-sm" href="{{ ADMINX_BASE }}{{ item.url }}" data-tooltip-left="Редактировать" aria-label="Редактировать"><i class="ti ti-pencil"></i></a></td>
</tr>
{% else %}
<tr><td colspan="3"><div class="empty-state"><i class="ti ti-template-off"></i><b>Общих шаблонов нет</b><span>Создайте шаблон сайта и назначьте его рубрике.</span></div></td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card public-site-card public-template-product-layer">
<div class="public-site-card-head">
<div class="public-site-card-title"><span class="icon-tile"><i class="ti ti-package"></i></span><div><h2>Товарное оформление</h2><p class="text-secondary">Отдельные представления карточек и фильтров</p></div></div>
{% if template_map.product.available %}<span class="badge badge-green">Подключено</span>{% else %}<span class="badge badge-gray">Не установлено</span>{% endif %}
</div>
{% if template_map.product.available %}
<div class="public-template-product-links">
<a href="{{ ADMINX_BASE }}{{ template_map.product.cards.url }}">
<span class="icon-tile"><i class="ti ti-layout-cards"></i></span>
<span><b>Карточки товаров</b><small>{{ template_map.product.cards.published }} опубликовано · {{ template_map.product.cards.used }} используется</small></span>
<i class="ti ti-arrow-up-right"></i>
</a>
<a href="{{ ADMINX_BASE }}{{ template_map.product.filters.url }}">
<span class="icon-tile"><i class="ti ti-adjustments-horizontal"></i></span>
<span><b>Вид фильтров</b><small>{{ template_map.product.filters.published }} опубликовано · {{ template_map.product.filters.used }} используется</small></span>
<i class="ti ti-arrow-up-right"></i>
</a>
<a href="{{ ADMINX_BASE }}/catalog/public-templates">
<span class="icon-tile"><i class="ti ti-template"></i></span>
<span><b>Шаблоны страницы товара</b><small>Категория, выдача, карточка и полная страница</small></span>
<i class="ti ti-arrow-up-right"></i>
</a>
</div>
{% else %}
<div class="empty-state public-template-product-empty">
<i class="ti ti-package-off"></i><b>Товарный слой не установлен</b>
<span>После установки модуля «Товары» здесь появятся карточки, фильтры и шаблоны страницы товара.</span>
<a class="btn btn-secondary" href="{{ ADMINX_BASE }}/modules"><i class="ti ti-package-import"></i>Открыть модули</a>
</div>
{% endif %}
</div>
</div>
<div class="card public-site-card public-template-rubrics-card">
<div class="public-site-card-head">
<div class="public-site-card-title"><span class="icon-tile"><i class="ti ti-folders"></i></span><div><h2>Рубрики и их шаблоны</h2><p class="text-secondary">{{ template_map.rubrics|length }} типов контента в текущей установке</p></div></div>
<a class="btn btn-ghost btn-icon" href="{{ ADMINX_BASE }}/rubrics" data-tooltip-left="Все рубрики" aria-label="Все рубрики"><i class="ti ti-arrow-up-right"></i></a>
</div>
<div class="table-scroll">
<table class="table table-compact public-template-rubrics-table">
<thead><tr><th>Рубрика</th><th>Назначение</th><th>Общий шаблон</th><th>Шаблон документа</th><th>Документы</th><th></th></tr></thead>
<tbody>
{% for item in template_map.rubrics %}
<tr>
<td data-label="Рубрика"><div class="public-site-name"><b>{{ item.title }}</b><span><code>{{ item.alias ?: 'без alias' }}</code> · ID {{ item.id }}</span></div></td>
<td data-label="Назначение"><span class="badge {{ item.purpose == 'Каталог' ? 'badge-violet' : (item.purpose == 'Документы и подборки' ? 'badge-blue' : 'badge-gray') }}">{{ item.purpose }}</span></td>
<td data-label="Общий шаблон">{% if item.template_title %}<div class="public-site-template"><b>{{ item.template_title }}</b><span>ID {{ item.template_id }}</span></div>{% else %}<span class="badge badge-red">Не назначен</span>{% endif %}</td>
<td data-label="Шаблон документа"><div class="public-template-rubric-state"><span class="badge {{ item.has_main_template ? 'badge-green' : 'badge-red' }}">{{ item.has_main_template ? 'Основной задан' : 'Основной пуст' }}</span>{% if item.extra_templates_count %}<small>+ {{ item.extra_templates_count }} дополнительных</small>{% endif %}</div></td>
<td data-label="Документы"><b>{{ item.documents_count }}</b></td>
<td data-label="Действия"><a class="btn btn-ghost btn-icon btn-sm" href="{{ ADMINX_BASE }}{{ item.url }}" data-tooltip-left="Открыть шаблоны рубрики" aria-label="Открыть шаблоны рубрики"><i class="ti ti-pencil"></i></a></td>
</tr>
{% else %}
<tr><td colspan="6"><div class="empty-state"><i class="ti ti-folders-off"></i><b>Рубрик пока нет</b><span>После создания они появятся на карте автоматически.</span></div></td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</section>
<section class="public-site-panel public-template-components">
<div class="section-header public-site-panel-header">
<div class="section-icon"><i class="ti ti-components"></i></div>
<div>
<div class="section-eyebrow">Файловое оформление</div>
<h2>Компоненты активной темы</h2>
<p class="section-desc">Это не отдельные страницы. Компонент работает только тогда, когда его вызывает шаблон рубрики, запрос, блок или модуль.</p>
</div>
</div>
<div class="public-template-component-groups">
{% for group in template_map.component_groups %}
<details class="card public-template-component-group"{{ loop.index <= 2 ? ' open' : '' }}>
<summary>
<span><i class="ti ti-folder-code"></i><b>{{ group.title }}</b><small>{{ group.items|length }} компонентов</small></span>
<i class="ti ti-chevron-down"></i>
</summary>
<div class="public-template-component-list">
{% for item in group.items %}
<div class="public-template-component-row">
<span class="icon-tile"><i class="{{ item.icon }}"></i></span>
<div class="public-template-component-copy">
<div><b>{{ item.title }}</b><span class="badge {{ item.state_class }}">{{ item.source_label }}</span></div>
<p>{{ item.description }}</p>
<code>{{ item.path ?: item.namespace ~ '/' ~ item.file }}</code>
</div>
<a class="btn btn-ghost btn-icon" href="{{ ADMINX_BASE }}{{ item.url }}" data-tooltip-left="Открыть шаблон" aria-label="Открыть шаблон"><i class="ti ti-arrow-up-right"></i></a>
</div>
{% endfor %}
</div>
</details>
{% else %}
<div class="card"><div class="empty-state"><i class="ti ti-template-off"></i><b>Компоненты темы не найдены</b><span>Проверьте реестр активной темы.</span></div></div>
{% endfor %}
</div>
</section>
{% endblock %}
+6
View File
@@ -665,6 +665,12 @@
return $this->error('Шаблон рубрики не найден', array(), 404);
}
if (!$this->wantsJson()) {
$this->redirect($this->base() . '/rubrics?templates=' . (int) $item['rubric_id']
. '&template=' . (int) $item['id']);
return null;
}
return $this->success('', array('data' => $item));
}
+10 -2
View File
@@ -840,7 +840,10 @@
}
$rows = DB::query(
'SELECT t.*, (SELECT COUNT(*) FROM ' . self::docsTable() . ' d WHERE d.rubric_id = t.rubric_id AND d.rubric_tmpl_id = t.id) AS docs_count'
'SELECT t.*,'
. ' (SELECT COUNT(*) FROM ' . self::docsTable() . ' d WHERE d.rubric_id = t.rubric_id AND d.rubric_tmpl_id = t.id) AS docs_count,'
. ' (SELECT d.Id FROM ' . self::docsTable() . ' d WHERE d.rubric_id = t.rubric_id AND d.rubric_tmpl_id = t.id ORDER BY d.Id ASC LIMIT 1) AS first_document_id,'
. ' (SELECT d.document_title FROM ' . self::docsTable() . ' d WHERE d.rubric_id = t.rubric_id AND d.rubric_tmpl_id = t.id ORDER BY d.Id ASC LIMIT 1) AS first_document_title'
. ' FROM ' . self::templatesTable() . ' t WHERE t.rubric_id = %i ORDER BY t.id ASC',
(int) $rubricId
)->getAll();
@@ -882,7 +885,10 @@
public static function extraTemplate($id)
{
$row = DB::query(
'SELECT t.*, (SELECT COUNT(*) FROM ' . self::docsTable() . ' d WHERE d.rubric_id = t.rubric_id AND d.rubric_tmpl_id = t.id) AS docs_count'
'SELECT t.*,'
. ' (SELECT COUNT(*) FROM ' . self::docsTable() . ' d WHERE d.rubric_id = t.rubric_id AND d.rubric_tmpl_id = t.id) AS docs_count,'
. ' (SELECT d.Id FROM ' . self::docsTable() . ' d WHERE d.rubric_id = t.rubric_id AND d.rubric_tmpl_id = t.id ORDER BY d.Id ASC LIMIT 1) AS first_document_id,'
. ' (SELECT d.document_title FROM ' . self::docsTable() . ' d WHERE d.rubric_id = t.rubric_id AND d.rubric_tmpl_id = t.id ORDER BY d.Id ASC LIMIT 1) AS first_document_title'
. ' FROM ' . self::templatesTable() . ' t WHERE t.id = %i LIMIT 1',
(int) $id
)->getAssoc();
@@ -1457,6 +1463,8 @@
$row['author_id'] = (int) $row['author_id'];
$row['created'] = (int) $row['created'];
$row['docs_count'] = isset($row['docs_count']) ? (int) $row['docs_count'] : 0;
$row['first_document_id'] = isset($row['first_document_id']) ? (int) $row['first_document_id'] : 0;
$row['first_document_title'] = isset($row['first_document_title']) ? (string) $row['first_document_title'] : '';
$row['created_label'] = $row['created'] > 0 ? date('d.m.Y H:i', $row['created']) : 'не задано';
$row['size_label'] = self::formatBytes(strlen((string) $row['template']));
$row['can_delete'] = $row['docs_count'] === 0;
+71 -1
View File
@@ -1129,7 +1129,8 @@
overflow-x: hidden;
}
.rubrics-extra-template-panel {
width: min(66vw, 980px);
width: 66.6667vw;
max-width: 66.6667vw;
overflow-x: hidden;
}
.rubrics-drawer {
@@ -1141,6 +1142,63 @@
.rubrics-drawer .drawer-body {
overflow-x: hidden;
}
.rubrics-extra-template-body {
display: flex;
min-height: 0;
flex-direction: column;
gap: 16px;
}
.rubrics-extra-template-help {
flex: 0 0 auto;
align-items: center;
margin: 0;
}
.rubrics-extra-template-help > i {
flex: 0 0 auto;
color: var(--blue-600);
font-size: 20px;
}
.rubrics-extra-template-help > div {
display: grid;
flex: 1 1 auto;
gap: 3px;
min-width: 0;
}
.rubrics-extra-template-help b,
.rubrics-extra-template-help span {
display: block;
}
.rubrics-extra-template-help span {
color: var(--text-secondary);
font-size: 12px;
line-height: 1.45;
}
.rubrics-extra-template-help .btn {
flex: 0 0 auto;
max-width: 42%;
}
.rubrics-extra-template-help .btn span {
overflow: hidden;
color: inherit;
white-space: nowrap;
text-overflow: ellipsis;
}
.rubrics-extra-template-fields {
display: flex;
flex: 1 1 auto;
flex-direction: column;
gap: 14px;
min-height: 0;
}
.rubrics-extra-template-fields > .rubrics-code-field {
flex: 1 1 auto;
min-height: 360px;
}
.rubrics-extra-template-fields > .rubrics-code-field > .CodeMirror {
flex: 1 1 auto;
min-height: 320px;
height: auto !important;
}
/* Секции формы настроек поля: цветные плашки-заголовки и визуальное разделение */
.rubrics-field-form {
display: flex;
@@ -3859,12 +3917,24 @@
.rubrics-group-panel {
width: min(96vw, 980px);
}
.rubrics-extra-template-panel {
max-width: min(96vw, 980px);
}
#rubricDrawer {
width: 96vw;
max-width: 96vw;
}
}
@media (max-width: 760px) {
.rubrics-extra-template-help {
align-items: stretch;
flex-direction: column;
}
.rubrics-extra-template-help .btn {
justify-content: center;
max-width: none;
width: 100%;
}
.rubrics-builder-runtime {
grid-template-columns: 40px minmax(0, 1fr) auto;
}
+19
View File
@@ -1339,6 +1339,7 @@
this.extraTemplateForm.elements.rubric_id.value = this.currentRubricId;
this.setCodeValue(this.extraTemplateForm.elements.template, '');
document.getElementById('rubricExtraTemplateTitle').textContent = 'Новый дополнительный шаблон';
this.updateExtraTemplateDocumentLink({ rubric_id: this.currentRubricId, docs_count: 0 });
this.activeTemplateTextarea = this.extraTemplateForm.elements.template;
if (Adminx.Drawer) { Adminx.Drawer.open('rubricExtraTemplateDrawer'); }
this.refreshEditors();
@@ -1357,11 +1358,29 @@
self.extraTemplateForm.elements.title.value = item.title || '';
self.setCodeValue(self.extraTemplateForm.elements.template, item.template || '');
document.getElementById('rubricExtraTemplateTitle').textContent = 'Редактирование шаблона #' + item.id;
self.updateExtraTemplateDocumentLink(item);
self.activeTemplateTextarea = self.extraTemplateForm.elements.template;
self.refreshEditors();
});
},
updateExtraTemplateDocumentLink: function (item) {
var link = document.querySelector('[data-extra-template-document-link]');
var label;
if (!link) { return; }
item = item || {};
if ((parseInt(item.docs_count, 10) || 0) === 1 && (parseInt(item.first_document_id, 10) || 0) > 0) {
link.href = this.base() + '/documents/' + parseInt(item.first_document_id, 10) + '/edit';
label = item.first_document_title
? (link.getAttribute('data-edit-prefix') || '') + item.first_document_title
: link.getAttribute('data-edit-content');
} else {
link.href = this.base() + '/documents?rubric_id=' + (parseInt(item.rubric_id, 10) || this.currentRubricId || 0);
label = link.getAttribute('data-documents-label');
}
link.querySelector('span').textContent = label;
},
submitExtraTemplate: function () {
var self = this;
var id = this.extraTemplateForm.elements.id.value;
@@ -1140,4 +1140,8 @@
<phrase data="auto_a4bb945f71c3e11d">Replace all</phrase>
<phrase data="auto_582dbae7c8ee21b0">The old file will be moved to the trash after saving.</phrase>
<phrase data="auto_434e952b35fa2970">The current set will be moved to the trash after saving.</phrase>
<phrase data="rubrics_extra_template_help_title">The template controls presentation</phrase>
<phrase data="rubrics_extra_template_help_text">Headings, text, and other data are edited in the document that uses this template.</phrase>
<phrase data="rubrics_extra_template_edit_content">Edit content</phrase>
<phrase data="rubrics_extra_template_edit_prefix">Edit: </phrase>
</language>
@@ -1140,4 +1140,8 @@
<phrase data="auto_a4bb945f71c3e11d">Заменить все</phrase>
<phrase data="auto_582dbae7c8ee21b0">Старый файл будет перемещён в корзину после сохранения.</phrase>
<phrase data="auto_434e952b35fa2970">Текущий комплект будет перемещён в корзину после сохранения.</phrase>
<phrase data="rubrics_extra_template_help_title">Шаблон отвечает за оформление</phrase>
<phrase data="rubrics_extra_template_help_text">Заголовки, тексты и другие данные меняются в документе, который использует этот шаблон.</phrase>
<phrase data="rubrics_extra_template_edit_content">Редактировать содержимое</phrase>
<phrase data="rubrics_extra_template_edit_prefix">Редактировать: </phrase>
</language>
+1 -1
View File
@@ -17,7 +17,7 @@
return array(
'code' => 'rubrics',
'name' => 'Рубрики и поля',
'version' => '0.3.4',
'version' => '0.3.6',
'field_sets' => \App\Adminx\Rubrics\RubricFieldPresets::definitions(),
'hook_definitions' => array(
array(
+8 -3
View File
@@ -783,12 +783,17 @@
</div>
<button class="btn btn-ghost btn-icon" type="button" data-close-drawer aria-label="Закрыть"><i class="ti ti-x"></i></button>
</div>
<form id="rubricExtraTemplateForm" class="rubrics-drawer">
<form id="rubricExtraTemplateForm" class="rubrics-drawer rubrics-extra-template-form">
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
<input type="hidden" name="id" value="">
<input type="hidden" name="rubric_id" value="">
<div class="drawer-body">
<div class="form-grid">
<div class="drawer-body rubrics-extra-template-body">
<div class="alert alert-info rubrics-extra-template-help">
<i class="ti ti-info-circle"></i>
<div><b>Шаблон отвечает за оформление</b><span>Заголовки, тексты и другие данные меняются в документе, который использует этот шаблон.</span></div>
{% if can_view_documents %}<a class="btn btn-secondary btn-sm" href="{{ ADMINX_BASE }}/documents" data-extra-template-document-link data-edit-prefix="Редактировать: " data-edit-content="Редактировать содержимое" data-documents-label="Открыть документы"><i class="ti ti-file-pencil"></i><span>Открыть документы</span></a>{% endif %}
</div>
<div class="form-grid rubrics-extra-template-fields">
<label class="field col-12"><span class="field-label">Название</span><input class="input" type="text" name="title" required><span class="field-error" data-error="title"></span></label>
{{ ui.code_field('Код шаблона', 'template', 560, 'col-12', true) }}
</div>
+1 -1
View File
@@ -267,7 +267,7 @@
{
$compatibility = array(
'CACHE_DOC_FILE', 'CACHE_DOC_FULL', 'CACHE_DOC_FULL_ADMIN', 'CACHE_DOC_TPL',
'MEMORY_LIMIT_PANIC', 'PROFILING', 'REQUEST_BREAK_WORDS', 'REQUEST_ETC',
'MEMORY_LIMIT_PANIC', 'REQUEST_BREAK_WORDS', 'REQUEST_ETC',
'REQUEST_STRIP_TAGS', 'SEND_SQL_ERROR', 'SQL_QUERY_SANITIZE',
'USE_ENCODE_SERIALIZE', 'USE_GET_FIELDS', 'USE_STATIC_DATA',
);
@@ -12,7 +12,7 @@
overflow: hidden;
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
background: var(--background-card);
background: var(--surface-card);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.view-override-list-head {
@@ -163,6 +163,23 @@
.view-override-form {
padding: 16px 18px 18px;
}
body.view-override-fullscreen-open {
overflow: hidden;
}
.view-override-form.is-fullscreen {
position: fixed;
inset: 12px;
z-index: 500;
display: flex;
flex-direction: column;
width: auto;
height: auto;
overflow: hidden;
padding: 16px 18px 18px;
border-radius: var(--radius-lg);
background: var(--surface-card);
box-shadow: 0 18px 56px rgba(15, 23, 42, 0.2), 0 0 0 1px rgba(15, 23, 42, 0.08);
}
.view-override-toolbar,
.view-override-footer {
display: flex;
@@ -194,6 +211,23 @@
font-size: 13px;
line-height: 1.55;
}
.view-override-form.is-fullscreen .CodeMirror {
flex: 1 1 auto;
width: 100% !important;
height: auto !important;
min-height: 0;
}
.view-override-form.is-fullscreen .CodeMirror-scroll {
height: 100% !important;
}
.view-override-form.is-fullscreen .view-override-toolbar,
.view-override-form.is-fullscreen .view-override-lint,
.view-override-form.is-fullscreen .view-override-footer {
flex: none;
}
.view-override-form.is-fullscreen .view-override-variables {
display: none;
}
.view-override-lint {
display: block;
min-height: 20px;
@@ -357,6 +391,14 @@
height: 520px !important;
min-height: 360px;
}
.view-override-form.is-fullscreen {
inset: 6px;
padding: 12px;
}
.view-override-form.is-fullscreen .CodeMirror {
height: auto !important;
min-height: 0;
}
.view-override-save-state {
width: 100%;
margin-right: 0;
+44 -3
View File
@@ -40,6 +40,12 @@
event.preventDefault();
event.returnValue = '';
});
document.addEventListener('keydown', function (event) {
if (event.key !== 'Escape' || !self.form.classList.contains('is-fullscreen')) { return; }
event.preventDefault();
self.fullscreen(false);
});
window.setTimeout(function () { self.bindEditorKeys(); }, 0);
},
request: function (url, data) {
@@ -91,9 +97,44 @@
});
},
fullscreen: function () {
if (!this.field._adminxCodeMirror || !Adminx.CodeEditor) { return; }
Adminx.CodeEditor.toggleFullscreen(this.field._adminxCodeMirror);
bindEditorKeys: function () {
var self = this;
var editor = this.field._adminxCodeMirror;
if (!editor) { return; }
var keys = editor.getOption('extraKeys') || {};
keys.F11 = function () { self.fullscreen(); };
keys['Cmd-F11'] = function () { self.fullscreen(); };
keys.Esc = function () { self.fullscreen(false); };
editor.setOption('extraKeys', keys);
},
fullscreen: function (force) {
var editor = this.field._adminxCodeMirror;
if (!editor) { return; }
var active = this.form.classList.contains('is-fullscreen');
var next = typeof force === 'boolean' ? force : !active;
if (next === active) { return; }
if (Adminx.CodeEditor) { Adminx.CodeEditor.closeFullscreen(editor); }
this.form.classList.toggle('is-fullscreen', next);
document.body.classList.toggle('view-override-fullscreen-open', next);
this.updateFullscreenButton(next);
window.setTimeout(function () {
editor.refresh();
if (next) { editor.focus(); }
}, 0);
},
updateFullscreenButton: function (active) {
var button = this.form.querySelector('[data-view-override-fullscreen]');
if (!button) { return; }
var label = active ? 'Вернуться к обычному размеру' : 'На весь экран';
var icon = button.querySelector('i');
button.setAttribute('aria-label', label);
button.setAttribute('data-tooltip', label);
button.setAttribute('title', label);
button.setAttribute('aria-pressed', active ? 'true' : 'false');
if (icon) { icon.className = active ? 'ti ti-arrows-minimize' : 'ti ti-arrows-maximize'; }
},
removeOverride: function () {
+1 -1
View File
@@ -17,7 +17,7 @@
return array(
'code' => 'themes',
'name' => 'Темы',
'version' => '0.2.0',
'version' => '0.2.1',
'permissions' => array(
'key' => 'themes',
-1
View File
@@ -39,7 +39,6 @@
// Дополнительные домены, разрешённые помимо PUBLIC_SITE_URL.
'allowed_hosts' => array(),
'debug' => array(
'enabled' => true,
'groups' => array(1),
),
);
+44 -1
View File
@@ -315,6 +315,13 @@ Legacy продолжает использовать прежние поля и
поэтому дополнительные фотографии не требуют повторного сохранения товара или
переиндексации каталога.
Для медицинских кроватей набор нативных характеристик содержит отдельный
признак **Матрас в комплекте**. Установите значение **Да** только тогда, когда
матрас входит в базовую цену кровати. Если значение **Нет** или не задано,
стандартный шаблон товара предупреждает покупателя, что матрас приобретается
отдельно. Скидка на совместимый матрас и предложение «кровать + матрас»
настраиваются как акция или комплект и не включают этот признак автоматически.
### Подборки товаров
В **Товары → Подборки** создаются управляемые товарные ленты для главной,
@@ -410,7 +417,9 @@ CommerceML: интеграции не должны писать прямо в т
Количество товаров передается шаблону в `option.count`. Стандартный класс
счетчика — `.filter_checkbox-quantity`; его внешний вид можно менять в CSS
представления. Там же выбирается поведение вариантов с нулевым результатом:
показывать, отключать или скрывать.
показывать, отключать или скрывать. По умолчанию нулевые варианты скрыты,
чтобы не показывать покупателю заведомо пустой выбор. Уже выбранное значение
остаётся видимым, чтобы его можно было снять.
Технический контракт Twig описан в
`docs/development/catalog-filter-templates.md`.
@@ -429,6 +438,15 @@ CommerceML: интеграции не должны писать прямо в т
шаблона. Полный контракт описан в
`docs/development/catalog-card-templates.md`.
Поле **Подпись в карточке** предназначено для короткого отличия похожих
товаров: например, `Ширина 60 см`, `Усиленный` или `С педалью`. Подпись не
заменяет название и описание. В стандартном оформлении она показывается у
нижнего края фотографии карточки и под заголовком на странице товара.
В собственном Twig-представлении используйте `item.caption` для карточки и
`product.caption` для страницы товара. Если поле не заполнено, оба значения
будут пустыми и дополнительная разметка не выводится.
### Цвета и варианты товара
Модуль сразу добавляет две характеристики с областью **Вариант товара**:
@@ -520,6 +538,22 @@ CommerceML: интеграции не должны писать прямо в т
### Как быстро заполнить варианты
Начать работу можно прямо из карточки товара. Откройте товар и перейдите в
раздел **Варианты**:
- **Новая группа** создаёт группу для текущей модели; открытый товар становится
её первым и основным вариантом;
- **Существующая группа** добавляет товар в уже созданную группу без перехода в
общий список групп.
Если товар уже входит в группу, кнопка **Создать вариант копированием** создаёт
выключенный черновик, сразу добавляет его в эту же группу и открывает для
редактирования. Перед созданием система обязательно показывает подтверждение,
поэтому случайный щелчок не создаст лишний товар. После обычного сохранения
редактор возвращает в матрицу этой группы, где новому варианту можно назначить
цвет, комплектацию и другие отличия. Кнопка **Сохранить и остаться** оставляет
редактор открытым.
Откройте **Товары → Группы вариантов** и выберите группу. В таблице
**Матрица вариантов** каждая строка соответствует отдельному товару:
@@ -588,6 +622,8 @@ CommerceML: интеграции не должны писать прямо в т
[mod_product_relations:alternative]
[mod_product_relations:together]
[mod_product_bundles]
[mod_product_bundles:offers]
[mod_product_bundles:manual]
```
Тег без уточнения сначала выводит связи, которые администратор задал вручную.
@@ -597,6 +633,13 @@ CommerceML: интеграции не должны писать прямо в т
Теги с конкретным типом выводят только ручные связи выбранного типа и не
подменяют их автоматической подборкой.
Тег `[mod_product_bundles]` выводит сразу ручные комплекты и предложения из
активных акций. Если эти блоки нужны в разных местах шаблона, используйте
`[mod_product_bundles:offers]` только для акционных пар и
`[mod_product_bundles:manual]` только для ручных комплектов. Например,
компактное предложение матраса можно поставить под блоком покупки кровати, а
большой комплект из нескольких карточек оставить ниже описания.
Связанные товары используют те же центральные карточки, что каталог. Кнопка
комплекта добавляет каждую позицию в обычную корзину с текущей ценой. Если товар
выключен, удалён или у него не настроено поле цены, недоступный комплект не
+17
View File
@@ -29,6 +29,23 @@
Уровень определяется положением пункта в дереве. Перетаскивание меняет родителя,
уровень и порядок; отдельное числовое поле позиции для этого не требуется.
### Правая часть товарного каталога
Если навигация выбрана в настройках товарного каталога, у её пунктов появляется
поле **«Поведение в меню каталога»**:
- **Обычная ссылка** сразу открывает назначенный документ или URL.
- **Показать справа дочерние пункты** раскрывает вручную собранные подпункты.
- **Разделы с товарами по акции** автоматически показывает непустые разделы,
где текущая цена ниже старой.
- **Разделы с товарами СФР** автоматически показывает непустые разделы с
товарами, для которых включена программа СФР.
Динамические варианты читают товарный индекс и не требуют вручную поддерживать
дочерние ссылки. Порядок разделов берётся из дерева каталога. Отключённые,
удалённые и скрытые товары в расчёт не входят. Общая ссылка пункта сохраняется:
она доступна в правой панели как переход ко всем акциям или к странице СФР.
## Шаблоны уровней
Рендер выполняется в следующем порядке:
+36
View File
@@ -72,6 +72,42 @@
Блок **Редакторы компонентов** содержит быстрые переходы в штатные разделы.
Содержимое и настройки следует менять именно там.
## Вкладка «Карта шаблонов»
Эта вкладка отвечает на практический вопрос: **где менять конкретную часть
публичной страницы**. Она читает настройки текущей установки и показывает их,
но сама ничего не сохраняет.
В верхней части указан действующий способ сборки:
- **Штатные шаблоны AVE.cms** — общий шаблон сайта и шаблон рубрики собирают
страницу, а активная тема подключает стили и переоформляет только отдельные
публичные компоненты;
- **Оболочка темы** — внешний каркас страницы формирует указанный Twig-файл
темы; использование шаблонов рубрики зависит от настройки наследования.
Ниже приведены готовые цепочки для обычного документа, списка материалов,
раздела каталога, товарной карточки и полной страницы товара. Каждый шаг имеет
кнопку перехода в штатный редактор. Например:
```text
товар
→ опубликованное представление карточки
→ резервный Twig-компонент, если представление не назначено
→ товарная выдача
→ шаблон рубрики
→ общий шаблон сайта
```
Таблица рубрик показывает назначенный общий шаблон, наличие основного и
дополнительных шаблонов документа. Блок компонентов показывает фактический
источник Twig-разметки: активную тему, системный резерв или отсутствующий файл.
Важно: файловый компонент темы не является самостоятельной страницей. Он
работает только тогда, когда его вызывает шаблон рубрики, запрос, блок или
модуль. Поэтому содержимое документа следует хранить в полях документа, а не в
Twig-файле темы.
## Что такое представление
**Представление** — это необязательный слой оформления уже подготовленных
+9 -5
View File
@@ -80,17 +80,21 @@ desktop- и mobile-контейнерах темы. В списке он ост
## Включение
Публичная отладка настраивается в `configs/public.config.php`:
Общий режим публичной отладки задаёт системная константа `PROFILING`. Откройте
**Настройки → Константы → Разработка**, найдите `PROFILING` и выберите значение:
- `off` — полностью выключить панель и сбор её диагностических данных;
- `light`, `full` или `dev` — включить защищённую панель.
Доступ включённой панели дополнительно ограничивается в
`configs/public.config.php`:
```php
'debug' => array(
'enabled' => true,
'groups' => array(),
),
```
- `enabled` — общий выключатель. При `false` панель недоступна даже
администратору.
- `groups` — ID **публичных групп пользователей**, которым разрешён Debug
Toolbar без системного права.
- пустой `groups` оставляет доступ только системным пользователям с правом
@@ -130,7 +134,7 @@ Debug::panel($result, 'Ответ внешнего сервиса');
## Когда панель не появляется
1. Проверьте `debug.enabled` в `configs/public.config.php`.
1. Проверьте, что `PROFILING` не равен `off` в разделе **Настройки → Константы**.
2. Проверьте право `view_public_debug` текущей системной роли или ID публичной
группы в `debug.groups`.
3. Убедитесь, что вход в панель управления выполнен в том же браузере и на том
+41 -1
View File
@@ -290,6 +290,13 @@ listings. The detail page gets the full gallery from the document's media field,
therefore additional photos do not require re-saving the item or
directory reindexing.
The native medical-bed attribute set contains a dedicated **Mattress included**
flag. Set it to **Yes** only when the mattress is included in the bed's base
price. When it is **No** or not set, the standard product template tells the
customer that the mattress is sold separately. A discount on a compatible
mattress or a “bed + mattress” offer remains a promotion or bundle and does not
enable this flag automatically.
### Product selections
In **Products → Collections** managed product feeds are created for the main,
@@ -386,7 +393,9 @@ returns to the previous conclusion.
The quantity of products is transferred to the template in `option.count`. Standard class
counter - `.filter_checkbox-quantity`; its appearance can be changed in CSS
presentations. The behavior of options with a zero result is also selected there:
show, disable or hide.
show, disable or hide. Zero-result options are hidden by default so customers do
not see choices that cannot return a product. A selected option remains visible
so it can always be cleared.
The Twig technical contract is described in
`docs/development/catalog-filter-templates.md`.
@@ -405,6 +414,15 @@ In Twig, `context.code` is available: `catalog`, `favorites`, `viewed`, `search`
template. The complete contract is described in
`docs/development/catalog-card-templates.md`.
Use the **Card caption** field for a short distinction between similar products,
for example `60 cm wide`, `Heavy-duty`, or `With pedal`. It does not replace the
product title or description. The standard theme shows it at the bottom of the
card image and below the heading on the product page.
Custom Twig views can read `item.caption` in a card and `product.caption` on a
product page. Both values are empty and produce no extra markup when the field
is not filled in.
### Colors and product options
The module immediately adds two characteristics with the **Product Variant** area:
@@ -493,6 +511,21 @@ Custom product-card templates can use:
### How to quickly fill out options
You can start directly from a product editor. Open the product and switch to
**Variants**:
- **New group** creates a group for the current model; the open product becomes
its first and primary variant;
- **Existing group** adds the product to a group that already exists, without
returning to the full group list.
When the product already belongs to a group, **Create variant from copy** creates
a disabled draft, adds it to the same group and opens it for editing. The panel
always asks for confirmation first, so an accidental click does not create an
extra product. A regular save returns to that group's matrix, where you can set
the new variant's color, configuration and other distinguishing values. **Save
and stay** keeps the editor open.
Go to **Products → Option Groups** and select a group. In the table
**Option matrix** each row corresponds to a separate product:
@@ -559,6 +592,8 @@ product page template:
[mod_product_relations:alternative]
[mod_product_relations:together]
[mod_product_bundles]
[mod_product_bundles:offers]
[mod_product_bundles:manual]
```
An unqualified tag first displays relationships that the administrator has manually specified.
@@ -568,6 +603,11 @@ a general template for a product category, and gradually clarify important conne
Tags with a specific type only display manual links of the selected type and not
replace them with automatic selection.
`[mod_product_bundles]` renders both manual bundles and offers from active
promotions. Use `[mod_product_bundles:offers]` for promotional pairs only and
`[mod_product_bundles:manual]` for manually assembled bundles only when these
blocks need different positions in the product page template.
Related products use the same center cards as the catalog. Button
kit adds each item to the regular cart with the current price. If the product
disabled, deleted or does not have a price field configured, an unavailable kit is not
+17
View File
@@ -29,6 +29,23 @@ to consciously open a new tab - `_blank`.
The level is determined by the position of the item in the tree. Drag changes parent,
level and order; A separate numeric position field is not required for this.
### Product catalog side panel
When this navigation is selected in product catalog settings, every item gets a
**Catalog menu behavior** field:
- **Regular link** opens the assigned document or URL.
- **Show child items on the right** displays manually maintained child links.
- **Sections with sale products** displays non-empty sections where the current
product price is lower than its old price.
- **Sections with SFR products** displays non-empty sections containing products
enabled for the SFR payment program.
Dynamic sources read the product index, preserve the catalog tree order, and do
not require duplicate navigation items. Disabled, deleted, and hidden products
are ignored. The main item URL remains available as the link to all sales or to
the SFR information page.
## Level templates
Rendering is performed in the following order:
+25
View File
@@ -40,6 +40,31 @@ Each row represents one category. It shows its site template, document and field
counts, related requests and catalogs, and clear structural problems. The
diagnostics are informative and never repair data automatically.
## Template map
The **Template map** answers one practical question: where should a particular
part of a public page be edited? It reads the current installation and never
saves or switches anything by itself.
The current composition mode is shown first:
- **AVE.cms templates** means that the site template and rubric template build
the page, while the active theme supplies assets and selected component
overrides;
- **Theme shell** means that the configured Twig file builds the outer page
shell, with rubric inheritance controlled by the theme manifest.
The page then shows actual chains for a text document, a content list, a
catalog category, a product card, and a full product page. Every step links to
the editor that owns it. The rubric table shows site-template assignments and
whether the main or additional rubric templates exist. Component groups show
whether markup comes from the active theme, a system fallback, or a missing
file.
A theme component is not an independent page. It works only when a rubric
template, request, block, or module calls it. Document content should therefore
remain in document fields rather than in a theme Twig file.
## Presentations
A **presentation** is an optional visual layer for already prepared data. It
+9 -5
View File
@@ -78,17 +78,21 @@ receive cached pages and cards.
## Enable
Public debugging is configured in `configs/public.config.php`:
The `PROFILING` runtime constant controls public debugging globally. Open
**Settings → Constants → Development**, find `PROFILING`, and select a value:
- `off` fully disables the toolbar and its diagnostic collectors;
- `light`, `full`, or `dev` enables the protected toolbar.
Access to the enabled toolbar is additionally restricted in
`configs/public.config.php`:
```php
'debug' => array(
'enabled' => true,
'groups' => array(),
),
```
- `enabled` - general switch. With `false` the panel is not accessible even
administrator.
- `groups` — ID of **public user groups** to which Debug is allowed
Toolbar without system rights.
- empty `groups` leaves access only to system users with the right
@@ -128,7 +132,7 @@ Detailed use cases and limitations are on the page
## When the panel does not appear
1. Check `debug.enabled` to `configs/public.config.php`.
1. Check that `PROFILING` is not `off` under **Settings → Constants**.
2. Check the right `view_public_debug` of the current system role or public ID
groups in `debug.groups`.
3. Make sure that you are logged into the control panel in the same browser and on the same
+39 -13
View File
@@ -1,6 +1,7 @@
# SearchText - search text
`App\Helpers\SearchText` - small utilities for preparing search queries.
`App\Helpers\SearchText` provides shared query preparation for documents,
products, public suggestions, and control-panel global search.
```php
use App\Helpers\SearchText;
@@ -10,33 +11,58 @@ use App\Helpers\SearchText;
## Methods
###`containsRussian($text): bool`
Is there a Cyrillic alphabet in the text?
### `containsRussian($text): bool`
Checks whether the text contains Cyrillic characters.
```php
SearchText::containsRussian('кресло'); // true
SearchText::containsRussian('chair'); // false
```
###`toRussian($input)`
Reduces the layout to Russian - “Latin, typed by mistake” → Cyrillic (according to
keyboard layout).
### `toLatin($input)` and `toRussian($input)`
Create transliterated variants. They do not switch the keyboard layout.
```php
SearchText::toRussian('rhtckj'); // 'кресло'
SearchText::toLatin('ДБ-11'); // 'DB-11'
SearchText::toRussian('DB-11'); // 'ДБ-11'
```
### `switchKeyboardLayout($input)`
Corrects a query typed with the wrong keyboard layout.
```php
SearchText::switchKeyboardLayout('ВИ-11'); // 'DB-11'
```
### `variants($text): array`
Returns the literal spelling, transliteration, and corrected keyboard-layout
forms. For model codes without a separator it also adds a dash at letter-number
boundaries.
```php
SearchText::variants('ДБ11');
// includes: ДБ11, ДБ-11, DB11, DB-11
SearchText::variants('ВИ11');
// includes DB11 and DB-11 after correcting the layout
```
The literal query remains first and receives the highest search weight. Added
forms only broaden matching; stored product codes are not modified.
### `termGroups($text): array`
Splits a phrase into terms. Variants of one term form an OR group, while search
layers may require different groups with AND. Russian terms also receive a stem.
---
## Recipe
## Usage
**Auto-correct layout before searching:**
Do not replace the original query. Pass all variants to the search layer:
```php
$q = Request::getStr('q');
if ($q !== '' && !SearchText::containsRussian($q)) {
$fixed = SearchText::toRussian($q);
// искать и по $q, и по $fixed — пользователь мог забыть переключить раскладку
foreach (SearchText::variants(Request::getStr('q')) as $variant) {
// rank the literal form higher and use the rest as fallbacks
}
```
+5
View File
@@ -84,6 +84,11 @@ the file remains in its folder. When transferring a JPG or PNG, the adjacent Web
the same base name is transferred along with the original; automatic thumbnails
will be rebuilt using the new URL.
One selection may contain more than 20 files. The panel automatically splits
them into batches compatible with PHP's standard `max_file_uploads` limit and
shows aggregate progress. The same behaviour is used by **Media**, the shared
picker, and document or product media fields.
## Automatic generation
The thumbnail is created lazily - on the first HTTP request to its URL. Until the first
+14
View File
@@ -35,6 +35,20 @@ The order snapshot stores original and final prices, the promotion name, and
the gift marker. The usage journal keeps this information even when the rule is
later changed or deleted.
A fixed-price promotion between specific products is also exposed as a bundle
offer on both public product pages. The condition product page shows eligible
discounted products, while the discounted product page shows the products that
activate its special price. **Add bundle to cart** submits both products, and the server
revalidates the active promotion before applying the discount.
The block is rendered by `[mod_product_bundles]`. Use
`[mod_product_bundles:offers]` for promotional pairs only and
`[mod_product_bundles:manual]` for manual bundles only. Its markup is editable
under `Products -> Templates -> Product bundles`. `promotion_offers` contains offers
derived from active promotions, while `bundles` contains manually configured
product bundles, so the storefront markup does not require a PHP or theme
override.
## Online payment journal
The **Payment journal** tab shows the complete gateway sequence: payment
+6
View File
@@ -23,6 +23,12 @@ fields are shown in the editor and look like `[tag:fld:<id>]`.
The form can also be opened at `/contacts/<alias>`. Business address
`/callback` uses the form with alias `callback` if it exists.
For a Russian phone number, add
`type="tel" inputmode="tel" autocomplete="tel" data-contact-phone-mask` to the
field attributes. The public form formats typed or pasted input as
`+7 (900) 000-00-00` and rejects an incomplete number. The mask is opt-in, so
regular text fields and international phone numbers remain unchanged.
## Reception and protection
Public submission checks for CSRF, required values, field types, and uploads.
+22
View File
@@ -399,6 +399,28 @@ If the client needs a ready-made card, use `format=html`. For
simultaneously obtain structured data and ready-made markup, use
`format=both`.
## Dictionaries and typo correction
Search first executes the visitor's exact query. Only an empty result may be
retried with a conservative correction learned from indexed document titles and
their additional search names. The corrector handles a wrong keyboard layout,
`e/yo`, a missing or extra letter, and adjacent transpositions. Equally likely
matches are offered as links instead of being replaced at random. Product
codes, numbers and mixed letter-number identifiers are never corrected.
Use the **Dictionary** tab to map a visitor phrase to the canonical phrase.
A rule can be global or limited to one configured search scope. Rules can be
previewed, disabled, edited and deleted.
When the Products module is installed, a product editor also has a **Search**
section. It stores additional searchable names for that product only. These
names do not change the visible product title. After two characters, the field
suggests matching names already used by products and matching shared-dictionary
entries. A new value can still be entered manually. Rebuild the search index
once after upgrading; later document saves keep the typo vocabulary current.
Shared terminology remains explicit on the **Dictionary** tab; automatic typo
correction does not try to infer semantic synonyms.
## Algorithms and weights
Staff strategies:
+23 -2
View File
@@ -25,6 +25,10 @@ simultaneously belong to two profiles.
3. If necessary, register the sender's name.
4. Top up your balance and check if sending to the desired destinations is allowed.
The sender name must contain Latin letters and digits and be no longer than 11
characters, or be a numeric sender up to 15 digits. Register it in SMSC and wait
until it is approved. If the field is empty, SMSC uses its default sender.
You do not need to enter your account login and password in AVE.cms. Use a separate
An API key that can be revoked without changing the SMSC owner's password.
@@ -54,6 +58,7 @@ leaving existing users logged in.
| Resubmission | Minimum pause before a new SMS in the current session. |
| Input attempts | How many invalid inputs are allowed per challenge. |
| SMS per day | Daily limit for one room. |
| Keep history | Number of days to retain send and login events. |
Additionally, the kernel limits the frequency of requests by IP and number. New code
cancels the previous one, is associated with the current PHP session and can be used
@@ -63,7 +68,23 @@ If account creation is disabled and the number is not found, the public response
looks like a successful request, but the SMS is not sent. It doesn't allow iterating
registered phones and does not spend balance on unknown numbers.
## Secrets and environment variablesDefault settings are in private storage
## Balance and login history
The `Settings` tab shows the current SMSC balance, connection state,
and the selected sender status. `Refresh SMSC data` requests fresh information;
the result is cached for one minute between checks.
The separate `Login history` tab records successful and failed sends, rate limits, invalid, expired,
or reused codes, successful logins, and account creation. It includes the date,
masked and hashed phone, IP, browser, user, and SMSC message ID. The one-time code
and full phone number are never stored in this history.
Filters search by the phone mask, IP, or SMSC ID and narrow the list by event and
outcome. Old rows are removed automatically according to `Keep history`.
## Secrets and environment variables
Default settings are in private storage
`storage/secrets/modules/smsc_auth.php`. They can be overridden:
```dotenv
@@ -94,7 +115,7 @@ forms and rate limit.
## Removing a module
When deleted, SMSC settings and the history of incomplete codes are erased. Users,
When deleted, SMSC settings, incomplete challenges, and login history are erased. Users,
confirmed phone numbers, orders and profiles are saved because they are shared
AVE.cms data. Without an installed SMS provider, the phone login form is simple
not shown.
+41 -11
View File
@@ -1,6 +1,7 @@
# SearchText — текст для поиска
`App\Helpers\SearchText`мелкие утилиты подготовки поисковых запросов.
`App\Helpers\SearchText`единая подготовка поисковых запросов для документов,
товаров, публичных подсказок и глобального поиска панели.
```php
use App\Helpers\SearchText;
@@ -17,23 +18,52 @@ SearchText::containsRussian('кресло'); // true
SearchText::containsRussian('chair'); // false
```
### `toRussian($input)`
Приводит раскладку к русской — «латиница, набранная по ошибке» → кириллица (по
раскладке клавиатуры).
### `toLatin($input)` и `toRussian($input)`
Создают транслитерированный вариант. Это не исправление раскладки клавиатуры.
```php
SearchText::toRussian('rhtckj'); // 'кресло'
SearchText::toLatin('ДБ-11'); // 'DB-11'
SearchText::toRussian('DB-11'); // 'ДБ-11'
```
### `switchKeyboardLayout($input)`
Исправляет запрос, набранный в другой раскладке.
```php
SearchText::switchKeyboardLayout('ВИ-11'); // 'DB-11'
```
### `variants($text): array`
Возвращает исходное написание, транслитерацию и исправленную раскладку. Для
модельных кодов без разделителя дополнительно создаёт форму с дефисом на
границе букв и цифр.
```php
SearchText::variants('ДБ11');
// содержит: ДБ11, ДБ-11, DB11, DB-11
SearchText::variants('ВИ11');
// содержит DB11 и DB-11 после исправления раскладки
```
Исходный запрос остаётся первым и получает больший поисковый вес. Дополнительные
формы нужны только для расширения совпадений: сохранённый артикул не изменяется.
### `termGroups($text): array`
Разбивает фразу на слова. Варианты одного слова объединяются через «ИЛИ», а
разные слова могут быть обязательными через «И». Для русских слов добавляется
основа без окончания.
---
## Рецепт
## Использование
Не исправляйте строку вручную и не заменяйте исходный запрос. Передайте в
поисковый слой все варианты:
**Автоисправление раскладки перед поиском:**
```php
$q = Request::getStr('q');
if ($q !== '' && !SearchText::containsRussian($q)) {
$fixed = SearchText::toRussian($q);
// искать и по $q, и по $fixed — пользователь мог забыть переключить раскладку
foreach (SearchText::variants(Request::getStr('q')) as $variant) {
// исходный вариант ранжируется выше, остальные расширяют поиск
}
```
+10
View File
@@ -98,6 +98,16 @@ articles/doc_%id
же базовым именем переносится вместе с оригиналом; автоматические миниатюры
будут построены заново по новому URL.
В общем окне выбора медиа пользователь с правом управления может сразу создать
папку и загрузить в неё несколько изображений. После загрузки окно остаётся в
текущей папке и обновляет список, поэтому переходить в раздел медиатеки отдельно
не требуется.
За один выбор можно передать больше 20 файлов. Панель автоматически делит их на
пакеты, совместимые со стандартным ограничением PHP `max_file_uploads`, и
показывает общий прогресс. Это одинаково работает в разделе **Медиа**, общем
окне выбора и медиа-полях документа или товара.
## Автоматическая генерация
Миниатюра создаётся лениво — при первом HTTP-запросе к её URL. До первого
+2
View File
@@ -88,6 +88,7 @@ Manifest с вычисляемым runtime-содержимым объявляе
| [Комментарии](comments.md) | Обсуждения, премодерация, три шаблона, API и антиспам. |
| [Опросы](polls.md) | Одиночные и множественные голосования, архив, теги и API. |
| [Галереи](galleries.md) | Медиаколлекции, сетка, слайдер, миниатюры и шаблонные теги. |
| [Слайдеры](sliders.md) | Визуальные слайды, текст, кнопки, расписание, анимация и публичный тег. |
| [Поиск по сайту](search.md) | Индекс, области, вывод через запросы, JSON/HTML API, живой поиск и хуки. |
| [Подбор товара](quiz.md) | Пошаговый подбор по фильтрам каталога и шаблон публичной страницы. |
| [Публичные виджеты](public-widgets.md) | Курсы валют, подписка и Twig-override активной темы. |
@@ -143,6 +144,7 @@ Manifest с вычисляемым runtime-содержимым объявляе
| `content_packages` | Панель | `view_content_packages`, `export_content_packages`, `import_content_packages` | [Пакеты контента](content-packages.md) |
| `faq` | Паблик + панель | `view_faq`, `manage_faq` | [Вопросы и ответы](faq.md) |
| `galleries` | Паблик + панель | `view_galleries`, `manage_galleries` | [Галереи](galleries.md) |
| `sliders` | Паблик + панель | `view_sliders`, `manage_sliders` | [Слайдеры](sliders.md) |
| `polls` | Паблик + панель | `view_polls`, `manage_polls` | [Опросы](polls.md) |
| `ratings` | Паблик + панель | `view_ratings`, `manage_ratings` | [Рейтинги](ratings.md) |
| `related` | Паблик + панель | `view_related`, `manage_related` | [Похожие материалы](related.md) |
+15
View File
@@ -49,6 +49,21 @@
признак подарка. Отдельный журнал применения позволяет увидеть правило в
карточке заказа даже после последующего изменения или удаления самой акции.
Акция со специальной ценой на конкретный товар автоматически становится
комплектным предложением на публичных страницах обоих товаров. На странице
основного товара покупатель видит подходящий товар и его цену в комплекте. На
странице товара со скидкой показываются основные товары, с которыми действует
эта цена. Кнопка **В корзину** в блоке предложения кладёт обе позиции в корзину; сервер
повторно проверяет действующее правило и только затем применяет скидку.
Блок выводится штатным тегом `[mod_product_bundles]`. Вариант
`[mod_product_bundles:offers]` показывает только акционные пары, а
`[mod_product_bundles:manual]` — только ручные комплекты. Его разметка
редактируется в `Товары -> Шаблоны -> Комплекты товаров`. В шаблоне массив
`promotion_offers` содержит предложения из акций, а `bundles` — комплекты,
созданные вручную в разделе связей товаров. Поэтому оформление можно менять
без правки PHP и без отдельного шаблона темы.
## Журнал онлайн-оплаты
Вкладка **Журнал оплат** показывает всю последовательность работы платёжного
+6
View File
@@ -23,6 +23,12 @@ checkbox, radio, файл, один документ и несколько до
Форму также можно открыть по адресу `/contacts/<alias>`. Служебный адрес
`/callback` использует форму с alias `callback`, если она существует.
Для российского номера добавьте полю телефона атрибуты
`type="tel" inputmode="tel" autocomplete="tel" data-contact-phone-mask`.
Публичная форма приведёт ввод и вставленный номер к виду
`+7 (900) 000-00-00` и не отправит частично заполненный номер. Маска включается
явно, поэтому обычные текстовые поля и международные номера она не изменяет.
## Приём и защита
Публичная отправка проверяет CSRF, обязательные значения, типы полей и загрузки.
+58
View File
@@ -404,6 +404,57 @@ fetch('/api/v1/search?q=' + encodeURIComponent(query) +
одновременного получения структурированных данных и готовой разметки используйте
`format=both`.
## Словари и опечатки
Поиск исправляет только безопасные опечатки в обычных словах. Сначала всегда
выполняется запрос посетителя без изменений. Если он дал результат, корректор не
вмешивается. Если результат пуст, модуль сравнивает слова с заголовками и
дополнительными поисковыми названиями реально проиндексированных документов.
Он понимает ошибочную раскладку, `ё/е`, пропущенную или лишнюю букву и
перестановку соседних букв. Например, `rhjdfng` сначала превращается в
`кроватп`, а затем — в `кровать`. Если два варианта одинаково убедительны,
посетитель увидит ссылки для выбора вместо случайной подмены.
Артикулы, числа и смешанные обозначения корректор не меняет. Например, `DB-11`,
`ДБ11` и `ВИ-11` продолжает обрабатывать алгоритм артикулов, а не словарь
опечаток.
### Общие правила
На вкладке **Словарь** можно явно указать, что вводит посетитель и по какой фразе
нужно искать. Например, `кушетка``массажный стол`. Для правила выбирается
область:
- **Везде** — правило действует во всех разделах;
- конкретная область, например **Товары**, — правило не затрагивает новости и
остальные материалы.
Точное правило заменяет весь запрос. Режим **Входит в запрос** меняет указанную
часть более длинной фразы. Кнопка **Проверить** показывает замену и количество
результатов до сохранения правила. Правила можно выключать, редактировать и
удалять.
### Названия конкретного товара
Если установлен товарный модуль, откройте **Товары → нужный товар → Поиск**.
Здесь добавляются другие названия только этого товара: прежнее название,
разговорный вариант или понятное покупателю слово. Кнопка с плюсом добавляет
строку, корзина удаляет её, после чего нужно нажать **Сохранить**.
После ввода двух символов поле предлагает уже использованные названия товаров и
подходящие слова из общего словаря. Вариант можно выбрать мышью или стрелками и клавишей `Enter`.
Если подходящей подсказки нет, новое название вводится вручную.
Такие названия не выводятся в карточке и не меняют заголовок товара. Они только
добавляются в поисковый индекс. Например, одному складному массажному столу
можно добавить `кушетка-чемодан`, не заставляя все товары каталога реагировать
на эту фразу.
После обновления модуля один раз запустите **Переиндексировать**. Дальше словарь
опечаток обновляется автоматически при обычном сохранении документов, а
поисковые названия товара переиндексируют его сразу после своего сохранения.
Общие понятия и профессиональные термины по-прежнему добавляются вручную на
вкладке **Словарь**: автоматический корректор не пытается угадывать смысл слов.
## Алгоритмы и веса
Штатные стратегии:
@@ -417,6 +468,13 @@ fetch('/api/v1/search?q=' + encodeURIComponent(query) +
Веса точного совпадения, заголовка и содержимого общие для всех областей.
Область выбирает алгоритм, но не меняет эти коэффициенты.
Поиск понимает транслитерацию и неверную раскладку, но буквальное написание
всегда получает больший вес. Например, по запросу `DB-6` сначала выводятся
позиции с `DB-6`, а совпадения по вспомогательному варианту `VI-6` идут ниже.
В артикулах граница букв и цифр распознаётся без введённого разделителя:
`DB11`, `ДБ11` и набранное в неверной раскладке `ВИ11` также находят `DB-11`.
Это только поисковые варианты — значения артикулов в товарах не переписываются.
### Собственная стратегия
Другой модуль может зарегистрировать алгоритм в своём `services.php`:
+94
View File
@@ -0,0 +1,94 @@
# Слайдеры
← [Назад к разделу «Модули»](README.md)
Модуль `sliders` хранит слайдеры и слайды в собственных таблицах. Для баннера
не нужно создавать рубрику или служебный документ: в документах и списках
редактора остаётся только настоящий контент сайта.
## Быстрый запуск
1. Откройте **Модули → Слайдеры** и нажмите **Создать слайдер**.
2. Укажите понятное название и постоянный системный код, например `main`.
3. Сохраните слайдер и добавьте изображения из общей медиатеки.
4. Для каждого слайда настройте текст, ссылки, кнопки, положение и затемнение.
5. Расставьте слайды перетаскиванием за маркер слева.
6. Скопируйте тег и вставьте его в шаблон, блок или содержимое документа:
```text
[mod_slider:main]
```
Вместо кода разрешён числовой ID: `[mod_slider:3]`. Код удобнее, потому что
остаётся понятным после переноса данных между сайтами.
## Что настраивается у слайдера
| Параметр | Что делает |
| --- | --- |
| Переход | Перемещает соседний слайд или плавно растворяет текущий. |
| Высота | Отдельная высота для больших экранов и телефонов. |
| Ширина текста | Ограничивает рабочую область текста, но не ширину изображения. |
| Автопрокрутка | Меняет слайды через заданный интервал. |
| Длительность | Скорость самого перехода. |
| Бесконечный цикл | После последнего слайда возвращается к первому. |
| Пауза при наведении | Даёт посетителю спокойно прочитать текст и нажать кнопку. |
| Клавиатура и свайп | Переключение стрелками клавиатуры и жестом на телефоне. |
| Стрелки и точки | Независимо показывают или скрывают два вида навигации. |
Если в системе включено уменьшение движения, автопрокрутка и декоративная
анимация отключаются автоматически. Стрелки, точки и клавиатура имеют
доступные подписи для программ чтения с экрана.
## Что настраивается у слайда
Основное и мобильное изображения выбираются из медиатеки. Мобильная версия
необязательна: если она не задана, на всех экранах используется основной файл.
Положение фокуса по X и Y помогает оставить важную часть фотографии в кадре.
Если рядом с JPG или PNG лежит WebP-копия с тем же именем, модуль автоматически
добавляет её в `<picture>` как предпочтительный формат. Браузер, который не
поддерживает WebP, продолжит получать исходный JPG или PNG. Для мобильного
изображения действует то же правило.
Первый слайд загружается сразу с высоким приоритетом, потому что он виден при
открытии страницы. Все последующие изображения получают ленивую загрузку и не
задерживают первый экран.
Текстовый слой состоит из надзаголовка, заголовка и краткого текста. Для него
задаются горизонтальное и вертикальное положение, выравнивание, цвет текста,
акцентный цвет и анимация появления. Цвет и процент затемнения отвечают за
читаемость текста поверх светлой фотографии.
У слайда есть:
- ссылка со всей площади слайда;
- главная и дополнительная кнопки со своими подписями, URL и стилями;
- дата начала и завершения показа;
- выключатель публикации;
- дополнительный CSS-класс для оформления в активной теме.
Адрес для всего слайда и обеих кнопок можно написать вручную либо выбрать
документ кнопкой с иконкой поиска. При выборе документа модуль подставляет его
публичный alias, поэтому редактору не нужно искать и копировать адрес страницы.
Ручной ввод остаётся полезен для внешних сайтов, якорей и специальных ссылок.
Пустая кнопка не выводится. Если дата начала ещё не наступила или дата
завершения прошла, слайд остаётся в панели, но исчезает с публичного сайта.
## Оформление в теме
Модуль выводит устойчивые классы с префиксом `.ave-slider`. Тема может менять
шрифты, размеры, кнопки, стрелки и точки обычным CSS, не переписывая данные или
JavaScript. Индивидуальный класс слайда нужен для редких исключений, например
особого расположения текста в одной рекламной кампании.
Модуль подключает `sliders.css` и `sliders.js` один раз, даже если на странице
вставлено несколько слайдеров. Удаление слайда или всего слайдера удаляет только
его записи; исходные изображения в медиатеке сохраняются.
## Расширение
Hook `sliders.rendering` получает массив `slider` и готовый `html`. Его можно
использовать для проектной интеграции, если стандартных настроек и CSS темы
недостаточно. Обычная настройка слайдера hook не требует.
+24 -1
View File
@@ -25,6 +25,11 @@
3. При необходимости зарегистрируйте имя отправителя.
4. Пополните баланс и проверьте, разрешена ли отправка на нужные направления.
Имя отправителя должно состоять из латинских букв и цифр и занимать не более
11 символов либо быть цифровым номером длиной до 15 цифр. Сначала его нужно
зарегистрировать и дождаться допуска в кабинете SMSC. Если поле оставить пустым,
SMSC использует системное имя.
Логин и пароль кабинета в AVE.cms вводить не нужно. Используйте отдельный
API-ключ, который можно отозвать без смены пароля владельца SMSC.
@@ -54,6 +59,7 @@ API-ключ, который можно отозвать без смены па
| Повторная отправка | Минимальная пауза перед новым SMS в текущей сессии. |
| Попыток ввода | Сколько неверных вводов разрешено одному challenge. |
| SMS в сутки | Суточный предел для одного номера. |
| Хранить историю | Сколько дней сохранять журнал отправок и попыток входа. |
Дополнительно ядро ограничивает частоту запросов по IP и номеру. Новый код
отменяет предыдущий, связан с текущей PHP-сессией и может быть использован
@@ -63,6 +69,23 @@ API-ключ, который можно отозвать без смены па
выглядит как успешный запрос, но SMS не отправляется. Это не позволяет перебирать
зарегистрированные телефоны и не расходует баланс на неизвестные номера.
## Баланс и история входов
На вкладке `Настройки` показываются текущий баланс SMSC, состояние подключения
и статус выбранного имени отправителя. Кнопка `Обновить данные SMSC` запрашивает
свежие сведения у сервиса; между проверками результат кешируется на одну минуту.
На отдельной вкладке `История входов` собран журнал. В него попадают отправка и
ошибка отправки SMS, ограничения частоты, неверный,
просроченный или уже использованный код, успешный вход и создание аккаунта. Для
разбора проблем доступны дата, маска и хеш телефона, IP, браузер, пользователь и
ID сообщения SMSC. Сам одноразовый код и полный номер телефона в журнал никогда
не записываются.
Фильтры позволяют найти события по маске телефона, IP или ID SMSC и оставить
только нужное событие или результат. Старые строки удаляются автоматически по
сроку из настройки `Хранить историю`.
## Секреты и переменные окружения
Настройки по умолчанию находятся в закрытом хранилище
@@ -96,7 +119,7 @@ SMSC_SENDER=AVEcms
## Удаление модуля
При удалении стираются настройки SMSC и история незавершённых кодов. Пользователи,
При удалении стираются настройки SMSC, история незавершённых кодов и журнал входов. Пользователи,
подтверждённые телефоны, заказы и профили сохраняются, потому что это общие
данные AVE.cms. Без установленного SMS-провайдера форма входа по телефону просто
не показывается.
+1
View File
@@ -168,6 +168,7 @@
'catalog.product_shipping_saved' => 'Упаковка товара изменена',
'catalog.product_shipping_copied' => 'Упаковка скопирована вариантам',
'catalog.product_shipping_bulk_status' => 'Расчёт доставки у товаров изменён',
'catalog.product_search_aliases_saved' => 'Поисковые названия товара изменены',
'catalog.shipping_template_created' => 'Шаблон упаковки создан',
'catalog.shipping_template_saved' => 'Шаблон упаковки изменён',
'catalog.shipping_template_deleted' => 'Шаблон упаковки удалён',
+1 -1
View File
@@ -55,7 +55,7 @@
'SESSION_LIFETIME' => self::item('_CONST_SESSIONS', 86400, 'int', 'Время жизни сессии в секундах.'),
'COOKIE_LIFETIME' => self::item('_CONST_SESSIONS', 1209600, 'int', 'Время жизни cookie автоматического входа в секундах.'),
'DEV_MODE' => self::item('_CONST_DEV', false, 'bool', 'Режим разработки без SQL-кеша.'),
'PROFILING' => self::item('_CONST_DEV', 'full', 'select', 'Режим публичной панели отладки.', array('off', 'light', 'full', 'dev')),
'PROFILING' => self::item('_CONST_DEV', 'off', 'select', 'Режим публичной панели отладки: off полностью выключает панель.', array('off', 'light', 'full', 'dev')),
'SQL_PROFILING' => self::item('_CONST_DEV', true, 'bool', 'Собирать статистику SQL-запросов.'),
'PHP_DEBUGGING' => self::item('_CONST_DEV', false, 'bool', 'Собирать ошибки PHP средствами framework.'),
'PHP_DEBUGGING_FILE' => self::item('_CONST_DEV', false, 'bool', 'Записывать ошибки PHP в системный журнал.'),
+14 -2
View File
@@ -37,8 +37,8 @@
/**
* Additional top-level menu items are managed by the navigation selected
* in catalog settings. Navigation classes only describe presentation:
* catalog-menu-panel opens a pane, catalog-menu-sale/tcr set an accent.
* in catalog settings. Navigation classes describe presentation and an
* optional catalog-menu-source-* provider for dynamic panel contents.
*/
public static function navigation($purpose = 'commerce', $ttl = 600)
{
@@ -180,6 +180,17 @@
$classes = preg_split('/\s+/', trim(isset($row['css_class']) ? (string) $row['css_class'] : '')) ?: array();
$mode = in_array('catalog-menu-panel', $classes, true) ? 'panel' : 'link';
$tone = '';
$source = '';
foreach ($classes as $class) {
if (strpos($class, 'catalog-menu-source-') !== 0) { continue; }
$candidate = substr($class, strlen('catalog-menu-source-'));
if (preg_match('/^[a-z][a-z0-9_-]{1,31}$/', $candidate)) {
$source = $candidate;
break;
}
}
if ($source !== '') { $mode = 'panel'; }
foreach (array('sale', 'tcr') as $candidate) {
if (in_array('catalog-menu-' . $candidate, $classes, true)) {
$tone = $candidate;
@@ -206,6 +217,7 @@
'image' => trim(isset($row['image']) ? (string) $row['image'] : ''),
'mode' => $mode,
'tone' => $tone,
'source' => $source,
'children' => array(),
);
}
@@ -69,10 +69,25 @@
}
if ($alias === '/') { return null; }
if (self::ownedByDocument($path, $exceptId)) { return null; }
return PublicRouteRegistry::conflict($alias);
}
protected static function ownedByDocument($alias, $documentId)
{
$documentId = (int) $documentId;
if ($documentId <= 0 || $alias === '') { return false; }
return (int) DB::query(
'SELECT COUNT(*) FROM ' . ContentTables::table('documents')
. " WHERE Id = %i AND (TRIM(BOTH '/' FROM document_alias) = %s OR document_short_alias = %s)",
$documentId,
$alias,
$alias
)->getValue() > 0;
}
public static function available($alias, $exceptId = 0)
{
return self::conflict($alias, $exceptId) === null;
@@ -419,15 +419,21 @@
$status = $value('status', 'document_status', 0);
if (is_string($status)) { $status = in_array(strtolower($status), array('published', 'active', '1', 'true'), true) ? 1 : 0; }
$title = trim((string) $value('title', 'document_title', ''));
$parentId = (int) $value('parent_id', 'document_parent', 0);
$alias = DocumentAliasRegistry::normalize($value('alias', 'document_alias', ''));
if (empty($base) && ($alias === '' || strpos($alias, '/') === false)) {
$leaf = $alias !== '' ? $alias : Str::slug($title);
$alias = DocumentAliasTemplate::compose((string) $rubric['rubric_alias'], $leaf !== '' ? $leaf : 'document', $published);
$alias = DocumentAliasTemplate::compose(
(string) $rubric['rubric_alias'],
$leaf !== '' ? $leaf : 'document',
$published,
$this->parentAliasPath($parentId)
);
}
return array(
'rubric_id' => (int) $rubric['Id'], 'rubric_tmpl_id' => (int) $value('template_id', 'rubric_tmpl_id', 0),
'document_parent' => (int) $value('parent_id', 'document_parent', 0), 'document_alias' => DocumentAliasRegistry::normalize($alias),
'document_parent' => $parentId, 'document_alias' => DocumentAliasRegistry::normalize($alias),
'document_alias_header' => in_array((int) $value('redirect_code', 'document_alias_header', 301), array(301,302,307,308), true) ? (int) $value('redirect_code', 'document_alias_header', 301) : 301,
'document_alias_history' => (string) max(0, min(2, (int) $value('alias_history', 'document_alias_history', (int) $rubric['rubric_alias_history']))),
'document_short_alias' => substr(trim((string) $value('short_alias', 'document_short_alias', '')), 0, 10),
@@ -457,6 +463,18 @@
return $errors;
}
protected function parentAliasPath($parentId)
{
$parentId = (int) $parentId;
if ($parentId <= 0) { return ''; }
return trim((string) DB::query(
'SELECT document_alias FROM ' . ContentTables::table('documents')
. " WHERE Id=%i AND document_deleted!='1' LIMIT 1",
$parentId
)->getValue(), '/');
}
protected function writeDocument($id, array $data, array $rubric, $actorId, $existing, $bumpVersion = true)
{
$now = time();
@@ -40,6 +40,7 @@
$states = $conditionsEnabled ? FieldConditionEvaluator::states($fields, $values) : array();
foreach ($fields as $field) {
$field = (array) $field;
if (!empty($field['_condition_source'])) { continue; }
$id = isset($field['Id']) ? (int) $field['Id'] : 0;
if (isset($states[$id]) && empty($states[$id]['visible'])) { continue; }
if (isset($states[$id])) {
@@ -28,6 +28,18 @@
*/
class MediaFieldValue
{
/** Returns the first media URL from native JSON or any supported legacy value. */
public static function firstImageUrl($value)
{
$images = self::imageMega($value);
if (!empty($images[0]['url'])) {
return (string) $images[0]['url'];
}
$image = self::imageSingle($value);
return isset($image['url']) ? (string) $image['url'] : '';
}
public static function imageSingle($value)
{
if (is_array($value)) {
+1
View File
@@ -142,6 +142,7 @@
$user = Auth::publicUser();
Auth::publicLogout();
Auth::logout();
Lifecycle::event('auth.user.logged_out', 'user', 'logged_out', is_array($user) && isset($user['id']) ? (int) $user['id'] : 0, array(
'user' => $user,
), null, array(), 'auth_controller');
@@ -67,6 +67,16 @@
return $row ? (array) $row : null;
}
public function findForSession($publicId, $sessionHash)
{
$row = DB::query(
'SELECT * FROM `' . $this->table . '` WHERE public_id=%s AND session_hash=%s LIMIT 1',
(string) $publicId,
(string) $sessionHash
)->getAssoc();
return $row ? (array) $row : null;
}
public function registerAttempt(array $challenge)
{
DB::query(
@@ -38,6 +38,7 @@
$phone = Phone::normalize($phone);
if ($phone === '') {
$this->recordProviderEvent($provider, 'request_rejected', array('reason' => 'Передан некорректный номер телефона'));
throw new \InvalidArgumentException('Укажите корректный номер телефона.');
}
@@ -47,18 +48,21 @@
if (!empty($options['allow_registration'])
&& PublicAuthSettings::allowsRegistrationMethod('phone', $settings)
&& !$consent) {
$this->recordProviderEvent($provider, 'request_rejected', array('phone' => $phone, 'reason' => 'Не подтверждено согласие с политикой'));
throw new \InvalidArgumentException('Подтвердите согласие с политикой конфиденциальности.');
}
$resendKey = 'phone-auth-resend:' . $this->sessionHash() . ':' . $phoneKey;
if (!RateLimiter::attempt($resendKey, 1, $options['resend_after'])) {
$wait = max(1, RateLimiter::availableIn($resendKey));
$this->recordProviderEvent($provider, 'rate_limited', array('phone' => $phone, 'reason' => 'Повторный запрос раньше срока'));
throw new \InvalidArgumentException('Новый код можно запросить через ' . $wait . ' сек.');
}
if (!RateLimiter::attempt('phone-auth-ip:' . Request::ip(), 10, 900)
|| !RateLimiter::attempt('phone-auth-phone:' . $phoneKey, 3, 900)
|| !RateLimiter::attempt('phone-auth-day:' . $phoneKey, $options['daily_limit'], 86400)) {
$this->recordProviderEvent($provider, 'rate_limited', array('phone' => $phone, 'reason' => 'Превышен лимит запросов'));
throw new \InvalidArgumentException('Слишком много запросов кода. Попробуйте позже.');
}
@@ -79,12 +83,20 @@
);
if ($deliver) {
try {
$provider->sendCode($phone, $code);
$delivery = $provider->sendCode($phone, $code);
$this->recordProviderEvent($provider, 'code_sent', array(
'phone' => $phone,
'challenge' => $publicId,
'smsc_id' => is_array($delivery) && isset($delivery['id']) ? $delivery['id'] : '',
));
} catch (\Throwable $e) {
$this->recordProviderEvent($provider, 'send_failed', array('phone' => $phone, 'challenge' => $publicId, 'reason' => $e->getMessage()));
$repository->deleteByPublicId($publicId);
RateLimiter::clear($resendKey);
throw $e;
}
} else {
$this->recordProviderEvent($provider, 'delivery_skipped', array('phone' => $phone, 'challenge' => $publicId, 'reason' => 'Аккаунт не найден, регистрация выключена'));
}
if (random_int(1, 100) === 1) {
@@ -114,17 +126,41 @@
}
if (!preg_match('/^[a-f0-9]{32}$/', (string) $publicId) || !preg_match('/^[0-9]{4,8}$/', (string) $code)) {
$this->recordProviderEvent($provider, 'code_invalid', array('reason' => 'Некорректный формат кода или запроса'));
throw new \InvalidArgumentException('Проверьте код подтверждения.');
}
$repository = new ChallengeRepository($provider->challengeTable());
$challenge = $repository->findActive($publicId, $this->sessionHash());
if (!$challenge || !$repository->registerAttempt($challenge)
|| !password_verify((string) $code, (string) $challenge['code_hash'])) {
if (!$challenge) {
$previous = $repository->findForSession($publicId, $this->sessionHash());
$event = $previous && (int) $previous['consumed_at'] > 0 ? 'code_used' : 'code_expired';
$this->recordProviderEvent($provider, $event, array(
'phone' => $previous ? $previous['phone'] : '', 'challenge' => $publicId,
'attempts' => $previous ? $previous['attempts'] : 0,
'reason' => $previous ? ($event === 'code_used' ? 'Код уже использован' : 'Срок действия кода истёк') : 'Проверка не найдена',
));
throw new \InvalidArgumentException('Код неверен или срок его действия истёк.');
}
if (!$repository->registerAttempt($challenge)) {
$this->recordProviderEvent($provider, 'code_invalid', array(
'phone' => $challenge['phone'], 'challenge' => $publicId,
'attempts' => (int) $challenge['attempts'], 'reason' => 'Исчерпаны попытки ввода',
));
throw new \InvalidArgumentException('Код неверен или срок его действия истёк.');
}
if (!password_verify((string) $code, (string) $challenge['code_hash'])) {
$this->recordProviderEvent($provider, 'code_invalid', array(
'phone' => $challenge['phone'], 'challenge' => $publicId,
'attempts' => (int) $challenge['attempts'] + 1, 'reason' => 'Введён неверный код',
));
throw new \InvalidArgumentException('Код неверен или срок его действия истёк.');
}
if (!$repository->consume($challenge)) {
$this->recordProviderEvent($provider, 'code_used', array('phone' => $challenge['phone'], 'challenge' => $publicId, 'reason' => 'Код уже использован'));
throw new \InvalidArgumentException('Код уже использован.');
}
@@ -137,6 +173,7 @@
if (empty($options['allow_registration'])
|| !PublicAuthSettings::allowsRegistrationMethod('phone', $settings)
|| empty($challenge['consent'])) {
$this->recordProviderEvent($provider, 'account_missing', array('phone' => $challenge['phone'], 'challenge' => $publicId, 'reason' => 'Регистрация новых аккаунтов недоступна'));
throw new \InvalidArgumentException('Аккаунт с таким телефоном не найден.');
}
@@ -146,6 +183,7 @@
'mode' => 'phone',
), null, array(), 'phone_auth');
if ($registering->cancelled()) {
$this->recordProviderEvent($provider, 'auth_failed', array('phone' => $challenge['phone'], 'challenge' => $publicId, 'reason' => $registering->message()));
throw new \RuntimeException($registering->message() !== '' ? $registering->message() : 'Регистрация отменена модулем.');
}
@@ -155,6 +193,7 @@
} catch (\Throwable $e) {
$user = $users->findByPhone((string) $challenge['phone']);
if (!$user) {
$this->recordProviderEvent($provider, 'auth_failed', array('phone' => $challenge['phone'], 'challenge' => $publicId, 'reason' => $e->getMessage()));
throw $e;
}
}
@@ -167,11 +206,13 @@
}
if ((string) $user['status'] !== '1' || (string) $user['deleted'] === '1') {
$this->recordProviderEvent($provider, 'account_blocked', array('phone' => $challenge['phone'], 'challenge' => $publicId, 'user_id' => $user['Id'], 'reason' => 'Учётная запись отключена или удалена'));
throw new \InvalidArgumentException('Этот аккаунт недоступен.');
}
$users->verifyPhone((int) $user['Id'], (string) $challenge['phone']);
if (!Auth::publicLoginById((int) $user['Id'], !empty($remember))) {
$this->recordProviderEvent($provider, 'auth_failed', array('phone' => $challenge['phone'], 'challenge' => $publicId, 'user_id' => $user['Id'], 'reason' => 'Не удалось открыть пользовательскую сессию'));
throw new \RuntimeException('Не удалось открыть пользовательскую сессию.');
}
@@ -184,6 +225,9 @@
'target_id' => (int) $user['Id'],
'meta' => array('provider' => $provider->code(), 'phone' => Phone::mask((string) $challenge['phone'])),
));
$this->recordProviderEvent($provider, $created ? 'registered' : 'authenticated', array(
'phone' => $challenge['phone'], 'challenge' => $publicId, 'user_id' => $user['Id'],
));
return array(
'user' => $user,
'created' => $created,
@@ -208,4 +252,14 @@
return hash('sha256', (string) $id);
}
protected function recordProviderEvent($provider, $event, array $context = array())
{
if (!is_object($provider) || !method_exists($provider, 'recordAuthEvent')) { return; }
try {
$provider->recordAuthEvent((string) $event, $context);
} catch (\Throwable $e) {
// Журнал провайдера не должен мешать входу пользователя.
}
}
}
@@ -30,6 +30,17 @@
/** Protected, dependency-free profiler injected into public HTML responses. */
class PublicDebugToolbar
{
public static function mode()
{
$mode = defined('PROFILING') ? strtolower(trim((string) PROFILING)) : 'off';
return in_array($mode, array('light', 'full', 'dev'), true) ? $mode : 'off';
}
public static function enabled()
{
return self::mode() !== 'off';
}
public static function boot()
{
if (!self::allowed()) {
@@ -61,10 +72,13 @@
protected static function allowed()
{
if (!self::enabled()) {
return false;
}
$config = PublicConfiguration::all();
$debug = isset($config['debug']) && is_array($config['debug']) ? $config['debug'] : array();
if (empty($debug['enabled'])) { return false; }
$groups = isset($debug['groups']) && is_array($debug['groups']) ? array_map('intval', $debug['groups']) : array(1);
$groups = isset($debug['groups']) && is_array($debug['groups']) ? array_map('intval', $debug['groups']) : array();
$user = Auth::publicUser();
if (is_array($user) && in_array((int) $user['group'], $groups, true)) {
return true;
+23 -4
View File
@@ -87,17 +87,26 @@
return array();
}
$variants = array($text);
$variants[] = self::containsRussian($text) ? self::toLatin($text) : self::toRussian($text);
$semanticVariants = array($text);
$semanticVariants[] = self::containsRussian($text) ? self::toLatin($text) : self::toRussian($text);
$keyboard = self::switchKeyboardLayout($text);
$variants[] = $keyboard;
$semanticVariants[] = $keyboard;
if ($keyboard !== '') {
$variants[] = self::containsRussian($keyboard)
$semanticVariants[] = self::containsRussian($keyboard)
? self::toLatin($keyboard)
: self::toRussian($keyboard);
}
$variants = array();
foreach (self::unique($semanticVariants) as $variant) {
$variants[] = $variant;
$separated = self::separateIdentifierParts($variant);
if ($separated !== $variant) {
$variants[] = $separated;
}
}
return self::unique($variants);
}
@@ -167,6 +176,16 @@
return $result;
}
/** Adds a conventional dash at letter/number boundaries in model codes. */
protected static function separateIdentifierParts($value)
{
return (string) preg_replace(
array('/(?<=\p{L})(?=\p{N})/u', '/(?<=\p{N})(?=\p{L})/u'),
'-',
(string) $value
);
}
protected static function englishKeyboardMap()
{
static $map = null;
+1 -1
View File
@@ -17,5 +17,5 @@
return array(
'product' => 'AVE.cms',
'version' => '3.3',
'build' => '0.38',
'build' => '0.39',
);
+9 -1
View File
@@ -1 +1,9 @@
Deny from all
Options -Indexes
<IfModule mod_authz_core.c>
Require all denied
</IfModule>
<IfModule !mod_authz_core.c>
Order allow,deny
Deny from all
</IfModule>