mirror of
https://github.com/avecms/AVE.cms.git
synced 2026-08-01 00:45:44 +00:00
AVE.cms 3.3 build 0.37
This commit is contained in:
+43
-35
@@ -2,15 +2,15 @@
|
||||
"format": "ave-core-package-v2",
|
||||
"product": "AVE.cms",
|
||||
"version": "3.3",
|
||||
"build": "0.35",
|
||||
"build": "0.37",
|
||||
"package": "core",
|
||||
"built_at": "2026-07-30T08:43:49+00:00",
|
||||
"built_at": "2026-07-30T13:19:22+00:00",
|
||||
"installed_modules": 0,
|
||||
"bundled_optional_modules": [],
|
||||
"optional_after_install": [
|
||||
"setup/"
|
||||
],
|
||||
"core_fingerprint": "713dd207dba2d9f7242b30de3fd39a08a2b523cabd615a69873c3b3c48a133ea",
|
||||
"core_fingerprint": "ad00f4c5d033c613ea66b4bc56a4b5de126abdf00d79eaa64b0b1bbd1855d0da",
|
||||
"files": {
|
||||
".gitignore": "9aa78fb183d01611e3ce9789d244d09ffb862c6ae718438b2132a7edd53a16ec",
|
||||
".htaccess": "71c06b7bc41b09c347a562ca72768acaa0ee1ef8388c749fc67956c793d8a826",
|
||||
@@ -183,9 +183,9 @@
|
||||
"adminx/modules/Blocks/view/index.twig": "39f9225b6a95c65549d0c861afb7db7879ca338c251290653dded6ab7cb715cc",
|
||||
"adminx/modules/Catalog/Controller.php": "996c5454307133d98a1334584d917b2157f62c2fdc0812683fab93b7ab7f5988",
|
||||
"adminx/modules/Catalog/Model.php": "e7c7cffd91754e04dac322c31a64b420739c671079a7f8e7effb550f5019ee13",
|
||||
"adminx/modules/Catalog/assets/catalog-drawer.css": "c16f11497fafb379db09823b25234f06735341f5f4c8f54026b96de689d894dc",
|
||||
"adminx/modules/Catalog/assets/catalog-drawer.css": "b3e4f798d7bdf329aa91987ae5be435910ddccfeb8a32211059846ab9dc6b7c4",
|
||||
"adminx/modules/Catalog/assets/catalog.css": "49e7a33fc444f8330806e1cc5cbb0d523b673c64faafd25dbf0fe33067643efd",
|
||||
"adminx/modules/Catalog/assets/catalog.js": "24c1be107fdc88889a0b537b3cc93b32dcf0d83bfe97fefdf9bf10012a132b31",
|
||||
"adminx/modules/Catalog/assets/catalog.js": "1fc4acc30b05fd702dee9ff62958141aeab7593ca38d6f6360567af7c3dc9742",
|
||||
"adminx/modules/Catalog/language/en/client.xml": "9b156a155a1e6f1a2024f4be5ab8d1051b1ec2adf4b9a0ede71afe11e6d4558f",
|
||||
"adminx/modules/Catalog/language/en/interface.xml": "1007d3fb9158846dd36f519d13909502e16d3c0a9ca2c49dc24bec088c487ea7",
|
||||
"adminx/modules/Catalog/language/en/runtime.xml": "f30a0b64665a64de1682251402c720bc9e526138f682c5dd2e9fbf3ffb87f2b0",
|
||||
@@ -194,7 +194,7 @@
|
||||
"adminx/modules/Catalog/language/ru/runtime.xml": "fe59945f4ceb2bf2d85699623369e7e5889613b8cecd8551a390c78023531d14",
|
||||
"adminx/modules/Catalog/migrations/005_add_catalog_purpose.sql": "006ff237e25042a4fa9b9f74d5079741eeee1aa52db0cd996ea4c8b817e6700f",
|
||||
"adminx/modules/Catalog/module.php": "6d8b0d0bc67d314c3aee6f5813c36d32b6f4bc0de884515e13b059927c3aa9ce",
|
||||
"adminx/modules/Catalog/view/edit.twig": "eb428848b39c2672c5c25dcd04cece8325230c4b6ed15a3064bd332f31262899",
|
||||
"adminx/modules/Catalog/view/edit.twig": "7fa6f39eec641b23f2191a9df2186a4d4855e50d4b1cc506118a94313cf481a7",
|
||||
"adminx/modules/Catalog/view/index.twig": "754c7c4dc81176e8c99b3d9e8c1fda2f62f03df341c3df1f7393f72d43c6f8e2",
|
||||
"adminx/modules/Console/Controller.php": "af0309a50d7f4366279be8ea12959cf7718a3c68b895a65ec5846554fd6a4857",
|
||||
"adminx/modules/Console/Model.php": "53aad480b8d4e5d68c4e9345d219ea5cbff6d04eee7ce4aa15deab6f00dac1cc",
|
||||
@@ -210,10 +210,10 @@
|
||||
"adminx/modules/Console/migrations/001_create_console_snippets.sql": "70272e04d744eca3d48416d9ffc83d78ad00a02d2a6f68bc388618e350c427e5",
|
||||
"adminx/modules/Console/module.php": "1ff91297cbdf4a9cf0f83262f44b720d52ff235e59918b0f45544d26a079eff1",
|
||||
"adminx/modules/Console/view/index.twig": "c64fe9632db751357ed98cf2ddca524001e71200297e1b50f0d5c827483933af",
|
||||
"adminx/modules/Customers/Controller.php": "1aebb30a2d85343ace1719fca451a105cfe9e79cd3fcde6dbf8b6d907c2dee23",
|
||||
"adminx/modules/Customers/Model.php": "df097775eacfe0aac5c3b7ad4fdab7ff0816f57b77f94ed7993acd221725a3e6",
|
||||
"adminx/modules/Customers/assets/customers.css": "fdbd5c97456751080d134f8a97fc85b116098c7a29c4933600ac23fd0e33adbf",
|
||||
"adminx/modules/Customers/assets/customers.js": "babfd39619f4a45717e98b15dc3ec29976f7206a805880b0735c10a5de601b6a",
|
||||
"adminx/modules/Customers/Controller.php": "ccb32fb836387ab694e74607867ace0274d4bbdcc44566a5bcd2e7d6f1b2c75a",
|
||||
"adminx/modules/Customers/Model.php": "baa3362be7a06c222f4ebe35862500f69a16785234330b886485e73aee0cfa81",
|
||||
"adminx/modules/Customers/assets/customers.css": "62e0d5f0430079909cf5b77a761dd4983cd7f2179124257b180e94ff87fd3718",
|
||||
"adminx/modules/Customers/assets/customers.js": "08399baf41e7762979f54c561b32beebd26ab544cd7bdce70de1079859f0967c",
|
||||
"adminx/modules/Customers/language/en/client.xml": "21310e86bb84cc1fbaf65fad3f09bb4d058e820d919a9af6ae5e2700c2e8d1b7",
|
||||
"adminx/modules/Customers/language/en/interface.xml": "573a7ca126beeaff77a8865d6114dac0429b588d8061f939c5ed7af316d4070b",
|
||||
"adminx/modules/Customers/language/en/runtime.xml": "c9e6e3c654f97f0a7d047af51e07958a27da21659635f1a04d463070d4a112a3",
|
||||
@@ -226,8 +226,8 @@
|
||||
"adminx/modules/Customers/migrations/004_materialize_public_auth_schema.sql": "682fd96a76d21e8420c938378832f00691fce80963a798168827995234f765cd",
|
||||
"adminx/modules/Customers/migrations/005_checkout_registration.sql": "4e5ac6d4d5868f9e1c91b896fc03ad0982d5716287101e721a3820d21abe1744",
|
||||
"adminx/modules/Customers/migrations/006_phone_identity.php": "97e8aa457fd8f8ade781a03a98bdc7ee03cac5734782f9e72fb0b76bdbd2cdbb",
|
||||
"adminx/modules/Customers/module.php": "ecdbc7004a9096bccf8064c6ac2fc78262966dfa74e463069edd21b47f6c50b4",
|
||||
"adminx/modules/Customers/view/index.twig": "5f19c30e03367b0bcbc83256d727eb19ee045ab3475521531faf486a4d446fc4",
|
||||
"adminx/modules/Customers/module.php": "4db18f68c747765353d6d747ce43e3095067de8e6f76d06c44902e7f4c19c167",
|
||||
"adminx/modules/Customers/view/index.twig": "bd116c06b95df1a79b08654974d8133b84963f84fdecc3b90d7579221a120413",
|
||||
"adminx/modules/Dashboard/Controller.php": "7e6ad0610df7581bb010cee574d8d093c4737af439bd2b0763142f50ddee1c54",
|
||||
"adminx/modules/Dashboard/Widgets.php": "56fc7ba7c1f9e30d7eae8c61524b6441b1c706a2cd56b1db883bb8bff7186eed",
|
||||
"adminx/modules/Dashboard/assets/dashboard.css": "8e28b56cac2e55d9b57304ea6e265fd94a52400674c082fbdd78f1eed1872424",
|
||||
@@ -321,7 +321,7 @@
|
||||
"adminx/modules/Events/module.php": "d506c314dfc71abe7216ec80243f46549a833a6cc3371333d1e7a6435312c46b",
|
||||
"adminx/modules/Events/view/index.twig": "7057cee5abe6ace177509c54c2d1736ab88b96f9f17f6db2bec0553b4eb1ac5f",
|
||||
"adminx/modules/GlobalSearch/Controller.php": "6a109bbe2ca833df90138af056cb6cc32c313677d3bda071b3638448ca7e33fa",
|
||||
"adminx/modules/GlobalSearch/Model.php": "5cce4bc4358c2b7b0f54e76f77140470dc7d6413176f37e97808553f11aeefcb",
|
||||
"adminx/modules/GlobalSearch/Model.php": "b81c82cc43c6f90e411645b3f934b42d7457eaa7eea1f5afd7c8efa362f82d97",
|
||||
"adminx/modules/GlobalSearch/module.php": "b7b5d9c9179500edbbb82a7ca0583374ac2a13bc8aa3f2eca5c6b9242d6ddbeb",
|
||||
"adminx/modules/Groups/Controller.php": "37959bbc4cf73ea2968616c195763fbd1bbe14cea90c01d4dae5fad2970803d6",
|
||||
"adminx/modules/Groups/Model.php": "889ba3e4c93ee59ddd6515f4fa4b5ce4c82335e91b3dc6eb315dd095ce6f8fa2",
|
||||
@@ -342,19 +342,21 @@
|
||||
"adminx/modules/Groups/migrations/006_normalize_control_panel_permission.sql": "eb58f4d6a59efe656359d003d81f995e989393d7235a3fae7e38c0733fe174fc",
|
||||
"adminx/modules/Groups/module.php": "4f1d59f9aa351b18b111396d74001597ffefa744cd55240c7339fee06f009542",
|
||||
"adminx/modules/Groups/view/list.twig": "48949134b29f8c2f62bef3d90e1438131e5dbc53cba84214e11e5cbed10040ed",
|
||||
"adminx/modules/Media/Controller.php": "961186cbc05fdefae407d7af92075bc7871415769cd40eba7aebc5b6f52fb762",
|
||||
"adminx/modules/Media/Model.php": "c23ffb325ba7d3f86b1088778c24ebc6860047fc19ba038fc061a10d121d2c0e",
|
||||
"adminx/modules/Media/assets/media.css": "277e91e70607ef85c2a71a0a2d1598cfc3c87a70e0a3f45b73548397fef74278",
|
||||
"adminx/modules/Media/assets/media.js": "1fac7fee094cef4f4726f92fd966b8fc02bfe7cbae960e75d9fc7cef901d2d64",
|
||||
"adminx/modules/Media/Controller.php": "a2aef03585bf4144d60821ef2a4ed075a1393bbdfd7d28da672cad3dd67584d5",
|
||||
"adminx/modules/Media/ImagePresets.php": "8874cbf436a1a33f655c9a395b5f16e143c04c7df1d957c43e3d4ce3ae0645d5",
|
||||
"adminx/modules/Media/Model.php": "657f2be87d6639d541d2494ccde70e8e5cf69e6ee7f798b1c92ea9f8364f9d30",
|
||||
"adminx/modules/Media/assets/media.css": "df70c5a04119c46ea39537aea6b08abae381fbf56e367e82e0223d9d5efe9bb7",
|
||||
"adminx/modules/Media/assets/media.js": "3364bd7ea9353072cd9067111f17bd11d7cb717469555bd821dfc7fcdde5f4bb",
|
||||
"adminx/modules/Media/language/en/client.xml": "06cecdb6cacdd0871334a3da0a3c7be27d7f835e2a048d37ea31ae4f37469270",
|
||||
"adminx/modules/Media/language/en/interface.xml": "2eab0a0d6acf094c43c690e4c09683378a26dfaec7e258c23f3f7bfb1fc9e9a7",
|
||||
"adminx/modules/Media/language/en/runtime.xml": "8768433d736f0e8b3508b1a68de1281b2f841c2a9decbd8195603a31bff3a92f",
|
||||
"adminx/modules/Media/language/ru/client.xml": "272d1eddafc77c79533be4fa89d5daa2d560c1860bc2bab4a1fbf6d609849768",
|
||||
"adminx/modules/Media/language/ru/interface.xml": "743b1b2e5f498ce05225aca69f866abe2f24765a260baa9d81c1947954029fa9",
|
||||
"adminx/modules/Media/language/ru/runtime.xml": "538db360abf2d61e32d6476846173ea4f5115703021362314a9100df3854d6f7",
|
||||
"adminx/modules/Media/module.php": "6e5b38b7da0d2fd506900af70a8813162e3e6215fe403e969ab914a9c20ced1e",
|
||||
"adminx/modules/Media/view/file.twig": "c338f333ac0424c224e5d73d1e3ac1854000155f3a06d6137992660bb2f407b2",
|
||||
"adminx/modules/Media/view/index.twig": "d718bcac68d2de3046c4d6a551dab38577d5a9a8ee6a9f8733a2a8c640531bf9",
|
||||
"adminx/modules/Media/migrations/001_image_presets.sql": "303dec5f331135ab3fda9d264b7a7b674b987cdefa0a7904c544a5d63554f15a",
|
||||
"adminx/modules/Media/module.php": "97131d5f990b1eb43fa69b0f255ffaf993b03bb392092fa558dc498f156c75e9",
|
||||
"adminx/modules/Media/view/file.twig": "0fedbf7fb3d7b36ff9dd717b73b5d5077e9d101a8a3b5ba5f44db64bfc0394f8",
|
||||
"adminx/modules/Media/view/index.twig": "a834a9ce896c5b3d633f5b9a28de360f3a57efd14afcfd7957b0b02363ea65a1",
|
||||
"adminx/modules/Modules/Controller.php": "de3c800b32a05ab41dac31d4964242fb0635aef6ef32f69a332509159b8e755d",
|
||||
"adminx/modules/Modules/ModuleArchiveInstaller.php": "22f015a6695de9f4fea12a580adfb76837e59ee4e9de92c153782c5a40a97560",
|
||||
"adminx/modules/Modules/ModuleRepository.php": "651efb2eb73beefd4d486761d0a554d45725f58aab47bcb6e88d7d61a0b8b87c",
|
||||
@@ -430,7 +432,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": "3dcece2c975460c72c3da2e60a9481abc4a528b58e128fbaa52106c48215d336",
|
||||
"adminx/modules/Rubrics/Controller.php": "f3895259809f9b536fbe8d827dec5c3ea2909133ea7b98391bdde6b999c223bf",
|
||||
"adminx/modules/Rubrics/FieldAdapter.php": "edc4b730499111917360670632573b8d28cb7c192f71e88e8c60c7c8c3e981d1",
|
||||
"adminx/modules/Rubrics/FieldAdminEditors.php": "d3bb0a8cd7e4b44db93b780ec95c49d3974bb3fc3167110a2f8ef044e921c348",
|
||||
"adminx/modules/Rubrics/FieldConditionImpact.php": "77600152ef7e6a98cbfcdff1bc638aa6b1a664048bc2b839188b56a1ebf0a735",
|
||||
@@ -442,15 +444,16 @@
|
||||
"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": "ecbf5c3d8d91c3979866c8675754572f40947a72d262713809a01b5ba42e7982",
|
||||
"adminx/modules/Rubrics/Model.php": "97007d19f651bf8fce9599d542bace4ed4408bc0f65407383ba5589b8b7eaa48",
|
||||
"adminx/modules/Rubrics/RubricFieldPresets.php": "80109bd9f066f077ad19e48d7c7599e4dbf7b1d0c06c54db9fc921992bca2e77",
|
||||
"adminx/modules/Rubrics/RubricFieldSetLinks.php": "41cd27e5947680eb184dafba5fd95f707ccad0c20fab2ca948cf882ada0adefe",
|
||||
"adminx/modules/Rubrics/RubricRevisions.php": "084118e4b55b077244bacc7916674a26b3cbe86a08d2e12084dc05f2798b2090",
|
||||
"adminx/modules/Rubrics/RubricRevisions.php": "83d3659ae0b1d2ce75fe02b0ebfc11d04857556ff352dc2b5408a1481291c658",
|
||||
"adminx/modules/Rubrics/RubricSchemaImpact.php": "e82f93175f031d5772d9d8fd71541748e505273d1622027615abb80e3a97bd5f",
|
||||
"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": "fbf07a777744d18d3653c00ca2e1177f4fe685f2b07b00e3bdf251db5ea1273d",
|
||||
"adminx/modules/Rubrics/assets/rubrics.js": "d23435b0c05cc15e3fb6a18e5e1e7cdeabb8d3595f5437a6ea95a4e066408113",
|
||||
"adminx/modules/Rubrics/assets/rubrics.css": "98ec76c4301d02bdfef75de9af85e3ae493bfb3c491827b2a9c6e2552fdcae10",
|
||||
"adminx/modules/Rubrics/assets/rubrics.js": "38ddc91374036bf9aa12cc55abd5ee412b829a729379ed7f9727d84bc12be84d",
|
||||
"adminx/modules/Rubrics/language/en/interface.xml": "e9f7ec72f954e35c81838e982ae58580ee0922b0a34b1885119db931366945ba",
|
||||
"adminx/modules/Rubrics/language/en/runtime.xml": "4cad3d97c6a2695d038286cedc2c4509230c3bc9fa789dba2da9db89e8d9c93d",
|
||||
"adminx/modules/Rubrics/language/ru/interface.xml": "24df4dc3e08f8ac35d261c269e618c4b613eac6585493c58fa180a945016223e",
|
||||
@@ -470,10 +473,11 @@
|
||||
"adminx/modules/Rubrics/migrations/013_group_form_conditions.php": "86544b9f2304b2c75917fe49b33da255c7677aedcbc9b76fbe13f9cd65568695",
|
||||
"adminx/modules/Rubrics/migrations/014_rubric_purpose.php": "2913a1a7bb6f4c5665ad05704dcbbde78c7ab6b75b0ad7643fd00e482af69411",
|
||||
"adminx/modules/Rubrics/migrations/015_merge_directory_permissions.php": "648b3e232e9426f84d69238295515342e42695e46288b901950cde2e8979b0e6",
|
||||
"adminx/modules/Rubrics/module.php": "74392f60d8d43cf91fff4d49faa54f0792eece8949f11c51a5697c0329b6cc9b",
|
||||
"adminx/modules/Rubrics/migrations/016_rubric_trash.sql": "09385e8d14d5d4d977db59c60035338404019783cc44c6c784cc2172faab08f0",
|
||||
"adminx/modules/Rubrics/module.php": "3fbfbc6f5c3919d76b1055e505bac915798743d6dbbbf1fd178ef9d42d07ba77",
|
||||
"adminx/modules/Rubrics/view/_content_tabs.twig": "e4c0a9b899b5085aeaec2e1793b8eeac4a7619959fffc7ce39151af1250e0d9f",
|
||||
"adminx/modules/Rubrics/view/field-sets.twig": "911dd7cc9546411e1b29724466328c7d2db8db630b2b52d33e8443a066b16810",
|
||||
"adminx/modules/Rubrics/view/index.twig": "4e87c3457cd6f26df601a7e13f701e16227a36034eadec40fec002f92ffc9ce6",
|
||||
"adminx/modules/Rubrics/view/index.twig": "5b715531de7a7aaad85b92657a2b22a1f16bafbf8198a62fe2175fecac83bce5",
|
||||
"adminx/modules/Security/Controller.php": "c7f2263f460057191402dacf8948b822f9b33bbacbe94e6e9ad871613147fc86",
|
||||
"adminx/modules/Security/assets/security.css": "fd8c477d07c38450587153402380040b72fa9a92c60edd532c4b21046cfe375a",
|
||||
"adminx/modules/Security/assets/security.js": "f270dd0b910b9aa4815680b8d80d66ffb31bc7862f74e628cc60efba4205a99b",
|
||||
@@ -527,7 +531,7 @@
|
||||
"adminx/modules/Themes/Controller.php": "85af532e079d799c0bad87921b99587c946fe92ec073b4016eea650e5c4ef12d",
|
||||
"adminx/modules/Themes/Model.php": "e291c6d4bbd964e7086180e7329df2867e2748c8f007596a95c88a954159e320",
|
||||
"adminx/modules/Themes/Revisions.php": "04ee9814b4f0f329ad9c1983333f5f923343bd0179d68897e21102dfea97c9df",
|
||||
"adminx/modules/Themes/assets/themes.css": "e4346dcd646702a204a59339b9d6d8ace4be59337343a4b12d72c857a780103c",
|
||||
"adminx/modules/Themes/assets/themes.css": "c54dd5451693f5117a22c9b7d7a742847e6c6d97a7700d38dd28111337392d38",
|
||||
"adminx/modules/Themes/assets/themes.js": "6c1b2346f475f97299b8657ca3d867293680c40df929ca467c4c30dc81d76c0e",
|
||||
"adminx/modules/Themes/language/en/client.xml": "5f9d0a1ca7320e8590468e626433fff92b55cb9ca38d8723d4a363feb77dc3b5",
|
||||
"adminx/modules/Themes/language/en/interface.xml": "30365f6c02c048fa2d0e94feb82bda45bdf8cc9fa665a7943593bb60636f368a",
|
||||
@@ -551,7 +555,7 @@
|
||||
"adminx/modules/Updates/module.php": "c26612bb4383e4433543a9e822bc18360b57815af67875b29434bfdc8e263dd2",
|
||||
"adminx/modules/Updates/view/index.twig": "8d975051ca96fd99902714bd60b6a85131d6304578ea65a3dbf732523b638ece",
|
||||
"adminx/modules/Users/Controller.php": "ed5728b5794d250fcbec2e2bc83bf7812a4279e028949b2373ed83f7873d72d3",
|
||||
"adminx/modules/Users/Model.php": "9937eca84e631dbb3e256b7ab100d6120ddc853e66a065218377a33b303f2241",
|
||||
"adminx/modules/Users/Model.php": "7e30cd6dacb7ef7b7f619f0e888d84cc8ffa470b6e76cac8cb6049aa13d89326",
|
||||
"adminx/modules/Users/assets/users.css": "19b53f5a235b79b5ff34576692ace47cab69e85586958f763118f59a07b2920d",
|
||||
"adminx/modules/Users/assets/users.js": "a9d7bd3b5b2e44988ac1c5d270d44f896b7cf1796608961caa0d8129508d11bb",
|
||||
"adminx/modules/Users/language/en/client.xml": "d1d17febaf2fbc74243f0e799bae71b32c656ceed81da7fc62f13dbf73ca7b4e",
|
||||
@@ -563,7 +567,7 @@
|
||||
"adminx/modules/Users/migrations/001_normalize_public_session_activity.sql": "e72f1cacea2cff5f74f7baebe3cb1f154b87fd7368541b1a3a5d2300a0deae86",
|
||||
"adminx/modules/Users/migrations/002_harden_public_remember_tokens.php": "85c92acd3ed962dfc4bf85b78e07de87569b1f0aacb2c616287c95bb39fbee2a",
|
||||
"adminx/modules/Users/module.php": "81c13d81fd73878aaa372590b0ed5f2a8c19ec59d409c5199a304271b1011746",
|
||||
"adminx/modules/Users/view/list.twig": "231abe6ddb6324a7573f2da75e1a75e29dd7dfbd4ed010a3b38a5cfa313d8ae2",
|
||||
"adminx/modules/Users/view/list.twig": "b64d5ee4c2e680a3d49dfbd2efe17c7da9c057746fd3b0884b4bb536cacc363e",
|
||||
"adminx/view/404.twig": "86f93ca1cb44b0c225a03a33704b903f8a96e1697a05d40b0b71dedb8f665769",
|
||||
"adminx/view/_pagination.twig": "9d5e761594ae97d7cb7b518592028adfd2041b5d99f736b24a318b2766e350dd",
|
||||
"adminx/view/_saved_views.twig": "51217212a3b0ef7437de4dd55a07fdf55aaa3138155b00f65fd85af686412da1",
|
||||
@@ -863,14 +867,14 @@
|
||||
"setup/PackageExtractor.php": "57cb5473905992333936d7984221e5188cd72756d6e48206a42d96e8ab59442d",
|
||||
"setup/assets/installer.css": "79e43dc7123f37206429e8535d92c5069c53231402c67b63ea9481bbea60ab42",
|
||||
"setup/assets/installer.js": "892a982b872390fea21455562353408da906114b973ccbfc3a3cce06465ab1e8",
|
||||
"setup/core-migrations.php": "493713e0fc6ade5b3bf866a4bf11b2db1e9e64ab5c5859aaa169df5cd643b8f0",
|
||||
"setup/core-migrations.php": "65d30c41652f5e6a61e70c05a198c784b1062560e607bb5f158b876a5b0c4e20",
|
||||
"setup/index.php": "63bff35919aa2f4cc9ac817b92020d01d5e8a8d6ec387071e0d1ece1edaedc15",
|
||||
"setup/language/en.php": "963dd284ee244e6d006dede8522c1118dc0909974dcd24d477fd06b7e1965fa1",
|
||||
"setup/language/ru.php": "8522b13c4f147804538eaa857585296db811c736e1361e6b403daecea05f34f8",
|
||||
"setup/recovery.php": "359ae9b79508a382bdde175824b18a5bdb0fd69ddb7776c7d18ac9eb826d127a",
|
||||
"setup/schema.sql": "c4e134bf6ec81dad3a46dfa6d2f990e10104e188f54ab21cdbd0a9cb4e53e2af",
|
||||
"setup/schema.sql": "5c496281fe216cefb416477ebd9e3fb1d11b5ec3236b857dcda0a2bbe57a1930",
|
||||
"storage/.htaccess": "ca761a92f2b6eb47feffcf200a8fd846e7fdd8abe065008d29beff0319ca3496",
|
||||
"system/App/Common/AdminAssets.php": "2dbf63a3b5e69ca430391e17420ef9b1db7f6329f28fe510e8b9da5e7b20f68a",
|
||||
"system/App/Common/AdminAssets.php": "dcd58f19fda48263b2912a10dfdc24e3f12967683649c4f614a8d54c62d51b86",
|
||||
"system/App/Common/AdminLocation.php": "d2729b0d627ddbdeb7f0c4b110664ed949f373946f80929c51688391c64352de",
|
||||
"system/App/Common/ApiTokenRepository.php": "2d750efef3e5f8624e3f307a6a8074886d983f324d74d6455eb3ed0061bb57a4",
|
||||
"system/App/Common/AuditLog.php": "2961c0545d395e42d21119e1a8beafc0c3ffb3015ee67dd20edac4d1f302082e",
|
||||
@@ -1181,7 +1185,7 @@
|
||||
"system/App/Frontend/Feeds/FeedPresets.php": "bff8edea9f095a63fa2c127ee6afb35c5a3ce4434ca309e9a8a7dd047ba16428",
|
||||
"system/App/Frontend/Feeds/Generator.php": "4234369a293bad237fe0d36dd3c78da616d03ce37070ac72988893f796773da9",
|
||||
"system/App/Frontend/Feeds/PathController.php": "faf72ea5ba08256c8566a5ee9a73adfcabd59ef0f510c108a6003b659659635b",
|
||||
"system/App/Frontend/Feeds/Repository.php": "d45113f59327efbba03747f2acafee018e0253815838275f3a6879cdee939584",
|
||||
"system/App/Frontend/Feeds/Repository.php": "1f5188ade6ea32e13861598647319202c44c632393a50a5ec55fc96279b2b189",
|
||||
"system/App/Frontend/Feeds/Schema.php": "a4603347d631e6a748e0b245901f952736754e4e5e73c96289ed17a1998f5a31",
|
||||
"system/App/Frontend/Feeds/Service.php": "3d0f3badbc07a9d47e5902c106be270a24a71e2d64a636ca909b2393bdeb59f7",
|
||||
"system/App/Frontend/FieldEndpoint.php": "0bb0200a52169ecb77498df32d6ee3053324844cfa2e9200d6b3b819dd4c6aa8",
|
||||
@@ -1275,7 +1279,7 @@
|
||||
"system/language/ru/lang.xml": "7d46cc9822f064a1af4d6528ccfe2dd4a9cc318912e327fd807befd2ca3dfd11",
|
||||
"system/preload.php": "66bfb1aaf36ba85945b729b09a37e96ead0be1cc2749ebd9abd4b9da4f25ba0e",
|
||||
"system/public.php": "3f72b5bf44bc0ba5ba6fb6b3adc2e2f9d5e9902d9cdfa3d7b748f4aebd179b87",
|
||||
"system/release.php": "c91d409134695fbf4d0016568b4ca5db774cec3b924364b604305d49178eb081",
|
||||
"system/release.php": "e49c5a7ff2bd517746dc5f3a173014739abeb085b3fc0b4a413c568ff7543549",
|
||||
"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",
|
||||
@@ -2196,6 +2200,7 @@
|
||||
"adminx/modules/Groups/module.php": "core",
|
||||
"adminx/modules/Groups/view/list.twig": "core",
|
||||
"adminx/modules/Media/Controller.php": "core",
|
||||
"adminx/modules/Media/ImagePresets.php": "core",
|
||||
"adminx/modules/Media/Model.php": "core",
|
||||
"adminx/modules/Media/assets/media.css": "core",
|
||||
"adminx/modules/Media/assets/media.js": "core",
|
||||
@@ -2205,6 +2210,7 @@
|
||||
"adminx/modules/Media/language/ru/client.xml": "core",
|
||||
"adminx/modules/Media/language/ru/interface.xml": "core",
|
||||
"adminx/modules/Media/language/ru/runtime.xml": "core",
|
||||
"adminx/modules/Media/migrations/001_image_presets.sql": "core",
|
||||
"adminx/modules/Media/module.php": "core",
|
||||
"adminx/modules/Media/view/file.twig": "core",
|
||||
"adminx/modules/Media/view/index.twig": "core",
|
||||
@@ -2300,6 +2306,7 @@
|
||||
"adminx/modules/Rubrics/RubricFieldSetLinks.php": "core",
|
||||
"adminx/modules/Rubrics/RubricRevisions.php": "core",
|
||||
"adminx/modules/Rubrics/RubricSchemaImpact.php": "core",
|
||||
"adminx/modules/Rubrics/RubricTrash.php": "core",
|
||||
"adminx/modules/Rubrics/assets/field-sets.css": "core",
|
||||
"adminx/modules/Rubrics/assets/field-sets.js": "core",
|
||||
"adminx/modules/Rubrics/assets/rubrics.css": "core",
|
||||
@@ -2323,6 +2330,7 @@
|
||||
"adminx/modules/Rubrics/migrations/013_group_form_conditions.php": "core",
|
||||
"adminx/modules/Rubrics/migrations/014_rubric_purpose.php": "core",
|
||||
"adminx/modules/Rubrics/migrations/015_merge_directory_permissions.php": "core",
|
||||
"adminx/modules/Rubrics/migrations/016_rubric_trash.sql": "core",
|
||||
"adminx/modules/Rubrics/module.php": "core",
|
||||
"adminx/modules/Rubrics/view/_content_tabs.twig": "core",
|
||||
"adminx/modules/Rubrics/view/field-sets.twig": "core",
|
||||
|
||||
@@ -5,11 +5,128 @@
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.catalog-tree > .catalog-tree-item > .catalog-tree-row {
|
||||
border-top: 1px solid var(--border-strong);
|
||||
.catalog-tree {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
padding: 12px;
|
||||
overflow-x: hidden;
|
||||
background: var(--background-subtle);
|
||||
}
|
||||
.catalog-tree-row {
|
||||
grid-template-columns: 30px 22px minmax(180px, 1fr) 112px 70px 84px;
|
||||
.catalog-tree.is-drag-active {
|
||||
user-select: none;
|
||||
}
|
||||
.catalog-tree.is-order-saving::after {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 4;
|
||||
content: '';
|
||||
cursor: wait;
|
||||
background: rgba(248, 250, 252, 0.36);
|
||||
}
|
||||
.catalog-tree > .catalog-tree-item {
|
||||
width: calc(100% - var(--catalog-indent, 0px));
|
||||
min-width: 0;
|
||||
margin-left: var(--catalog-indent, 0px);
|
||||
}
|
||||
.catalog-tree-row,
|
||||
.catalog-tree > .catalog-tree-item > .catalog-tree-row {
|
||||
display: grid;
|
||||
grid-template-columns: 30px 30px minmax(180px, 1fr) auto 104px 258px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 54px;
|
||||
padding: 7px 9px;
|
||||
border: 0;
|
||||
border-radius: var(--radius-md);
|
||||
outline: 0;
|
||||
background: var(--background-card);
|
||||
box-shadow: inset 0 0 0 1px var(--border-default);
|
||||
cursor: pointer;
|
||||
transition-property: background-color, box-shadow, opacity;
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
.catalog-tree-row:hover {
|
||||
background: var(--blue-50);
|
||||
box-shadow: inset 0 0 0 1px var(--blue-300);
|
||||
}
|
||||
.catalog-tree-row.is-selected {
|
||||
background: var(--blue-50);
|
||||
box-shadow: inset 3px 0 var(--blue-500), inset 0 0 0 1px var(--blue-300);
|
||||
}
|
||||
.catalog-tree-position {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--background-muted);
|
||||
color: var(--text-secondary);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.catalog-tree-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
.catalog-tree-meta .badge {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.catalog-tree-count {
|
||||
min-width: 42px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
}
|
||||
.catalog-tree-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
.catalog-tree-actions .btn-icon {
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
.catalog-tree-actions .btn-icon:active {
|
||||
transform: scale(0.96);
|
||||
}
|
||||
.catalog-tree-actions .btn-icon:disabled {
|
||||
opacity: 0.28;
|
||||
cursor: default;
|
||||
}
|
||||
.catalog-action-level {
|
||||
color: var(--blue-600);
|
||||
}
|
||||
.catalog-action-sibling {
|
||||
color: var(--green-600);
|
||||
}
|
||||
.catalog-action-child {
|
||||
color: var(--cyan-700);
|
||||
}
|
||||
.catalog-action-edit {
|
||||
color: var(--blue-600);
|
||||
}
|
||||
.catalog-action-delete {
|
||||
color: var(--red-600);
|
||||
}
|
||||
.catalog-drag {
|
||||
border-radius: var(--radius-sm);
|
||||
outline: 0;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
.catalog-drag:hover,
|
||||
.catalog-drag:focus-visible {
|
||||
background: var(--background-muted);
|
||||
color: var(--blue-600);
|
||||
}
|
||||
.catalog-tree-tools {
|
||||
display: flex;
|
||||
@@ -39,21 +156,53 @@
|
||||
}
|
||||
.catalog-tree-item.is-inactive > .catalog-tree-row {
|
||||
background: var(--amber-50);
|
||||
box-shadow: inset 3px 0 var(--amber-500);
|
||||
box-shadow: inset 3px 0 var(--amber-500), inset 0 0 0 1px var(--amber-200);
|
||||
}
|
||||
.catalog-tree-item.is-drop-target > .catalog-tree-row {
|
||||
background: var(--blue-50);
|
||||
.catalog-tree-row.drag-over-top {
|
||||
box-shadow: inset 0 2px var(--blue-500);
|
||||
}
|
||||
.catalog-tree-item.is-drop-target.is-drop-after > .catalog-tree-row {
|
||||
.catalog-tree-row.drag-over-bottom {
|
||||
box-shadow: inset 0 -2px var(--blue-500);
|
||||
}
|
||||
.catalog-tree-item.is-drop-target.is-drop-inside > .catalog-tree-row {
|
||||
background: var(--blue-50);
|
||||
box-shadow: inset 0 0 0 2px var(--blue-500);
|
||||
.catalog-tree > .catalog-tree-item.is-dragging,
|
||||
.catalog-tree > .catalog-tree-item.is-dragging-child {
|
||||
display: none;
|
||||
}
|
||||
.catalog-tree-item.is-dragging > .catalog-tree-row {
|
||||
opacity: 0.58;
|
||||
.catalog-tree-placeholder {
|
||||
min-height: 52px;
|
||||
border: 1px dashed var(--blue-500);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--blue-50);
|
||||
box-shadow: inset 0 0 0 3px rgba(59, 130, 246, 0.08);
|
||||
}
|
||||
.catalog-tree-ghost {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 3000;
|
||||
max-width: calc(100vw - 24px);
|
||||
box-sizing: border-box;
|
||||
pointer-events: none;
|
||||
opacity: 0.96;
|
||||
box-shadow: 0 14px 34px rgba(15, 23, 42, 0.2);
|
||||
will-change: transform;
|
||||
}
|
||||
.catalog-tree-ghost .catalog-tree-meta,
|
||||
.catalog-tree-ghost .catalog-tree-status,
|
||||
.catalog-tree-ghost .catalog-tree-actions {
|
||||
display: none;
|
||||
}
|
||||
.catalog-tree-ghost {
|
||||
grid-template-columns: 30px 30px minmax(160px, 1fr);
|
||||
}
|
||||
.catalog-tree-dragging,
|
||||
.catalog-tree-dragging * {
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
.catalog-tree.is-filtering .catalog-action-level,
|
||||
.catalog-tree.is-filtering .catalog-drag {
|
||||
opacity: 0.28;
|
||||
pointer-events: none;
|
||||
}
|
||||
.catalog-item-form {
|
||||
display: flex;
|
||||
@@ -741,8 +890,12 @@
|
||||
.catalog-behavior-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.catalog-tree-row {
|
||||
grid-template-columns: 30px 18px minmax(140px, 1fr) 60px 70px;
|
||||
.catalog-tree-row,
|
||||
.catalog-tree > .catalog-tree-item > .catalog-tree-row {
|
||||
grid-template-columns: 30px 30px minmax(140px, 1fr) 94px 258px;
|
||||
}
|
||||
.catalog-tree-meta {
|
||||
display: none;
|
||||
}
|
||||
.catalog-tree-status > span {
|
||||
display: none;
|
||||
@@ -773,8 +926,33 @@
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.catalog-tree-row {
|
||||
grid-template-columns: 28px minmax(120px, 1fr) 54px 76px;
|
||||
@media (max-width: 760px) {
|
||||
.catalog-tree {
|
||||
padding: 9px;
|
||||
}
|
||||
.catalog-tree > .catalog-tree-item {
|
||||
width: calc(100% - min(var(--catalog-indent, 0px), 80px));
|
||||
margin-left: min(var(--catalog-indent, 0px), 80px);
|
||||
}
|
||||
.catalog-tree-row,
|
||||
.catalog-tree > .catalog-tree-item > .catalog-tree-row {
|
||||
grid-template-columns: 28px 26px minmax(0, 1fr) auto;
|
||||
gap: 6px;
|
||||
padding: 8px;
|
||||
}
|
||||
.catalog-tree-name {
|
||||
grid-column: 3;
|
||||
}
|
||||
.catalog-tree-status {
|
||||
grid-column: 4;
|
||||
}
|
||||
.catalog-tree-actions {
|
||||
grid-column: 1 / -1;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.catalog-tree-actions .btn-icon {
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
'use strict';
|
||||
var Adminx = window.Adminx || (window.Adminx = {});
|
||||
Adminx.Catalog = {
|
||||
itemForm: null, settingsForm: null, dragRow: null, dragParent: null, dragSnapshot: '', dragTarget: null, filterOrder: [], itemSaveTimer: null, settingsSaveTimer: null, conditionContext: null,
|
||||
itemForm: null, settingsForm: null, dragItem: null, dragGroup: [], dragPlaceholder: null, dragGhost: null, dragPointerId: null, dragStartX: 0, dragStartY: 0, dragStarted: false, dragOrderSnapshot: '', orderSaving: false, filterOrder: [], itemSaveTimer: null, settingsSaveTimer: null, conditionContext: null,
|
||||
init: function () {
|
||||
this.itemForm = document.getElementById('catalogItemForm');
|
||||
this.settingsForm = document.getElementById('catalogSettingsForm');
|
||||
@@ -12,11 +12,19 @@
|
||||
if (pageTab) { self.tab('[data-catalog-page-tab]', '[data-catalog-page-panel]', pageTab.getAttribute('data-catalog-page-tab')); }
|
||||
var drawerTab = e.target.closest('[data-catalog-drawer-tab]');
|
||||
if (drawerTab) { self.tab('[data-catalog-drawer-tab]', '[data-catalog-drawer-panel]', drawerTab.getAttribute('data-catalog-drawer-tab')); }
|
||||
if (e.target.closest('[data-catalog-item-new]')) { self.newItem(); }
|
||||
if (e.target.closest('[data-catalog-item-new]')) { self.newItem(0); }
|
||||
var indent = e.target.closest('[data-catalog-item-indent]');
|
||||
if (indent) { self.changeItemLevel(indent.closest('[data-catalog-item]'), parseInt(indent.getAttribute('data-catalog-item-indent'), 10) || 0); return; }
|
||||
var sibling = e.target.closest('[data-catalog-item-sibling]');
|
||||
if (sibling) { var siblingNode = sibling.closest('[data-catalog-item]'); self.newItem(Number(siblingNode.getAttribute('data-parent-id')) || 0); return; }
|
||||
var child = e.target.closest('[data-catalog-item-child]');
|
||||
if (child) { self.newItem(Number(child.getAttribute('data-catalog-item-child')) || 0); return; }
|
||||
var edit = e.target.closest('[data-catalog-item-edit]');
|
||||
if (edit) { self.editItem(edit.getAttribute('data-catalog-item-edit')); }
|
||||
if (edit) { self.editItem(edit.getAttribute('data-catalog-item-edit')); return; }
|
||||
var del = e.target.closest('[data-catalog-item-delete]');
|
||||
if (del) { self.deleteItem(del.getAttribute('data-catalog-item-delete')); }
|
||||
if (del) { self.deleteItem(del.getAttribute('data-catalog-item-delete')); return; }
|
||||
var treeRow = e.target.closest('[data-catalog-builder-item]');
|
||||
if (treeRow && !e.target.closest('button, input, label, a, select')) { self.editItem(treeRow.getAttribute('data-catalog-builder-item')); return; }
|
||||
if (e.target.closest('[data-catalog-document-pick]')) { self.openDocumentPicker(); }
|
||||
if (e.target.closest('[data-catalog-document-clear]')) { self.setDocument(null); self.scheduleItemSave(); }
|
||||
var conditionView = e.target.closest('[data-catalog-condition-view]');
|
||||
@@ -45,10 +53,11 @@
|
||||
if (this.settingsForm) { this.settingsForm.addEventListener('submit', function (e) { e.preventDefault(); self.saveSettings(false); }); this.updateSettingsGroups(); this.updateCommerceSettings(); }
|
||||
var createForm = document.querySelector('[data-catalog-create]');
|
||||
if (createForm) { createForm.addEventListener('submit', function (e) { e.preventDefault(); self.createCatalog(createForm); }); }
|
||||
document.addEventListener('dragstart', function (e) { self.dragStart(e); });
|
||||
document.addEventListener('dragover', function (e) { self.dragOver(e); });
|
||||
document.addEventListener('drop', function (e) { if (self.dragRow) { e.preventDefault(); } });
|
||||
document.addEventListener('dragend', function () { self.dragEnd(); });
|
||||
document.addEventListener('pointerdown', function (e) { self.treeDragStart(e); });
|
||||
document.addEventListener('pointermove', function (e) { self.treeDragOver(e); });
|
||||
document.addEventListener('pointerup', function (e) { self.treeDrop(e); });
|
||||
document.addEventListener('pointercancel', function () { self.treeDragEnd(); });
|
||||
this.normalizeTreeHierarchy();
|
||||
this.openRequestedItem();
|
||||
},
|
||||
base: function () { return (this.itemForm || this.settingsForm).getAttribute('data-base'); },
|
||||
@@ -66,8 +75,11 @@
|
||||
document.querySelectorAll(tabs).forEach(function (el) { var active = el.getAttribute(tabs.indexOf('drawer') >= 0 ? 'data-catalog-drawer-tab' : 'data-catalog-page-tab') === value; el.classList.toggle('is-active', active); el.setAttribute('aria-selected', active ? 'true' : 'false'); });
|
||||
document.querySelectorAll(panels).forEach(function (el) { el.hidden = el.getAttribute(panels.indexOf('drawer') >= 0 ? 'data-catalog-drawer-panel' : 'data-catalog-page-panel') !== value; });
|
||||
},
|
||||
newItem: function () {
|
||||
newItem: function (parentId) {
|
||||
this.itemForm.reset(); this.itemForm.querySelector('[name="id"]').value = ''; this.itemForm.querySelector('[name="status"]').checked = true;
|
||||
this.itemForm.querySelectorAll('[name="parent_id"] option').forEach(function (option) { option.disabled = false; });
|
||||
var parent = this.itemForm.querySelector('[name="parent_id"]');
|
||||
if (parent) { parent.value = String(Number(parentId) || 0); }
|
||||
var defaultFields = this.settingsValues('fields_default[]'), defaultFilters = this.settingsValues('filters_default[]');
|
||||
this.checkValues('fields_use[]', defaultFields); this.checkValues('filters_use[]', defaultFilters);
|
||||
defaultFilters.forEach(function (id) { var source = this.settingsForm.querySelector('[name="filter_style[' + id + ']"]'), target = this.itemForm.querySelector('[name="filter_style[' + id + ']"]'); if (source && target) { target.value = source.value; } }, this);
|
||||
@@ -80,7 +92,7 @@
|
||||
updateSettingsGroups: function () { if (!this.settingsForm) { return; } this.settingsForm.querySelectorAll('[data-catalog-settings-group]').forEach(function (group) { var name = group.getAttribute('data-catalog-settings-group'), selected = group.querySelectorAll('[name="' + name + '"]:checked').length, badge = group.querySelector('[data-catalog-settings-selected]'); group.querySelectorAll('.catalog-choice-row, .catalog-filter-row').forEach(function (row) { var input = row.querySelector('[name="' + name + '"]'); row.classList.toggle('is-selected', !!input && input.checked); }); if (badge) { badge.textContent = 'Включено: ' + selected; badge.classList.toggle('badge-blue', selected > 0); badge.classList.toggle('badge-gray', selected === 0); } }); },
|
||||
updateCommerceSettings: function () { if (!this.settingsForm) { return; } var purpose = this.settingsForm.querySelector('[name="purpose"]'), visible = purpose && purpose.value === 'commerce'; this.settingsForm.querySelectorAll('[data-catalog-commerce-fields]').forEach(function (section) { section.hidden = !visible; }); },
|
||||
editItem: function (id, requestedTab) {
|
||||
var self = this; Adminx.Loader.show();
|
||||
var self = this; this.markSelectedItem(id); Adminx.Loader.show();
|
||||
fetch(this.base() + '/catalog/items/' + encodeURIComponent(id), { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }).then(this.json).then(function (payload) {
|
||||
self.fill(payload.data || {}); Adminx.Drawer.open('catalogItemDrawer');
|
||||
if (['main', 'fields', 'filters'].indexOf(requestedTab) >= 0) { self.tab('[data-catalog-drawer-tab]', '[data-catalog-drawer-panel]', requestedTab); }
|
||||
@@ -94,13 +106,19 @@
|
||||
},
|
||||
fill: function (item) {
|
||||
this.itemForm.reset(); this.itemForm.querySelector('[name="id"]').value = item.id || ''; this.itemForm.querySelector('[name="name"]').value = item.name || '';
|
||||
this.itemForm.querySelectorAll('[name="parent_id"] option').forEach(function (option) { option.disabled = false; });
|
||||
this.itemForm.querySelector('[name="parent_id"]').value = item.parent_id || 0; this.itemForm.querySelector('[name="status"]').checked = Number(item.status) === 1;
|
||||
this.setDocument(item.document_id ? { id: item.document_id, title: item.document_title || '', alias: item.document_alias || '' } : null);
|
||||
this.filterOrder = (item.filters_use || []).map(function (id) { return String(id); }); this.syncFilterOrderInput(); this.checkValues('fields_use[]', item.fields_use || []); this.checkValues('filters_use[]', item.filters_use || []);
|
||||
Object.keys(item.filter_styles || {}).forEach(function (id) { var select = this.itemForm.querySelector('[name="filter_style[' + id + ']"]'); if (select) { select.value = item.filter_styles[id]; } }, this);
|
||||
var own = this.itemForm.querySelector('[name="parent_id"] option[value="' + item.id + '"]'); if (own) { own.disabled = true; }
|
||||
var treeItem = document.querySelector('[data-catalog-item][data-id="' + item.id + '"]');
|
||||
this.treeBranch(treeItem).slice(1).forEach(function (child) {
|
||||
var option = this.itemForm.querySelector('[name="parent_id"] option[value="' + child.getAttribute('data-id') + '"]');
|
||||
if (option) { option.disabled = true; }
|
||||
}, this);
|
||||
this.renderConditionContext(item.condition_context || null);
|
||||
document.querySelector('[data-catalog-drawer-title]').textContent = item.name || 'Раздел каталога'; this.tab('[data-catalog-drawer-tab]', '[data-catalog-drawer-panel]', 'main'); this.updateCounts(); this.setState('item', '');
|
||||
document.querySelector('[data-catalog-drawer-title]').textContent = item.name || 'Раздел каталога'; this.markSelectedItem(item.id); this.tab('[data-catalog-drawer-tab]', '[data-catalog-drawer-panel]', 'main'); this.updateCounts(); this.setState('item', '');
|
||||
},
|
||||
checkValues: function (name, values) { var map = {}; values.forEach(function (id) { map[String(id)] = true; }); this.itemForm.querySelectorAll('[name="' + name + '"]').forEach(function (el) { el.checked = !!map[el.value]; }); },
|
||||
updateCounts: function () { ['fields', 'filters'].forEach(function (type) { var count = this.itemForm.querySelectorAll('[name="' + type + '_use[]"]:checked').length; var el = document.querySelector('[data-catalog-' + type + '-count]'); if (el) { el.textContent = count; } this.itemForm.querySelectorAll('[name="' + type + '_use[]"]').forEach(function (toggle) { var row = toggle.closest('.catalog-choice-row, .catalog-filter-row'); if (row) { row.classList.toggle('is-selected', toggle.checked); } }); this.itemForm.querySelectorAll('[data-catalog-field-group="' + type + '"]').forEach(function (group) { var selected = group.querySelectorAll('[name="' + type + '_use[]"]:checked').length; var badge = group.querySelector('[data-catalog-group-selected]'); if (badge) { badge.textContent = 'Включено: ' + selected; badge.classList.toggle('badge-blue', selected > 0); badge.classList.toggle('badge-gray', selected === 0); } }); }, this); var orderButton = this.itemForm.querySelector('[data-catalog-filter-order]'); if (orderButton) { orderButton.disabled = this.filterOrder.length < 2; } this.renderConditionContext(this.conditionContext); },
|
||||
@@ -160,7 +178,7 @@
|
||||
},
|
||||
saveSettings: function (auto) { var self = this; this.ajax(this.url() + '/settings', new FormData(this.settingsForm), function (payload) { if (auto) { self.setState('settings', 'Сохранено', 'ok'); } else { self.setState('settings', 'Сохранено', 'ok'); Adminx.Toast.show(payload.message, 'success'); } }, { quiet: !!auto, fail: function () { self.setState('settings', 'Не сохранено', 'error'); } }); },
|
||||
setState: function (type, message, state) { var el = document.querySelector('[data-catalog-' + type + '-state]'); if (!el) { return; } el.textContent = message || ''; el.classList.toggle('is-ok', state === 'ok'); el.classList.toggle('is-error', state === 'error'); },
|
||||
syncTreeRow: function (id) { var row = document.querySelector('[data-catalog-item][data-id="' + id + '"]'); if (!row) { return; } var toggle = row.querySelector('[data-catalog-item-status]'), active = this.itemForm.querySelector('[name="status"]').checked, filterCount = this.itemForm.querySelectorAll('[name="filters_use[]"]:checked').length; if (toggle) { toggle.checked = active; this.updateTreeStatus(toggle); } row.setAttribute('data-filter-count', String(filterCount)); var count = row.querySelector('.catalog-tree-count'); if (count) { count.textContent = this.itemForm.querySelectorAll('[name="fields_use[]"]:checked').length + ' / ' + filterCount; } },
|
||||
syncTreeRow: function (id) { var row = document.querySelector('[data-catalog-item][data-id="' + id + '"]'); if (!row) { return; } var toggle = row.querySelector('[data-catalog-item-status]'), active = this.itemForm.querySelector('[name="status"]').checked, filterCount = this.itemForm.querySelectorAll('[name="filters_use[]"]:checked').length, name = row.querySelector('.catalog-tree-name b'); if (toggle) { toggle.checked = active; this.updateTreeStatus(toggle); } if (name) { name.textContent = this.itemForm.querySelector('[name="name"]').value || 'Без названия'; } row.setAttribute('data-filter-count', String(filterCount)); var count = row.querySelector('.catalog-tree-count'); if (count) { count.textContent = this.itemForm.querySelectorAll('[name="fields_use[]"]:checked').length + ' / ' + filterCount; } },
|
||||
updateTreeStatus: function (input) { var item = input.closest('[data-catalog-item]'), active = input.checked, label = item.querySelector('[data-catalog-item-status-label]'), control = input.closest('.switch'); item.classList.toggle('is-inactive', !active); if (label) { label.textContent = active ? 'активен' : 'скрыт'; } input.setAttribute('aria-label', active ? 'Скрыть раздел' : 'Включить раздел'); if (control) { control.setAttribute('data-tooltip', active ? 'Скрыть раздел' : 'Включить раздел'); } },
|
||||
setTreeStatus: function (input) { var self = this, id = input.getAttribute('data-catalog-item-status'), previous = !input.checked, data = new FormData(); this.updateTreeStatus(input); input.disabled = true; data.append('_csrf', this.csrf()); data.append('status', input.checked ? '1' : '0'); this.ajax(this.url() + '/items/' + encodeURIComponent(id) + '/status', data, function (payload) { input.checked = Number(payload.data.status) === 1; input.disabled = false; self.updateTreeStatus(input); }, { quiet: true, fail: function () { input.checked = previous; input.disabled = false; self.updateTreeStatus(input); } }); },
|
||||
setDocument: function (item) { if (!this.itemForm) { return; } var input = this.itemForm.querySelector('[name="document_id"]'); var label = this.itemForm.querySelector('[data-catalog-document-label]'); var clear = this.itemForm.querySelector('[data-catalog-document-clear]'); var id = item && item.id ? Number(item.id) : 0; input.value = id || ''; if (label) { label.textContent = id ? ('#' + id + ' · ' + (item.title || item.alias || 'Без названия')) : 'Документ не выбран'; } if (clear) { clear.disabled = !id; } },
|
||||
@@ -180,12 +198,239 @@
|
||||
var self = this, run = function () { var data = new FormData(); data.append('_csrf', self.csrf()); self.ajax(self.url() + '/items/' + id + '/delete', data, function (payload) { Adminx.Toast.show(payload.message, 'success'); window.location.reload(); }); };
|
||||
if (Adminx.Confirm) { Adminx.Confirm.open({ kind: 'error', title: 'Удалить раздел?', message: 'Будут удалены также все вложенные разделы. Документы останутся.', confirmLabel: 'Удалить', confirmClass: 'btn-danger', onConfirm: run }); } else if (confirm('Удалить раздел и все вложенные?')) { run(); }
|
||||
},
|
||||
search: function (value) { value = String(value || '').trim().toLowerCase(); document.querySelectorAll('[data-catalog-item]').forEach(function (row) { row.classList.toggle('is-filtered', value && row.textContent.toLowerCase().indexOf(value) < 0); }); },
|
||||
dragStart: function (e) { var handle = e.target.closest('.catalog-drag[draggable="true"]'), row = handle ? handle.closest('[data-catalog-item]') : null; if (!row) { e.preventDefault(); return; } this.dragRow = row; this.dragParent = row.parentNode; this.dragSnapshot = this.siblingOrder(this.dragParent); row.classList.add('is-dragging'); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', row.getAttribute('data-id')); },
|
||||
dragOver: function (e) { if (!this.dragRow) { return; } var target = e.target.closest('[data-catalog-item]'); if (!target || target === this.dragRow || this.dragRow.contains(target)) { return; } var row = target.querySelector(':scope > .catalog-tree-row'), rect = row.getBoundingClientRect(), ratio = (e.clientY - rect.top) / rect.height, mode = ratio < .28 ? 'before' : (ratio > .72 ? 'after' : 'inside'), destination = null, reference = null, parentId = 0; e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if (this.dragTarget && this.dragTarget !== target) { this.dragTarget.classList.remove('is-drop-target', 'is-drop-after', 'is-drop-inside'); } this.dragTarget = target; target.classList.add('is-drop-target'); target.classList.toggle('is-drop-after', mode === 'after'); target.classList.toggle('is-drop-inside', mode === 'inside'); if (mode === 'inside') { destination = target.querySelector(':scope > .catalog-tree-children'); if (!destination) { destination = document.createElement('ol'); destination.className = 'catalog-tree-children'; target.appendChild(destination); } parentId = Number(target.getAttribute('data-id')); } else { destination = target.parentNode; reference = mode === 'before' ? target : target.nextSibling; parentId = Number(target.getAttribute('data-parent-id')); } if (destination !== this.dragRow.parentNode || (reference !== this.dragRow && this.dragRow.nextSibling !== reference)) { destination.insertBefore(this.dragRow, reference); this.dragRow.setAttribute('data-parent-id', String(parentId)); } },
|
||||
dragEnd: function () { if (!this.dragRow) { return; } var changed = this.dragSnapshot !== this.siblingOrder(this.dragParent); this.dragRow.classList.remove('is-dragging'); if (this.dragTarget) { this.dragTarget.classList.remove('is-drop-target', 'is-drop-after', 'is-drop-inside'); } document.querySelectorAll('.catalog-tree-children').forEach(function (list) { if (!list.querySelector(':scope > [data-catalog-item]')) { list.remove(); } }); this.dragRow = null; this.dragParent = null; this.dragTarget = null; this.dragSnapshot = ''; if (changed) { this.saveOrder(); } },
|
||||
siblingOrder: function (parent) { return Array.prototype.filter.call(parent ? parent.children : [], function (el) { return el.matches('[data-catalog-item]'); }).map(function (el) { return el.getAttribute('data-id'); }).join(','); },
|
||||
saveOrder: function () { var rows = []; document.querySelectorAll('[data-catalog-item]').forEach(function (row) { var siblings = Array.prototype.filter.call(row.parentNode.children, function (el) { return el.matches('[data-catalog-item]'); }); rows.push({ id: Number(row.getAttribute('data-id')), parent_id: Number(row.getAttribute('data-parent-id')), position: siblings.indexOf(row) }); }); var data = new FormData(); data.append('_csrf', this.csrf()); data.append('order', JSON.stringify(rows)); this.ajax(this.url() + '/reorder', data, function (payload) { Adminx.Toast.show(payload.message, 'success'); }); },
|
||||
markSelectedItem: function (id) {
|
||||
document.querySelectorAll('[data-catalog-builder-item]').forEach(function (row) {
|
||||
row.classList.toggle('is-selected', String(row.getAttribute('data-catalog-builder-item')) === String(id));
|
||||
});
|
||||
},
|
||||
treeRoot: function () { return document.querySelector('[data-catalog-tree]'); },
|
||||
treeNodes: function () {
|
||||
var root = this.treeRoot();
|
||||
return root ? Array.prototype.filter.call(root.children, function (node) { return node.matches('[data-catalog-item]'); }) : [];
|
||||
},
|
||||
search: function (value) {
|
||||
value = String(value || '').trim().toLowerCase();
|
||||
var nodes = this.treeNodes(), byId = {}, visible = {};
|
||||
nodes.forEach(function (node) { byId[String(node.getAttribute('data-id'))] = node; node.classList.add('is-filtered'); });
|
||||
if (!value) {
|
||||
nodes.forEach(function (node) { node.classList.remove('is-filtered'); });
|
||||
} else {
|
||||
nodes.forEach(function (node) {
|
||||
var name = node.querySelector('.catalog-tree-name');
|
||||
if (!name || name.textContent.toLowerCase().indexOf(value) < 0) { return; }
|
||||
var current = node;
|
||||
while (current && !visible[current.getAttribute('data-id')]) {
|
||||
visible[current.getAttribute('data-id')] = true;
|
||||
current = byId[String(current.getAttribute('data-parent-id'))] || null;
|
||||
}
|
||||
});
|
||||
Object.keys(visible).forEach(function (id) { if (byId[id]) { byId[id].classList.remove('is-filtered'); } });
|
||||
}
|
||||
var root = this.treeRoot();
|
||||
if (root) { root.classList.toggle('is-filtering', !!value); }
|
||||
},
|
||||
treeDragStart: function (e) {
|
||||
var handle = e.target.closest('[data-catalog-drag-handle]');
|
||||
var root = handle ? handle.closest('[data-catalog-tree]') : null;
|
||||
if (!handle || !root || handle.disabled || this.orderSaving || root.classList.contains('is-filtering') || (typeof e.button === 'number' && e.button !== 0)) { return; }
|
||||
this.dragItem = handle.closest('[data-catalog-item]');
|
||||
if (!this.dragItem) { return; }
|
||||
this.dragPointerId = e.pointerId;
|
||||
this.dragStartX = e.clientX;
|
||||
this.dragStartY = e.clientY;
|
||||
this.dragStarted = false;
|
||||
this.dragOrderSnapshot = this.treeOrderSignature();
|
||||
if (handle.setPointerCapture) { handle.setPointerCapture(e.pointerId); }
|
||||
e.preventDefault();
|
||||
},
|
||||
beginTreeDrag: function (e) {
|
||||
if (!this.dragItem || this.dragStarted) { return; }
|
||||
var row = this.dragItem.querySelector('[data-catalog-builder-item]');
|
||||
var firstRect = this.dragItem.getBoundingClientRect();
|
||||
var rowRect = row.getBoundingClientRect();
|
||||
this.dragStarted = true;
|
||||
this.dragGroup = this.treeBranch(this.dragItem);
|
||||
this.dragPlaceholder = document.createElement('li');
|
||||
this.dragPlaceholder.className = 'catalog-tree-placeholder';
|
||||
this.dragPlaceholder.setAttribute('aria-hidden', 'true');
|
||||
var lastRect = this.dragGroup[this.dragGroup.length - 1].getBoundingClientRect();
|
||||
this.dragPlaceholder.style.height = Math.max(52, lastRect.bottom - firstRect.top) + 'px';
|
||||
this.dragItem.parentNode.insertBefore(this.dragPlaceholder, this.dragItem);
|
||||
this.dragGhost = row.cloneNode(true);
|
||||
this.dragGhost.classList.remove('is-selected');
|
||||
this.dragGhost.classList.add('catalog-tree-ghost');
|
||||
this.dragGhost.style.width = Math.min(rowRect.width, 520, Math.max(1, window.innerWidth - 24)) + 'px';
|
||||
document.body.appendChild(this.dragGhost);
|
||||
this.positionTreeDragGhost(e);
|
||||
document.body.classList.add('catalog-tree-dragging');
|
||||
this.treeRoot().classList.add('is-drag-active');
|
||||
this.dragGroup.forEach(function (node, index) { node.classList.add(index === 0 ? 'is-dragging' : 'is-dragging-child'); });
|
||||
},
|
||||
treeDragOver: function (e) {
|
||||
if (!this.dragItem || e.pointerId !== this.dragPointerId) { return; }
|
||||
if (!this.dragStarted) {
|
||||
if (Math.abs(e.clientX - this.dragStartX) < 5 && Math.abs(e.clientY - this.dragStartY) < 5) { return; }
|
||||
this.beginTreeDrag(e);
|
||||
}
|
||||
e.preventDefault();
|
||||
this.positionTreeDragGhost(e);
|
||||
this.scrollTree(e.clientY);
|
||||
var target = null, bottom = false, self = this;
|
||||
this.treeNodes().some(function (node) {
|
||||
if (self.dragGroup.indexOf(node) !== -1 || node.classList.contains('is-filtered')) { return false; }
|
||||
var candidate = node.querySelector('[data-catalog-builder-item]');
|
||||
var rect = candidate ? candidate.getBoundingClientRect() : null;
|
||||
if (!rect || e.clientY >= rect.bottom) { return false; }
|
||||
target = node;
|
||||
bottom = e.clientY > rect.top + rect.height / 2;
|
||||
return true;
|
||||
});
|
||||
document.querySelectorAll('.catalog-tree-row.drag-over-top, .catalog-tree-row.drag-over-bottom').forEach(function (row) { row.classList.remove('drag-over-top', 'drag-over-bottom'); });
|
||||
if (!target) { this.treeRoot().appendChild(this.dragPlaceholder); return; }
|
||||
var targetRow = target.querySelector('[data-catalog-builder-item]');
|
||||
var branch = this.treeBranch(target);
|
||||
var anchor = bottom ? branch[branch.length - 1].nextSibling : target;
|
||||
if (anchor !== this.dragPlaceholder) { target.parentNode.insertBefore(this.dragPlaceholder, anchor); }
|
||||
targetRow.classList.toggle('drag-over-top', !bottom);
|
||||
targetRow.classList.toggle('drag-over-bottom', bottom);
|
||||
},
|
||||
treeDrop: function (e) {
|
||||
if (!this.dragItem || e.pointerId !== this.dragPointerId) { return; }
|
||||
e.preventDefault();
|
||||
if (!this.dragStarted || !this.dragPlaceholder || !this.dragPlaceholder.parentNode) { this.treeDragEnd(); return; }
|
||||
var placeholder = this.dragPlaceholder;
|
||||
this.dragGroup.forEach(function (node) { placeholder.parentNode.insertBefore(node, placeholder); });
|
||||
placeholder.parentNode.removeChild(placeholder);
|
||||
this.dragPlaceholder = null;
|
||||
this.normalizeTreeHierarchy();
|
||||
var changed = this.treeOrderSignature() !== this.dragOrderSnapshot;
|
||||
this.treeDragEnd();
|
||||
if (changed) { this.persistTreeOrder(); }
|
||||
},
|
||||
treeDragEnd: function () {
|
||||
this.dragGroup.forEach(function (node) { node.classList.remove('is-dragging', 'is-dragging-child'); });
|
||||
if (this.dragPlaceholder && this.dragPlaceholder.parentNode) { this.dragPlaceholder.parentNode.removeChild(this.dragPlaceholder); }
|
||||
document.querySelectorAll('.catalog-tree-row.drag-over-top, .catalog-tree-row.drag-over-bottom').forEach(function (row) { row.classList.remove('drag-over-top', 'drag-over-bottom'); });
|
||||
var root = this.treeRoot();
|
||||
if (root) { root.classList.remove('is-drag-active'); }
|
||||
if (this.dragGhost && this.dragGhost.parentNode) { this.dragGhost.parentNode.removeChild(this.dragGhost); }
|
||||
document.body.classList.remove('catalog-tree-dragging');
|
||||
this.dragItem = null;
|
||||
this.dragGroup = [];
|
||||
this.dragPlaceholder = null;
|
||||
this.dragGhost = null;
|
||||
this.dragPointerId = null;
|
||||
this.dragStarted = false;
|
||||
this.dragOrderSnapshot = '';
|
||||
},
|
||||
positionTreeDragGhost: function (e) {
|
||||
if (!this.dragGhost) { return; }
|
||||
var left = Math.min(e.clientX + 14, window.innerWidth - this.dragGhost.offsetWidth - 12);
|
||||
var top = Math.min(e.clientY + 12, window.innerHeight - this.dragGhost.offsetHeight - 12);
|
||||
this.dragGhost.style.transform = 'translate3d(' + Math.max(12, left) + 'px,' + Math.max(12, top) + 'px,0)';
|
||||
},
|
||||
scrollTree: function (clientY) {
|
||||
var edge = 72;
|
||||
if (clientY < edge) { window.scrollBy(0, -Math.ceil((edge - clientY) / 5)); }
|
||||
if (clientY > window.innerHeight - edge) { window.scrollBy(0, Math.ceil((clientY - window.innerHeight + edge) / 5)); }
|
||||
},
|
||||
treeOrderSignature: function () {
|
||||
return this.treeNodes().map(function (node) { return node.getAttribute('data-id') + ':' + node.getAttribute('data-level'); }).join('|');
|
||||
},
|
||||
treeBranch: function (node) {
|
||||
if (!node) { return []; }
|
||||
var branch = [node], level = parseInt(node.getAttribute('data-level'), 10) || 0, next = node.nextElementSibling;
|
||||
while (next) {
|
||||
if (next === this.dragPlaceholder) { next = next.nextElementSibling; continue; }
|
||||
if (!next.matches('[data-catalog-item]') || (parseInt(next.getAttribute('data-level'), 10) || 0) <= level) { break; }
|
||||
branch.push(next);
|
||||
next = next.nextElementSibling;
|
||||
}
|
||||
return branch;
|
||||
},
|
||||
canIndentItem: function (node) {
|
||||
var level = parseInt(node.getAttribute('data-level'), 10) || 0, previous = node.previousElementSibling;
|
||||
while (previous) {
|
||||
if (!previous.matches('[data-catalog-item]')) { previous = previous.previousElementSibling; continue; }
|
||||
var previousLevel = parseInt(previous.getAttribute('data-level'), 10) || 0;
|
||||
if (previousLevel < level) { return false; }
|
||||
if (previousLevel === level) { return true; }
|
||||
previous = previous.previousElementSibling;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
changeItemLevel: function (node, delta) {
|
||||
if (!node || !delta || this.orderSaving) { return; }
|
||||
var root = this.treeRoot();
|
||||
if (root && root.classList.contains('is-filtering')) { Adminx.Toast.show('Очистите поиск перед изменением структуры', 'info'); return; }
|
||||
var level = parseInt(node.getAttribute('data-level'), 10) || 0;
|
||||
if (delta > 0 && !this.canIndentItem(node)) { return; }
|
||||
if (delta < 0 && level === 0) { return; }
|
||||
var shift = delta > 0 ? 1 : -1;
|
||||
this.treeBranch(node).forEach(function (branchNode) {
|
||||
var branchLevel = parseInt(branchNode.getAttribute('data-level'), 10) || 0;
|
||||
branchNode.setAttribute('data-level', String(Math.max(0, branchLevel + shift)));
|
||||
});
|
||||
this.normalizeTreeHierarchy();
|
||||
this.persistTreeOrder();
|
||||
},
|
||||
normalizeTreeHierarchy: function () {
|
||||
var nodes = this.treeNodes(), parents = [], positions = {}, previousLevel = 0, self = this;
|
||||
nodes.forEach(function (node, index) {
|
||||
var level = Math.max(0, parseInt(node.getAttribute('data-level'), 10) || 0);
|
||||
if (index === 0) { level = 0; }
|
||||
if (level > previousLevel + 1) { level = previousLevel + 1; }
|
||||
var parentId = level > 0 && parents[level - 1] ? parents[level - 1] : 0;
|
||||
if (level > 0 && !parentId) { level = 0; parentId = 0; }
|
||||
var id = parseInt(node.getAttribute('data-id'), 10) || 0;
|
||||
var key = String(parentId);
|
||||
var position = positions[key] || 0;
|
||||
positions[key] = position + 1;
|
||||
parents[level] = id;
|
||||
parents.length = level + 1;
|
||||
previousLevel = level;
|
||||
node.setAttribute('data-level', String(level));
|
||||
node.setAttribute('data-parent-id', String(parentId));
|
||||
node.setAttribute('data-position', String(position));
|
||||
node.style.setProperty('--catalog-indent', Math.min(level, 10) * 28 + 'px');
|
||||
var number = node.querySelector('.catalog-tree-position');
|
||||
if (number) { number.textContent = String(position + 1); }
|
||||
var badge = node.querySelector('[data-catalog-level-badge]');
|
||||
if (badge) { badge.textContent = 'уровень ' + (level + 1); }
|
||||
});
|
||||
nodes.forEach(function (node) {
|
||||
var level = parseInt(node.getAttribute('data-level'), 10) || 0;
|
||||
var decrease = node.querySelector('[data-catalog-item-indent="-1"]');
|
||||
var increase = node.querySelector('[data-catalog-item-indent="1"]');
|
||||
if (decrease) { decrease.disabled = level === 0; }
|
||||
if (increase) { increase.disabled = !self.canIndentItem(node); }
|
||||
});
|
||||
},
|
||||
persistTreeOrder: function () {
|
||||
if (this.orderSaving) { return; }
|
||||
this.normalizeTreeHierarchy();
|
||||
var rows = this.treeNodes().map(function (node) {
|
||||
return {
|
||||
id: Number(node.getAttribute('data-id')),
|
||||
parent_id: Number(node.getAttribute('data-parent-id')),
|
||||
position: Number(node.getAttribute('data-position'))
|
||||
};
|
||||
});
|
||||
var data = new FormData(), self = this, root = this.treeRoot();
|
||||
data.append('_csrf', this.csrf());
|
||||
data.append('order', JSON.stringify(rows));
|
||||
this.orderSaving = true;
|
||||
if (root) { root.classList.add('is-order-saving'); }
|
||||
this.ajax(this.url() + '/reorder', data, function (payload) {
|
||||
self.orderSaving = false;
|
||||
if (root) { root.classList.remove('is-order-saving'); }
|
||||
Adminx.Toast.show(payload.message || 'Порядок сохранён', 'success');
|
||||
}, { quiet: true, fail: function () {
|
||||
self.orderSaving = false;
|
||||
if (root) { root.classList.remove('is-order-saving'); }
|
||||
setTimeout(function () { window.location.reload(); }, 400);
|
||||
} });
|
||||
},
|
||||
ajax: function (url, data, done, options) { options = options || {}; if (!options.quiet) { Adminx.Loader.show(); } fetch(url, { method: 'POST', body: data, headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }).then(this.json).then(function (payload) { if (!payload.success) { throw payload; } done(payload); }).catch(function (payload) { if (options.fail) { options.fail(payload); } Adminx.Catalog.error(payload); }).finally(function () { if (!options.quiet) { Adminx.Loader.hide(); } }); },
|
||||
json: function (res) { return res.json().then(function (payload) { if (!res.ok) { throw payload; } return payload; }); },
|
||||
esc: function (value) { var node = document.createElement('div'); node.textContent = String(value == null ? '' : value); return node.innerHTML; },
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
{% extends '@adminx/main.twig' %}
|
||||
{% block title %}{{ catalog.rubric_title }} · Каталог{% endblock %}
|
||||
{% macro rows(items, can_manage) %}
|
||||
{% macro rows(items, can_manage, level) %}
|
||||
{% set level = level|default(0) %}
|
||||
{% for item in items %}
|
||||
<li class="catalog-tree-item{{ item.status ? '' : ' is-inactive' }}" data-catalog-item data-id="{{ item.id }}" data-parent-id="{{ item.parent_id }}" data-filter-count="{{ item.filters_use|length }}">
|
||||
<div class="catalog-tree-row">
|
||||
<button class="catalog-drag" type="button" draggable="{{ can_manage ? 'true' : 'false' }}" aria-label="Перетащить" data-tooltip="Изменить порядок"><i class="ti ti-grip-vertical"></i></button>
|
||||
<span class="catalog-tree-branch"><i class="ti ti-corner-down-right"></i></span>
|
||||
<li class="catalog-tree-item{{ item.status ? '' : ' is-inactive' }}" data-catalog-item data-id="{{ item.id }}" data-parent-id="{{ item.parent_id }}" data-position="{{ item.position }}" data-level="{{ level }}" data-filter-count="{{ item.filters_use|length }}" style="--catalog-indent:{{ level > 10 ? 280 : level * 28 }}px">
|
||||
<div class="catalog-tree-row" data-catalog-builder-item="{{ item.id }}">
|
||||
<button class="catalog-drag" type="button" aria-label="Перетащить раздел" data-tooltip="Перетащить раздел вместе с подразделами" data-catalog-drag-handle{{ can_manage ? '' : ' disabled' }}><i class="ti ti-grip-vertical"></i></button>
|
||||
<span class="catalog-tree-position" data-tooltip="Позиция внутри уровня">{{ item.position + 1 }}</span>
|
||||
<div class="catalog-tree-name"><b>{{ item.name }}</b><small>#{{ item.id }}{% if item.document_id %} · документ #{{ item.document_id }}{% endif %}</small></div>
|
||||
<div class="catalog-tree-meta"><span class="badge badge-gray" data-catalog-level-badge>уровень {{ level + 1 }}</span><span class="catalog-tree-count" data-tooltip="Поля / фильтры">{{ item.fields_use|length }} / {{ item.filters_use|length }}</span></div>
|
||||
<div class="catalog-tree-status"><span data-catalog-item-status-label>{{ item.status ? 'активен' : 'скрыт' }}</span><label class="switch" data-tooltip="{{ item.status ? 'Скрыть раздел' : 'Включить раздел' }}"><input type="checkbox" data-catalog-item-status="{{ item.id }}"{{ item.status ? ' checked' : '' }} aria-label="{{ item.status ? 'Скрыть раздел' : 'Включить раздел' }}"></label></div>
|
||||
<span class="catalog-tree-count" data-tooltip="Поля / фильтры">{{ item.fields_use|length }} / {{ item.filters_use|length }}</span>
|
||||
{% if can_manage %}<div class="catalog-tree-actions"><button class="btn btn-ghost btn-icon btn-sm catalog-action-edit" type="button" data-catalog-item-edit="{{ item.id }}" data-tooltip="Редактировать"><i class="ti ti-edit"></i></button><button class="btn btn-ghost btn-icon btn-sm catalog-action-delete" type="button" data-catalog-item-delete="{{ item.id }}" data-tooltip="Удалить"><i class="ti ti-trash"></i></button></div>{% endif %}
|
||||
{% if can_manage %}<div class="catalog-tree-actions"><button class="btn btn-ghost btn-icon btn-sm catalog-action-level" type="button" data-catalog-item-indent="-1" data-tooltip="Поднять на уровень выше"{{ level == 0 ? ' disabled' : '' }}><i class="ti ti-arrow-left"></i></button><button class="btn btn-ghost btn-icon btn-sm catalog-action-level" type="button" data-catalog-item-indent="1" data-tooltip="Сделать подразделом"><i class="ti ti-arrow-right"></i></button><button class="btn btn-ghost btn-icon btn-sm catalog-action-sibling" type="button" data-catalog-item-sibling="{{ item.id }}" data-tooltip="Добавить рядом"><i class="ti ti-row-insert-bottom"></i></button><button class="btn btn-ghost btn-icon btn-sm catalog-action-child" type="button" data-catalog-item-child="{{ item.id }}" data-tooltip="Добавить подраздел"><i class="ti ti-subtask"></i></button><button class="btn btn-ghost btn-icon btn-sm catalog-action-edit" type="button" data-catalog-item-edit="{{ item.id }}" data-tooltip="Редактировать"><i class="ti ti-edit"></i></button><button class="btn btn-ghost btn-icon btn-sm catalog-action-delete" type="button" data-catalog-item-delete="{{ item.id }}" data-tooltip="Удалить"><i class="ti ti-trash"></i></button></div>{% endif %}
|
||||
</div>
|
||||
{% if item.children %}<ol class="catalog-tree-children">{{ _self.rows(item.children, can_manage) }}</ol>{% endif %}
|
||||
</li>
|
||||
{% if item.children %}{{ _self.rows(item.children, can_manage, level + 1) }}{% endif %}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
{% block content %}
|
||||
@@ -25,7 +26,7 @@
|
||||
<div class="section-header catalog-panel-header"><div class="section-icon"><i class="ti ti-list-tree"></i></div><div><div class="section-eyebrow">Структура</div><h2>Разделы каталога</h2><p class="section-desc">Порядок и вложенность используются в каталожном поле документа.</p></div></div>
|
||||
<div class="card catalog-card">
|
||||
<div class="catalog-section-head"><div class="catalog-section-title"><span class="icon-tile catalog-head-icon" style="--tile-bg:var(--violet-100);--tile-fg:var(--violet-600)"><i class="ti ti-list-tree"></i></span><div><h2>Дерево разделов</h2><p class="text-secondary">Перетаскивайте строки за маркер слева.</p></div></div><div class="catalog-tree-tools"><label class="input-wrap catalog-tree-search"><i class="ti ti-search"></i><input class="input" type="search" placeholder="Найти раздел" data-catalog-tree-search></label>{% if can_manage %}<button class="btn btn-secondary" type="button" data-catalog-recompile-all><i class="ti ti-refresh"></i>Пересобрать фильтры</button>{% endif %}</div></div>
|
||||
<ol class="catalog-tree" data-catalog-tree>{{ _self.rows(tree, can_manage) }}</ol>
|
||||
<ol class="catalog-tree" data-catalog-tree>{{ _self.rows(tree, can_manage, 0) }}</ol>
|
||||
{% if not tree %}<div class="empty-state">Разделов пока нет.</div>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -31,14 +31,20 @@
|
||||
{
|
||||
if(!Permission::check('view_customers')){Response::forbidden();return '';}AdminAssets::addStyle($this->base().'/modules/Customers/assets/customers.css',50);AdminAssets::addScript($this->base().'/modules/Customers/assets/customers.js',50);CodeEditor::useCodeMirror('htmlmixed');
|
||||
$oauthModules=Model::oauthModules();foreach($oauthModules as &$oauthModule){$oauthModule['can_open']=Permission::check($oauthModule['permission']);}unset($oauthModule);
|
||||
$tab=Request::getStr('tab','customers');if(!in_array($tab,array('customers','fields','auth','pages'),true)){$tab='customers';}return $this->render('@customers/index.twig',array('tab'=>$tab,'customers'=>Model::customers(Request::getStr('q','')),'fields'=>Model::fields(),'stats'=>Model::stats(),'auth_settings'=>Model::authSettings(),'checkout_access_template_default'=>\App\Common\PublicAuthSettings::defaultCheckoutAccessTemplate(),'oauth_modules'=>$oauthModules,'customer_groups'=>Model::groups(),'admin_roles'=>Roles::map(),'page_templates'=>Model::pageTemplates(),'auth_forms'=>Model::authFormDefinitions(),'q'=>Request::getStr('q',''),'can_manage'=>Permission::check('manage_customers'),'can_manage_admin_access'=>Permission::check('manage_users')));
|
||||
$tab=Request::getStr('tab','customers');if(!in_array($tab,array('customers','fields','auth','pages'),true)){$tab='customers';}return $this->render('@customers/index.twig',array('tab'=>$tab,'customers'=>Model::customers(Request::getStr('q','')),'fields'=>Model::fields(),'stats'=>Model::stats(),'auth_settings'=>Model::authSettings(),'checkout_access_template_default'=>\App\Common\PublicAuthSettings::defaultCheckoutAccessTemplate(),'oauth_modules'=>$oauthModules,'customer_groups'=>Model::groups(),'admin_roles'=>Roles::map(),'page_templates'=>Model::pageTemplates(),'auth_forms'=>Model::authFormDefinitions(),'q'=>Request::getStr('q',''),'can_manage'=>Permission::check('manage_customers'),'can_manage_admin_access'=>Permission::check('manage_users'),'current_public_user_id'=>Model::publicIdForSystem(Auth::id())));
|
||||
}
|
||||
|
||||
public function toggle(array $params=array())
|
||||
{
|
||||
if(($e=$this->guard())!==null){return $e;}
|
||||
try{$active=Model::toggle(isset($params['id'])?$params['id']:0,Auth::id());}catch(\InvalidArgumentException $e){return $this->error($e->getMessage(),array(),422);}
|
||||
return $this->success($active?'Пользователь включён':'Пользователь отключён');
|
||||
}
|
||||
|
||||
public function toggle(array $params=array()){if(($e=$this->guard())!==null){return $e;}return $this->success(Model::toggle(isset($params['id'])?$params['id']:0)?'Пользователь включён':'Пользователь отключён');}
|
||||
public function customer(array $params=array())
|
||||
{
|
||||
if(!Permission::check('view_customers')){return $this->error('Недостаточно прав',array(),403);}
|
||||
$customer=Model::customer(isset($params['id'])?$params['id']:0);
|
||||
$customer=Model::customer(isset($params['id'])?$params['id']:0,Auth::id());
|
||||
return $customer?$this->success('',array('data'=>$customer)):$this->error('Пользователь не найден',array(),404);
|
||||
}
|
||||
|
||||
@@ -46,11 +52,28 @@
|
||||
{
|
||||
if(($e=$this->guard())!==null){return $e;}
|
||||
$id=isset($params['id'])?(int)$params['id']:0;$input=Request::postAll();
|
||||
if(!Permission::check('manage_users')){$current=Model::customer($id);$input['admin_access']=!empty($current['system']['is_active'])?'1':'';$input['admin_role']=!empty($current['system']['role'])?(string)$current['system']['role']:'manager';}
|
||||
if(!Permission::check('manage_users')){$current=Model::customer($id,Auth::id());$input['admin_access']=!empty($current['system']['is_active'])?'1':'';$input['admin_role']=!empty($current['system']['role'])?(string)$current['system']['role']:'manager';}
|
||||
try{$customer=Model::updateCustomer($id,$input,Auth::id());}catch(\InvalidArgumentException $e){return $this->error($e->getMessage(),array(),422);}catch(\Throwable $e){return $this->error('Не удалось сохранить пользователя',array(),500);}
|
||||
return $this->success('Профиль пользователя сохранён',array('data'=>$customer));
|
||||
}
|
||||
|
||||
public function deleteCustomer(array $params = array())
|
||||
{
|
||||
if (($e = $this->guard()) !== null) {
|
||||
return $e;
|
||||
}
|
||||
|
||||
try {
|
||||
Model::deleteCustomer(isset($params['id']) ? $params['id'] : 0, Auth::id());
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return $this->error($e->getMessage(), array(), 422);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error('Не удалось удалить пользователя', array(), 500);
|
||||
}
|
||||
|
||||
return $this->success('Пользователь удалён', array('reload' => true));
|
||||
}
|
||||
|
||||
public function saveField(array $params=array()){if(($e=$this->guard())!==null){return $e;}try{$id=Model::saveField(isset($params['id'])?$params['id']:0,Request::postAll());}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}return $this->success('Поле сохранено',array('data'=>array('id'=>$id),'reload'=>true));}
|
||||
public function deleteField(array $params=array()){if(($e=$this->guard())!==null){return $e;}Model::deleteField(isset($params['id'])?$params['id']:0);return $this->success('Поле удалено',array('reload'=>true));}
|
||||
public function toggleField(array $params=array()){if(($e=$this->guard())!==null){return $e;}$active=Model::toggleField(isset($params['id'])?$params['id']:0);return $this->success($active?'Поле включено':'Поле скрыто',array('data'=>array('is_active'=>$active?1:0)));}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
use App\Adminx\Support\Roles;
|
||||
use App\Common\Auth\IdentityLinker;
|
||||
use App\Common\ModuleManager;
|
||||
use App\Common\SystemTables;
|
||||
use App\Content\PublicUserTables;
|
||||
use App\Content\ContentTables;
|
||||
use App\Common\PublicAuthSettings;
|
||||
@@ -43,15 +44,24 @@
|
||||
$table=self::table('users');return array('total'=>(int)DB::query('SELECT COUNT(*) FROM '.$table.' WHERE deleted!=%s','1')->getValue(),'active'=>(int)DB::query('SELECT COUNT(*) FROM '.$table.' WHERE deleted!=%s AND status=%s','1','1')->getValue(),'verified'=>(int)DB::query('SELECT COUNT(*) FROM '.$table.' WHERE deleted!=%s AND (email_verified_at>0 OR phone_verified_at>0)','1')->getValue(),'fields'=>count(self::fields()));
|
||||
}
|
||||
|
||||
public static function toggle($id)
|
||||
public static function toggle($id, $currentSystemId = 0)
|
||||
{
|
||||
$customer = self::customer($id, $currentSystemId);
|
||||
if (!$customer) {
|
||||
throw new \InvalidArgumentException('Пользователь не найден');
|
||||
}
|
||||
|
||||
if (!empty($customer['is_current'])) {
|
||||
throw new \InvalidArgumentException('Нельзя отключить собственную учётную запись');
|
||||
}
|
||||
|
||||
DB::query('UPDATE '.self::table('users')." SET status=IF(status='1','0','1') WHERE Id=%i AND deleted!=%s",(int)$id,'1');
|
||||
$active=(string)DB::query('SELECT status FROM '.self::table('users').' WHERE Id=%i',(int)$id)->getValue()==='1';
|
||||
if(!$active){self::invalidateCustomerSessions((int)$id);}
|
||||
return $active;
|
||||
}
|
||||
|
||||
public static function customer($id)
|
||||
public static function customer($id, $currentSystemId = 0)
|
||||
{
|
||||
$row=DB::query('SELECT Id AS id,email,email_verified_at,firstname,lastname,user_name,phone,phone_normalized,phone_verified_at,company,city,street,street_nr,zipcode,birthday,description,user_group,status,reg_time,last_visit FROM '.self::table('users').' WHERE Id=%i AND deleted!=%s LIMIT 1',(int)$id,'1')->getAssoc();
|
||||
if(!$row){return null;}
|
||||
@@ -63,14 +73,43 @@
|
||||
$system=IdentityLinker::systemForPublic((int)$id,(string)$row['email']);
|
||||
return array('user'=>$row,'extra'=>$extra,'system'=>$system?array(
|
||||
'id'=>(int)$system['id'],'role'=>(string)$system['role'],'is_active'=>(int)$system['is_active'],
|
||||
):null);
|
||||
):null,'is_current'=>$system&&(int)$system['id']===(int)$currentSystemId);
|
||||
}
|
||||
|
||||
public static function publicIdForSystem($systemId)
|
||||
{
|
||||
$system = DB::query(
|
||||
'SELECT * FROM ' . SystemTables::table('users') . ' WHERE id = %i LIMIT 1',
|
||||
(int) $systemId
|
||||
)->getAssoc();
|
||||
if (!$system) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$public = IdentityLinker::publicForSystem((array) $system);
|
||||
return $public ? (int) $public['Id'] : 0;
|
||||
}
|
||||
|
||||
public static function updateCustomer($id,array $input,$currentSystemId=0)
|
||||
{
|
||||
$id=(int)$id;
|
||||
$current=self::customer($id);
|
||||
$current=self::customer($id,$currentSystemId);
|
||||
if(!$current){throw new \InvalidArgumentException('Пользователь не найден');}
|
||||
if(!empty($current['is_current'])){
|
||||
$currentSystem=$current['system'];
|
||||
if(isset($input['user_group'])&&(int)$input['user_group']!==(int)$current['user']['user_group']){
|
||||
throw new \InvalidArgumentException('Нельзя изменить собственную публичную группу');
|
||||
}
|
||||
|
||||
if(isset($input['admin_role'])&&(string)$input['admin_role']!==(string)$currentSystem['role']){
|
||||
throw new \InvalidArgumentException('Нельзя изменить собственную роль в панели управления');
|
||||
}
|
||||
|
||||
$input['user_group']=(string)$current['user']['user_group'];
|
||||
$input['status']=(string)$current['user']['status']==='1'?'1':'';
|
||||
$input['admin_access']=!empty($currentSystem['is_active'])?'1':'';
|
||||
$input['admin_role']=(string)$currentSystem['role'];
|
||||
}
|
||||
|
||||
$email=mb_strtolower(trim((string)(isset($input['email'])?$input['email']:'')));
|
||||
$phoneInput=trim((string)(isset($input['phone'])?$input['phone']:''));
|
||||
@@ -132,6 +171,40 @@
|
||||
return self::customer($id);
|
||||
}
|
||||
|
||||
public static function deleteCustomer($id, $currentSystemId = 0)
|
||||
{
|
||||
$id = (int) $id;
|
||||
$current = self::customer($id, $currentSystemId);
|
||||
if (!$current) {
|
||||
throw new \InvalidArgumentException('Пользователь не найден');
|
||||
}
|
||||
|
||||
if (!empty($current['is_current'])) {
|
||||
throw new \InvalidArgumentException('Нельзя удалить собственную учётную запись');
|
||||
}
|
||||
|
||||
if (!empty($current['system']['is_active'])) {
|
||||
throw new \InvalidArgumentException('Сначала отключите пользователю доступ к панели управления');
|
||||
}
|
||||
|
||||
DB::startTransaction();
|
||||
try {
|
||||
DB::Update(self::table('users'), array(
|
||||
'status' => '0',
|
||||
'deleted' => '1',
|
||||
'del_time' => time(),
|
||||
), 'Id = %i', $id);
|
||||
self::invalidateCustomerSessions($id);
|
||||
DB::Delete(self::table('user_identities'), 'user_id = %i', $id);
|
||||
DB::commit();
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function fields()
|
||||
{
|
||||
$rows = DB::query('SELECT * FROM '.self::table('user_profile_fields').' ORDER BY position,id')->getAll() ?: array();
|
||||
|
||||
@@ -97,6 +97,16 @@
|
||||
.customers-editor-access .customers-switch-card:last-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.customers-editor-input {
|
||||
width: 100%;
|
||||
}
|
||||
.customers-editor-input .input {
|
||||
width: 100%;
|
||||
}
|
||||
.customers-switch-card.is-locked {
|
||||
opacity: 0.68;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.customers-editor-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
document.addEventListener('click', function (event) {
|
||||
var customerEdit = event.target.closest('[data-customer-edit]');
|
||||
if (customerEdit) { event.preventDefault(); self.openCustomer(customerEdit); return; }
|
||||
var customerDelete = event.target.closest('[data-customer-delete]');
|
||||
if (customerDelete) { event.preventDefault(); self.deleteCustomer(customerDelete); return; }
|
||||
if (event.target.closest('[data-field-new]')) { self.fieldForm(null); }
|
||||
var edit = event.target.closest('[data-field-edit]');
|
||||
if (edit) { self.fieldForm(JSON.parse(edit.closest('[data-field]').getAttribute('data-field'))); }
|
||||
@@ -135,6 +137,7 @@
|
||||
if (!form) { return; }
|
||||
form.reset();
|
||||
form.dataset.id = user.id || '0';
|
||||
form.dataset.current = data.is_current ? '1' : '0';
|
||||
['firstname', 'lastname', 'email', 'user_name', 'phone', 'company', 'birthday', 'description', 'city', 'street', 'street_nr', 'zipcode', 'user_group'].forEach(function (name) {
|
||||
if (form.elements[name]) { form.elements[name].value = user[name] == null ? '' : user[name]; }
|
||||
});
|
||||
@@ -145,6 +148,7 @@
|
||||
form.elements.phone_verified.checked = Number(user.phone_verified_at) > 0;
|
||||
form.elements.admin_access.checked = !!(data.system && Number(data.system.is_active) === 1);
|
||||
form.elements.admin_role.value = data.system && data.system.role ? data.system.role : 'manager';
|
||||
this.syncCurrentAccountProtection();
|
||||
this.syncAdminAccess();
|
||||
Object.keys(extra).forEach(function (id) {
|
||||
var input = form.elements['extra[' + id + ']'];
|
||||
@@ -179,9 +183,51 @@
|
||||
var form = document.querySelector('[data-customer-editor]');
|
||||
var field = form ? form.querySelector('[data-customer-admin-role]') : null;
|
||||
var toggle = form && form.elements.admin_access ? form.elements.admin_access : null;
|
||||
var isCurrent = form && form.dataset.current === '1';
|
||||
var canManage = form && form.dataset.canManageAdminAccess === '1';
|
||||
if (!field || !toggle) { return; }
|
||||
field.hidden = !toggle.checked;
|
||||
if (form.elements.admin_role) { form.elements.admin_role.disabled = !toggle.checked; }
|
||||
if (form.elements.admin_role) { form.elements.admin_role.disabled = !toggle.checked || !canManage || isCurrent; }
|
||||
},
|
||||
|
||||
syncCurrentAccountProtection: function () {
|
||||
var form = document.querySelector('[data-customer-editor]');
|
||||
var isCurrent = form && form.dataset.current === '1';
|
||||
var canManage = form && form.dataset.canManageAdminAccess === '1';
|
||||
var accountHint = form ? form.querySelector('[data-customer-account-hint]') : null;
|
||||
var adminHint = form ? form.querySelector('[data-customer-admin-hint]') : null;
|
||||
var groupHint = form ? form.querySelector('[data-customer-group-hint]') : null;
|
||||
var roleHint = form ? form.querySelector('[data-customer-admin-role-hint]') : null;
|
||||
var accountCard = form ? form.querySelector('[data-customer-account-card]') : null;
|
||||
var adminCard = form ? form.querySelector('[data-customer-admin-card]') : null;
|
||||
if (!form) { return; }
|
||||
form.elements.user_group.disabled = isCurrent;
|
||||
form.elements.status.disabled = isCurrent;
|
||||
form.elements.admin_access.disabled = isCurrent || !canManage;
|
||||
if (accountCard) { accountCard.classList.toggle('is-locked', isCurrent); }
|
||||
if (adminCard) { adminCard.classList.toggle('is-locked', isCurrent || !canManage); }
|
||||
if (accountHint) {
|
||||
accountHint.textContent = isCurrent
|
||||
? 'Собственную учётную запись нельзя отключить.'
|
||||
: 'Может входить на сайт и в личный кабинет.';
|
||||
}
|
||||
if (adminHint) {
|
||||
adminHint.textContent = isCurrent
|
||||
? 'Собственный доступ к панели нельзя отключить.'
|
||||
: (canManage
|
||||
? 'Та же учётная запись сможет работать в административном интерфейсе.'
|
||||
: 'Изменение требует права управления системными пользователями.');
|
||||
}
|
||||
if (groupHint) {
|
||||
groupHint.textContent = isCurrent
|
||||
? 'Собственную публичную группу изменяйте через другого администратора.'
|
||||
: 'Определяет права пользователя на публичной части сайта.';
|
||||
}
|
||||
if (roleHint) {
|
||||
roleHint.textContent = isCurrent
|
||||
? 'Собственную роль изменяйте через другого администратора.'
|
||||
: 'Права роли настраиваются в разделе «Роли и права».';
|
||||
}
|
||||
},
|
||||
|
||||
formatTimestamp: function (value) {
|
||||
@@ -206,6 +252,22 @@
|
||||
});
|
||||
},
|
||||
|
||||
deleteCustomer: function (button) {
|
||||
var self = this;
|
||||
Adminx.Confirm.open({
|
||||
kind: 'error',
|
||||
title: 'Удалить пользователя сайта?',
|
||||
message: 'Аккаунт будет отключён, а его публичные сессии завершены. История заказов и связанные записи сохранятся.',
|
||||
confirmLabel: 'Удалить',
|
||||
confirmClass: 'btn-danger',
|
||||
onConfirm: function () {
|
||||
self.request(button.getAttribute('data-url'), new FormData()).then(function () {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
toggleField: function (input) {
|
||||
var self = this;
|
||||
var item = input.closest('[data-field-id]');
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||||
|
||||
return array(
|
||||
'code' => 'customers', 'name' => 'Пользователи сайта', 'version' => '0.3.0',
|
||||
'code' => 'customers', 'name' => 'Пользователи сайта', 'version' => '0.3.1',
|
||||
'permissions' => array('key' => 'customers', 'items' => array(
|
||||
array(
|
||||
'code' => 'view_customers',
|
||||
@@ -48,6 +48,7 @@
|
||||
array('GET', '/system/customers', array(\App\Adminx\Customers\Controller::class, 'index')),
|
||||
array('GET', '/system/customers/users/{id}', array(\App\Adminx\Customers\Controller::class, 'customer')),
|
||||
array('POST', '/system/customers/users/{id}', array(\App\Adminx\Customers\Controller::class, 'updateCustomer')),
|
||||
array('POST', '/system/customers/users/{id}/delete', array(\App\Adminx\Customers\Controller::class, 'deleteCustomer')),
|
||||
array('POST', '/system/customers/{id}/toggle', array(\App\Adminx\Customers\Controller::class, 'toggle')),
|
||||
array('POST', '/system/customers/fields/reorder', array(\App\Adminx\Customers\Controller::class, 'reorderFields')),
|
||||
array('POST', '/system/customers/fields/{id}', array(\App\Adminx\Customers\Controller::class, 'saveField')),
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<div class="card ax-list-card">
|
||||
<div class="ax-list-head"><div class="ax-list-title"><span class="icon-tile" style="--tile-bg:var(--green-100);--tile-fg:var(--green-600)"><i class="ti ti-users-group"></i></span><div><h2>Публичные аккаунты</h2><p class="text-secondary">Посетители, зарегистрированные на сайте, их контакты и доступ к личному кабинету.</p></div></div><span class="badge badge-blue">{{ customers|length }}</span></div>
|
||||
<div class="table-scroll"><table class="table customers-table"><thead><tr><th>ID</th><th>Пользователь</th><th>Контакты</th><th>Компания</th><th>Регистрация</th><th>Статус</th>{% if can_manage %}<th class="customers-actions-col">Действия</th>{% endif %}</tr></thead><tbody>
|
||||
{% for customer in customers %}<tr data-customer-row data-id="{{ customer.id }}"><td class="mono text-muted">{{ customer.id }}</td><td data-customer-name><b>{{ (customer.firstname ~ ' ' ~ customer.lastname)|trim ?: customer.user_name }}</b><small class="mono">{{ customer.user_name }}</small></td><td data-customer-contacts><span>{{ customer.email ?: customer.phone }}</span>{% if customer.email and customer.phone %}<small>{{ customer.phone }}</small>{% endif %}</td><td data-customer-company>{{ customer.company ?: '—' }}</td><td class="mono text-sm">{{ customer.reg_time ? customer.reg_time|date('d.m.Y') : '—' }}</td><td>{% if can_manage %}<label class="switch" data-tooltip="Изменить активность"><input type="checkbox" data-customer-toggle data-url="{{ ADMINX_BASE }}/system/customers/{{ customer.id }}/toggle"{{ customer.status=='1'?' checked':'' }}><span></span></label>{% else %}<span class="badge {{ customer.status=='1'?'badge-green':'badge-gray' }}">{{ customer.status=='1'?'Активен':'Отключён' }}</span>{% endif %}</td>{% if can_manage %}<td><button class="btn btn-icon btn-ghost ax-act ax-act-edit" type="button" data-customer-edit data-url="{{ ADMINX_BASE }}/system/customers/users/{{ customer.id }}" data-tooltip="Редактировать" aria-label="Редактировать"><i class="ti ti-pencil"></i></button></td>{% endif %}</tr>
|
||||
{% for customer in customers %}<tr data-customer-row data-id="{{ customer.id }}"><td class="mono text-muted">{{ customer.id }}</td><td data-customer-name><b>{{ (customer.firstname ~ ' ' ~ customer.lastname)|trim ?: customer.user_name }}</b><small class="mono">{{ customer.user_name }}</small></td><td data-customer-contacts><span>{{ customer.email ?: customer.phone }}</span>{% if customer.email and customer.phone %}<small>{{ customer.phone }}</small>{% endif %}</td><td data-customer-company>{{ customer.company ?: '—' }}</td><td class="mono text-sm">{{ customer.reg_time ? customer.reg_time|date('d.m.Y') : '—' }}</td><td>{% if can_manage %}<label class="switch" data-tooltip="{{ customer.id==current_public_user_id ? 'Собственную учётную запись нельзя отключить' : 'Изменить активность' }}"><input type="checkbox" data-customer-toggle data-url="{{ ADMINX_BASE }}/system/customers/{{ customer.id }}/toggle"{{ customer.status=='1'?' checked':'' }}{{ customer.id==current_public_user_id?' disabled':'' }}><span></span></label>{% else %}<span class="badge {{ customer.status=='1'?'badge-green':'badge-gray' }}">{{ customer.status=='1'?'Активен':'Отключён' }}</span>{% endif %}</td>{% if can_manage %}<td><button class="btn btn-icon btn-ghost ax-act ax-act-edit" type="button" data-customer-edit data-url="{{ ADMINX_BASE }}/system/customers/users/{{ customer.id }}" data-tooltip="Редактировать" aria-label="Редактировать"><i class="ti ti-pencil"></i></button>{% if customer.id==current_public_user_id %}<button class="btn btn-icon btn-ghost ax-act" type="button" disabled data-tooltip="Собственную учётную запись удалить нельзя" aria-label="Удаление недоступно"><i class="ti ti-lock"></i></button>{% else %}<button class="btn btn-icon btn-ghost ax-act ax-act-danger" type="button" data-customer-delete data-url="{{ ADMINX_BASE }}/system/customers/users/{{ customer.id }}/delete" data-tooltip="Удалить" aria-label="Удалить"><i class="ti ti-trash"></i></button>{% endif %}</td>{% endif %}</tr>
|
||||
{% else %}<tr><td colspan="{{ can_manage ? 7 : 6 }}"><div class="empty-state">Пользователи не найдены</div></td></tr>{% endfor %}
|
||||
</tbody></table></div>
|
||||
</div>
|
||||
@@ -173,27 +173,27 @@
|
||||
{% if can_manage %}
|
||||
<aside class="drawer drawer-right customer-editor-drawer" id="customerEditorDrawer" role="dialog" aria-modal="true" aria-labelledby="customerEditorTitle" hidden>
|
||||
<div class="drawer-header"><div><h3 id="customerEditorTitle" data-customer-editor-title>Пользователь сайта</h3><p>Профиль, доступ и данные личного кабинета</p></div><button class="btn btn-icon btn-ghost" type="button" data-close-drawer aria-label="Закрыть"><i class="ti ti-x"></i></button></div>
|
||||
<form data-customer-editor data-base="{{ ADMINX_BASE }}">
|
||||
<form data-customer-editor data-base="{{ ADMINX_BASE }}" data-can-manage-admin-access="{{ can_manage_admin_access ? '1' : '0' }}">
|
||||
<div class="drawer-body">
|
||||
<div class="customers-editor-meta"><span><i class="ti ti-id"></i><b data-customer-meta-id>#—</b></span><span><i class="ti ti-user-plus"></i><b data-customer-meta-created>—</b><small>Регистрация</small></span><span><i class="ti ti-clock"></i><b data-customer-meta-visit>—</b><small>Последний вход</small></span></div>
|
||||
<div class="customers-editor-sections">
|
||||
<section class="customers-form-section"><div class="customers-form-section-head"><span class="icon-tile" style="--tile-bg:var(--blue-100);--tile-fg:var(--blue-600)"><i class="ti ti-user"></i></span><div><b>Основные данные</b><span>Имя, контакты и данные организации.</span></div></div><div class="form-grid"><label class="field col-6"><span class="field-label">Имя</span><input class="input" name="firstname" maxlength="50"></label><label class="field col-6"><span class="field-label">Фамилия</span><input class="input" name="lastname" maxlength="50"></label><label class="field col-6"><span class="field-label">Email</span><input class="input" type="email" name="email" maxlength="100" required></label><label class="field col-6"><span class="field-label">Логин</span><input class="input mono" name="user_name" maxlength="50" required><span class="field-hint">Можно использовать для входа наравне с email.</span></label><label class="field col-6"><span class="field-label">Телефон</span><input class="input" type="tel" name="phone" maxlength="50"></label><label class="field col-6"><span class="field-label">Компания</span><input class="input" name="company" maxlength="255"></label><label class="field col-4"><span class="field-label">Дата рождения</span><input class="input" type="date" name="birthday"></label><label class="field col-8"><span class="field-label">О пользователе</span><textarea class="textarea" name="description" rows="2"></textarea></label></div></section>
|
||||
<section class="customers-form-section"><div class="customers-form-section-head"><span class="icon-tile" style="--tile-bg:var(--cyan-100);--tile-fg:var(--cyan-600)"><i class="ti ti-map-pin"></i></span><div><b>Адрес</b><span>Данные для профиля и предзаполнения заказов.</span></div></div><div class="form-grid"><label class="field col-4"><span class="field-label">Город</span><input class="input" name="city"></label><label class="field col-4"><span class="field-label">Улица</span><input class="input" name="street"></label><label class="field col-2"><span class="field-label">Дом</span><input class="input" name="street_nr"></label><label class="field col-2"><span class="field-label">Индекс</span><input class="input" name="zipcode"></label></div></section>
|
||||
<section class="customers-form-section"><div class="customers-form-section-head"><span class="icon-tile" style="--tile-bg:var(--blue-100);--tile-fg:var(--blue-600)"><i class="ti ti-user"></i></span><div><b>Основные данные</b><span>Имя, контакты и данные организации.</span></div></div><div class="form-grid"><label class="field col-6"><span class="field-label">Имя</span><span class="input-wrap customers-editor-input"><i class="ti ti-user"></i><input class="input" name="firstname" maxlength="50"></span></label><label class="field col-6"><span class="field-label">Фамилия</span><span class="input-wrap customers-editor-input"><i class="ti ti-user"></i><input class="input" name="lastname" maxlength="50"></span></label><label class="field col-6"><span class="field-label">Email</span><span class="input-wrap customers-editor-input"><i class="ti ti-mail"></i><input class="input" type="email" name="email" maxlength="100" required></span></label><label class="field col-6"><span class="field-label">Логин</span><span class="input-wrap customers-editor-input"><i class="ti ti-at"></i><input class="input mono" name="user_name" maxlength="50" required></span><span class="field-hint">Можно использовать для входа наравне с email.</span></label><label class="field col-6"><span class="field-label">Телефон</span><span class="input-wrap customers-editor-input"><i class="ti ti-phone"></i><input class="input" type="tel" name="phone" maxlength="50"></span></label><label class="field col-6"><span class="field-label">Компания</span><span class="input-wrap customers-editor-input"><i class="ti ti-building"></i><input class="input" name="company" maxlength="255"></span></label><label class="field col-4"><span class="field-label">Дата рождения</span><span class="input-wrap customers-editor-input"><i class="ti ti-calendar"></i><input class="input" type="date" name="birthday"></span></label><label class="field col-8"><span class="field-label">О пользователе</span><textarea class="textarea" name="description" rows="2"></textarea></label></div></section>
|
||||
<section class="customers-form-section"><div class="customers-form-section-head"><span class="icon-tile" style="--tile-bg:var(--cyan-100);--tile-fg:var(--cyan-600)"><i class="ti ti-map-pin"></i></span><div><b>Адрес</b><span>Данные для профиля и предзаполнения заказов.</span></div></div><div class="form-grid"><label class="field col-4"><span class="field-label">Город</span><span class="input-wrap customers-editor-input"><i class="ti ti-building-community"></i><input class="input" name="city"></span></label><label class="field col-4"><span class="field-label">Улица</span><span class="input-wrap customers-editor-input"><i class="ti ti-road"></i><input class="input" name="street"></span></label><label class="field col-2"><span class="field-label">Дом</span><span class="input-wrap customers-editor-input"><i class="ti ti-home"></i><input class="input" name="street_nr"></span></label><label class="field col-2"><span class="field-label">Индекс</span><span class="input-wrap customers-editor-input"><i class="ti ti-mailbox"></i><input class="input" name="zipcode"></span></label></div></section>
|
||||
<section class="customers-form-section">
|
||||
<div class="customers-form-section-head"><span class="icon-tile" style="--tile-bg:var(--green-100);--tile-fg:var(--green-600)"><i class="ti ti-shield-check"></i></span><div><b>Доступ</b><span>Публичная группа, состояние аккаунта и роль в панели управления.</span></div></div>
|
||||
<div class="form-grid">
|
||||
<label class="field col-6"><span class="field-label">Группа публичных пользователей</span><select class="select" name="user_group" required>{% for group in customer_groups %}<option value="{{ group.id }}">#{{ group.id }} · {{ group.name }} · {{ group.permissions_count }} разрешений</option>{% endfor %}</select></label>
|
||||
<label class="field col-6" data-customer-admin-role><span class="field-label">Роль в панели управления</span><select class="select" name="admin_role"{{ can_manage_admin_access ? '' : ' disabled' }}>{% for code, label in admin_roles %}<option value="{{ code }}">{{ label }}</option>{% endfor %}</select><span class="field-hint">Права роли настраиваются в разделе «Роли и права».</span></label>
|
||||
<label class="field col-6"><span class="field-label">Группа публичных пользователей</span><select class="select" name="user_group" required>{% for group in customer_groups %}<option value="{{ group.id }}">#{{ group.id }} · {{ group.name }} · {{ group.permissions_count }} разрешений</option>{% endfor %}</select><span class="field-hint" data-customer-group-hint>Определяет права пользователя на публичной части сайта.</span></label>
|
||||
<label class="field col-6" data-customer-admin-role><span class="field-label">Роль в панели управления</span><select class="select" name="admin_role"{{ can_manage_admin_access ? '' : ' disabled' }}>{% for code, label in admin_roles %}<option value="{{ code }}">{{ label }}</option>{% endfor %}</select><span class="field-hint" data-customer-admin-role-hint>Права роли настраиваются в разделе «Роли и права».</span></label>
|
||||
</div>
|
||||
<div class="customers-switch-grid customers-editor-access">
|
||||
<label class="customers-switch-card"><span><b>Аккаунт включён</b><small>Может входить на сайт и в личный кабинет.</small></span><span class="switch"><input type="checkbox" name="status" value="1"><span></span></span></label>
|
||||
<label class="customers-switch-card"><span><b>Доступ в панель управления</b><small>{{ can_manage_admin_access ? 'Та же учётная запись сможет работать в административном интерфейсе.' : 'Изменение требует права управления системными пользователями.' }}</small></span><span class="switch"><input type="checkbox" name="admin_access" value="1" data-customer-admin-access{{ can_manage_admin_access ? '' : ' disabled' }}><span></span></span></label>
|
||||
<label class="customers-switch-card" data-customer-account-card><span><b>Аккаунт включён</b><small data-customer-account-hint>Может входить на сайт и в личный кабинет.</small></span><span class="switch"><input type="checkbox" name="status" value="1"><span></span></span></label>
|
||||
<label class="customers-switch-card" data-customer-admin-card><span><b>Доступ в панель управления</b><small data-customer-admin-hint>{{ can_manage_admin_access ? 'Та же учётная запись сможет работать в административном интерфейсе.' : 'Изменение требует права управления системными пользователями.' }}</small></span><span class="switch"><input type="checkbox" name="admin_access" value="1" data-customer-admin-access{{ can_manage_admin_access ? '' : ' disabled' }}><span></span></span></label>
|
||||
<label class="customers-switch-card"><span><b>Email подтверждён</b><small>Администратор может изменить статус вручную.</small></span><span class="switch"><input type="checkbox" name="email_verified" value="1"><span></span></span></label>
|
||||
<label class="customers-switch-card"><span><b>Телефон подтверждён</b><small>Подготовлено для будущего SMS-гейта.</small></span><span class="switch"><input type="checkbox" name="phone_verified" value="1"><span></span></span></label>
|
||||
</div>
|
||||
</section>
|
||||
{% if fields %}<section class="customers-form-section"><div class="customers-form-section-head"><span class="icon-tile" style="--tile-bg:var(--violet-100);--tile-fg:var(--violet-600)"><i class="ti ti-forms"></i></span><div><b>Дополнительные поля</b><span>Активные поля из конструктора публичного профиля.</span></div></div><div class="form-grid">{% for field in fields %}{% if field.is_active %}<label class="field col-6"><span class="field-label">{{ field.name }}{% if field.is_required %} <span class="text-danger">*</span>{% endif %}</span>{% if field.type=='textarea' %}<textarea class="textarea" name="extra[{{ field.id }}]" rows="3"{{ field.is_required?' required':'' }}></textarea>{% elseif field.type=='select' %}<select class="select" name="extra[{{ field.id }}]"{{ field.is_required?' required':'' }}><option value="">Выберите значение</option>{% for choice in field.choices %}<option value="{{ choice|e('html_attr') }}">{{ choice }}</option>{% endfor %}</select>{% elseif field.type=='checkbox' %}<span class="customers-editor-checkbox"><span class="switch"><input type="checkbox" name="extra[{{ field.id }}]" value="1"><span></span></span><span>Да / нет</span></span>{% else %}<input class="input" type="{{ field.type in ['date','number','email','tel'] ? field.type : 'text' }}" name="extra[{{ field.id }}]"{{ field.is_required?' required':'' }}>{% endif %}<span class="field-hint mono">{{ field.code }}</span></label>{% endif %}{% endfor %}</div></section>{% endif %}
|
||||
<section class="customers-form-section"><div class="customers-form-section-head"><span class="icon-tile" style="--tile-bg:var(--red-100);--tile-fg:var(--red-600)"><i class="ti ti-key"></i></span><div><b>Безопасность</b><span>Пароль изменится, только если заполнить поле.</span></div></div><label class="field customers-editor-password"><span class="field-label">Новый пароль</span><input class="input" type="password" name="password" autocomplete="new-password" placeholder="Оставьте пустым, чтобы не менять"><span class="field-hint">При смене пароля будут отозваны remember-сессии и ссылки восстановления.</span></label></section>
|
||||
<section class="customers-form-section"><div class="customers-form-section-head"><span class="icon-tile" style="--tile-bg:var(--red-100);--tile-fg:var(--red-600)"><i class="ti ti-key"></i></span><div><b>Безопасность</b><span>Пароль изменится, только если заполнить поле.</span></div></div><label class="field customers-editor-password"><span class="field-label">Новый пароль</span><span class="input-wrap customers-editor-input"><i class="ti ti-lock"></i><input class="input" type="password" name="password" autocomplete="new-password" placeholder="Оставьте пустым, чтобы не менять"></span><span class="field-hint">При смене пароля будут отозваны remember-сессии и ссылки восстановления.</span></label></section>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="drawer-footer"><button class="btn btn-secondary" type="button" data-close-drawer>Закрыть</button><button class="btn btn-primary" type="submit"><i class="ti ti-device-floppy"></i>Сохранить</button></footer>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||||
|
||||
use App\Common\DatabaseSchema;
|
||||
use App\Common\SystemTables;
|
||||
use App\Adminx\Support\GlobalSearchRegistry;
|
||||
use App\Adminx\Support\ModuleExtensions;
|
||||
use App\Content\ContentTables;
|
||||
@@ -57,7 +58,7 @@
|
||||
self::decode($row['document_title']),
|
||||
'#' . (int) $row['Id'] . ' · /' . (string) $row['document_alias'],
|
||||
'/documents/' . (int) $row['Id'] . '/edit',
|
||||
'ti-file-text'
|
||||
'ti ti-file-text'
|
||||
);
|
||||
$item['score'] = (float) $row['search_relevance'];
|
||||
$out[] = $item;
|
||||
@@ -79,7 +80,7 @@
|
||||
)->getAll() ?: array();
|
||||
$out = array();
|
||||
foreach ($rows as $row) {
|
||||
$out[] = self::item('rubric', 'Рубрики', self::decode($row['rubric_title']), '#' . (int) $row['Id'] . ' · ' . (string) $row['rubric_alias'], '/rubrics?edit=' . (int) $row['Id'], 'ti-forms');
|
||||
$out[] = self::item('rubric', 'Рубрики', self::decode($row['rubric_title']), '#' . (int) $row['Id'] . ' · ' . (string) $row['rubric_alias'], '/rubrics?edit=' . (int) $row['Id'], 'ti ti-forms');
|
||||
}
|
||||
|
||||
return $out;
|
||||
@@ -98,7 +99,7 @@
|
||||
)->getAll() ?: array();
|
||||
$out = array();
|
||||
foreach ($rows as $row) {
|
||||
$out[] = self::item('block', 'Блоки', self::decode($row['sysblock_name']), '#' . (int) $row['id'] . ' · ' . (string) $row['sysblock_alias'], '/blocks?edit=' . (int) $row['id'], 'ti-blockquote');
|
||||
$out[] = self::item('block', 'Блоки', self::decode($row['sysblock_name']), '#' . (int) $row['id'] . ' · ' . (string) $row['sysblock_alias'], '/blocks?edit=' . (int) $row['id'], 'ti ti-blockquote');
|
||||
}
|
||||
|
||||
return $out;
|
||||
@@ -117,7 +118,7 @@
|
||||
)->getAll() ?: array();
|
||||
$out = array();
|
||||
foreach ($rows as $row) {
|
||||
$out[] = self::item('request', 'Запросы', self::decode($row['request_title']), '#' . (int) $row['Id'] . ' · ' . (string) $row['request_alias'], '/requests/' . (int) $row['Id'], 'ti-list-search');
|
||||
$out[] = self::item('request', 'Запросы', self::decode($row['request_title']), '#' . (int) $row['Id'] . ' · ' . (string) $row['request_alias'], '/requests/' . (int) $row['Id'], 'ti ti-list-search');
|
||||
}
|
||||
|
||||
return $out;
|
||||
@@ -142,7 +143,50 @@
|
||||
self::decode($row['title']),
|
||||
'#' . (int) $row['navigation_item_id'] . ' · ' . (string) $row['alias'],
|
||||
'/navigation?navigation=' . (int) $row['navigation_id'] . '&item=' . (int) $row['navigation_item_id'],
|
||||
'ti-menu-2'
|
||||
'ti ti-menu-2'
|
||||
);
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
public static function users($query, $limit = 6)
|
||||
{
|
||||
$table = SystemTables::table('users');
|
||||
if (!self::available($table)) { return array(); }
|
||||
// У таблицы пользователей смешанные коллации (email обычно ascii,
|
||||
// name/login — utf8mb4), поэтому приводим сравниваемые колонки к
|
||||
// charset соединения (utf8 = utf8mb3, см. dbchar): иначе
|
||||
// кириллический запрос роняет провайдер на «illegal mix of
|
||||
// collations». Ведущий подстановочный знак и так исключает
|
||||
// использование индекса, так что CONVERT ничего не стоит.
|
||||
$rows = DB::query(
|
||||
'SELECT id,name,login,email,is_active FROM ' . $table
|
||||
. ' WHERE CONVERT(name USING utf8) LIKE %ss'
|
||||
. ' OR CONVERT(login USING utf8) LIKE %ss'
|
||||
. ' OR CONVERT(email USING utf8) LIKE %ss OR id=%i'
|
||||
. ' ORDER BY is_active DESC,name LIMIT ' . max(1, min(12, (int) $limit)),
|
||||
$query,
|
||||
$query,
|
||||
$query,
|
||||
(int) $query
|
||||
)->getAll() ?: array();
|
||||
$out = array();
|
||||
foreach ($rows as $row) {
|
||||
$name = trim((string) $row['name']);
|
||||
$login = trim((string) $row['login']);
|
||||
$email = trim((string) $row['email']);
|
||||
$title = $name !== '' ? $name : ($login !== '' ? '@' . $login : $email);
|
||||
$parts = array('#' . (int) $row['id']);
|
||||
if ($email !== '') { $parts[] = $email; }
|
||||
if (empty($row['is_active'])) { $parts[] = 'выключен'; }
|
||||
$out[] = self::item(
|
||||
'user',
|
||||
'Пользователи',
|
||||
self::decode($title !== '' ? $title : ('#' . (int) $row['id'])),
|
||||
implode(' · ', $parts),
|
||||
'/users/' . (int) $row['id'],
|
||||
'ti ti-user'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -168,6 +212,7 @@
|
||||
'blocks' => array('provider' => array(self::class, 'blocks'), 'permission' => 'view_blocks', 'priority' => 30, 'limit' => 6),
|
||||
'requests' => array('provider' => array(self::class, 'requests'), 'permission' => 'view_requests', 'priority' => 40, 'limit' => 6),
|
||||
'navigation' => array('provider' => array(self::class, 'navigation'), 'permission' => 'view_navigation', 'priority' => 50, 'limit' => 5),
|
||||
'users' => array('provider' => array(self::class, 'users'), 'permission' => 'view_users', 'priority' => 60, 'limit' => 6),
|
||||
) as $code => $definition) {
|
||||
GlobalSearchRegistry::register('core.' . $code, $definition);
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
'parent_dir' => Model::parentDir($file['path']),
|
||||
'can_manage' => Permission::check('manage_media'),
|
||||
'supports_webp' => Model::supportsWebp(),
|
||||
'presets' => ImagePresets::options(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -261,6 +262,7 @@
|
||||
'height' => Request::postInt('height', 240),
|
||||
'format' => Request::postStr('format', 'original'),
|
||||
'quality' => Request::postInt('quality', 82),
|
||||
'webp_twin' => Request::postInt('webp_twin', 0),
|
||||
'crop_enabled' => Request::postStr('crop_enabled', ''),
|
||||
'crop_x' => Request::postInt('crop_x', 0),
|
||||
'crop_y' => Request::postInt('crop_y', 0),
|
||||
@@ -339,6 +341,53 @@
|
||||
));
|
||||
}
|
||||
|
||||
public function presets(array $params = array())
|
||||
{
|
||||
if (!Permission::check('view_media')) {
|
||||
return $this->error('Недостаточно прав', array(), 403);
|
||||
}
|
||||
|
||||
return $this->success('', array('data' => array(
|
||||
'items' => ImagePresets::all(),
|
||||
'modes' => ImagePresets::modes(),
|
||||
'formats' => ImagePresets::formats(),
|
||||
'can_manage' => Permission::check('manage_media'),
|
||||
)));
|
||||
}
|
||||
|
||||
public function storePreset(array $params = array())
|
||||
{
|
||||
if (($err = $this->guard()) !== null) { return $err; }
|
||||
list($errors, $data) = ImagePresets::validate(Request::postAll(), 0);
|
||||
if (!empty($errors)) { return $this->error('Проверьте поля', $errors, 422); }
|
||||
|
||||
$id = ImagePresets::save(0, $data);
|
||||
return $this->success('Вид создан', array('data' => array('id' => $id)));
|
||||
}
|
||||
|
||||
public function updatePreset(array $params = array())
|
||||
{
|
||||
if (($err = $this->guard()) !== null) { return $err; }
|
||||
$id = isset($params['id']) ? (int) $params['id'] : 0;
|
||||
if (!ImagePresets::one($id)) { return $this->error('Вид не найден', array(), 404); }
|
||||
|
||||
list($errors, $data) = ImagePresets::validate(Request::postAll(), $id);
|
||||
if (!empty($errors)) { return $this->error('Проверьте поля', $errors, 422); }
|
||||
|
||||
ImagePresets::save($id, $data);
|
||||
return $this->success('Вид сохранён', array('data' => array('id' => $id)));
|
||||
}
|
||||
|
||||
public function deletePreset(array $params = array())
|
||||
{
|
||||
if (($err = $this->guard()) !== null) { return $err; }
|
||||
if (!ImagePresets::delete(isset($params['id']) ? (int) $params['id'] : 0)) {
|
||||
return $this->error('Вид не найден', array(), 404);
|
||||
}
|
||||
|
||||
return $this->success('Вид удалён');
|
||||
}
|
||||
|
||||
protected function guard()
|
||||
{
|
||||
return $this->guardPermission('manage_media');
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------------
|
||||
| AVE.cms
|
||||
|--------------------------------------------------------------------------------------
|
||||
| @package AVE.cms
|
||||
| @file adminx/modules/Media/ImagePresets.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\Media;
|
||||
|
||||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||||
|
||||
use App\Common\DatabaseSchema;
|
||||
use App\Content\ContentTables;
|
||||
use DB;
|
||||
|
||||
/**
|
||||
* Справочник «видов изображений»: именованные наборы параметров обрезки
|
||||
* (режим + размер + формат + качество), которые подставляются в редактор
|
||||
* медиа и привязываются к полям-картинкам рубрик.
|
||||
*/
|
||||
class ImagePresets
|
||||
{
|
||||
const MAX_SIDE = 2400;
|
||||
|
||||
public static function table()
|
||||
{
|
||||
return ContentTables::table('media_image_presets');
|
||||
}
|
||||
|
||||
public static function available()
|
||||
{
|
||||
try { return DatabaseSchema::tableExists(self::table()); }
|
||||
catch (\Throwable $e) { return false; }
|
||||
}
|
||||
|
||||
public static function modes()
|
||||
{
|
||||
return array(
|
||||
'cover' => 'Обрезать под размер',
|
||||
'contain' => 'Вписать целиком',
|
||||
'fit' => 'Уменьшить (без обрезки)',
|
||||
'stretch' => 'Растянуть',
|
||||
);
|
||||
}
|
||||
|
||||
public static function formats()
|
||||
{
|
||||
return array(
|
||||
'original' => 'Как есть',
|
||||
'jpg' => 'JPG',
|
||||
'png' => 'PNG',
|
||||
'webp' => 'WebP',
|
||||
);
|
||||
}
|
||||
|
||||
public static function all()
|
||||
{
|
||||
if (!self::available()) { return array(); }
|
||||
$rows = DB::query(
|
||||
'SELECT * FROM ' . self::table() . ' ORDER BY sort_order ASC, title ASC, id ASC'
|
||||
)->getAll() ?: array();
|
||||
|
||||
return array_map(array(__CLASS__, 'format'), $rows);
|
||||
}
|
||||
|
||||
public static function one($id)
|
||||
{
|
||||
if (!self::available()) { return null; }
|
||||
$row = DB::query('SELECT * FROM ' . self::table() . ' WHERE id=%i LIMIT 1', (int) $id)->getAssoc();
|
||||
return $row ? self::format($row) : null;
|
||||
}
|
||||
|
||||
public static function byCode($code)
|
||||
{
|
||||
$code = self::slug($code);
|
||||
if ($code === '' || !self::available()) { return null; }
|
||||
$row = DB::query('SELECT * FROM ' . self::table() . ' WHERE code=%s LIMIT 1', $code)->getAssoc();
|
||||
return $row ? self::format($row) : null;
|
||||
}
|
||||
|
||||
public static function count()
|
||||
{
|
||||
if (!self::available()) { return 0; }
|
||||
return (int) DB::query('SELECT COUNT(*) FROM ' . self::table())->getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить и нормализовать вход. Возвращает [errors, data].
|
||||
*/
|
||||
public static function validate(array $input, $id = 0)
|
||||
{
|
||||
$errors = array();
|
||||
$title = trim((string) (isset($input['title']) ? $input['title'] : ''));
|
||||
if ($title === '') { $errors['title'] = 'Укажите название'; }
|
||||
|
||||
$code = self::slug(isset($input['code']) && trim((string) $input['code']) !== '' ? $input['code'] : $title);
|
||||
if ($code === '') { $errors['code'] = 'Не удалось построить код из названия'; }
|
||||
|
||||
$mode = (string) (isset($input['mode']) ? $input['mode'] : 'cover');
|
||||
if (!array_key_exists($mode, self::modes())) { $errors['mode'] = 'Неизвестный режим'; }
|
||||
|
||||
$format = (string) (isset($input['format']) ? $input['format'] : 'original');
|
||||
if (!array_key_exists($format, self::formats())) { $errors['format'] = 'Неизвестный формат'; }
|
||||
|
||||
$width = (int) (isset($input['width']) ? $input['width'] : 0);
|
||||
$height = (int) (isset($input['height']) ? $input['height'] : 0);
|
||||
foreach (array('width' => $width, 'height' => $height) as $key => $value) {
|
||||
if ($value < 0 || $value > self::MAX_SIDE) {
|
||||
$errors[$key] = 'Размер от 0 до ' . self::MAX_SIDE;
|
||||
}
|
||||
}
|
||||
|
||||
// «Вписать/растянуть/обрезать» требуют обе стороны, «уменьшить» — хотя бы одну.
|
||||
if ($mode === 'fit') {
|
||||
if ($width < 1 && $height < 1) { $errors['width'] = 'Для «уменьшить» задайте ширину или высоту'; }
|
||||
} elseif ($width < 1 || $height < 1) {
|
||||
$errors['width'] = 'Задайте ширину и высоту';
|
||||
}
|
||||
|
||||
$quality = (int) (isset($input['quality']) ? $input['quality'] : 82);
|
||||
if ($quality < 1 || $quality > 100) { $errors['quality'] = 'Качество 1–100'; }
|
||||
|
||||
if (empty($errors) && self::codeExists($code, (int) $id)) {
|
||||
$errors['code'] = 'Вид с таким кодом уже есть';
|
||||
}
|
||||
|
||||
return array($errors, array(
|
||||
'code' => $code,
|
||||
'title' => mb_substr($title, 0, 190, 'UTF-8'),
|
||||
'group_label' => mb_substr(trim((string) (isset($input['group_label']) ? $input['group_label'] : '')), 0, 120, 'UTF-8'),
|
||||
'mode' => $mode,
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
'format' => $format,
|
||||
'quality' => $quality,
|
||||
'webp_twin' => empty($input['webp_twin']) ? 0 : 1,
|
||||
'sort_order' => max(0, min(9999, (int) (isset($input['sort_order']) ? $input['sort_order'] : 100))),
|
||||
));
|
||||
}
|
||||
|
||||
public static function save($id, array $data)
|
||||
{
|
||||
$id = (int) $id;
|
||||
$now = time();
|
||||
if ($id > 0) {
|
||||
$data['updated_at'] = $now;
|
||||
DB::Update(self::table(), $data, 'id=%i', $id);
|
||||
return $id;
|
||||
}
|
||||
|
||||
$data['created_at'] = $now;
|
||||
$data['updated_at'] = $now;
|
||||
DB::Insert(self::table(), $data);
|
||||
return (int) DB::insertId();
|
||||
}
|
||||
|
||||
public static function delete($id)
|
||||
{
|
||||
if (!self::available()) { return false; }
|
||||
$exists = (int) DB::query('SELECT COUNT(*) FROM ' . self::table() . ' WHERE id=%i', (int) $id)->getValue();
|
||||
if ($exists < 1) { return false; }
|
||||
DB::Delete(self::table(), 'id=%i', (int) $id);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Опции для выпадающих списков (редактор, настройки поля). */
|
||||
public static function options()
|
||||
{
|
||||
$out = array();
|
||||
foreach (self::all() as $preset) {
|
||||
$out[] = array(
|
||||
'code' => $preset['code'],
|
||||
'title' => $preset['title'],
|
||||
'group_label' => $preset['group_label'],
|
||||
'mode' => $preset['mode'],
|
||||
'width' => $preset['width'],
|
||||
'height' => $preset['height'],
|
||||
'format' => $preset['format'],
|
||||
'quality' => $preset['quality'],
|
||||
'webp_twin' => $preset['webp_twin'],
|
||||
'label' => $preset['label'],
|
||||
);
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
protected static function codeExists($code, $exceptId = 0)
|
||||
{
|
||||
if (!self::available()) { return false; }
|
||||
return (bool) DB::query(
|
||||
'SELECT id FROM ' . self::table() . ' WHERE code=%s AND id!=%i LIMIT 1',
|
||||
self::slug($code),
|
||||
(int) $exceptId
|
||||
)->getValue();
|
||||
}
|
||||
|
||||
protected static function format(array $row)
|
||||
{
|
||||
$modes = self::modes();
|
||||
$width = (int) (isset($row['width']) ? $row['width'] : 0);
|
||||
$height = (int) (isset($row['height']) ? $row['height'] : 0);
|
||||
$mode = (string) (isset($row['mode']) ? $row['mode'] : 'cover');
|
||||
$size = $width && $height ? ($width . '×' . $height) : ($width ? ($width . '×авто') : ($height ? ('авто×' . $height) : 'авто'));
|
||||
return array(
|
||||
'id' => (int) $row['id'],
|
||||
'code' => (string) $row['code'],
|
||||
'title' => (string) $row['title'],
|
||||
'group_label' => (string) (isset($row['group_label']) ? $row['group_label'] : ''),
|
||||
'mode' => $mode,
|
||||
'mode_label' => isset($modes[$mode]) ? $modes[$mode] : $mode,
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
'format' => (string) (isset($row['format']) ? $row['format'] : 'original'),
|
||||
'quality' => (int) (isset($row['quality']) ? $row['quality'] : 82),
|
||||
'webp_twin' => (int) (isset($row['webp_twin']) ? $row['webp_twin'] : 1),
|
||||
'sort_order' => (int) (isset($row['sort_order']) ? $row['sort_order'] : 100),
|
||||
'label' => $row['title'] . ' · ' . $size,
|
||||
);
|
||||
}
|
||||
|
||||
protected static function slug($value)
|
||||
{
|
||||
$value = mb_strtolower(trim((string) $value), 'UTF-8');
|
||||
$map = array(
|
||||
'а'=>'a','б'=>'b','в'=>'v','г'=>'g','д'=>'d','е'=>'e','ё'=>'e','ж'=>'zh','з'=>'z','и'=>'i','й'=>'y',
|
||||
'к'=>'k','л'=>'l','м'=>'m','н'=>'n','о'=>'o','п'=>'p','р'=>'r','с'=>'s','т'=>'t','у'=>'u','ф'=>'f',
|
||||
'х'=>'h','ц'=>'c','ч'=>'ch','ш'=>'sh','щ'=>'sch','ъ'=>'','ы'=>'y','ь'=>'','э'=>'e','ю'=>'yu','я'=>'ya',
|
||||
);
|
||||
$value = strtr($value, $map);
|
||||
$value = preg_replace('/[^a-z0-9]+/', '-', $value);
|
||||
return substr(trim((string) $value, '-'), 0, 64);
|
||||
}
|
||||
}
|
||||
@@ -556,6 +556,15 @@
|
||||
$name = self::uniqueName($outAbsDir, self::safeName($base, false), $r['format']);
|
||||
$out = $outAbsDir . DIRECTORY_SEPARATOR . $name;
|
||||
self::saveImage($dst, $out, $r['format'], $r['quality']);
|
||||
|
||||
// WebP-двойник рядом: если вид просит и результат ещё не webp —
|
||||
// кладём тот же кадр в .webp с тем же базовым именем (foto-...-.webp),
|
||||
// чтобы сайт отдавал webp с jpg-фолбэком. Делаем до imagedestroy.
|
||||
if (!empty($input['webp_twin']) && $r['format'] !== 'webp' && self::supportsWebp()) {
|
||||
$webpName = pathinfo($name, PATHINFO_FILENAME) . '.webp';
|
||||
self::saveImage($dst, $outAbsDir . DIRECTORY_SEPARATOR . $webpName, 'webp', $r['quality']);
|
||||
}
|
||||
|
||||
imagedestroy($dst);
|
||||
return rtrim($outDir, '/') . '/' . $name;
|
||||
}
|
||||
|
||||
@@ -737,3 +737,17 @@ button.media-thumb {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.media-presets-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.media-preset-row { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 10px; background: var(--surface-2, #f6f8fb); }
|
||||
.media-preset-row .media-preset-main { flex: 1; min-width: 0; }
|
||||
.media-preset-row .media-preset-main small { color: var(--text-secondary, #667085); display: block; margin-top: 2px; }
|
||||
.media-presets-form { margin-top: 16px; }
|
||||
|
||||
.media-preset-pick { margin-bottom: 12px; }
|
||||
.media-preset-pick-field { display: flex; flex-direction: column; gap: 4px; }
|
||||
.media-preset-pick-field > span { font-size: 13px; color: var(--text-secondary, #667085); }
|
||||
.media-preset-warn { margin-top: 8px; padding: 8px 10px; border-radius: 8px; background: var(--amber-100, #fef3c7); color: var(--amber-700, #b45309); font-size: 13px; }
|
||||
|
||||
.media-preset-check { flex-direction: row; align-items: center; gap: 8px; }
|
||||
.media-preset-check > span { color: var(--text-secondary, #667085); font-size: 13px; }
|
||||
|
||||
@@ -62,7 +62,16 @@
|
||||
if (clearThumbs) { self.clearThumbnails(clearThumbs); return; }
|
||||
if (webp) { self.convertWebp(webp); return; }
|
||||
if (zoom) { e.preventDefault(); self.openResultLarge(); return; }
|
||||
if (copy) { self.copy(copy.getAttribute('data-copy')); }
|
||||
if (copy) { self.copy(copy.getAttribute('data-copy')); return; }
|
||||
|
||||
var presetsOpen = e.target.closest('[data-media-presets-open]');
|
||||
if (presetsOpen) { self.loadPresets(); return; }
|
||||
if (e.target.closest('[data-preset-save]')) { self.savePreset(); return; }
|
||||
if (e.target.closest('[data-preset-reset]')) { self.resetPresetForm(); return; }
|
||||
var presetEdit = e.target.closest('[data-preset-edit]');
|
||||
if (presetEdit) { self.editPreset(presetEdit.getAttribute('data-preset-edit')); return; }
|
||||
var presetDelete = e.target.closest('[data-preset-delete]');
|
||||
if (presetDelete) { self.deletePreset(presetDelete.getAttribute('data-preset-delete')); return; }
|
||||
});
|
||||
},
|
||||
|
||||
@@ -70,6 +79,119 @@
|
||||
return Adminx.base();
|
||||
},
|
||||
|
||||
presetData: null,
|
||||
|
||||
loadPresets: function () {
|
||||
var self = this;
|
||||
Adminx.Loader.show();
|
||||
Adminx.Ajax.request(this.base() + '/media/presets').then(function (payload) {
|
||||
Adminx.Loader.hide();
|
||||
var r = payload.data || {};
|
||||
if (!r.success || !r.data) { Adminx.Toast.show(r.message || 'Не удалось загрузить виды', 'error'); return; }
|
||||
self.presetData = r.data;
|
||||
self.fillPresetSelects(r.data.modes || {}, r.data.formats || {});
|
||||
self.renderPresets(r.data.items || []);
|
||||
}).catch(function () { Adminx.Loader.hide(); Adminx.Toast.show('Ошибка сети', 'error'); });
|
||||
},
|
||||
|
||||
fillPresetSelects: function (modes, formats) {
|
||||
var m = document.querySelector('[data-preset-modes]');
|
||||
var f = document.querySelector('[data-preset-formats]');
|
||||
if (m && !m.getAttribute('data-filled')) {
|
||||
m.innerHTML = Object.keys(modes).map(function (k) { return '<option value="' + esc(k) + '">' + esc(modes[k]) + '</option>'; }).join('');
|
||||
m.setAttribute('data-filled', '1');
|
||||
}
|
||||
if (f && !f.getAttribute('data-filled')) {
|
||||
f.innerHTML = Object.keys(formats).map(function (k) { return '<option value="' + esc(k) + '">' + esc(formats[k]) + '</option>'; }).join('');
|
||||
f.setAttribute('data-filled', '1');
|
||||
}
|
||||
},
|
||||
|
||||
renderPresets: function (items) {
|
||||
var list = document.querySelector('[data-preset-list]');
|
||||
if (!list) { return; }
|
||||
if (!items.length) { list.innerHTML = '<div class="empty-state">Виды ещё не заданы. Добавьте первый в форме ниже.</div>'; return; }
|
||||
var canManage = this.presetData && this.presetData.can_manage;
|
||||
list.innerHTML = items.map(function (item) {
|
||||
return '<div class="media-preset-row">'
|
||||
+ '<span class="icon-tile" style="--tile-bg:var(--blue-100);--tile-fg:var(--blue-600)"><i class="ti ti-aspect-ratio"></i></span>'
|
||||
+ '<div class="media-preset-main"><div><b>' + esc(item.title) + '</b>'
|
||||
+ (item.group_label ? ' <span class="badge badge-gray">' + esc(item.group_label) + '</span>' : '') + '</div>'
|
||||
+ '<small>' + esc(item.mode_label) + ' · ' + item.width + '×' + item.height + (item.format !== 'original' ? ' · ' + esc(item.format) : '') + ' · q' + item.quality + (item.webp_twin ? ' · +WebP' : '') + '</small></div>'
|
||||
+ (canManage ? '<div class="cluster"><button class="btn btn-ghost btn-icon btn-sm" type="button" data-preset-edit="' + item.id + '" data-tooltip="Изменить" aria-label="Изменить"><i class="ti ti-pencil"></i></button>'
|
||||
+ '<button class="btn btn-ghost btn-icon btn-sm media-action-danger" type="button" data-preset-delete="' + item.id + '" data-tooltip="Удалить" aria-label="Удалить"><i class="ti ti-trash"></i></button></div>' : '')
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
},
|
||||
|
||||
editPreset: function (id) {
|
||||
if (!this.presetData) { return; }
|
||||
var item = (this.presetData.items || []).filter(function (p) { return String(p.id) === String(id); })[0];
|
||||
var form = document.getElementById('mediaPresetForm');
|
||||
if (!item || !form) { return; }
|
||||
form.querySelector('[name="id"]').value = item.id;
|
||||
form.querySelector('[name="title"]').value = item.title;
|
||||
form.querySelector('[name="group_label"]').value = item.group_label || '';
|
||||
form.querySelector('[name="mode"]').value = item.mode;
|
||||
form.querySelector('[name="format"]').value = item.format;
|
||||
form.querySelector('[name="width"]').value = item.width;
|
||||
form.querySelector('[name="height"]').value = item.height;
|
||||
form.querySelector('[name="quality"]').value = item.quality;
|
||||
var twin = form.querySelector('[name="webp_twin"]');
|
||||
if (twin) { twin.checked = !!item.webp_twin; }
|
||||
form.querySelectorAll('[data-error]').forEach(function (el) { el.textContent = ''; });
|
||||
},
|
||||
|
||||
resetPresetForm: function () {
|
||||
var form = document.getElementById('mediaPresetForm');
|
||||
if (!form) { return; }
|
||||
form.reset();
|
||||
form.querySelector('[name="id"]').value = '';
|
||||
form.querySelectorAll('[data-error]').forEach(function (el) { el.textContent = ''; });
|
||||
},
|
||||
|
||||
savePreset: function () {
|
||||
var form = document.getElementById('mediaPresetForm');
|
||||
if (!form) { return; }
|
||||
var self = this;
|
||||
var id = (form.querySelector('[name="id"]').value || '').trim();
|
||||
var url = this.base() + '/media/presets' + (id ? '/' + id : '');
|
||||
form.querySelectorAll('[data-error]').forEach(function (el) { el.textContent = ''; });
|
||||
Adminx.Loader.show();
|
||||
Adminx.Ajax.post(url, new FormData(form)).then(function (payload) {
|
||||
Adminx.Loader.hide();
|
||||
var r = payload.data || {};
|
||||
if (!r.success) {
|
||||
if (r.errors) { Object.keys(r.errors).forEach(function (k) { var el = form.querySelector('[data-error="' + k + '"]'); if (el) { el.textContent = r.errors[k]; } }); }
|
||||
Adminx.Toast.show(r.message || 'Проверьте поля', 'error');
|
||||
return;
|
||||
}
|
||||
Adminx.Toast.show(r.message || 'Сохранено', 'success');
|
||||
self.resetPresetForm();
|
||||
self.loadPresets();
|
||||
}).catch(function () { Adminx.Loader.hide(); Adminx.Toast.show('Ошибка сети', 'error'); });
|
||||
},
|
||||
|
||||
deletePreset: function (id) {
|
||||
var self = this;
|
||||
Adminx.Confirm.open({
|
||||
kind: 'danger',
|
||||
title: 'Удалить вид?',
|
||||
message: 'Он исчезнет из списка подгонки. Уже обрезанные картинки не изменятся.',
|
||||
confirmLabel: 'Удалить',
|
||||
onConfirm: function () {
|
||||
Adminx.Loader.show();
|
||||
Adminx.Ajax.post(self.base() + '/media/presets/' + id + '/delete').then(function (payload) {
|
||||
Adminx.Loader.hide();
|
||||
var r = payload.data || {};
|
||||
if (!r.success) { Adminx.Toast.show(r.message || 'Не удалось удалить', 'error'); return; }
|
||||
Adminx.Toast.show(r.message || 'Удалено', 'success');
|
||||
self.loadPresets();
|
||||
}).catch(function () { Adminx.Loader.hide(); Adminx.Toast.show('Ошибка сети', 'error'); });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
modalInput: function (cfg, done) {
|
||||
var overlay = document.createElement('div');
|
||||
overlay.className = 'overlay';
|
||||
@@ -547,6 +669,52 @@
|
||||
if (ratioSelect) {
|
||||
ratioSelect.addEventListener('change', function (e) { setRatio(e.target.value); });
|
||||
}
|
||||
|
||||
var presetSelect = form.querySelector('[data-crop-preset]');
|
||||
var presetWarn = form.querySelector('[data-crop-preset-warning]');
|
||||
if (presetSelect) {
|
||||
presetSelect.addEventListener('change', function () {
|
||||
var opt = presetSelect.options[presetSelect.selectedIndex];
|
||||
var webpTwinInput = form.querySelector('[data-webp-twin]');
|
||||
if (!opt || !opt.value) {
|
||||
if (webpTwinInput) { webpTwinInput.value = '0'; }
|
||||
if (presetWarn) { presetWarn.hidden = true; }
|
||||
return;
|
||||
}
|
||||
if (webpTwinInput) { webpTwinInput.value = opt.getAttribute('data-webp-twin') === '1' ? '1' : '0'; }
|
||||
var pw = parseInt(opt.getAttribute('data-width'), 10) || 0;
|
||||
var ph = parseInt(opt.getAttribute('data-height'), 10) || 0;
|
||||
var pmode = opt.getAttribute('data-mode');
|
||||
var pformat = opt.getAttribute('data-format');
|
||||
var pquality = parseInt(opt.getAttribute('data-quality'), 10) || 82;
|
||||
var modeInput = form.querySelector('[name="mode"]');
|
||||
var formatInput = form.querySelector('[name="format"]');
|
||||
var qualityInput = form.querySelector('[name="quality"]');
|
||||
if (modeInput && pmode) { modeInput.value = pmode; }
|
||||
if (formatInput && pformat && formatInput.querySelector('option[value="' + pformat + '"]')) { formatInput.value = pformat; }
|
||||
if (qualityInput) { qualityInput.value = pquality; }
|
||||
// рамку кропа — под соотношение вида (если заданы обе стороны)
|
||||
if (pw > 0 && ph > 0) {
|
||||
if (ratioSelect) { ratioSelect.value = 'free'; }
|
||||
setRatio(pw + ':' + ph);
|
||||
}
|
||||
// выходной размер — точные размеры вида (перекрывает пересчёт из sync)
|
||||
if (inputs.outputW && pw > 0) { inputs.outputW.value = pw; }
|
||||
if (inputs.outputH && ph > 0) { inputs.outputH.value = ph; }
|
||||
if (inputs.outputW) { inputs.outputW.dispatchEvent(new Event('input', { bubbles: true })); }
|
||||
// предупреждение о растягивании
|
||||
if (presetWarn) {
|
||||
var nw = img.naturalWidth || 0;
|
||||
var nh = img.naturalHeight || 0;
|
||||
if ((pw && nw && nw < pw) || (ph && nh && nh < ph)) {
|
||||
presetWarn.textContent = 'Исходник ' + nw + '×' + nh + ' меньше вида ' + pw + '×' + ph + ' — картинка растянется и будет мыльной.';
|
||||
presetWarn.hidden = false;
|
||||
} else {
|
||||
presetWarn.hidden = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
form.querySelector('[data-crop-reset]').addEventListener('click', function () {
|
||||
state = { x: 0, y: 0, w: 100, h: 100, drag: null, sx: 0, sy: 0 };
|
||||
sync(true);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
CREATE TABLE IF NOT EXISTS `{{prefix}}_media_image_presets` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`code` VARCHAR(64) CHARACTER SET ascii NOT NULL DEFAULT '',
|
||||
`title` VARCHAR(190) NOT NULL DEFAULT '',
|
||||
`group_label` VARCHAR(120) NOT NULL DEFAULT '',
|
||||
`mode` VARCHAR(16) CHARACTER SET ascii NOT NULL DEFAULT 'cover',
|
||||
`width` SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`height` SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`format` VARCHAR(16) CHARACTER SET ascii NOT NULL DEFAULT 'original',
|
||||
`quality` TINYINT UNSIGNED NOT NULL DEFAULT 82,
|
||||
`webp_twin` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`sort_order` SMALLINT UNSIGNED NOT NULL DEFAULT 100,
|
||||
`created_at` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`updated_at` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_code` (`code`),
|
||||
KEY `idx_sort` (`sort_order`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET=utf8mb4;
|
||||
@@ -17,7 +17,7 @@
|
||||
return array(
|
||||
'code' => 'media',
|
||||
'name' => 'Медиа',
|
||||
'version' => '0.1.0',
|
||||
'version' => '0.2.3',
|
||||
|
||||
'permissions' => array(
|
||||
'key' => 'media',
|
||||
@@ -56,8 +56,16 @@
|
||||
),
|
||||
),
|
||||
|
||||
'migrations' => array(
|
||||
array('id' => '001_image_presets', 'file' => 'migrations/001_image_presets.sql'),
|
||||
),
|
||||
|
||||
'routes' => array(
|
||||
array('GET', '/media', array(\App\Adminx\Media\Controller::class, 'index')),
|
||||
array('GET', '/media/presets', array(\App\Adminx\Media\Controller::class, 'presets')),
|
||||
array('POST', '/media/presets', array(\App\Adminx\Media\Controller::class, 'storePreset'), array('permission' => 'manage_media')),
|
||||
array('POST', '/media/presets/{id}', array(\App\Adminx\Media\Controller::class, 'updatePreset'), array('permission' => 'manage_media')),
|
||||
array('POST', '/media/presets/{id}/delete', array(\App\Adminx\Media\Controller::class, 'deletePreset'), array('permission' => 'manage_media')),
|
||||
array('GET', '/media/picker', array(\App\Adminx\Media\Controller::class, 'picker')),
|
||||
array('GET', '/media/folder-files', array(\App\Adminx\Media\Controller::class, 'folderFiles')),
|
||||
array('GET', '/media/file', array(\App\Adminx\Media\Controller::class, 'file')),
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="path" value="{{ file.path }}">
|
||||
<input type="hidden" name="crop_enabled" value="0" data-crop-enabled>
|
||||
<input type="hidden" name="webp_twin" value="0" data-webp-twin>
|
||||
<input type="hidden" name="crop_x" value="0" data-crop-input="x">
|
||||
<input type="hidden" name="crop_y" value="0" data-crop-input="y">
|
||||
<input type="hidden" name="crop_w" value="{{ file.width ?: 1 }}" data-crop-input="w">
|
||||
@@ -89,6 +90,19 @@
|
||||
<button class="media-result-stage" type="button" data-media-result-zoom data-tooltip="Открыть крупно" aria-label="Открыть результат крупно"><img alt="Результат" data-media-result-img><span class="media-result-empty" data-media-result-empty>Меняйте настройки — результат обновится здесь</span></button>
|
||||
</div>
|
||||
|
||||
{% if presets %}
|
||||
<div class="media-preset-pick">
|
||||
<label class="media-preset-pick-field"><span><i class="ti ti-aspect-ratio"></i> Подогнать под вид</span>
|
||||
<select class="select" data-crop-preset>
|
||||
<option value="">— вручную —</option>
|
||||
{% for p in presets %}
|
||||
<option value="{{ p.code }}" data-mode="{{ p.mode }}" data-width="{{ p.width }}" data-height="{{ p.height }}" data-format="{{ p.format }}" data-quality="{{ p.quality }}" data-webp-twin="{{ p.webp_twin }}">{{ p.label }}{% if p.webp_twin %} · +WebP{% endif %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<p class="media-preset-warn" data-crop-preset-warning hidden></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="media-form-grid">
|
||||
<label><span>Кадр</span><select class="select" data-crop-ratio><option value="original">Оригинал</option><option value="free">Свободный</option><option value="1:1">1:1</option><option value="4:3">4:3</option><option value="16:9">16:9</option><option value="9:16">9:16</option></select></label>
|
||||
<label><span>Режим</span><select class="select" name="mode"><option value="cover">Обрезать</option><option value="contain">Вписать</option><option value="fit">Уменьшить</option><option value="stretch">Растянуть</option></select></label>
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
</div>
|
||||
{% if can_manage %}
|
||||
<div class="cluster">
|
||||
<button class="btn btn-secondary" type="button" data-media-presets-open data-open-drawer="mediaPresetsDrawer"><i class="ti ti-aspect-ratio"></i>Виды изображений</button>
|
||||
<button class="btn btn-secondary" type="button" data-media-folder><i class="ti ti-folder-plus"></i>Папка</button>
|
||||
<button class="btn btn-danger-soft" type="button" data-media-clear-thumbs data-path="{{ dir }}" data-name="{{ dir }}" data-tooltip="Удалить превью в текущей и вложенных папках"><i class="ti ti-photo-off"></i>Очистить превью</button>
|
||||
<button class="btn btn-primary" type="button" data-media-upload-trigger><i class="ti ti-upload"></i>Загрузить</button>
|
||||
@@ -23,6 +24,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside class="drawer drawer-right drawer-lg" id="mediaPresetsDrawer" role="dialog" aria-modal="true" aria-labelledby="mediaPresetsTitle" hidden>
|
||||
<div class="drawer-header">
|
||||
<div>
|
||||
<h3 id="mediaPresetsTitle">Виды изображений</h3>
|
||||
<p class="text-secondary text-sm">Готовые размеры для подгонки картинок — новости, баннеры, галерея. Выбираются в редакторе изображения.</p>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-icon" type="button" data-close-drawer aria-label="Закрыть"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
<div class="drawer-body">
|
||||
<div class="media-presets-list" data-preset-list><div class="empty-state">Загрузка...</div></div>
|
||||
{% if can_manage %}
|
||||
<form id="mediaPresetForm" class="form-grid media-presets-form" style="margin-top:16px">
|
||||
<input type="hidden" name="id" value="">
|
||||
<label class="field col-12"><span class="field-label">Название</span><input class="input" name="title" placeholder="Баннер шапки" required><span class="field-error" data-error="title"></span></label>
|
||||
<label class="field col-12"><span class="field-label">Раздел (необязательно)</span><input class="input" name="group_label" placeholder="Новости, Баннеры, Галерея"></label>
|
||||
<label class="field col-6"><span class="field-label">Режим</span><select class="select" name="mode" data-preset-modes></select><span class="field-error" data-error="mode"></span></label>
|
||||
<label class="field col-6"><span class="field-label">Формат</span><select class="select" name="format" data-preset-formats></select><span class="field-error" data-error="format"></span></label>
|
||||
<label class="field col-4"><span class="field-label">Ширина, px</span><input class="input" type="number" name="width" min="0" max="2400" value="0"><span class="field-error" data-error="width"></span></label>
|
||||
<label class="field col-4"><span class="field-label">Высота, px</span><input class="input" type="number" name="height" min="0" max="2400" value="0"><span class="field-error" data-error="height"></span></label>
|
||||
<label class="field col-4"><span class="field-label">Качество</span><input class="input" type="number" name="quality" min="1" max="100" value="82"><span class="field-error" data-error="quality"></span></label>
|
||||
<label class="field col-12 media-preset-check"><input type="checkbox" name="webp_twin" value="1" checked> <span>Создавать WebP рядом с результатом (webp + исходный формат для fallback)</span></label>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if can_manage %}
|
||||
<div class="drawer-footer">
|
||||
<button class="btn btn-ghost" type="button" data-close-drawer><i class="ti ti-x"></i>Закрыть</button>
|
||||
<button class="btn btn-ghost" type="button" data-preset-reset style="margin-left:auto"><i class="ti ti-plus"></i>Новый</button>
|
||||
<button class="btn btn-primary" type="button" data-preset-save><i class="ti ti-check"></i>Сохранить</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</aside>
|
||||
|
||||
<div class="media-summary">
|
||||
<div class="card media-stat">
|
||||
<span class="icon-tile" style="--tile-bg:#e8f3ff;--tile-fg:#2563eb"><i class="ti ti-files"></i></span>
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
'rubric_alias_tokens' => DocumentAliasTemplate::tokens(),
|
||||
'filters' => array('q' => $q, 'state' => $state),
|
||||
'active_tab' => $activeTab,
|
||||
'trash_count' => RubricTrash::count(),
|
||||
'can_manage' => Permission::check('manage_rubrics'),
|
||||
'can_view_documents' => Permission::check('view_documents'),
|
||||
'can_manage_documents' => Permission::check('manage_documents'),
|
||||
@@ -542,12 +543,12 @@
|
||||
}
|
||||
|
||||
try {
|
||||
Model::deleteRubric(isset($params['id']) ? (int) $params['id'] : 0);
|
||||
Model::deleteRubric(isset($params['id']) ? (int) $params['id'] : 0, Auth::id());
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage(), array(), 422);
|
||||
}
|
||||
|
||||
return $this->success('Рубрика удалена');
|
||||
return $this->success('Рубрика перемещена в корзину');
|
||||
}
|
||||
|
||||
public function storeField(array $params = array())
|
||||
@@ -1003,6 +1004,47 @@
|
||||
return $this->success('Ревизии удалены', array('data' => array('id' => $rubricId, 'count' => $count)));
|
||||
}
|
||||
|
||||
public function trash(array $params = array())
|
||||
{
|
||||
return $this->success('', array('data' => array(
|
||||
'items' => RubricTrash::listing(),
|
||||
'count' => RubricTrash::count(),
|
||||
)));
|
||||
}
|
||||
|
||||
public function restoreTrash(array $params = array())
|
||||
{
|
||||
if (($err = $this->guard()) !== null) { return $err; }
|
||||
try {
|
||||
$rubricId = RubricTrash::restore(isset($params['id']) ? (int) $params['id'] : 0);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->error($e->getMessage(), array(), 422);
|
||||
}
|
||||
|
||||
AuditLog::record('rubric.restored_from_trash', array(
|
||||
'actor_id' => Auth::id(),
|
||||
'target_type' => 'rubric',
|
||||
'target_id' => (int) $rubricId,
|
||||
));
|
||||
return $this->success('Рубрика восстановлена', array('data' => array('id' => (int) $rubricId)));
|
||||
}
|
||||
|
||||
public function purgeTrash(array $params = array())
|
||||
{
|
||||
if (($err = $this->guard()) !== null) { return $err; }
|
||||
$trashId = isset($params['id']) ? (int) $params['id'] : 0;
|
||||
if (!RubricTrash::purge($trashId)) {
|
||||
return $this->error('Запись корзины не найдена', array(), 404);
|
||||
}
|
||||
|
||||
AuditLog::record('rubric.purged', array(
|
||||
'actor_id' => Auth::id(),
|
||||
'target_type' => 'rubric_trash',
|
||||
'target_id' => $trashId,
|
||||
));
|
||||
return $this->success('Рубрика удалена окончательно', array('data' => array('id' => $trashId)));
|
||||
}
|
||||
|
||||
protected function captureSchemaBefore($rubricId, $comment)
|
||||
{
|
||||
RubricRevisions::capture((int) $rubricId, 'baseline', Auth::id(), (string) $comment);
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
));
|
||||
}
|
||||
|
||||
public static function deleteRubric($id)
|
||||
public static function deleteRubric($id, $authorId = 0)
|
||||
{
|
||||
$id = (int) $id;
|
||||
if ($id <= 1) {
|
||||
@@ -195,18 +195,37 @@
|
||||
}
|
||||
}
|
||||
|
||||
DB::Delete(self::fieldsTable(), 'rubric_id = %i', $id);
|
||||
DB::Delete(self::groupsTable(), 'rubric_id = %i', $id);
|
||||
DB::Delete(self::templatesTable(), 'rubric_id = %i', $id);
|
||||
DB::Delete(self::permissionsTable(), 'rubric_id = %i', $id);
|
||||
DB::Delete(AdminView::table(), 'rubric_id = %i', $id);
|
||||
RubricFieldSetLinks::deleteForRubric($id);
|
||||
DB::Delete(self::rubricsTable(), 'Id = %i', $id);
|
||||
RubricRevisions::deleteForRubric($id);
|
||||
// Перед необратимым каскадом снимаем полный слепок рубрики в
|
||||
// корзину: удаление разрешено только для пустой рубрики без
|
||||
// зависимостей, поэтому её можно честно убрать из активных таблиц
|
||||
// и восстановить позже. Слепок и каскад — в одной транзакции.
|
||||
DB::startTransaction();
|
||||
try {
|
||||
RubricTrash::capture($id, (int) $authorId);
|
||||
DB::Delete(self::fieldsTable(), 'rubric_id = %i', $id);
|
||||
DB::Delete(self::groupsTable(), 'rubric_id = %i', $id);
|
||||
DB::Delete(self::templatesTable(), 'rubric_id = %i', $id);
|
||||
DB::Delete(self::permissionsTable(), 'rubric_id = %i', $id);
|
||||
DB::Delete(AdminView::table(), 'rubric_id = %i', $id);
|
||||
RubricFieldSetLinks::deleteForRubric($id);
|
||||
DB::Delete(self::rubricsTable(), 'Id = %i', $id);
|
||||
RubricRevisions::deleteForRubric($id);
|
||||
DB::commit();
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
self::clearRubricCache($id);
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Публичный сброс кэша рубрики (для восстановления из корзины). */
|
||||
public static function clearRubricCachePublic($id)
|
||||
{
|
||||
self::clearRubricCache((int) $id);
|
||||
}
|
||||
|
||||
public static function fieldsForRubric($rubricId)
|
||||
{
|
||||
$groupSettings = self::hasGroupSettingsColumn() ? ', g.group_settings' : ', NULL AS group_settings';
|
||||
|
||||
@@ -16,14 +16,16 @@
|
||||
|
||||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||||
|
||||
use App\Common\SystemTables;
|
||||
use App\Content\ContentTables;
|
||||
use App\Content\Fields\FieldRegistry;
|
||||
use App\Content\Revisions\JsonRevisionStore;
|
||||
use App\Helpers\Json;
|
||||
use DB;
|
||||
|
||||
class RubricRevisions
|
||||
{
|
||||
protected static $store;
|
||||
|
||||
public static function table()
|
||||
{
|
||||
return ContentTables::table('rubric_schema_revisions');
|
||||
@@ -56,13 +58,10 @@
|
||||
public static function listForRubric($rubricId, $limit = 80)
|
||||
{
|
||||
if (!self::available()) { return array(); }
|
||||
$rows = DB::query(
|
||||
'SELECT * FROM ' . self::table() . ' WHERE rubric_id=%i ORDER BY created_at DESC,id DESC LIMIT %i',
|
||||
(int) $rubricId,
|
||||
max(1, min(200, (int) $limit))
|
||||
)->getAll() ?: array();
|
||||
$out = array();
|
||||
foreach ($rows as $row) { $out[] = self::format($row, false); }
|
||||
foreach (self::store()->listing($rubricId, $limit) as $row) {
|
||||
$out[] = self::format($row, false);
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
@@ -70,7 +69,7 @@
|
||||
public static function one($id)
|
||||
{
|
||||
if (!self::available()) { return null; }
|
||||
$row = DB::query('SELECT * FROM ' . self::table() . ' WHERE id=%i LIMIT 1', (int) $id)->getAssoc();
|
||||
$row = self::store()->one($id);
|
||||
return $row ? self::format($row, true) : null;
|
||||
}
|
||||
|
||||
@@ -81,27 +80,15 @@
|
||||
$snapshot = $snapshot === null ? Model::schemaSnapshot($rubricId) : Model::normalizeSchemaSnapshot($snapshot);
|
||||
if (!$snapshot) { return 0; }
|
||||
|
||||
$json = Json::encode($snapshot, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$hash = sha1((string) $json);
|
||||
$last = DB::query(
|
||||
'SELECT snapshot_hash FROM ' . self::table() . ' WHERE rubric_id=%i ORDER BY created_at DESC,id DESC LIMIT 1',
|
||||
$rubricId
|
||||
)->getValue();
|
||||
if ($last && hash_equals((string) $last, $hash)) { return 0; }
|
||||
|
||||
DB::Insert(self::table(), array(
|
||||
'rubric_id' => $rubricId,
|
||||
'action' => (string) $action,
|
||||
'snapshot_hash' => $hash,
|
||||
'snapshot_json' => $json,
|
||||
'comment' => mb_substr(trim((string) $comment), 0, 500, 'UTF-8'),
|
||||
'author_id' => (int) $authorId,
|
||||
'author_name' => self::authorName((int) $authorId),
|
||||
'source_revision_id' => (int) $sourceRevisionId,
|
||||
'created_at' => time(),
|
||||
));
|
||||
|
||||
return (int) DB::insertId();
|
||||
return self::store()->capture(
|
||||
$rubricId,
|
||||
$action,
|
||||
$snapshot,
|
||||
(int) $authorId,
|
||||
mb_substr(trim((string) $comment), 0, 500, 'UTF-8'),
|
||||
array('source_revision_id' => (int) $sourceRevisionId),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
public static function previewRestore($revisionId)
|
||||
@@ -185,20 +172,17 @@
|
||||
|
||||
public static function delete($revisionId)
|
||||
{
|
||||
$revision = self::one((int) $revisionId);
|
||||
if (!$revision) { return 0; }
|
||||
DB::Delete(self::table(), 'id=%i', (int) $revisionId);
|
||||
if (!self::available()) { return 0; }
|
||||
$owner = self::store()->delete($revisionId);
|
||||
|
||||
return (int) $revision['rubric_id'];
|
||||
return $owner === false ? 0 : (int) $owner;
|
||||
}
|
||||
|
||||
public static function deleteForRubric($rubricId)
|
||||
{
|
||||
if (!self::available()) { return 0; }
|
||||
$count = (int) DB::query('SELECT COUNT(*) FROM ' . self::table() . ' WHERE rubric_id=%i', (int) $rubricId)->getValue();
|
||||
DB::Delete(self::table(), 'rubric_id=%i', (int) $rubricId);
|
||||
|
||||
return $count;
|
||||
return self::store()->deleteFor($rubricId);
|
||||
}
|
||||
|
||||
public static function compareSnapshots(array $current, array $target)
|
||||
@@ -680,42 +664,34 @@
|
||||
|
||||
protected static function format(array $row, $withSnapshot)
|
||||
{
|
||||
$labels = self::labels();
|
||||
$action = (string) $row['action'];
|
||||
$meta = isset($labels[$action]) ? $labels[$action] : array('label' => $action, 'badge' => 'badge-gray');
|
||||
$decoded = Json::toArray((string) $row['snapshot_json']);
|
||||
$snapshot = $withSnapshot ? $decoded : null;
|
||||
$created = (int) $row['created_at'];
|
||||
$groups = isset($decoded['groups']) ? count($decoded['groups']) : 0;
|
||||
$fields = isset($decoded['fields']) ? count($decoded['fields']) : 0;
|
||||
$base = self::store()->formatRow($row, $withSnapshot);
|
||||
$decoded = Json::toArray(isset($row['snapshot_json']) ? (string) $row['snapshot_json'] : '');
|
||||
|
||||
return array(
|
||||
'id' => (int) $row['id'],
|
||||
'rubric_id' => (int) $row['rubric_id'],
|
||||
'action' => $action,
|
||||
'action_label' => $meta['label'],
|
||||
'badge' => $meta['badge'],
|
||||
'comment' => (string) $row['comment'],
|
||||
'author_id' => (int) $row['author_id'],
|
||||
'author_name' => (string) $row['author_name'],
|
||||
'source_revision_id' => (int) $row['source_revision_id'],
|
||||
'created_at' => $created,
|
||||
'created_label' => $created ? date('d.m.Y H:i:s', $created) : '-',
|
||||
'snapshot_hash' => (string) $row['snapshot_hash'],
|
||||
'groups_count' => $groups,
|
||||
'fields_count' => $fields,
|
||||
'snapshot' => $snapshot,
|
||||
'id' => (int) $base['id'],
|
||||
'rubric_id' => (int) $base['rubric_id'],
|
||||
'action' => $base['action'],
|
||||
'action_label' => $base['action_label'],
|
||||
'badge' => $base['badge'],
|
||||
'comment' => (string) (isset($row['comment']) ? $row['comment'] : ''),
|
||||
'author_id' => (int) $base['author_id'],
|
||||
'author_name' => (string) $base['author_name'],
|
||||
'source_revision_id' => (int) (isset($row['source_revision_id']) ? $row['source_revision_id'] : 0),
|
||||
'created_at' => $base['created_at'],
|
||||
'created_label' => $base['created_label'],
|
||||
'snapshot_hash' => (string) (isset($row['snapshot_hash']) ? $row['snapshot_hash'] : ''),
|
||||
'groups_count' => isset($decoded['groups']) ? count($decoded['groups']) : 0,
|
||||
'fields_count' => isset($decoded['fields']) ? count($decoded['fields']) : 0,
|
||||
'snapshot' => $withSnapshot ? $decoded : null,
|
||||
);
|
||||
}
|
||||
|
||||
protected static function authorName($id)
|
||||
protected static function store()
|
||||
{
|
||||
if ((int) $id <= 0) { return ''; }
|
||||
$row = DB::query('SELECT name,login,email FROM ' . SystemTables::table('users') . ' WHERE id=%i LIMIT 1', (int) $id)->getAssoc();
|
||||
if (!$row) { return '#' . (int) $id; }
|
||||
if (!empty($row['name'])) { return (string) $row['name']; }
|
||||
if (!empty($row['login'])) { return '@' . (string) $row['login']; }
|
||||
if (!self::$store) {
|
||||
self::$store = new JsonRevisionStore(self::table(), 'rubric_id', self::labels());
|
||||
}
|
||||
|
||||
return !empty($row['email']) ? (string) $row['email'] : '#' . (int) $id;
|
||||
return self::$store;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------------------
|
||||
| AVE.cms
|
||||
|--------------------------------------------------------------------------------------
|
||||
| @package AVE.cms
|
||||
| @file adminx/modules/Rubrics/RubricTrash.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\Rubrics;
|
||||
|
||||
defined('BASEPATH') || die('Direct access to this location is not allowed.');
|
||||
|
||||
use App\Common\DatabaseSchema;
|
||||
use App\Common\SystemTables;
|
||||
use App\Content\ContentTables;
|
||||
use App\Helpers\Json;
|
||||
use DB;
|
||||
|
||||
/**
|
||||
* Корзина рубрик: полный обратимый снимок рубрики со всей её схемой.
|
||||
*
|
||||
* Удаление рубрики разрешено только для пустой рубрики без зависимостей
|
||||
* (см. Model::deleteRubric), поэтому на её строки, поля и группы ничто не
|
||||
* ссылается. Это позволяет честно убрать рубрику из активных таблиц (никакие
|
||||
* списки/пикеры фильтровать не нужно), а рядом сохранить слепок для
|
||||
* восстановления. Идентификаторы автоинкрементные — MySQL не переиспользует
|
||||
* освободившиеся, поэтому восстановление с исходными id безопасно.
|
||||
*/
|
||||
class RubricTrash
|
||||
{
|
||||
const VERSION = 1;
|
||||
|
||||
public static function table()
|
||||
{
|
||||
return ContentTables::table('rubric_trash');
|
||||
}
|
||||
|
||||
public static function available()
|
||||
{
|
||||
try { return DatabaseSchema::tableExists(self::table()); }
|
||||
catch (\Throwable $e) { return false; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Логические таблицы рубрики в порядке восстановления (родитель первым).
|
||||
* Ключ — это столбец связи с рубрикой; для самой таблицы рубрик это Id.
|
||||
*/
|
||||
protected static function tables()
|
||||
{
|
||||
return array(
|
||||
array('name' => 'rubric', 'table' => Model::rubricsTable(), 'key' => 'Id'),
|
||||
array('name' => 'group', 'table' => Model::groupsTable(), 'key' => 'rubric_id'),
|
||||
array('name' => 'field', 'table' => Model::fieldsTable(), 'key' => 'rubric_id'),
|
||||
array('name' => 'template', 'table' => Model::templatesTable(), 'key' => 'rubric_id'),
|
||||
array('name' => 'permission', 'table' => Model::permissionsTable(), 'key' => 'rubric_id'),
|
||||
array('name' => 'view', 'table' => AdminView::table(), 'key' => 'rubric_id'),
|
||||
array('name' => 'fieldset_link', 'table' => RubricFieldSetLinks::linksTable(), 'key' => 'rubric_id'),
|
||||
array('name' => 'revision', 'table' => RubricRevisions::table(), 'key' => 'rubric_id'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Снять полный слепок рубрики в корзину. Вызывается внутри транзакции
|
||||
* удаления, до каскадных DELETE. Возвращает id записи корзины или 0.
|
||||
*/
|
||||
public static function capture($rubricId, $authorId = 0)
|
||||
{
|
||||
$rubricId = (int) $rubricId;
|
||||
if ($rubricId <= 0 || !self::available()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$rubricRow = DB::query('SELECT * FROM ' . Model::rubricsTable() . ' WHERE Id=%i LIMIT 1', $rubricId)->getAssoc();
|
||||
if (!$rubricRow) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$payload = array('version' => self::VERSION, 'rubric_id' => $rubricId, 'tables' => array());
|
||||
$fieldsCount = 0;
|
||||
$groupsCount = 0;
|
||||
foreach (self::tables() as $definition) {
|
||||
if (!self::tableReady($definition['table'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rows = DB::query(
|
||||
'SELECT * FROM ' . $definition['table'] . ' WHERE ' . $definition['key'] . '=%i',
|
||||
$rubricId
|
||||
)->getAll() ?: array();
|
||||
$payload['tables'][$definition['name']] = $rows;
|
||||
if ($definition['name'] === 'field') { $fieldsCount = count($rows); }
|
||||
if ($definition['name'] === 'group') { $groupsCount = count($rows); }
|
||||
}
|
||||
|
||||
$json = Json::encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($json)) {
|
||||
throw new \RuntimeException('Не удалось сохранить рубрику в корзину');
|
||||
}
|
||||
|
||||
DB::Insert(self::table(), array(
|
||||
'rubric_id' => $rubricId,
|
||||
'rubric_title' => mb_substr((string) (isset($rubricRow['rubric_title']) ? $rubricRow['rubric_title'] : ''), 0, 255, 'UTF-8'),
|
||||
'rubric_alias' => mb_substr((string) (isset($rubricRow['rubric_alias']) ? $rubricRow['rubric_alias'] : ''), 0, 255, 'UTF-8'),
|
||||
'fields_count' => $fieldsCount,
|
||||
'groups_count' => $groupsCount,
|
||||
'payload_json' => $json,
|
||||
'author_id' => (int) $authorId,
|
||||
'author_name' => self::authorName((int) $authorId),
|
||||
'deleted_at' => time(),
|
||||
));
|
||||
|
||||
return (int) DB::insertId();
|
||||
}
|
||||
|
||||
public static function listing($limit = 100)
|
||||
{
|
||||
if (!self::available()) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$rows = DB::query(
|
||||
'SELECT id,rubric_id,rubric_title,rubric_alias,fields_count,groups_count,author_id,author_name,deleted_at'
|
||||
. ' FROM ' . self::table() . ' ORDER BY deleted_at DESC,id DESC LIMIT %i',
|
||||
max(1, min(500, (int) $limit))
|
||||
)->getAll() ?: array();
|
||||
|
||||
return array_map(array(__CLASS__, 'formatRow'), $rows);
|
||||
}
|
||||
|
||||
public static function one($id)
|
||||
{
|
||||
if (!self::available()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = DB::query('SELECT * FROM ' . self::table() . ' WHERE id=%i LIMIT 1', (int) $id)->getAssoc();
|
||||
return $row ? self::formatRow($row) : null;
|
||||
}
|
||||
|
||||
public static function count()
|
||||
{
|
||||
if (!self::available()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) DB::query('SELECT COUNT(*) FROM ' . self::table())->getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Восстановить рубрику из корзины: реинсертить все строки с исходными id
|
||||
* и убрать запись корзины. Бросает исключение при конфликте id.
|
||||
*/
|
||||
public static function restore($trashId)
|
||||
{
|
||||
if (!self::available()) {
|
||||
throw new \RuntimeException('Корзина недоступна');
|
||||
}
|
||||
|
||||
$row = DB::query('SELECT * FROM ' . self::table() . ' WHERE id=%i LIMIT 1', (int) $trashId)->getAssoc();
|
||||
if (!$row) {
|
||||
throw new \RuntimeException('Запись корзины не найдена');
|
||||
}
|
||||
|
||||
$payload = Json::toArray((string) $row['payload_json']);
|
||||
$rubricId = (int) (isset($payload['rubric_id']) ? $payload['rubric_id'] : $row['rubric_id']);
|
||||
$stored = isset($payload['tables']) && is_array($payload['tables']) ? $payload['tables'] : array();
|
||||
if ($rubricId <= 0 || empty($stored['rubric'])) {
|
||||
throw new \RuntimeException('Слепок рубрики повреждён');
|
||||
}
|
||||
|
||||
if (DB::query('SELECT Id FROM ' . Model::rubricsTable() . ' WHERE Id=%i LIMIT 1', $rubricId)->getValue()) {
|
||||
throw new \RuntimeException('Рубрика с id #' . $rubricId . ' уже существует — восстановление невозможно');
|
||||
}
|
||||
|
||||
DB::startTransaction();
|
||||
try {
|
||||
foreach (self::tables() as $definition) {
|
||||
$rows = isset($stored[$definition['name']]) && is_array($stored[$definition['name']])
|
||||
? $stored[$definition['name']]
|
||||
: array();
|
||||
if (!$rows || !self::tableReady($definition['table'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($rows as $record) {
|
||||
if (is_array($record) && $record) {
|
||||
DB::Insert($definition['table'], $record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DB::Delete(self::table(), 'id=%i', (int) $trashId);
|
||||
DB::commit();
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
Model::clearRubricCachePublic($rubricId);
|
||||
return $rubricId;
|
||||
}
|
||||
|
||||
public static function purge($trashId)
|
||||
{
|
||||
if (!self::available()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$exists = (int) DB::query('SELECT COUNT(*) FROM ' . self::table() . ' WHERE id=%i', (int) $trashId)->getValue();
|
||||
if ($exists < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DB::Delete(self::table(), 'id=%i', (int) $trashId);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected static function formatRow(array $row)
|
||||
{
|
||||
$deleted = isset($row['deleted_at']) ? (int) $row['deleted_at'] : 0;
|
||||
return array(
|
||||
'id' => (int) $row['id'],
|
||||
'rubric_id' => (int) $row['rubric_id'],
|
||||
'rubric_title' => (string) (isset($row['rubric_title']) ? $row['rubric_title'] : ''),
|
||||
'rubric_alias' => (string) (isset($row['rubric_alias']) ? $row['rubric_alias'] : ''),
|
||||
'fields_count' => (int) (isset($row['fields_count']) ? $row['fields_count'] : 0),
|
||||
'groups_count' => (int) (isset($row['groups_count']) ? $row['groups_count'] : 0),
|
||||
'author_id' => (int) (isset($row['author_id']) ? $row['author_id'] : 0),
|
||||
'author_name' => (string) (isset($row['author_name']) ? $row['author_name'] : ''),
|
||||
'deleted_at' => $deleted,
|
||||
'deleted_label' => $deleted > 0 ? date('d.m.Y H:i:s', $deleted) : '-',
|
||||
);
|
||||
}
|
||||
|
||||
protected static function tableReady($table)
|
||||
{
|
||||
try { return DatabaseSchema::tableExists($table); }
|
||||
catch (\Throwable $e) { return false; }
|
||||
}
|
||||
|
||||
protected static function authorName($id)
|
||||
{
|
||||
if ((int) $id <= 0) { return ''; }
|
||||
$row = DB::query('SELECT name,login,email FROM ' . SystemTables::table('users') . ' WHERE id=%i LIMIT 1', (int) $id)->getAssoc();
|
||||
if (!$row) { return '#' . (int) $id; }
|
||||
if (!empty($row['name'])) { return (string) $row['name']; }
|
||||
if (!empty($row['login'])) { return '@' . (string) $row['login']; }
|
||||
|
||||
return !empty($row['email']) ? (string) $row['email'] : '#' . (int) $id;
|
||||
}
|
||||
}
|
||||
@@ -1850,7 +1850,7 @@
|
||||
}
|
||||
.rubrics-builder {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(210px, 240px) minmax(520px, 1fr) minmax(290px, 330px);
|
||||
grid-template-columns: minmax(220px, 230px) minmax(560px, 1fr) minmax(270px, 280px);
|
||||
gap: 14px;
|
||||
align-items: start;
|
||||
min-width: 0;
|
||||
@@ -2407,6 +2407,17 @@
|
||||
color: var(--text-tertiary);
|
||||
font-size: 10.5px;
|
||||
}
|
||||
.rubrics-builder-field.is-third .rubrics-builder-field-main,
|
||||
.rubrics-builder-field.is-quarter .rubrics-builder-field-main {
|
||||
display: grid;
|
||||
justify-content: stretch;
|
||||
gap: 3px;
|
||||
}
|
||||
.rubrics-builder-field.is-third .rubrics-builder-field-main code,
|
||||
.rubrics-builder-field.is-quarter .rubrics-builder-field-main code {
|
||||
justify-self: start;
|
||||
max-width: 100%;
|
||||
}
|
||||
.rubrics-builder-field-preview {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
@@ -3825,7 +3836,7 @@
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.rubrics-builder {
|
||||
grid-template-columns: minmax(210px, 240px) minmax(0, 1fr);
|
||||
grid-template-columns: minmax(220px, 230px) minmax(0, 1fr);
|
||||
}
|
||||
.rubrics-builder-inspector {
|
||||
position: static;
|
||||
|
||||
@@ -126,6 +126,11 @@
|
||||
if (e.target.closest('[data-schema-revision-delete]')) { self.deleteSchemaRevision(); }
|
||||
if (e.target.closest('[data-schema-revisions-clear]')) { self.clearSchemaRevisions(); }
|
||||
if (e.target.closest('[data-schema-revision-restore]')) { self.restoreSchemaRevision(); }
|
||||
if (e.target.closest('[data-rubric-trash-open]')) { self.openTrash(); }
|
||||
var trashRestore = e.target.closest('[data-rubric-trash-restore]');
|
||||
if (trashRestore) { self.restoreTrash(trashRestore.getAttribute('data-rubric-trash-restore')); }
|
||||
var trashPurge = e.target.closest('[data-rubric-trash-purge]');
|
||||
if (trashPurge) { self.purgeTrash(trashPurge.getAttribute('data-rubric-trash-purge')); }
|
||||
var adminViewMode = e.target.closest('[data-admin-view-mode]');
|
||||
if (adminViewMode) { self.setAdminViewMode(adminViewMode.getAttribute('data-admin-view-mode')); }
|
||||
var adminViewAdd = e.target.closest('[data-admin-view-add]');
|
||||
@@ -3583,6 +3588,95 @@
|
||||
});
|
||||
},
|
||||
|
||||
openTrash: function () {
|
||||
if (Adminx.Drawer) { Adminx.Drawer.open('rubricTrashDrawer'); }
|
||||
this.refreshTrash();
|
||||
},
|
||||
|
||||
refreshTrash: function () {
|
||||
var self = this;
|
||||
Adminx.Loader.show();
|
||||
Adminx.Ajax.request(this.base() + '/rubrics/trash').then(function (payload) {
|
||||
Adminx.Loader.hide();
|
||||
var response = payload.data || {};
|
||||
if (!response.success || !response.data) {
|
||||
Adminx.Toast.show(response.message || 'Не удалось загрузить корзину', 'error');
|
||||
return;
|
||||
}
|
||||
self.renderTrash(response.data.items || []);
|
||||
}).catch(function () {
|
||||
Adminx.Loader.hide();
|
||||
Adminx.Toast.show('Ошибка сети при загрузке корзины', 'error');
|
||||
});
|
||||
},
|
||||
|
||||
renderTrash: function (items) {
|
||||
var list = document.querySelector('[data-rubric-trash-list]');
|
||||
var count = document.querySelector('[data-rubric-trash-count]');
|
||||
if (count) { count.textContent = items.length ? (items.length + ' в корзине') : 'Корзина пуста'; }
|
||||
if (!list) { return; }
|
||||
if (!items.length) {
|
||||
list.innerHTML = '<div class="empty-state">Корзина пуста. Удалённые рубрики появятся здесь.</div>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = items.map(function (item) {
|
||||
return '<div class="rubrics-revision-row">'
|
||||
+ '<span class="icon-tile rubrics-revision-row-icon"><i class="ti ti-trash"></i></span>'
|
||||
+ '<div class="rubrics-revision-row-main"><div><b>' + esc(item.rubric_title || ('Рубрика #' + item.rubric_id)) + '</b>'
|
||||
+ (item.rubric_alias ? '<span class="badge badge-gray">' + esc(item.rubric_alias) + '</span>' : '') + '</div>'
|
||||
+ '<small>Удалена ' + esc(item.deleted_label || '-') + (item.author_name ? ' · ' + esc(item.author_name) : '') + '</small></div>'
|
||||
+ '<div class="rubrics-revision-row-counts"><span>' + item.fields_count + ' полей</span><span>' + item.groups_count + ' групп</span></div>'
|
||||
+ '<div class="cluster">'
|
||||
+ '<button class="btn btn-ghost btn-icon btn-sm" type="button" data-rubric-trash-restore="' + item.id + '" data-tooltip="Восстановить" aria-label="Восстановить"><i class="ti ti-restore"></i></button>'
|
||||
+ '<button class="btn btn-ghost btn-icon btn-sm rubrics-action-danger" type="button" data-rubric-trash-purge="' + item.id + '" data-tooltip="Удалить окончательно" aria-label="Удалить окончательно"><i class="ti ti-trash-x"></i></button>'
|
||||
+ '</div></div>';
|
||||
}).join('');
|
||||
},
|
||||
|
||||
restoreTrash: function (id) {
|
||||
id = parseInt(id, 10) || 0;
|
||||
if (!id) { return; }
|
||||
var self = this;
|
||||
Adminx.Confirm.open({
|
||||
kind: 'warning',
|
||||
title: 'Восстановить рубрику?',
|
||||
message: 'Рубрика вернётся со всей схемой и прежними идентификаторами.',
|
||||
confirmLabel: 'Восстановить',
|
||||
onConfirm: function () {
|
||||
Adminx.Loader.show();
|
||||
Adminx.Ajax.post(self.base() + '/rubrics/trash/' + id + '/restore').then(function (payload) {
|
||||
Adminx.Loader.hide();
|
||||
var response = payload.data || {};
|
||||
if (!response.success) { Adminx.Toast.show(response.message || 'Не удалось восстановить', 'error'); return; }
|
||||
Adminx.Toast.show(response.message || 'Рубрика восстановлена', 'success');
|
||||
window.location.reload();
|
||||
}).catch(function () { Adminx.Loader.hide(); Adminx.Toast.show('Ошибка сети', 'error'); });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
purgeTrash: function (id) {
|
||||
id = parseInt(id, 10) || 0;
|
||||
if (!id) { return; }
|
||||
var self = this;
|
||||
Adminx.Confirm.open({
|
||||
kind: 'danger',
|
||||
title: 'Удалить рубрику окончательно?',
|
||||
message: 'Слепок будет стёрт без возможности восстановления.',
|
||||
confirmLabel: 'Удалить окончательно',
|
||||
onConfirm: function () {
|
||||
Adminx.Loader.show();
|
||||
Adminx.Ajax.post(self.base() + '/rubrics/trash/' + id + '/purge').then(function (payload) {
|
||||
Adminx.Loader.hide();
|
||||
var response = payload.data || {};
|
||||
if (!response.success) { Adminx.Toast.show(response.message || 'Не удалось удалить', 'error'); return; }
|
||||
Adminx.Toast.show(response.message || 'Удалено окончательно', 'success');
|
||||
self.refreshTrash();
|
||||
}).catch(function () { Adminx.Loader.hide(); Adminx.Toast.show('Ошибка сети', 'error'); });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
openSchemaRevisions: function (row) {
|
||||
if (!row) { return; }
|
||||
this.currentSchemaRevisionRubricId = parseInt(row.getAttribute('data-id'), 10) || 0;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE IF NOT EXISTS `{{prefix}}_rubric_trash` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`rubric_id` SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`rubric_title` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`rubric_alias` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`fields_count` SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`groups_count` SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`payload_json` LONGTEXT NOT NULL,
|
||||
`author_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`author_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`deleted_at` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_deleted_at` (`deleted_at`),
|
||||
KEY `idx_rubric_id` (`rubric_id`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET=utf8mb4;
|
||||
@@ -17,7 +17,7 @@
|
||||
return array(
|
||||
'code' => 'rubrics',
|
||||
'name' => 'Рубрики и поля',
|
||||
'version' => '0.2.0',
|
||||
'version' => '0.3.2',
|
||||
'field_sets' => \App\Adminx\Rubrics\RubricFieldPresets::definitions(),
|
||||
'hook_definitions' => array(
|
||||
array(
|
||||
@@ -83,6 +83,7 @@
|
||||
array('id' => '013_group_form_conditions', 'file' => 'migrations/013_group_form_conditions.php'),
|
||||
array('id' => '014_rubric_purpose', 'file' => 'migrations/014_rubric_purpose.php'),
|
||||
array('id' => '015_merge_directory_permissions', 'file' => 'migrations/015_merge_directory_permissions.php'),
|
||||
array('id' => '016_rubric_trash', 'file' => 'migrations/016_rubric_trash.sql'),
|
||||
),
|
||||
|
||||
'routes' => array(
|
||||
@@ -101,6 +102,9 @@
|
||||
array('POST', '/rubrics/schema-revisions/{revision}/delete', array(\App\Adminx\Rubrics\Controller::class, 'deleteSchemaRevision')),
|
||||
array('GET', '/rubrics/{id}/schema-revisions', array(\App\Adminx\Rubrics\Controller::class, 'schemaRevisions')),
|
||||
array('POST', '/rubrics/{id}/schema-revisions/delete', array(\App\Adminx\Rubrics\Controller::class, 'deleteSchemaRevisions')),
|
||||
array('GET', '/rubrics/trash', array(\App\Adminx\Rubrics\Controller::class, 'trash')),
|
||||
array('POST', '/rubrics/trash/{id}/restore', array(\App\Adminx\Rubrics\Controller::class, 'restoreTrash'), array('permission' => 'manage_rubrics')),
|
||||
array('POST', '/rubrics/trash/{id}/purge', array(\App\Adminx\Rubrics\Controller::class, 'purgeTrash'), array('permission' => 'manage_rubrics')),
|
||||
array('GET', '/rubrics/{id}', array(\App\Adminx\Rubrics\Controller::class, 'showRubric')),
|
||||
array('POST', '/rubrics/{id}', array(\App\Adminx\Rubrics\Controller::class, 'updateRubric'), array('permission' => 'manage_rubrics', 'sensitive' => 'stored_php.write', 'reauth' => true)),
|
||||
array('POST', '/rubrics/{id}/delete', array(\App\Adminx\Rubrics\Controller::class, 'destroyRubric')),
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
</div>
|
||||
<div class="cluster">
|
||||
{% if can_manage %}
|
||||
<button class="btn btn-secondary" type="button" data-rubric-trash-open><i class="ti ti-trash"></i>Корзина{% if trash_count %} <span class="badge badge-gray" data-rubric-trash-badge>{{ trash_count }}</span>{% endif %}</button>
|
||||
<button class="btn btn-primary" type="button" data-rubric-new data-open-drawer="rubricDrawer"><i class="ti ti-plus"></i>Создать рубрику</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -945,6 +946,30 @@
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<aside class="drawer drawer-right rubrics-revisions-panel" id="rubricTrashDrawer" role="dialog" aria-modal="true" aria-labelledby="rubricTrashTitle" hidden>
|
||||
<div class="drawer-header">
|
||||
<div>
|
||||
<h3 id="rubricTrashTitle">Корзина рубрик</h3>
|
||||
<p class="text-secondary text-sm">Удалённые рубрики со всей схемой. Можно восстановить или удалить окончательно.</p>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-icon" type="button" data-close-drawer aria-label="Закрыть"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
<div class="drawer-body rubrics-revisions-body">
|
||||
<section class="rubrics-revisions-section" aria-label="Удалённые рубрики">
|
||||
<div class="rubrics-revisions-head">
|
||||
<span class="icon-tile" style="--tile-bg:var(--red-100);--tile-fg:var(--red-600)"><i class="ti ti-trash"></i></span>
|
||||
<div><h4>Удалённые рубрики</h4><p class="text-secondary" data-rubric-trash-count>Загрузка...</p></div>
|
||||
</div>
|
||||
<div class="rubrics-revisions-list" data-rubric-trash-list>
|
||||
<div class="empty-state">Загрузка...</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="drawer-footer">
|
||||
<button class="btn btn-ghost" type="button" data-close-drawer><i class="ti ti-x"></i>Закрыть</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<aside class="drawer drawer-right drawer-lg rubrics-group-panel" id="rubricGroupDrawer" role="dialog" aria-modal="true" aria-labelledby="rubricGroupTitle" hidden>
|
||||
<div class="drawer-header">
|
||||
<div>
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
.themes-tabs > .tabs-list {
|
||||
margin-bottom: 18px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.themes-tabs {
|
||||
@@ -69,6 +70,7 @@
|
||||
}
|
||||
.themes-tabs > .tabs-list .tab {
|
||||
min-height: 40px;
|
||||
margin-bottom: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.themes-tabs > [data-tab-panel] {
|
||||
|
||||
@@ -47,12 +47,15 @@
|
||||
/** Список с фильтром по поиску (имя/email/логин) и роли. */
|
||||
public static function all($search = '', $role = '')
|
||||
{
|
||||
$sql = 'SELECT id, name, email, login, phone, role, is_active, created_at, updated_at FROM ' . self::table() . ' WHERE 1=1';
|
||||
$sql = 'SELECT id, name, email, login, phone, role, is_active, created_at, updated_at, last_login_at FROM ' . self::table() . ' WHERE 1=1';
|
||||
$args = [];
|
||||
|
||||
$search = trim((string) $search);
|
||||
if ($search !== '') {
|
||||
$sql .= ' AND (name LIKE %ss OR email LIKE %ss OR login LIKE %ss)';
|
||||
// email обычно ascii, name/login — utf8mb4: без приведения к
|
||||
// charset соединения (utf8) кириллический запрос падает на
|
||||
// «illegal mix of collations». Ведущий % и так гасит индекс.
|
||||
$sql .= ' AND (CONVERT(name USING utf8) LIKE %ss OR CONVERT(email USING utf8) LIKE %ss OR CONVERT(login USING utf8) LIKE %ss)';
|
||||
$args[] = $search;
|
||||
$args[] = $search;
|
||||
$args[] = $search;
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
<th style="width:120px">Роль</th>
|
||||
<th style="width:110px">Статус</th>
|
||||
<th style="width:150px">Создан</th>
|
||||
<th style="width:150px">Последний вход</th>
|
||||
{% if can_manage %}<th style="width:96px"></th>{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -102,6 +103,10 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-muted text-sm">{{ u.created_at ? u.created_at|slice(0,10) : '—' }}</td>
|
||||
<td class="text-sm">
|
||||
{% if u.last_login_at %}<span class="text-muted">{{ u.last_login_at|slice(0,16) }}</span>
|
||||
{% else %}<span class="badge badge-amber badge-dot">никогда</span>{% endif %}
|
||||
</td>
|
||||
{% if can_manage %}
|
||||
<td>
|
||||
<div class="cluster" style="gap:4px">
|
||||
@@ -114,7 +119,7 @@
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="{{ can_manage ? 8 : 6 }}"><div class="empty-state">Ничего не найдено</div></td></tr>
|
||||
<tr><td colspan="{{ can_manage ? 9 : 7 }}"><div class="empty-state">Ничего не найдено</div></td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -72,6 +72,9 @@
|
||||
'005_register_development_site_permission' => array('{{prefix}}_permissions'),
|
||||
'006_normalize_control_panel_permission' => array('{{prefix}}_permissions'),
|
||||
)),
|
||||
'media' => array('directory' => 'Media', 'ids' => array(
|
||||
'001_image_presets',
|
||||
), 'backup' => array('001_image_presets' => array())),
|
||||
'navigation' => array('directory' => 'Navigation', 'ids' => array('001_create_navigation_tables')),
|
||||
'notfound' => array('directory' => 'NotFound', 'optional' => true, 'ids' => array(
|
||||
'001_register_notfound_permissions', '002_create_not_found_log',
|
||||
@@ -102,7 +105,7 @@
|
||||
'007_rubric_open_graph', '008_normalize_field_layout_width',
|
||||
'009_expand_clean_starter_rubric', '010_form_conditions', '011_schema_revisions',
|
||||
'012_linked_field_sets', '013_group_form_conditions', '014_rubric_purpose',
|
||||
'015_merge_directory_permissions',
|
||||
'015_merge_directory_permissions', '016_rubric_trash',
|
||||
), 'backup' => array(
|
||||
'012_linked_field_sets' => array(),
|
||||
'013_group_form_conditions' => array('{{content_prefix}}_rubric_fields_group'),
|
||||
@@ -111,6 +114,7 @@
|
||||
'{{prefix}}_permissions',
|
||||
'{{prefix}}_role_permissions',
|
||||
),
|
||||
'016_rubric_trash' => array(),
|
||||
)),
|
||||
'security' => array('directory' => 'Security', 'ids' => array('001_create_ip_blocks')),
|
||||
'settings' => array('directory' => 'Settings', 'ids' => array(
|
||||
|
||||
@@ -152,6 +152,25 @@ CREATE TABLE IF NOT EXISTS `{{prefix}}_module_migration_attempts` (
|
||||
KEY `idx_status` (`status`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `{{prefix}}_media_image_presets` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`code` VARCHAR(64) CHARACTER SET ascii NOT NULL DEFAULT '',
|
||||
`title` VARCHAR(190) NOT NULL DEFAULT '',
|
||||
`group_label` VARCHAR(120) NOT NULL DEFAULT '',
|
||||
`mode` VARCHAR(16) CHARACTER SET ascii NOT NULL DEFAULT 'cover',
|
||||
`width` SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`height` SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`format` VARCHAR(16) CHARACTER SET ascii NOT NULL DEFAULT 'original',
|
||||
`quality` TINYINT UNSIGNED NOT NULL DEFAULT 82,
|
||||
`webp_twin` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`sort_order` SMALLINT UNSIGNED NOT NULL DEFAULT 100,
|
||||
`created_at` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`updated_at` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_code` (`code`),
|
||||
KEY `idx_sort` (`sort_order`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `{{prefix}}_modules` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`code` VARCHAR(100) NOT NULL,
|
||||
@@ -635,6 +654,22 @@ CREATE TABLE IF NOT EXISTS `{{prefix}}_rubric_schema_revisions` (
|
||||
KEY `idx_snapshot_hash` (`snapshot_hash`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `{{prefix}}_rubric_trash` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`rubric_id` SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`rubric_title` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`rubric_alias` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`fields_count` SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`groups_count` SMALLINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`payload_json` LONGTEXT NOT NULL,
|
||||
`author_id` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
`author_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`deleted_at` INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_deleted_at` (`deleted_at`),
|
||||
KEY `idx_rubric_id` (`rubric_id`)
|
||||
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `{{prefix}}_rubric_field_sets` (
|
||||
`code` VARCHAR(64) CHARACTER SET ascii NOT NULL,
|
||||
`title` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
|
||||
@@ -71,6 +71,16 @@
|
||||
$modulePath = $modulePosition !== false
|
||||
? substr('/' . ltrim($path, './'), $modulePosition)
|
||||
: '';
|
||||
if ($modulePath !== ''
|
||||
&& preg_match('#^/modules/[A-Z][A-Za-z0-9_-]*/assets/#', $modulePath)
|
||||
&& defined('ADMINX_BASE')) {
|
||||
$query = parse_url($url, PHP_URL_QUERY);
|
||||
$fragment = parse_url($url, PHP_URL_FRAGMENT);
|
||||
return rtrim((string) ADMINX_BASE, '/') . $modulePath
|
||||
. ($query !== null && $query !== false ? '?' . $query : '')
|
||||
. ($fragment !== null && $fragment !== false ? '#' . $fragment : '');
|
||||
}
|
||||
|
||||
if ($modulePath === ''
|
||||
|| !preg_match('#^/modules/[a-z][a-z0-9_-]*/admin/assets/#', $modulePath)) {
|
||||
return $url;
|
||||
|
||||
@@ -44,7 +44,28 @@
|
||||
|
||||
public function categoryTree($rubricId = 0, $fieldId = 0)
|
||||
{
|
||||
$where=" WHERE s.purpose='commerce'";$args=array();if((int)$rubricId>0){$where.=' AND i.rubric_id=%i';$args[]=(int)$rubricId;}if((int)$fieldId>0){$where.=' AND i.field_id=%i';$args[]=(int)$fieldId;}$sql="SELECT i.id,i.parent_id,i.name,i.status,i.position FROM ".CatalogTables::table('module_catalog_items')." i INNER JOIN ".CatalogTables::table('module_catalog_settings')." s ON s.rubric_id=i.rubric_id AND s.field_id=i.field_id".$where." ORDER BY i.parent_id,i.position,i.id";return call_user_func_array(array('DB','query'),array_merge(array($sql),$args))->getAll()?:array();
|
||||
$where = " WHERE s.purpose='commerce'";
|
||||
$args = array();
|
||||
if ((int) $rubricId > 0) {
|
||||
$where .= ' AND i.rubric_id=%i';
|
||||
$args[] = (int) $rubricId;
|
||||
}
|
||||
|
||||
if ((int) $fieldId > 0) {
|
||||
$where .= ' AND i.field_id=%i';
|
||||
$args[] = (int) $fieldId;
|
||||
}
|
||||
|
||||
$sql = 'SELECT i.id,i.parent_id,i.name,i.status,i.position,i.level'
|
||||
. ' FROM ' . CatalogTables::table('module_catalog_items') . ' i'
|
||||
. ' INNER JOIN ' . CatalogTables::table('module_catalog_settings') . ' s'
|
||||
. ' ON s.rubric_id=i.rubric_id AND s.field_id=i.field_id'
|
||||
. $where
|
||||
. ' ORDER BY i.parent_id,i.position,i.id';
|
||||
return call_user_func_array(
|
||||
array('DB', 'query'),
|
||||
array_merge(array($sql), $args)
|
||||
)->getAll() ?: array();
|
||||
}
|
||||
|
||||
public function products(array $feed, $limit = 0)
|
||||
@@ -75,18 +96,43 @@
|
||||
protected function selectedCategoryIds(array $feed)
|
||||
{
|
||||
$selection = $this->categories($feed['id']);
|
||||
$tree = $this->categoryTree(isset($feed['rubric_id'])?$feed['rubric_id']:0,isset($feed['catalog_field_id'])?$feed['catalog_field_id']:0); $children = array();
|
||||
if (!$selection['include']) {
|
||||
$ids = array();
|
||||
foreach ($tree as $row) { if ((int) $row['status'] === 1) { $ids[] = (int) $row['id']; } }
|
||||
return array_values(array_diff($ids, $selection['exclude']));
|
||||
$tree = $this->categoryTree(
|
||||
isset($feed['rubric_id']) ? $feed['rubric_id'] : 0,
|
||||
isset($feed['catalog_field_id']) ? $feed['catalog_field_id'] : 0
|
||||
);
|
||||
$children = array();
|
||||
$active = array();
|
||||
foreach ($tree as $row) {
|
||||
if ((int) $row['status'] === 1) {
|
||||
$active[(int) $row['id']] = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($tree as $row) { $children[(int) $row['parent_id']][] = (int) $row['id']; }
|
||||
$ids = $selection['include'];
|
||||
if (!$selection['include']) {
|
||||
return array_values(array_diff(array_keys($active), $selection['exclude']));
|
||||
}
|
||||
|
||||
foreach ($tree as $row) {
|
||||
if (isset($active[(int) $row['id']])) {
|
||||
$children[(int) $row['parent_id']][] = (int) $row['id'];
|
||||
}
|
||||
}
|
||||
|
||||
$ids = array_values(array_filter(
|
||||
$selection['include'],
|
||||
function ($id) use ($active) { return isset($active[(int) $id]); }
|
||||
));
|
||||
if (!empty($feed['include_descendants'])) {
|
||||
$queue = $ids;
|
||||
while ($queue) { $parent = array_shift($queue); foreach (isset($children[$parent]) ? $children[$parent] : array() as $child) { if (!in_array($child, $ids, true)) { $ids[] = $child; $queue[] = $child; } } }
|
||||
while ($queue) {
|
||||
$parent = array_shift($queue);
|
||||
foreach (isset($children[$parent]) ? $children[$parent] : array() as $child) {
|
||||
if (!in_array($child, $ids, true)) {
|
||||
$ids[] = $child;
|
||||
$queue[] = $child;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_diff(array_unique(array_map('intval', $ids)), $selection['exclude']));
|
||||
|
||||
+1
-1
@@ -17,5 +17,5 @@
|
||||
return array(
|
||||
'product' => 'AVE.cms',
|
||||
'version' => '3.3',
|
||||
'build' => '0.35',
|
||||
'build' => '0.37',
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user