mirror of
https://github.com/avecms/AVE.cms.git
synced 2026-08-26 03:11:51 +00:00
release: AVE.cms 3.3 build 0.39
This commit is contained in:
+46
-1
@@ -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
@@ -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",
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
return [
|
||||
'code' => 'database',
|
||||
'name' => 'База данных',
|
||||
'version' => '0.2.0',
|
||||
'version' => '0.2.2',
|
||||
|
||||
'permissions' => [
|
||||
'key' => 'database',
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
return array(
|
||||
'code' => 'media',
|
||||
'name' => 'Медиа',
|
||||
'version' => '0.6.2',
|
||||
'version' => '0.6.4',
|
||||
|
||||
'permissions' => array(
|
||||
'key' => 'media',
|
||||
|
||||
@@ -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'),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
return array(
|
||||
'code' => 'themes',
|
||||
'name' => 'Темы',
|
||||
'version' => '0.2.0',
|
||||
'version' => '0.2.1',
|
||||
|
||||
'permissions' => array(
|
||||
'key' => 'themes',
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
// Дополнительные домены, разрешённые помимо PUBLIC_SITE_URL.
|
||||
'allowed_hosts' => array(),
|
||||
'debug' => array(
|
||||
'enabled' => true,
|
||||
'groups' => array(1),
|
||||
),
|
||||
);
|
||||
|
||||
+44
-1
@@ -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]` только для ручных комплектов. Например,
|
||||
компактное предложение матраса можно поставить под блоком покупки кровати, а
|
||||
большой комплект из нескольких карточек оставить ниже описания.
|
||||
|
||||
Связанные товары используют те же центральные карточки, что каталог. Кнопка
|
||||
комплекта добавляет каждую позицию в обычную корзину с текущей ценой. Если товар
|
||||
выключен, удалён или у него не настроено поле цены, недоступный комплект не
|
||||
|
||||
@@ -29,6 +29,23 @@
|
||||
Уровень определяется положением пункта в дереве. Перетаскивание меняет родителя,
|
||||
уровень и порядок; отдельное числовое поле позиции для этого не требуется.
|
||||
|
||||
### Правая часть товарного каталога
|
||||
|
||||
Если навигация выбрана в настройках товарного каталога, у её пунктов появляется
|
||||
поле **«Поведение в меню каталога»**:
|
||||
|
||||
- **Обычная ссылка** сразу открывает назначенный документ или URL.
|
||||
- **Показать справа дочерние пункты** раскрывает вручную собранные подпункты.
|
||||
- **Разделы с товарами по акции** автоматически показывает непустые разделы,
|
||||
где текущая цена ниже старой.
|
||||
- **Разделы с товарами СФР** автоматически показывает непустые разделы с
|
||||
товарами, для которых включена программа СФР.
|
||||
|
||||
Динамические варианты читают товарный индекс и не требуют вручную поддерживать
|
||||
дочерние ссылки. Порядок разделов берётся из дерева каталога. Отключённые,
|
||||
удалённые и скрытые товары в расчёт не входят. Общая ссылка пункта сохраняется:
|
||||
она доступна в правой панели как переход ко всем акциям или к странице СФР.
|
||||
|
||||
## Шаблоны уровней
|
||||
|
||||
Рендер выполняется в следующем порядке:
|
||||
|
||||
@@ -72,6 +72,42 @@
|
||||
Блок **Редакторы компонентов** содержит быстрые переходы в штатные разделы.
|
||||
Содержимое и настройки следует менять именно там.
|
||||
|
||||
## Вкладка «Карта шаблонов»
|
||||
|
||||
Эта вкладка отвечает на практический вопрос: **где менять конкретную часть
|
||||
публичной страницы**. Она читает настройки текущей установки и показывает их,
|
||||
но сама ничего не сохраняет.
|
||||
|
||||
В верхней части указан действующий способ сборки:
|
||||
|
||||
- **Штатные шаблоны AVE.cms** — общий шаблон сайта и шаблон рубрики собирают
|
||||
страницу, а активная тема подключает стили и переоформляет только отдельные
|
||||
публичные компоненты;
|
||||
- **Оболочка темы** — внешний каркас страницы формирует указанный Twig-файл
|
||||
темы; использование шаблонов рубрики зависит от настройки наследования.
|
||||
|
||||
Ниже приведены готовые цепочки для обычного документа, списка материалов,
|
||||
раздела каталога, товарной карточки и полной страницы товара. Каждый шаг имеет
|
||||
кнопку перехода в штатный редактор. Например:
|
||||
|
||||
```text
|
||||
товар
|
||||
→ опубликованное представление карточки
|
||||
→ резервный Twig-компонент, если представление не назначено
|
||||
→ товарная выдача
|
||||
→ шаблон рубрики
|
||||
→ общий шаблон сайта
|
||||
```
|
||||
|
||||
Таблица рубрик показывает назначенный общий шаблон, наличие основного и
|
||||
дополнительных шаблонов документа. Блок компонентов показывает фактический
|
||||
источник Twig-разметки: активную тему, системный резерв или отсутствующий файл.
|
||||
|
||||
Важно: файловый компонент темы не является самостоятельной страницей. Он
|
||||
работает только тогда, когда его вызывает шаблон рубрики, запрос, блок или
|
||||
модуль. Поэтому содержимое документа следует хранить в полях документа, а не в
|
||||
Twig-файле темы.
|
||||
|
||||
## Что такое представление
|
||||
|
||||
**Представление** — это необязательный слой оформления уже подготовленных
|
||||
|
||||
@@ -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. Убедитесь, что вход в панель управления выполнен в том же браузере и на том
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
// исходный вариант ранжируется выше, остальные расширяют поиск
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -98,6 +98,16 @@ articles/doc_%id
|
||||
же базовым именем переносится вместе с оригиналом; автоматические миниатюры
|
||||
будут построены заново по новому URL.
|
||||
|
||||
В общем окне выбора медиа пользователь с правом управления может сразу создать
|
||||
папку и загрузить в неё несколько изображений. После загрузки окно остаётся в
|
||||
текущей папке и обновляет список, поэтому переходить в раздел медиатеки отдельно
|
||||
не требуется.
|
||||
|
||||
За один выбор можно передать больше 20 файлов. Панель автоматически делит их на
|
||||
пакеты, совместимые со стандартным ограничением PHP `max_file_uploads`, и
|
||||
показывает общий прогресс. Это одинаково работает в разделе **Медиа**, общем
|
||||
окне выбора и медиа-полях документа или товара.
|
||||
|
||||
## Автоматическая генерация
|
||||
|
||||
Миниатюра создаётся лениво — при первом HTTP-запросе к её URL. До первого
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -49,6 +49,21 @@
|
||||
признак подарка. Отдельный журнал применения позволяет увидеть правило в
|
||||
карточке заказа даже после последующего изменения или удаления самой акции.
|
||||
|
||||
Акция со специальной ценой на конкретный товар автоматически становится
|
||||
комплектным предложением на публичных страницах обоих товаров. На странице
|
||||
основного товара покупатель видит подходящий товар и его цену в комплекте. На
|
||||
странице товара со скидкой показываются основные товары, с которыми действует
|
||||
эта цена. Кнопка **В корзину** в блоке предложения кладёт обе позиции в корзину; сервер
|
||||
повторно проверяет действующее правило и только затем применяет скидку.
|
||||
|
||||
Блок выводится штатным тегом `[mod_product_bundles]`. Вариант
|
||||
`[mod_product_bundles:offers]` показывает только акционные пары, а
|
||||
`[mod_product_bundles:manual]` — только ручные комплекты. Его разметка
|
||||
редактируется в `Товары -> Шаблоны -> Комплекты товаров`. В шаблоне массив
|
||||
`promotion_offers` содержит предложения из акций, а `bundles` — комплекты,
|
||||
созданные вручную в разделе связей товаров. Поэтому оформление можно менять
|
||||
без правки PHP и без отдельного шаблона темы.
|
||||
|
||||
## Журнал онлайн-оплаты
|
||||
|
||||
Вкладка **Журнал оплат** показывает всю последовательность работы платёжного
|
||||
|
||||
@@ -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, обязательные значения, типы полей и загрузки.
|
||||
|
||||
@@ -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`:
|
||||
|
||||
@@ -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 не требует.
|
||||
@@ -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-провайдера форма входа по телефону просто
|
||||
не показывается.
|
||||
|
||||
@@ -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' => 'Шаблон упаковки удалён',
|
||||
|
||||
@@ -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 в системный журнал.'),
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -17,5 +17,5 @@
|
||||
return array(
|
||||
'product' => 'AVE.cms',
|
||||
'version' => '3.3',
|
||||
'build' => '0.38',
|
||||
'build' => '0.39',
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user