{% set value = attribute(sample,role) %}{% if value is same as(true) %}{{ 'Да'|admin_trans }}{% elseif value is same as(false) %}{{ 'Нет'|admin_trans }}{% elseif value is iterable %}{% if value is empty %}{{ 'Не заполнено'|admin_trans }}{% else %}{{ value|slice(0, 3)|join(', ') }}{% if value|length > 3 %} · {{ 'ещё'|admin_trans }} {{ value|length - 3 }}{% endif %}{% endif %}{% elseif value is same as('') %}{{ 'Не заполнено'|admin_trans }}{% else %}{{ value }}{% endif %}
+ {% endif %}{% endfor %}
+
+ {% else %}
{{ 'Документ этой рубрики не найден.'|admin_trans }}
diff --git a/adminx/modules/Catalog/view/_product-roles.twig b/adminx/modules/Catalog/view/_product-roles.twig
new file mode 100644
index 0000000..2b65888
--- /dev/null
+++ b/adminx/modules/Catalog/view/_product-roles.twig
@@ -0,0 +1,20 @@
+
+
{{ 'Назначение полей товара'|admin_trans }}
+
+ {% for role,definition in product_roles.definitions %}{% if not definition.optional %}
+
+ {% endif %}{% endfor %}
+
+ {{ 'Дополнительные поля проекта'|admin_trans }}
+ {% for role,definition in product_roles.definitions %}{% if definition.optional %}
+
+ {% endif %}{% endfor %}
+
+ {% include '@catalog/_product-role-preview.twig' %}
+
diff --git a/adminx/modules/Catalog/view/_product-selection-preview.twig b/adminx/modules/Catalog/view/_product-selection-preview.twig
new file mode 100644
index 0000000..6c0aefa
--- /dev/null
+++ b/adminx/modules/Catalog/view/_product-selection-preview.twig
@@ -0,0 +1,24 @@
+{% set selection = product_roles.selection %}
+{% set options = product_roles.selection_options %}
+
+
{{ 'Проверка выборки'|admin_trans }}
+
+
+
+
+
+
+
+
+
+
+
{{ 'Параметры изменены, результат не актуален'|admin_trans }}
+ {% if product_roles.selection_result %}
+ {% set result = product_roles.selection_result %}
+
+
{{ (result.matched is same as(true) ? 'Входит в выборку' : (result.matched is same as(false) ? 'Не входит в выборку' : 'Проверка не завершена'))|admin_trans }}
+
{% for step in result.steps %}
{{ step.label|admin_trans }}{% if step.detail != '' %}: {{ step.detail }}{% endif %} {{ (step.pass is same as(true) ? 'Да' : (step.pass is same as(false) ? 'Нет' : ''))|admin_trans }}
{% set commerce_fields={'product_title_field_id':'Название товара','product_article_field_id':'Артикул','product_price_field_id':'Цена','product_old_price_field_id':'Старая цена','product_stock_field_id':'Остаток','product_images_field_id':'Изображения'} %}{% for key,label in commerce_fields %}{% endfor %}
+ {% if products_available %}{% include '@catalog/_product-roles.twig' %}
Карточка товара
Единый вид для главной страницы и разделов товарного каталога.
@@ -95,11 +95,12 @@
-{% if can_manage %}
- {{ runner_enabled ? 'Локальное выполнение' : 'Выполнение отключено' }}
+ {{ runner_enabled ? 'Выполнение включено' : 'Выполнение отключено' }}
diff --git a/adminx/modules/Customers/Controller.php b/adminx/modules/Customers/Controller.php
index 1255ba2..ba51215 100644
--- a/adminx/modules/Customers/Controller.php
+++ b/adminx/modules/Customers/Controller.php
@@ -102,7 +102,9 @@
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);}
+ $id=isset($params['id'])?(int)$params['id']:0;
+ try{$active=Model::toggle($id,Auth::id());}catch(\InvalidArgumentException $e){return $this->error($e->getMessage(),array(),422);}
+ AuditLog::record('customers.status_changed',array('actor_id'=>Auth::id(),'target_type'=>'public_user','target_id'=>$id,'meta'=>array('active'=>$active)));
return $this->success($active?'Пользователь включён':'Пользователь отключён');
}
@@ -119,6 +121,7 @@
$id=isset($params['id'])?(int)$params['id']:0;$input=Request::postAll();
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);}
+ AuditLog::record('customers.updated',array('actor_id'=>Auth::id(),'target_type'=>'public_user','target_id'=>$id));
return $this->success('Профиль пользователя сохранён',array('data'=>$customer));
}
@@ -128,14 +131,17 @@
return $e;
}
+ $id = isset($params['id']) ? (int) $params['id'] : 0;
try {
- Model::deleteCustomer(isset($params['id']) ? $params['id'] : 0, Auth::id());
+ Model::deleteCustomer($id, Auth::id());
} catch (\InvalidArgumentException $e) {
return $this->error($e->getMessage(), array(), 422);
} catch (\Throwable $e) {
return $this->error('Не удалось удалить пользователя', array(), 500);
}
+ AuditLog::record('customers.deleted', array('actor_id' => Auth::id(), 'target_type' => 'public_user', 'target_id' => $id));
+
return $this->success('Пользователь удалён', array('reload' => true));
}
@@ -143,10 +149,10 @@
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)));}
public function reorderFields(array $params=array()){if(($e=$this->guard())!==null){return $e;}$ids=json_decode(Request::postStr('order','[]'),true);if(!is_array($ids)){return $this->error('Некорректный порядок полей',array(),422);}$count=Model::reorderFields($ids);return $this->success('Порядок полей сохранён',array('data'=>array('count'=>$count)));}
- public function saveAuthSettings(array $params=array()){if(($e=$this->guard())!==null){return $e;}try{$settings=Model::saveAuthSettings(Request::postAll());}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}return $this->success('Настройки регистрации сохранены',array('data'=>array('settings'=>$settings)));}
- public function saveAuthPages(array $params=array()){if(($e=$this->guard())!==null){return $e;}try{$settings=Model::saveAuthPages(Request::postAll());}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}return $this->success('Страницы входа сохранены',array('data'=>array('settings'=>$settings)));}
+ public function saveAuthSettings(array $params=array()){if(($e=$this->guard())!==null){return $e;}try{$settings=Model::saveAuthSettings(Request::postAll());}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}AuditLog::record('customers.auth_settings_updated',array('actor_id'=>Auth::id(),'target_type'=>'public_auth'));return $this->success('Настройки регистрации сохранены',array('data'=>array('settings'=>$settings)));}
+ public function saveAuthPages(array $params=array()){if(($e=$this->guard())!==null){return $e;}try{$settings=Model::saveAuthPages(Request::postAll());}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}AuditLog::record('customers.auth_pages_updated',array('actor_id'=>Auth::id(),'target_type'=>'public_auth'));return $this->success('Страницы входа сохранены',array('data'=>array('settings'=>$settings)));}
public function authForm(array $params=array()){if(!Permission::check('view_customers')){return $this->error('Недостаточно прав',array(),403);}try{$form=Model::authForm(isset($params['key'])?(string)$params['key']:'');}catch(\Throwable $e){return $this->error($e->getMessage(),array(),404);}return $this->success('',array('data'=>$form));}
- public function saveAuthForm(array $params=array()){if(($e=$this->guard())!==null){return $e;}try{$template=Model::saveAuthForm(isset($params['key'])?(string)$params['key']:'',Request::postStr('template',''),Auth::id());}catch(\Throwable $e){return $this->error($e->getMessage(),array('template'=>$e->getMessage()),422);}return $this->success('Шаблон формы сохранён',array('data'=>array('template'=>$template,'customized'=>1)));}
- public function resetAuthForm(array $params=array()){if(($e=$this->guard())!==null){return $e;}try{$template=Model::resetAuthForm(isset($params['key'])?(string)$params['key']:'');}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}return $this->success('Восстановлен штатный шаблон формы',array('data'=>array('template'=>$template,'customized'=>0)));}
+ public function saveAuthForm(array $params=array()){if(($e=$this->guard())!==null){return $e;}$key=isset($params['key'])?(string)$params['key']:'';try{$template=Model::saveAuthForm($key,Request::postStr('template',''),Auth::id());}catch(\Throwable $e){return $this->error($e->getMessage(),array('template'=>$e->getMessage()),422);}AuditLog::record('customers.auth_form_updated',array('actor_id'=>Auth::id(),'target_type'=>'public_auth_form','meta'=>array('form'=>$key)));return $this->success('Шаблон формы сохранён',array('data'=>array('template'=>$template,'customized'=>1)));}
+ public function resetAuthForm(array $params=array()){if(($e=$this->guard())!==null){return $e;}$key=isset($params['key'])?(string)$params['key']:'';try{$template=Model::resetAuthForm($key);}catch(\Throwable $e){return $this->error($e->getMessage(),array(),422);}AuditLog::record('customers.auth_form_reset',array('actor_id'=>Auth::id(),'target_type'=>'public_auth_form','meta'=>array('form'=>$key)));return $this->success('Восстановлен штатный шаблон формы',array('data'=>array('template'=>$template,'customized'=>0)));}
protected function guard(){return $this->guardPermission('manage_customers');}
}
diff --git a/adminx/modules/Customers/CustomerCenter.php b/adminx/modules/Customers/CustomerCenter.php
index 1321e3b..29498e2 100644
--- a/adminx/modules/Customers/CustomerCenter.php
+++ b/adminx/modules/Customers/CustomerCenter.php
@@ -43,7 +43,7 @@
. ($orderJoin !== '' ? 'COALESCE(orders.orders_count,0)' : '0') . ' orders_count,'
. ($orderJoin !== '' ? 'COALESCE(orders.orders_total,0)' : '0') . ' orders_total,'
. ($orderJoin !== '' ? 'COALESCE(orders.last_order_at,0)' : '0') . ' last_order_at'
- . ' FROM ' . $users . ' u' . $orderJoin . ' WHERE u.deleted!=%s';
+ . ' FROM ' . $users . ' u' . $orderJoin . ' WHERE COALESCE(u.deleted,0)!=%s';
$args = array('1');
if ($query !== '') {
$sql .= ' AND (u.email LIKE %ss OR u.firstname LIKE %ss OR u.lastname LIKE %ss OR u.user_name LIKE %ss OR u.phone LIKE %ss OR u.company LIKE %ss';
@@ -78,7 +78,7 @@
$users = PublicUserTables::table('users');
$orders = BasketTables::table('module_basket_history');
$result = array(
- 'total' => (int) DB::query('SELECT COUNT(*) FROM ' . $users . ' WHERE deleted!=%s', '1')->getValue(),
+ 'total' => (int) DB::query('SELECT COUNT(*) FROM ' . $users . ' WHERE COALESCE(deleted,0)!=%s', '1')->getValue(),
'buyers' => 0, 'repeat' => 0, 'duplicates' => count(self::duplicateGroups()),
);
if (DatabaseSchema::tableExists($orders)) {
@@ -169,10 +169,10 @@
{
$table = PublicUserTables::table('users'); $groups = array();
$queries = array(
- 'email' => "SELECT LOWER(TRIM(email)) duplicate_value,GROUP_CONCAT(Id ORDER BY Id) ids,COUNT(*) amount FROM $table WHERE deleted!='1' AND email!='' GROUP BY LOWER(TRIM(email)) HAVING COUNT(*)>1 LIMIT 30",
+ 'email' => "SELECT LOWER(TRIM(email)) duplicate_value,GROUP_CONCAT(Id ORDER BY Id) ids,COUNT(*) amount FROM $table WHERE COALESCE(deleted,0)!='1' AND email!='' GROUP BY LOWER(TRIM(email)) HAVING COUNT(*)>1 LIMIT 30",
);
if (DatabaseSchema::columnExists($table, 'phone_normalized')) {
- $queries['phone'] = "SELECT phone_normalized duplicate_value,GROUP_CONCAT(Id ORDER BY Id) ids,COUNT(*) amount FROM $table WHERE deleted!='1' AND phone_normalized IS NOT NULL GROUP BY phone_normalized HAVING COUNT(*)>1 LIMIT 30";
+ $queries['phone'] = "SELECT phone_normalized duplicate_value,GROUP_CONCAT(Id ORDER BY Id) ids,COUNT(*) amount FROM $table WHERE COALESCE(deleted,0)!='1' AND phone_normalized IS NOT NULL GROUP BY phone_normalized HAVING COUNT(*)>1 LIMIT 30";
}
foreach ($queries as $kind => $sql) {
diff --git a/adminx/modules/Customers/GlobalSearchProvider.php b/adminx/modules/Customers/GlobalSearchProvider.php
index 9cc9e0d..71be493 100644
--- a/adminx/modules/Customers/GlobalSearchProvider.php
+++ b/adminx/modules/Customers/GlobalSearchProvider.php
@@ -25,7 +25,7 @@
{
$query = trim((string) $query); if ($query === '') { return array(); }
$sql = 'SELECT Id,email,firstname,lastname,user_name,phone,company,status FROM ' . PublicUserTables::table('users')
- . ' WHERE deleted!=%s AND (email LIKE %ss OR firstname LIKE %ss OR lastname LIKE %ss OR user_name LIKE %ss OR phone LIKE %ss OR company LIKE %ss';
+ . ' WHERE COALESCE(deleted,0)!=%s AND (email LIKE %ss OR firstname LIKE %ss OR lastname LIKE %ss OR user_name LIKE %ss OR phone LIKE %ss OR company LIKE %ss';
$args = array('1', $query, $query, $query, $query, $query, $query);
if (ctype_digit($query)) { $sql .= ' OR Id=%i'; $args[] = (int) $query; }
$sql .= ') ORDER BY status DESC,Id DESC LIMIT ' . max(1, min(12, (int) $limit));
diff --git a/adminx/modules/Customers/Model.php b/adminx/modules/Customers/Model.php
index b7e8d93..8b3fc41 100644
--- a/adminx/modules/Customers/Model.php
+++ b/adminx/modules/Customers/Model.php
@@ -35,15 +35,15 @@
{
public static function customers($q='')
{
- $sql='SELECT Id AS id,email,firstname,lastname,user_name,phone,company,status,reg_time,last_visit FROM '.self::table('users').' WHERE deleted!=%s';$args=array('1');$q=trim((string)$q);
- if($q!==''){$sql.=' AND (email LIKE %ss OR firstname LIKE %ss OR lastname LIKE %ss OR phone LIKE %ss OR company LIKE %ss)';for($i=0;$i<5;$i++){$args[]=$q;}}$sql.=' ORDER BY Id DESC LIMIT 500';return call_user_func_array(array('DB','query'),array_merge(array($sql),$args))->getAll()?:array();
+ $sql='SELECT Id AS id,email,firstname,lastname,user_name,phone,company,status,reg_time,last_visit FROM '.self::table('users').' WHERE COALESCE(deleted,0)!=%s';$args=array('1');$q=trim((string)$q);
+ if($q!==''){$sql.=' AND (email LIKE %ss OR firstname LIKE %ss OR lastname LIKE %ss OR user_name LIKE %ss OR phone LIKE %ss OR company LIKE %ss';for($i=0;$i<6;$i++){$args[]=$q;}if(ctype_digit($q)){$sql.=' OR Id=%i';$args[]=(int)$q;}$sql.=')';}$sql.=' ORDER BY Id DESC LIMIT 500';return call_user_func_array(array('DB','query'),array_merge(array($sql),$args))->getAll()?:array();
}
public static function exportChunk($q, $beforeId, $limit = 500)
{
- $sql = 'SELECT Id AS id,email,firstname,lastname,user_name,phone,company,status,reg_time,last_visit FROM ' . self::table('users') . ' WHERE deleted!=%s';
+ $sql = 'SELECT Id AS id,email,firstname,lastname,user_name,phone,company,status,reg_time,last_visit FROM ' . self::table('users') . ' WHERE COALESCE(deleted,0)!=%s';
$args = array('1'); $q = trim((string) $q);
- if ($q !== '') { $sql .= ' AND (email LIKE %ss OR firstname LIKE %ss OR lastname LIKE %ss OR phone LIKE %ss OR company LIKE %ss)'; for ($i = 0; $i < 5; $i++) { $args[] = $q; } }
+ if ($q !== '') { $sql .= ' AND (email LIKE %ss OR firstname LIKE %ss OR lastname LIKE %ss OR user_name LIKE %ss OR phone LIKE %ss OR company LIKE %ss)'; for ($i = 0; $i < 6; $i++) { $args[] = $q; } }
if ((int) $beforeId > 0) { $sql .= ' AND Id<%i'; $args[] = (int) $beforeId; }
$sql .= ' ORDER BY Id DESC LIMIT ' . max(1, min(1000, (int) $limit));
return call_user_func_array(array('DB', 'query'), array_merge(array($sql), $args))->getAll() ?: array();
@@ -51,7 +51,7 @@
public static function stats()
{
- $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()));
+ $table=self::table('users');return array('total'=>(int)DB::query('SELECT COUNT(*) FROM '.$table.' WHERE COALESCE(deleted,0)!=%s','1')->getValue(),'active'=>(int)DB::query('SELECT COUNT(*) FROM '.$table.' WHERE COALESCE(deleted,0)!=%s AND status=%s','1','1')->getValue(),'verified'=>(int)DB::query('SELECT COUNT(*) FROM '.$table.' WHERE COALESCE(deleted,0)!=%s AND (email_verified_at>0 OR phone_verified_at>0)','1')->getValue(),'fields'=>count(self::fields()));
}
public static function toggle($id, $currentSystemId = 0)
@@ -65,7 +65,7 @@
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');
+ DB::query('UPDATE '.self::table('users')." SET status=IF(status='1','0','1') WHERE Id=%i AND COALESCE(deleted,0)!=%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;
@@ -73,9 +73,25 @@
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;}
- $row=(array)$row;
+ $source = (new UserRepository())->find((int) $id);
+ if (!$source) { return null; }
+
+ $defaults = array(
+ 'email' => null, 'email_verified_at' => 0, 'firstname' => '', 'lastname' => '',
+ 'user_name' => '', 'phone' => '', 'phone_normalized' => null, 'phone_verified_at' => 0,
+ 'company' => '', 'city' => '', 'street' => '', 'street_nr' => '', 'zipcode' => '',
+ 'birthday' => '', 'description' => '', 'user_group' => 0, 'status' => '0',
+ 'reg_time' => 0, 'last_visit' => 0,
+ );
+ $row = array('id' => (int) $source['Id']);
+ foreach ($defaults as $key => $default) {
+ $row[$key] = array_key_exists($key, $source) ? $source[$key] : $default;
+ }
+
+ if (empty($row['phone_normalized']) && !empty($row['phone'])) {
+ $row['phone_normalized'] = Phone::normalize($row['phone']);
+ }
+
$row['birthday']=(int)$row['birthday']>0?date('Y-m-d',(int)$row['birthday']):'';
$values=DB::query('SELECT field_id,value FROM '.self::table('user_profile_values').' WHERE user_id=%i',(int)$id)->getAll()?:array();
$extra=array();
@@ -137,9 +153,9 @@
if($email===''&&$phone===''){throw new \InvalidArgumentException('Укажите email или телефон');}
if($adminAccess&&$email===''){throw new \InvalidArgumentException('Для доступа в панель управления укажите email');}
if(mb_strlen($email)>100||mb_strlen($userName)>50||mb_strlen($firstName)>50||mb_strlen($lastName)>50){throw new \InvalidArgumentException('Имя, email или логин превышают допустимую длину');}
- if($email!==''&&DB::query('SELECT Id FROM '.self::table('users').' WHERE LOWER(email)=LOWER(%s) AND Id!=%i AND deleted!=%s LIMIT 1',$email,$id,'1')->getValue()){throw new \InvalidArgumentException('Этот email уже используется');}
- if($phone!==''&&DB::query('SELECT Id FROM '.self::table('users').' WHERE phone_normalized=%s AND Id!=%i AND deleted!=%s LIMIT 1',$phone,$id,'1')->getValue()){throw new \InvalidArgumentException('Этот телефон уже используется');}
- if(DB::query('SELECT Id FROM '.self::table('users').' WHERE LOWER(user_name)=LOWER(%s) AND Id!=%i AND deleted!=%s LIMIT 1',$userName,$id,'1')->getValue()){throw new \InvalidArgumentException('Этот логин уже используется');}
+ if($email!==''&&DB::query('SELECT Id FROM '.self::table('users').' WHERE LOWER(email)=LOWER(%s) AND Id!=%i AND COALESCE(deleted,0)!=%s LIMIT 1',$email,$id,'1')->getValue()){throw new \InvalidArgumentException('Этот email уже используется');}
+ if($phone!==''&&DB::query('SELECT Id FROM '.self::table('users').' WHERE phone_normalized=%s AND Id!=%i AND COALESCE(deleted,0)!=%s LIMIT 1',$phone,$id,'1')->getValue()){throw new \InvalidArgumentException('Этот телефон уже используется');}
+ if(DB::query('SELECT Id FROM '.self::table('users').' WHERE LOWER(user_name)=LOWER(%s) AND Id!=%i AND COALESCE(deleted,0)!=%s LIMIT 1',$userName,$id,'1')->getValue()){throw new \InvalidArgumentException('Этот логин уже используется');}
$groupIds=array_map(function($item){return (int)$item['id'];},self::groups());
if(!in_array($group,$groupIds,true)){throw new \InvalidArgumentException('Выберите активную группу публичных пользователей');}
$authSettings=PublicAuthSettings::all();
@@ -175,7 +191,10 @@
self::saveExtraValues($id,$extra);
if($password!==''){(new UserRepository())->setPassword($id,$password);}
IdentityLinker::setAdminAccess($id,$adminAccess,$adminRole,(int)$currentSystemId);
- if($password!==''||$active!=='1'){self::invalidateCustomerSessions($id);}
+ $securityChanged=$password!==''||$active!==(string)$current['user']['status']
+ ||$group!==(int)$current['user']['user_group']||$email!==mb_strtolower(trim((string)$current['user']['email']))
+ ||$phone!==(string)$current['user']['phone_normalized']||$userName!==(string)$current['user']['user_name'];
+ if($securityChanged){self::invalidateCustomerSessions($id);}
DB::commit();
}catch(\Throwable $e){DB::rollback();throw $e;}
return self::customer($id);
diff --git a/adminx/modules/Customers/module.php b/adminx/modules/Customers/module.php
index dc602f7..c94899e 100644
--- a/adminx/modules/Customers/module.php
+++ b/adminx/modules/Customers/module.php
@@ -17,7 +17,7 @@
use App\Adminx\Customers\GlobalSearchProvider;
return array(
- 'code' => 'customers', 'name' => 'Пользователи сайта', 'version' => '0.10.3',
+ 'code' => 'customers', 'name' => 'Пользователи сайта', 'version' => '0.10.4',
'permissions' => array('key' => 'customers', 'items' => array(
array(
'code' => 'view_customers',
diff --git a/adminx/modules/Customers/view/index.twig b/adminx/modules/Customers/view/index.twig
index 27263c1..d5ed0a1 100644
--- a/adminx/modules/Customers/view/index.twig
+++ b/adminx/modules/Customers/view/index.twig
@@ -177,7 +177,7 @@
#——Регистрация—Последний вход
-
Основные данныеИмя, контакты и данные организации.
+
Основные данныеИмя, контакты и данные организации.
АдресДанные для профиля и предзаполнения заказов.
ДоступПубличная группа, состояние аккаунта и роль в панели управления.
@@ -189,11 +189,11 @@
-
+
{% if fields %}
Дополнительные поляАктивные поля из конструктора публичного профиля.
{% for field in fields %}{% if field.is_active %}{% endif %}{% endfor %}
{% endif %}
-
БезопасностьПароль изменится, только если заполнить поле.
+
БезопасностьПароль изменится, только если заполнить поле.
diff --git a/adminx/modules/Documents/BulkEditor.php b/adminx/modules/Documents/BulkEditor.php
index 09356e9..ba8a188 100644
--- a/adminx/modules/Documents/BulkEditor.php
+++ b/adminx/modules/Documents/BulkEditor.php
@@ -34,7 +34,7 @@
*/
class BulkEditor
{
- const VERSION = 1;
+ const VERSION = 2;
const MAX_DOCUMENTS = 5000;
const CHUNK_SIZE = 20;
const EXPIRES_AFTER = 7200;
@@ -52,6 +52,19 @@
'document_property' => array('label' => 'Свойство / артикул', 'payload' => 'property'),
);
+ protected static $stateOperations = array(
+ 'search_include' => array('column'=>'document_in_search','payload'=>'in_search','value'=>'1','before'=>array('0'=>'Вне поиска','1'=>'В поиске')),
+ 'search_exclude' => array('column'=>'document_in_search','payload'=>'in_search','value'=>'0','before'=>array('0'=>'Вне поиска','1'=>'В поиске')),
+ 'robots_index' => array('column'=>'document_meta_robots','payload'=>'meta_robots','value'=>'index,follow','before'=>array()),
+ 'robots_noindex' => array('column'=>'document_meta_robots','payload'=>'meta_robots','value'=>'noindex,nofollow','before'=>array()),
+ 'sitemap_include' => array('column'=>'document_in_sitemap','payload'=>'in_sitemap','value'=>'1','before'=>array('0'=>'Не добавляется','1'=>'Добавляется')),
+ 'sitemap_exclude' => array('column'=>'document_in_sitemap','payload'=>'in_sitemap','value'=>'0','before'=>array('0'=>'Не добавляется','1'=>'Добавляется')),
+ 'technical' => array('column'=>'document_is_technical','payload'=>'is_technical','value'=>'1','before'=>array('0'=>'Обычный','1'=>'Служебный')),
+ 'public' => array('column'=>'document_is_technical','payload'=>'is_technical','value'=>'0','before'=>array('0'=>'Обычный','1'=>'Служебный')),
+ 'sitemap_frequency' => array('column'=>'document_sitemap_freq','payload'=>'sitemap_frequency','value_input'=>'sitemap_frequency','before'=>array('0'=>'always','1'=>'hourly','2'=>'daily','3'=>'weekly','4'=>'monthly','5'=>'yearly','6'=>'never')),
+ 'sitemap_priority' => array('column'=>'document_sitemap_pr','payload'=>'sitemap_priority','value_input'=>'sitemap_priority','before'=>array()),
+ );
+
public static function options()
{
return array(
@@ -198,6 +211,14 @@
if (!$row) { return false; }
$type = $operation['type'];
+ if (isset($operation['target_type']) && $operation['target_type'] === 'state') {
+ if ($type === 'technical' && Model::isProtectedDocument($documentId)) { return false; }
+ $current = (string) $row[$operation['target']];
+ if ($current === (string) $operation['value']) { return false; }
+ $service->save($documentId, array($operation['payload'] => $operation['value']), $actorId, 'bulk_editor');
+ return true;
+ }
+
if ($type === 'publish' || $type === 'unpublish') {
if (Model::isProtectedDocument($documentId)) { return false; }
$status = $type === 'publish' ? 1 : 0;
@@ -250,12 +271,32 @@
protected static function operation(array $input, array $filters)
{
$type = isset($input['operation']) ? (string) $input['operation'] : '';
- $allowed = array('fill', 'set', 'clear', 'replace', 'move', 'publish', 'unpublish', 'recalculate');
+ $allowed = array_merge(
+ array('fill', 'set', 'clear', 'replace', 'move', 'publish', 'unpublish', 'recalculate'),
+ array_keys(self::$stateOperations)
+ );
if (!in_array($type, $allowed, true)) {
throw new \InvalidArgumentException('Выберите действие');
}
$operation = array('type' => $type, 'label' => self::operationLabel($type));
+ if (isset(self::$stateOperations[$type])) {
+ $config = self::$stateOperations[$type];
+ $value = isset($config['value']) ? (string) $config['value'] : trim(isset($input[$config['value_input']]) ? (string) $input[$config['value_input']] : '');
+ if ($type === 'sitemap_frequency' && !array_key_exists($value, $config['before'])) {
+ throw new \InvalidArgumentException('Выберите частоту обновления sitemap');
+ }
+
+ if ($type === 'sitemap_priority' && (!is_numeric($value) || (float) $value < 0 || (float) $value > 1)) {
+ throw new \InvalidArgumentException('Приоритет sitemap должен быть от 0 до 1');
+ }
+
+ return array_merge($operation, $config, array(
+ 'target_type' => 'state', 'target' => $config['column'], 'value' => $value,
+ 'target_label' => self::operationTargetLabel($type),
+ ));
+ }
+
if (in_array($type, array('move', 'publish', 'unpublish', 'recalculate'), true)) {
if ($type === 'move') {
$targetRubricId = isset($input['target_rubric_id']) ? (int) $input['target_rubric_id'] : 0;
@@ -346,9 +387,9 @@
$currentValues = array();
$ids = array();
foreach ($rows as $row) { $ids[] = (int) $row['Id']; }
- if (isset($operation['target_type']) && $operation['target_type'] === 'document') {
+ if (isset($operation['target_type']) && in_array($operation['target_type'], array('document', 'state'), true)) {
$column = (string) $operation['target'];
- if (!isset(self::$documentFields[$column])) {
+ if ($operation['target_type'] === 'document' && !isset(self::$documentFields[$column])) {
throw new \InvalidArgumentException('Поле документа недоступно для массового изменения');
}
@@ -406,6 +447,17 @@
return array('before' => (string) $row['rubric_title'], 'after' => (string) $operation['target_rubric_title'], 'changed' => (int) $row['rubric_id'] !== (int) $operation['target_rubric_id'], 'note' => 'Совпадающие поля переносятся по системному имени');
}
+ if ($operation['target_type'] === 'state') {
+ if ($operation['type'] === 'technical' && Model::isProtectedDocument((int) $row['Id'])) {
+ return array('before'=>'Системный документ','after'=>'Без изменений','changed'=>false,'note'=>'Главную и страницу 404 нельзя сделать служебными');
+ }
+
+ $current = self::currentValue($row, $operation);
+ $before = isset($operation['before'][$current]) ? $operation['before'][$current] : $current;
+ $after = isset($operation['before'][(string) $operation['value']]) ? $operation['before'][(string) $operation['value']] : (string) $operation['value'];
+ return array('before'=>$before,'after'=>$after,'changed'=>$current !== (string) $operation['value'],'note'=>'');
+ }
+
$current = self::currentValue($row, $operation);
$next = self::changedValue($current, $operation);
return array('before' => $current, 'after' => $next['value'], 'changed' => $next['changed'], 'note' => $next['changed'] ? '' : 'Значение уже соответствует действию');
@@ -417,7 +469,7 @@
return (string) $row['__bulk_current'];
}
- if ($operation['target_type'] === 'document') {
+ if ($operation['target_type'] === 'document' || $operation['target_type'] === 'state') {
if (!array_key_exists($operation['target'], $row)) {
$value = DB::query(
'SELECT `' . $operation['target'] . '` FROM ' . ContentTables::table('documents') . ' WHERE Id=%i',
@@ -537,12 +589,29 @@
$labels = array(
'fill' => 'Заполнить пустые', 'set' => 'Установить значение', 'clear' => 'Очистить',
'replace' => 'Найти и заменить', 'move' => 'Перенести в рубрику',
- 'publish' => 'Опубликовать', 'unpublish' => 'Снять с публикации',
- 'recalculate' => 'Пересчитать поля и индексы',
- );
+ 'publish' => 'Опубликовать', 'unpublish' => 'Снять с публикации',
+ 'recalculate' => 'Пересчитать поля и индексы',
+ 'search_include' => 'Включить во внутренний поиск', 'search_exclude' => 'Исключить из внутреннего поиска',
+ 'robots_index' => 'Разрешить индексацию поисковиками', 'robots_noindex' => 'Запретить индексацию поисковиками',
+ 'sitemap_include' => 'Добавить в sitemap', 'sitemap_exclude' => 'Исключить из sitemap',
+ 'technical' => 'Сделать служебными', 'public' => 'Сделать обычными',
+ 'sitemap_frequency' => 'Изменить частоту sitemap', 'sitemap_priority' => 'Изменить приоритет sitemap',
+ );
return isset($labels[$type]) ? $labels[$type] : $type;
}
+ protected static function operationTargetLabel($type)
+ {
+ $labels = array(
+ 'search_include'=>'Внутренний поиск','search_exclude'=>'Внутренний поиск',
+ 'robots_index'=>'Meta robots','robots_noindex'=>'Meta robots',
+ 'sitemap_include'=>'Участие в sitemap','sitemap_exclude'=>'Участие в sitemap',
+ 'technical'=>'Публичный доступ','public'=>'Публичный доступ',
+ 'sitemap_frequency'=>'Частота sitemap','sitemap_priority'=>'Приоритет sitemap',
+ );
+ return isset($labels[$type]) ? $labels[$type] : '';
+ }
+
protected static function shortValue($value)
{
$value = trim(preg_replace('/\s+/u', ' ', strip_tags((string) $value)));
diff --git a/adminx/modules/Documents/DocumentHookRunner.php b/adminx/modules/Documents/DocumentHookRunner.php
index f12fce0..da363f0 100644
--- a/adminx/modules/Documents/DocumentHookRunner.php
+++ b/adminx/modules/Documents/DocumentHookRunner.php
@@ -226,8 +226,8 @@
'document_title', 'document_alias', 'document_alias_header', 'document_alias_history',
'document_short_alias', 'document_breadcrumb_title', 'document_excerpt',
'document_meta_keywords', 'document_meta_description', 'document_meta_robots',
- 'document_sitemap_freq', 'document_sitemap_pr', 'document_tags', 'document_property',
- 'guid', 'document_status', 'document_in_search', 'document_parent', 'rubric_tmpl_id',
+ 'document_sitemap_freq', 'document_sitemap_pr', 'document_in_sitemap', 'document_tags', 'document_property',
+ 'guid', 'document_status', 'document_in_search', 'document_is_technical', 'document_parent', 'rubric_tmpl_id',
'document_linked_navi_id', 'document_position', 'document_published', 'document_expire',
'document_author_id',
) as $key) {
diff --git a/adminx/modules/Documents/Model.php b/adminx/modules/Documents/Model.php
index 794618e..def8d3f 100644
--- a/adminx/modules/Documents/Model.php
+++ b/adminx/modules/Documents/Model.php
@@ -111,7 +111,7 @@
$countArgs = array_merge(array('SELECT COUNT(*) FROM ' . self::documentsTable() . ' d' . $where), $args);
$total = (int) call_user_func_array(array('DB', 'query'), $countArgs)->getValue();
- $sql = 'SELECT d.*, r.rubric_title, r.rubric_alias,(' . $score . ') AS search_relevance'
+ $sql = 'SELECT d.*, r.rubric_title, r.rubric_alias,COALESCE(r.rubric_is_technical,0) rubric_is_technical,(' . $score . ') AS search_relevance'
. ' FROM ' . self::documentsTable() . ' d'
. ' LEFT JOIN ' . self::rubricsTable() . ' r ON r.Id = d.rubric_id'
. $where
@@ -190,7 +190,7 @@
public static function one($id)
{
$row = DB::query(
- 'SELECT d.*, r.rubric_title, r.rubric_alias FROM ' . self::documentsTable() . ' d'
+ 'SELECT d.*, r.rubric_title, r.rubric_alias,COALESCE(r.rubric_is_technical,0) rubric_is_technical FROM ' . self::documentsTable() . ' d'
. ' LEFT JOIN ' . self::rubricsTable() . ' r ON r.Id = d.rubric_id'
. ' WHERE d.Id = %i LIMIT 1',
(int) $id
@@ -222,6 +222,8 @@
'guid' => '',
'document_status' => 1,
'document_in_search' => 1,
+ 'document_is_technical' => $rubric && !empty($rubric['rubric_is_technical']) ? 1 : 0,
+ 'document_in_sitemap' => 1,
'document_author_id' => 1,
'document_parent' => 0,
'rubric_tmpl_id' => 0,
@@ -624,8 +626,9 @@
foreach (array(
'document_title', 'document_alias', 'document_alias_header', 'document_alias_history',
'document_short_alias', 'document_breadcrumb_title', 'document_excerpt', 'document_meta_keywords',
- 'document_meta_description', 'document_meta_robots', 'document_sitemap_freq', 'document_sitemap_pr',
- 'document_tags', 'document_property', 'guid', 'document_status', 'document_in_search',
+ 'document_meta_description', 'document_meta_robots', 'document_sitemap_freq', 'document_sitemap_pr',
+ 'document_in_sitemap', 'document_tags', 'document_property', 'guid', 'document_status', 'document_in_search',
+ 'document_is_technical',
'document_parent', 'rubric_tmpl_id', 'document_linked_navi_id', 'document_position',
'document_author_id',
) as $key) {
@@ -658,6 +661,10 @@
$data['document_alias_header'] = 301;
}
+ if ($id > 0 && self::isProtectedDocument($id)) {
+ $data['document_is_technical'] = 0;
+ }
+
$data['rubric_id'] = $rubricId;
$data['document_changed'] = $now;
$data['document_version'] = $existing ? max(1, (int) $existing['document_version']) + 1 : 1;
@@ -944,6 +951,7 @@
$source['document_deleted'] = '0';
$source['document_count_print'] = 0;
$source['document_count_view'] = 0;
+ $source['module_catalog'] = '';
$source['document_author_id'] = self::legacyAuthorId($authorId);
$source['document_changed'] = time();
$source['document_version'] = 1;
@@ -1458,13 +1466,15 @@
'document_meta_keywords' => trim($get('document_meta_keywords')),
'document_meta_description' => trim($get('document_meta_description')),
'document_meta_robots' => self::robotsValue($get('document_meta_robots')),
- 'document_sitemap_freq' => self::sitemapFreqValue($get('document_sitemap_freq')),
- 'document_sitemap_pr' => self::sitemapPrValue($get('document_sitemap_pr')),
+ 'document_sitemap_freq' => self::sitemapFreqValue($get('document_sitemap_freq')),
+ 'document_sitemap_pr' => self::sitemapPrValue($get('document_sitemap_pr')),
+ 'document_in_sitemap' => (int) $get('document_in_sitemap') === 1 ? 1 : 0,
'document_tags' => trim($get('document_tags')),
'document_property' => trim($get('document_property')),
'guid' => substr(trim($get('guid')), 0, 100),
- 'document_status' => (int) $get('document_status') === 1 ? '1' : '0',
- 'document_in_search' => (int) $get('document_in_search') === 1 ? '1' : '0',
+ 'document_status' => (int) $get('document_status') === 1 ? '1' : '0',
+ 'document_in_search' => (int) $get('document_in_search') === 1 ? '1' : '0',
+ 'document_is_technical' => (int) $get('document_is_technical') === 1 ? 1 : 0,
'document_parent' => (int) $get('document_parent'),
'rubric_tmpl_id' => (int) $get('rubric_tmpl_id'),
'document_linked_navi_id' => (int) $get('document_linked_navi_id'),
@@ -1662,6 +1672,10 @@
$row['document_status'] = (int) $row['document_status'];
$row['document_deleted'] = (int) $row['document_deleted'];
$row['document_in_search'] = (int) $row['document_in_search'];
+ $row['document_is_technical'] = isset($row['document_is_technical']) ? (int) $row['document_is_technical'] : 0;
+ $row['document_in_sitemap'] = isset($row['document_in_sitemap']) ? (int) $row['document_in_sitemap'] : 1;
+ $row['rubric_is_technical'] = isset($row['rubric_is_technical']) ? (int) $row['rubric_is_technical'] : 0;
+ $row['is_technical_effective'] = $row['document_is_technical'] || $row['rubric_is_technical'];
$row['document_published'] = (int) $row['document_published'];
$row['document_expire'] = (int) $row['document_expire'];
$row['document_changed'] = (int) $row['document_changed'];
@@ -2711,11 +2725,16 @@
protected static function clearDocumentCache($documentId)
{
- ContentCacheInvalidator::document($documentId, false);
+ DB::afterCommit(function () use ($documentId) { ContentCacheInvalidator::document($documentId, false); });
}
protected static function buildDocumentSnapshot($documentId)
{
+ if (DB::$transaction_in_progress) {
+ DB::afterCommit(function () use ($documentId) { ContentCacheInvalidator::document($documentId, true); });
+ return null;
+ }
+
return ContentCacheInvalidator::document($documentId, true);
}
diff --git a/adminx/modules/Documents/Revisions.php b/adminx/modules/Documents/Revisions.php
index a38f2d7..7dd3ec1 100644
--- a/adminx/modules/Documents/Revisions.php
+++ b/adminx/modules/Documents/Revisions.php
@@ -230,6 +230,7 @@
'document_parent' => 'Родитель', 'rubric_tmpl_id' => 'Шаблон', 'document_linked_navi_id' => 'Навигация',
'document_position' => 'Позиция', 'document_published' => 'Дата публикации', 'document_expire' => 'Дата окончания',
'document_author_id' => 'Автор', 'document_sitemap_freq' => 'Sitemap: частота', 'document_sitemap_pr' => 'Sitemap: приоритет',
+ 'document_in_sitemap' => 'Sitemap: участие', 'document_is_technical' => 'Служебный документ',
);
$out = array();
foreach ($document as $key => $value) {
@@ -238,8 +239,12 @@
$value = (int) $value > 0 ? date('d.m.Y H:i:s', (int) $value) : 'не задано';
} elseif ($key === 'document_status') {
$value = (int) $value === 1 ? 'Опубликован' : 'Черновик';
- } elseif ($key === 'document_in_search') {
- $value = (int) $value === 1 ? 'В поиске' : 'Скрыт';
+ } elseif ($key === 'document_in_search') {
+ $value = (int) $value === 1 ? 'В поиске' : 'Скрыт';
+ } elseif ($key === 'document_in_sitemap') {
+ $value = (int) $value === 1 ? 'Добавляется' : 'Не добавляется';
+ } elseif ($key === 'document_is_technical') {
+ $value = (int) $value === 1 ? 'Служебный' : 'Обычный';
}
$out[] = array('key' => $key, 'title' => $labels[$key], 'value' => (string) $value, 'value_preview' => self::shorten((string) $value, 420));
diff --git a/adminx/modules/Documents/assets/documents.css b/adminx/modules/Documents/assets/documents.css
index 787a282..5686f3e 100644
--- a/adminx/modules/Documents/assets/documents.css
+++ b/adminx/modules/Documents/assets/documents.css
@@ -1243,6 +1243,10 @@
.documents-bulk-controls .field {
min-width: 0;
}
+.documents-bulk-controls .field[hidden],
+[data-bulk-technical-help][hidden] {
+ display: none;
+}
.documents-bulk-search,
.documents-bulk-value {
grid-column: span 2;
@@ -1270,6 +1274,23 @@
margin-top: 1px;
font-size: 16px;
}
+.documents-bulk-technical-help {
+ margin: 0 16px 16px;
+}
+.documents-bulk-technical-help b,
+.documents-bulk-technical-help span {
+ display: block;
+}
+.documents-bulk-technical-help span {
+ margin-top: 2px;
+}
+.documents-technical-alert b,
+.documents-technical-alert span {
+ display: block;
+}
+.documents-technical-alert span {
+ margin-top: 2px;
+}
.documents-bulk-step-footer,
.documents-bulk-run {
display: flex;
@@ -2654,22 +2675,25 @@
}
.documents-value-row {
display: grid;
- grid-template-columns: 34px minmax(0, 1fr) 34px;
+ grid-template-columns: 106px minmax(0, 1fr) 34px;
gap: 10px;
- align-items: start;
+ align-items: center;
min-width: 0;
padding: 12px;
border-radius: var(--radius-lg);
background: var(--bg-soft);
box-shadow: inset 0 0 0 1px var(--border-default);
}
-.documents-value-row-tools,
+.documents-value-row-tools {
+ display: grid;
+ grid-template-columns: repeat(3, 34px);
+ gap: 2px;
+ min-width: 0;
+}
.documents-value-row-fields {
display: grid;
gap: 8px;
min-width: 0;
-}
-.documents-value-row-fields {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.documents-value-row-list_single .documents-value-row-fields {
@@ -2687,6 +2711,22 @@
}
}
@media (max-width: 640px) {
+ .documents-value-row {
+ grid-template-columns: minmax(0, 1fr) 34px;
+ }
+ .documents-value-row-tools {
+ grid-column: 1;
+ grid-row: 2;
+ justify-content: start;
+ }
+ .documents-value-row-fields {
+ grid-column: 1 / -1;
+ grid-row: 1;
+ }
+ .documents-value-remove {
+ grid-column: 2;
+ grid-row: 2;
+ }
.documents-value-row-packages .documents-value-row-fields {
grid-template-columns: minmax(0, 1fr);
}
diff --git a/adminx/modules/Documents/assets/documents.js b/adminx/modules/Documents/assets/documents.js
index 2eb4160..c2527be 100644
--- a/adminx/modules/Documents/assets/documents.js
+++ b/adminx/modules/Documents/assets/documents.js
@@ -3669,7 +3669,8 @@
var help = root.querySelector('[data-bulk-target-help]');
if (!help || !target) { return; }
var selected = target.options[target.selectedIndex];
- var message = selected ? String(selected.getAttribute('data-help') || '') : '';
+ var editsField = operation && ['fill', 'set', 'clear', 'replace'].indexOf(operation.value) !== -1;
+ var message = editsField && selected ? String(selected.getAttribute('data-help') || '') : '';
if (selected && selected.value === 'document_title' && scope && scope.value === 'products') {
message = 'Это заголовок документа в панели. Название товара на сайте обычно берётся из поля рубрики, помеченного «витрина: название товара на сайте».';
}
@@ -3704,8 +3705,11 @@
var editsField = ['fill', 'set', 'clear', 'replace'].indexOf(value) !== -1;
setVisibility('[data-bulk-target-wrap]', editsField);
setVisibility('[data-bulk-search-wrap]', value === 'replace');
- setVisibility('[data-bulk-value-wrap]', value === 'fill' || value === 'set' || value === 'replace');
- setVisibility('[data-bulk-rubric-target-wrap]', value === 'move');
+ setVisibility('[data-bulk-value-wrap]', value === 'fill' || value === 'set' || value === 'replace');
+ setVisibility('[data-bulk-rubric-target-wrap]', value === 'move');
+ setVisibility('[data-bulk-sitemap-frequency-wrap]', value === 'sitemap_frequency');
+ setVisibility('[data-bulk-sitemap-priority-wrap]', value === 'sitemap_priority');
+ setVisibility('[data-bulk-technical-help]', value === 'technical' || value === 'public');
updateTargetHelp();
}
diff --git a/adminx/modules/Documents/language/en/client.xml b/adminx/modules/Documents/language/en/client.xml
index 75f9d75..9f6da8c 100644
--- a/adminx/modules/Documents/language/en/client.xml
+++ b/adminx/modules/Documents/language/en/client.xml
@@ -1,302 +1,48 @@
- xAdd more...
- The address of the document will be written into the link field.
- Up to 10 characters, optional.
- Token copied
- <button class="btn btn-ghost btn-icon btn-sm documents-media-remove" type="button" data-document-media-remove data-tooltip="Delete" aria-label="Delete"><i class="ti ti-trash"></i></button>
- field
- Clear all field lines?
- Parameter
- Rebuild JSON snapshots?New
- click to select
- Failed to check alias
- API token createdAdd a keyword
- <div class="modal-body"><div class="input-wrap documents-relation-search"><i class="ti ti-search"></i><input class="input" type="search" placeholder="ID, title or alias" data-relation-search></div><div class="documents-picker-status" data-relation-status>Loading...</div><div class="documents-relation-list" data-relation-list></div></div>
- JSON snapshot
- toCreation preset
- uh
- Teaser ID/link
- Internal server error. Please refresh the page and try again.
- <button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-media-down data-tooltip="Below" aria-label="Below"><i class="ti ti-arrow-down"></i></button>
- "><button class="btn btn-secondary btn-icon btn-sm" type="button" data-document-media-pick data-tooltip="Select file" aria-label="Select file"><i class="ti ti-
- ts
- The document ID will be written in the field.
- after savingCreation preset savedFailed to load list
- h
- h
- <section class="documents-revision-system"><div class="documents-revision-subhead"><i class="ti ti-settings"></i><b>Basic settings</b><span>
- [link]" data-media-key="link" value="" placeholder="Link or document" data-document-media-url data-document-picker-type="all">
- Restore
- <button class="btn btn-secondary btn-icon btn-sm" type="button" data-document-media-doc-pick data-tooltip="Select document" aria-label="Select document"><i class="ti ti-file-search"></i></button></div>
- </span><label class="documents-revision-group-check"><input type="checkbox" data-document-revision-group="field" checked><span>All</span></label></div>
- ID, name or alias
- Select file
- Column 3
- Redirect saved
- The secret is ready to be copied.
- The server returned an incorrect response.
- Refresh page
- Untitled
- JSON snapshots reassembled:
- <button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-media-up data-tooltip="Above" aria-label="Above"><i class="ti ti-arrow-up"></i></button>
- Document ID
- Note added
- newnew
- Draft
- Title
- Revision deleted
- Document title
- Files uploaded
- Select revision
- Rebuild JSONClear daily statistics?
- Column 1
- fields
- th
- There are no matching files in the folderStatistics cleared
- not created yet
- Copy
- Redirect removed
- Audits
- I
- l
- a
- b
- Document saved
- <span class="badge badge-gray">not created</span>
- Failed to read folder
- Note deleted
- File not selected
- f
- o
- <div><dt>File</dt><dd class="mono">
- kg
- r
- . The window must be left open until completion.in the section
- sAll view_count lines will be removed. General document counters will remain unchanged.
- <button class="btn btn-secondary btn-icon btn-sm" type="button" data-document-media-pick data-tooltip="Select file" aria-label="Select file"><i class="ti ti-paperclip"></i></button>
- Rebuild
- <span class="badge badge-amber">needs reassembly</span>doc.
- 0 B
- d<div class="documents-term-status is-error"><i class="ti ti-alert-circle"></i><span>Failed to load options</span></div>
- <div class="empty-state">There are no revisions yet. The first snapshot will appear after saving the document.</div>
- <div><label class="documents-revision-check" aria-label="Restore
- Clear the field?
- Column 2
- Alias can be left empty
- e
- Length
- fDelete "
- Withdrawn
- </p></div><button class="modal-close" type="button" data-relation-close aria-label="Close"><i class="ti ti-x"></i></button></div>
- <span class="badge badge-green">relevant</span>URL copiedThere are no saved values yet
- [description]" data-media-key="description" rows="2" placeholder="Description"></textarea>
- <button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-value-down data-tooltip="Below" aria-label="Below"><i class="ti ti-arrow-down"></i></button>
- The integration will immediately lose access. It will be impossible to return this token.
- Errors found:
- Category template: /
- , errors:
- TemplateFailed to load statistics
- API token revokedFind or add a tag
- n
- Open the desired folder and confirm your selection. All suitable files from it will be added to the field.
- <div><dt>State</dt><dd>
- <div class="empty-state">Loading...</div>
- The contents of the snapshot will appear after selecting a revision.Delete
- Copied
- optional
- The description will appear after filling out the meta description.
- Documents
- The document has been restored
- gNo matches
- <button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-value-up data-tooltip="Above" aria-label="Above"><i class="ti ti-arrow-up"></i></button>
- Permissions for one external integration.
- The document has changedRequest Error
- fieldsClear
- Revoke
- Changes saved
- [title]" data-media-key="title" value="" placeholder="Image title">
- Remove
- n
- Added from folder:
- Edit
- Failed to load revisions
- Meaning
- Select document
- <div><dt>Formed by</dt><dd>
- Fields only
- b
- The category does not have a path template: the document alias is used from the root of the site.
- Failed to generate alias
- and
- Height,
- No image selected
- <div><dt>Size</dt><dd>
- JSON snapshot rebuilt
- Document revisions
- HeadingLoading...
- Remove document
- Add from this folder
- All lines in this field will be removed from the document after saving.
- <button class="btn btn-ghost btn-icon btn-sm documents-drag-handle" type="button" data-doc-drag draggable="true" data-tooltip="Drag" aria-label="Drag"><i class="ti ti-grip-vertical"></i></button>Preset deleted
- Pictures
- / + document alias. The date is taken from the publication.
- <div><dt>Fields</dt><dd class="mono">
- Batch rebuild stopped
- <article class="documents-revision-field documents-revision-system-field"><div><label class="documents-revision-check" aria-label="Restore
- Failed to get snapshot status
- with
- These files are already in the field
- The pictures will be sequentially reassembled to
- pictures
- yu
- [name]" data-media-key="name" value="" placeholder="File name">
- Revoke an API token?
- <section class="documents-revision-content"><div class="documents-revision-subhead"><i class="ti ti-forms"></i><b>Category fields</b><span>
- efrom
- m
- wFailed to apply filters
- Field
- Read onlyClick to find documentAdd "<div class="documents-term-status"><i class="ti ti-loader-2"></i><span>Looking for matches...</span></div>presetDelete preset
- Failed to generate short alias
- can be left blank
- Select at least one resolution
- Width,
- Delete revision
- <div><dt>Status</dt><dd>Loading...</dd></div>
- All elements of this field will be removed from the document after saving.
- Weight,
- History of field values
- Additionally
- at
- Failed to check short alias
- Checking...
- draftAssignment document
- Add files from a folder
- Revisions deleted
- cm
- <button class="btn btn-ghost btn-icon btn-sm documents-value-remove" type="button" data-document-value-remove data-tooltip="Delete" aria-label="Delete"><i class="ti ti-trash"></i></button>
- ъ
- t
- </span><label class="documents-revision-group-check"><input type="checkbox" data-document-revision-group="document" checked><span>All</span></label></div>
- The document has already been changed. Refresh the page.
- <button class="documents-media-thumb" type="button" data-document-media-pick aria-label="Select file"><span><i class="ti ti-
- The story is still emptyIncorrect answer
- Clear all field elements?Find or add a keywordin
- Stay
- <div class="modal-footer"><div class="mf-left documents-picker-count" data-relation-count></div><button class="btn btn-ghost" type="button" data-relation-close>Close</button></div>No document selectedNew redirect
- Old URL
- <div class="modal-header"><span class="dialog-icon info"><i class="ti ti-file-search"></i></span><div style="flex:1"><h3>Select document</h3><p class="text-secondary" style="margin-top:4px">Add a tag
- The path will be written in the document field.
- A newer version is already saved on the server. Refresh the page, check the changes, and save the document again.
- sch
- Action
- Action documents.
- errors
- processedunchanged
- n
- Title
- t
- ъ
- th
- d
- Fields only
- e
- h
- e
- Note added
- n
- yu
- h
- s
- JSON snapshot rebuilt
- w
- o
- to
- not created yet
- cm
- The document has been restored
- API token revoked
- Draft
- Revisions deleted
- r
- f
- ts
- m
- a
- Field #
- Note deleted
- b
- I
- Document saved
- with
- g
- Revision deletedin
- b
- x
- sch
- at
- can be left blank
- kgCreation preset saved
- B
- Redirect removed
- lPreset deleted
- fDocument #
- Redirect saved
- API token created
- uh
- and
- after savingNew
- Template
- Remove all items?
- After saving, the items will be removed from the document. Unused files can be restored from the media trash.
- Remove the image?
- After saving, the image will be removed from the document. The unused file can be restored from the media trash.
- Remove the image from the document?
- The document was saved, but some old files were not moved to the trash.
diff --git a/adminx/modules/Documents/language/ru/client.xml b/adminx/modules/Documents/language/ru/client.xml
index 14447ba..60a7dce 100644
--- a/adminx/modules/Documents/language/ru/client.xml
+++ b/adminx/modules/Documents/language/ru/client.xml
@@ -1,302 +1,48 @@
- хДобавить ещё…
- В поле-ссылку будет записан адрес документа.
- До 10 символов, необязательно.
- Токен скопирован
- <button class="btn btn-ghost btn-icon btn-sm documents-media-remove" type="button" data-document-media-remove data-tooltip="Удалить" aria-label="Удалить"><i class="ti ti-trash"></i></button>
- поле
- Очистить все строки поля?
- Параметр
- Пересобрать JSON-снимки?Новый
- нажмите, чтобы выбрать
- Не удалось проверить alias
- API-токен созданДобавить ключевое слово
- <div class="modal-body"><div class="input-wrap documents-relation-search"><i class="ti ti-search"></i><input class="input" type="search" placeholder="ID, название или alias" data-relation-search></div><div class="documents-picker-status" data-relation-status>Загрузка...</div><div class="documents-relation-list" data-relation-list></div></div>
- JSON-снимок
- кПресет создания
- э
- ID тизера / ссылка
- Внутренняя ошибка сервера. Обновите страницу и повторите попытку.
- <button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-media-down data-tooltip="Ниже" aria-label="Ниже"><i class="ti ti-arrow-down"></i></button>
- "><button class="btn btn-secondary btn-icon btn-sm" type="button" data-document-media-pick data-tooltip="Выбрать файл" aria-label="Выбрать файл"><i class="ti ti-
- ц
- В поле будет записан ID документа.
- после сохраненияПресет создания сохранёнНе удалось загрузить список
- ч
- з
- <section class="documents-revision-system"><div class="documents-revision-subhead"><i class="ti ti-settings"></i><b>Основные настройки</b><span>
- [link]" data-media-key="link" value="" placeholder="Ссылка или документ" data-document-media-url data-document-picker-type="all">
- Восстановить
- <button class="btn btn-secondary btn-icon btn-sm" type="button" data-document-media-doc-pick data-tooltip="Выбрать документ" aria-label="Выбрать документ"><i class="ti ti-file-search"></i></button></div>
- </span><label class="documents-revision-group-check"><input type="checkbox" data-document-revision-group="field" checked><span>Все</span></label></div>
- ID, название или alias
- Выбрать файл
- Колонка 3
- Редирект сохранён
- Секрет готов к копированию.
- Сервер вернул некорректный ответ.
- Обновить страницу
- Без названия
- JSON-снимки пересобраны:
- <button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-media-up data-tooltip="Выше" aria-label="Выше"><i class="ti ti-arrow-up"></i></button>
- ID документа
- Заметка добавлена
- новыйновое
- Черновик
- Название
- Ревизия удалена
- Заголовок документа
- Файлы загружены
- Выберите ревизию
- Пересобрать JSONОчистить подневную статистику?
- Колонка 1
- поля
- й
- В папке нет подходящих файловСтатистика очищена
- ещё не создан
- Копировать
- Редирект удалён
- Ревизии
- я
- л
- а
- ь
- Документ сохранён
- <span class="badge badge-gray">не создан</span>
- Не удалось прочитать папку
- Заметка удалена
- Файл не выбран
- ф
- о
- <div><dt>Файл</dt><dd class="mono">
- кг
- р
- . Окно нужно оставить открытым до завершения.в рубрике
- ыВсе строки view_count будут удалены. Общие счётчики документов останутся без изменений.
- <button class="btn btn-secondary btn-icon btn-sm" type="button" data-document-media-pick data-tooltip="Выбрать файл" aria-label="Выбрать файл"><i class="ti ti-paperclip"></i></button>
- Пересобрать
- <span class="badge badge-amber">нужна пересборка</span>док.
- 0 Б
- д<div class="documents-term-status is-error"><i class="ti ti-alert-circle"></i><span>Не удалось загрузить варианты</span></div>
- <div class="empty-state">Ревизий пока нет. Первый снимок появится после сохранения документа.</div>
- <div><label class="documents-revision-check" aria-label="Восстановить
- Очистить поле?
- Колонка 2
- Alias можно оставить пустым
- ё
- Длина,
- жУдалить «
- Отозван
- </p></div><button class="modal-close" type="button" data-relation-close aria-label="Закрыть"><i class="ti ti-x"></i></button></div>
- <span class="badge badge-green">актуален</span>URL скопированСохранённых значений пока нет
- [description]" data-media-key="description" rows="2" placeholder="Описание"></textarea>
- <button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-value-down data-tooltip="Ниже" aria-label="Ниже"><i class="ti ti-arrow-down"></i></button>
- Интеграция сразу потеряет доступ. Вернуть этот токен будет невозможно.
- Найдено ошибок:
- Шаблон рубрики: /
- , ошибок:
- ШаблонНе удалось загрузить статистику
- API-токен отозванНайти или добавить тег
- н
- Откройте нужную папку и подтвердите выбор. В поле добавятся все подходящие файлы из неё.
- <div><dt>Состояние</dt><dd>
- <div class="empty-state">Загрузка...</div>
- Содержимое снимка появится после выбора ревизии.Удалить
- Скопировано
- необязательно
- Описание появится после заполнения meta description.
- Документов
- Документ восстановлен
- гСовпадений нет
- <button class="btn btn-ghost btn-icon btn-sm" type="button" data-document-value-up data-tooltip="Выше" aria-label="Выше"><i class="ti ti-arrow-up"></i></button>
- Права доступа для одной внешней интеграции.
- Документ изменилсяОшибка запроса
- полейОчистить
- Отозвать
- Изменения сохранены
- [title]" data-media-key="title" value="" placeholder="Заголовок изображения">
- Убрать
- п
- Добавлено из папки:
- Изменить
- Не удалось загрузить ревизии
- Значение
- Выбрать документ
- <div><dt>Сформирован</dt><dd>
- Только поля
- б
- У рубрики нет шаблона пути: alias документа используется от корня сайта.
- Не удалось сгенерировать alias
- и
- Высота,
- Изображение не выбрано
- <div><dt>Размер</dt><dd>
- JSON-снимок пересобран
- Ревизии документа
- ЗаголовокЗагрузка...
- Убрать документ
- Добавить из этой папки
- Все строки этого поля будут удалены из документа после сохранения.
- <button class="btn btn-ghost btn-icon btn-sm documents-drag-handle" type="button" data-doc-drag draggable="true" data-tooltip="Перетащить" aria-label="Перетащить"><i class="ti ti-grip-vertical"></i></button>Пресет удалён
- Снимки
- / + alias документа. Дата берётся из публикации.
- <div><dt>Поля</dt><dd class="mono">
- Пакетная пересборка остановлена
- <article class="documents-revision-field documents-revision-system-field"><div><label class="documents-revision-check" aria-label="Восстановить
- Не удалось получить статус снимка
- с
- Эти файлы уже есть в поле
- Снимки будут последовательно пересобраны для
- снимков
- ю
- [name]" data-media-key="name" value="" placeholder="Название файла">
- Отозвать API-токен?
- <section class="documents-revision-content"><div class="documents-revision-subhead"><i class="ti ti-forms"></i><b>Поля рубрики</b><span>
- еиз
- м
- шНе удалось применить фильтры
- Поле
- Только чтениеНажмите, чтобы найти документДобавить «<div class="documents-term-status"><i class="ti ti-loader-2"></i><span>Ищем совпадения…</span></div>пресетУдалить пресет
- Не удалось сгенерировать короткий алиас
- можно оставить пустым
- Выберите хотя бы одно разрешение
- Ширина,
- Удалить ревизию
- <div><dt>Состояние</dt><dd>Загрузка...</dd></div>
- Все элементы этого поля будут удалены из документа после сохранения.
- Вес,
- История значений полей
- Дополнительно
- у
- Не удалось проверить короткий alias
- Проверка...
- черновикДокумент назначения
- Добавить файлы из папки
- Ревизии удалены
- см
- <button class="btn btn-ghost btn-icon btn-sm documents-value-remove" type="button" data-document-value-remove data-tooltip="Удалить" aria-label="Удалить"><i class="ti ti-trash"></i></button>
- ъ
- т
- </span><label class="documents-revision-group-check"><input type="checkbox" data-document-revision-group="document" checked><span>Все</span></label></div>
- Документ уже изменён. Обновите страницу.
- <button class="documents-media-thumb" type="button" data-document-media-pick aria-label="Выбрать файл"><span><i class="ti ti-
- История пока пустаяНекорректный ответ
- Очистить все элементы поля?Найти или добавить ключевое словов
- Остаться
- <div class="modal-footer"><div class="mf-left documents-picker-count" data-relation-count></div><button class="btn btn-ghost" type="button" data-relation-close>Закрыть</button></div>Документ не выбранНовый редирект
- Старый URL
- <div class="modal-header"><span class="dialog-icon info"><i class="ti ti-file-search"></i></span><div style="flex:1"><h3>Выбрать документ</h3><p class="text-secondary" style="margin-top:4px">Добавить тег
- Путь будет записан в поле документа.
- На сервере уже сохранена более новая версия. Обновите страницу, проверьте изменения и сохраните документ повторно.
- щ
- Действие
- Действие документов.
- ошибок
- обработанобез изменений
- н
- Название
- т
- ъ
- й
- д
- Только поля
- е
- з
- ё
- Заметка добавлена
- п
- ю
- ч
- ы
- JSON-снимок пересобран
- ш
- о
- к
- ещё не создан
- см
- Документ восстановлен
- API-токен отозван
- Черновик
- Ревизии удалены
- р
- ф
- ц
- м
- а
- Поле #
- Заметка удалена
- б
- я
- Документ сохранён
- с
- г
- Ревизия удаленав
- ь
- х
- щ
- у
- можно оставить пустым
- кгПресет создания сохранён
- Б
- Редирект удалён
- лПресет удалён
- жДокумент #
- Редирект сохранён
- API-токен создан
- э
- и
- после сохраненияНовый
- Шаблон
- Удалить все элементы?
- После сохранения элементы исчезнут из документа. Неиспользуемые файлы можно будет восстановить из корзины медиа.
- Удалить изображение?
- После сохранения изображение исчезнет из документа. Неиспользуемый файл можно будет восстановить из корзины медиа.
- Удалить изображение из документа?
- Документ сохранён, но часть старых файлов не перемещена в корзину.
diff --git a/adminx/modules/Documents/migrations/017_document_public_visibility.sql b/adminx/modules/Documents/migrations/017_document_public_visibility.sql
new file mode 100644
index 0000000..31d297a
--- /dev/null
+++ b/adminx/modules/Documents/migrations/017_document_public_visibility.sql
@@ -0,0 +1,4 @@
+ALTER TABLE `{{prefix}}_documents`
+ ADD COLUMN IF NOT EXISTS `document_is_technical` TINYINT UNSIGNED NOT NULL DEFAULT 0 AFTER `document_in_search`,
+ ADD COLUMN IF NOT EXISTS `document_in_sitemap` TINYINT UNSIGNED NOT NULL DEFAULT 1 AFTER `document_sitemap_pr`,
+ ADD KEY IF NOT EXISTS `idx_public_visibility` (`document_is_technical`,`document_in_sitemap`);
diff --git a/adminx/modules/Documents/module.php b/adminx/modules/Documents/module.php
index 89191c7..e016a10 100644
--- a/adminx/modules/Documents/module.php
+++ b/adminx/modules/Documents/module.php
@@ -17,7 +17,7 @@
return array(
'code' => 'documents',
'name' => 'Документы',
- 'version' => '0.3.9',
+ 'version' => '0.4.2',
'permissions' => array(
'key' => 'documents',
@@ -131,6 +131,7 @@
array('id' => '014_document_relation_edges', 'file' => 'migrations/014_document_relation_edges.sql'),
array('id' => '015_document_creation_presets', 'file' => 'migrations/015_document_creation_presets.sql'),
array('id' => '016_reconcile_document_content_columns', 'file' => 'migrations/016_reconcile_document_content_columns.php'),
+ array('id' => '017_document_public_visibility', 'file' => 'migrations/017_document_public_visibility.sql'),
),
'view_globals' => array(
diff --git a/adminx/modules/Documents/view/bulk-editor.twig b/adminx/modules/Documents/view/bulk-editor.twig
index 4700213..94ac5d3 100644
--- a/adminx/modules/Documents/view/bulk-editor.twig
+++ b/adminx/modules/Documents/view/bulk-editor.twig
@@ -79,9 +79,19 @@
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+