diff --git a/PACKAGE-MANIFEST.json b/PACKAGE-MANIFEST.json index 5a817cc..dffaf78 100644 --- a/PACKAGE-MANIFEST.json +++ b/PACKAGE-MANIFEST.json @@ -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", diff --git a/adminx/modules/Catalog/assets/catalog-drawer.css b/adminx/modules/Catalog/assets/catalog-drawer.css index 996216d..dfedb84 100644 --- a/adminx/modules/Catalog/assets/catalog-drawer.css +++ b/adminx/modules/Catalog/assets/catalog-drawer.css @@ -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; } } diff --git a/adminx/modules/Catalog/assets/catalog.js b/adminx/modules/Catalog/assets/catalog.js index 41e017a..1c3ea6b 100644 --- a/adminx/modules/Catalog/assets/catalog.js +++ b/adminx/modules/Catalog/assets/catalog.js @@ -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; }, diff --git a/adminx/modules/Catalog/view/edit.twig b/adminx/modules/Catalog/view/edit.twig index 498f11a..8087f74 100644 --- a/adminx/modules/Catalog/view/edit.twig +++ b/adminx/modules/Catalog/view/edit.twig @@ -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 %} -
  • -
    - - +
  • +
    + + {{ item.position + 1 }}
    {{ item.name }}#{{ item.id }}{% if item.document_id %} · документ #{{ item.document_id }}{% endif %}
    +
    уровень {{ level + 1 }}{{ item.fields_use|length }} / {{ item.filters_use|length }}
    {{ item.status ? 'активен' : 'скрыт' }}
    - {{ item.fields_use|length }} / {{ item.filters_use|length }} - {% if can_manage %}
    {% endif %} + {% if can_manage %}
    {% endif %}
    - {% if item.children %}
      {{ _self.rows(item.children, can_manage) }}
    {% endif %}
  • + {% if item.children %}{{ _self.rows(item.children, can_manage, level + 1) }}{% endif %} {% endfor %} {% endmacro %} {% block content %} @@ -25,7 +26,7 @@
    Структура

    Разделы каталога

    Порядок и вложенность используются в каталожном поле документа.

    Дерево разделов

    Перетаскивайте строки за маркер слева.

    {% if can_manage %}{% endif %}
    -
      {{ _self.rows(tree, can_manage) }}
    +
      {{ _self.rows(tree, can_manage, 0) }}
    {% if not tree %}
    Разделов пока нет.
    {% endif %}
    diff --git a/adminx/modules/Customers/Controller.php b/adminx/modules/Customers/Controller.php index 47d609c..98fcfd3 100644 --- a/adminx/modules/Customers/Controller.php +++ b/adminx/modules/Customers/Controller.php @@ -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)));} diff --git a/adminx/modules/Customers/Model.php b/adminx/modules/Customers/Model.php index ab18c5e..81e40dd 100644 --- a/adminx/modules/Customers/Model.php +++ b/adminx/modules/Customers/Model.php @@ -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(); diff --git a/adminx/modules/Customers/assets/customers.css b/adminx/modules/Customers/assets/customers.css index d1e5077..f76cb31 100644 --- a/adminx/modules/Customers/assets/customers.css +++ b/adminx/modules/Customers/assets/customers.css @@ -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; diff --git a/adminx/modules/Customers/assets/customers.js b/adminx/modules/Customers/assets/customers.js index 23cbdce..c5d957e 100644 --- a/adminx/modules/Customers/assets/customers.js +++ b/adminx/modules/Customers/assets/customers.js @@ -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]'); diff --git a/adminx/modules/Customers/module.php b/adminx/modules/Customers/module.php index 7deb334..e90cdcd 100644 --- a/adminx/modules/Customers/module.php +++ b/adminx/modules/Customers/module.php @@ -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')), diff --git a/adminx/modules/Customers/view/index.twig b/adminx/modules/Customers/view/index.twig index 13a648a..f9ade17 100644 --- a/adminx/modules/Customers/view/index.twig +++ b/adminx/modules/Customers/view/index.twig @@ -24,7 +24,7 @@

    Публичные аккаунты

    Посетители, зарегистрированные на сайте, их контакты и доступ к личному кабинету.

    {{ customers|length }}
    {% if can_manage %}{% endif %} - {% for customer in customers %}{% if can_manage %}{% endif %} + {% for customer in customers %}{% if can_manage %}{% endif %} {% else %}{% endfor %}
    IDПользовательКонтактыКомпанияРегистрацияСтатусДействия
    {{ customer.id }}{{ (customer.firstname ~ ' ' ~ customer.lastname)|trim ?: customer.user_name }}{{ customer.user_name }}{{ customer.email ?: customer.phone }}{% if customer.email and customer.phone %}{{ customer.phone }}{% endif %}{{ customer.company ?: '—' }}{{ customer.reg_time ? customer.reg_time|date('d.m.Y') : '—' }}{% if can_manage %}{% else %}{{ customer.status=='1'?'Активен':'Отключён' }}{% endif %}
    {{ customer.id }}{{ (customer.firstname ~ ' ' ~ customer.lastname)|trim ?: customer.user_name }}{{ customer.user_name }}{{ customer.email ?: customer.phone }}{% if customer.email and customer.phone %}{{ customer.phone }}{% endif %}{{ customer.company ?: '—' }}{{ customer.reg_time ? customer.reg_time|date('d.m.Y') : '—' }}{% if can_manage %}{% else %}{{ customer.status=='1'?'Активен':'Отключён' }}{% endif %}{% if customer.id==current_public_user_id %}{% else %}{% endif %}
    Пользователи не найдены
    @@ -173,27 +173,27 @@ {% if can_manage %} + +