Selaa lähdekoodia

added dbg instance filter in ViewObject

Piotr Labudda 9 vuotta sitten
vanhempi
commit
7812a0b7a9

+ 33 - 0
SE/se-lib/ACL.php

@@ -308,6 +308,31 @@ SQL;
 
 		return "default_db/{$objectItem['_rootTableName']}";
 	}
+	public static function getNamespaceSiblings($namespace) {
+		return array_map(function ($row) {
+			return $row['namespace'];
+		}, DB::getPDO()->fetchAll("
+			select s.namespace
+			from CRM_INSTANCE_CONFIG c
+				join CRM_INSTANCE_CONFIG s on ( s.rootNamespace = c.rootNamespace and s.namespace != c.rootNamespace )
+			where c.namespace = :namespace
+		", [
+			'namespace' => $namespace
+		]));
+	}
+	public static function getFeatureNamespaces($namespace, $pk) {
+		$instanceTable = self::getInstanceTable($namespace);
+		return array_map(function ($row) {
+			return $row['namespace'];
+		}, DB::getPDO()->fetchAll("
+			select c.namespace
+			from `{$instanceTable}` i
+				join `CRM_INSTANCE_CONFIG` c on ( c.id = i.idInstance )
+			where i.pk = :pk
+		", [
+			'pk' => $pk,
+		]));
+	}
 	public static function getInstanceTable($namespace) {
 		$conf = self::getInstanceConfig($namespace);
 		if (!empty($conf['idInstanceBase'])) return "CRM__#INSTANCE_TABLE__{$conf['idInstanceBase']}";
@@ -339,4 +364,12 @@ SQL;
 		return $instanceTableName;
 	}
 
+	// @params $from - ( ACL | tableName | namespace | etc... - only ACL)
+	public static function query($from) {
+		Lib::loadClass('AclQueryBuilder');
+		$query = new AclQueryBuilder();
+		$query->from($from);
+		return $query;
+	}
+
 }

+ 199 - 0
SE/se-lib/AclQueryBuilder.php

@@ -0,0 +1,199 @@
+<?php
+
+Lib::loadClass('P5');
+Lib::loadClass('Core_AclBase');
+Lib::loadClass('AntAclBase');
+Lib::loadClass('ACL');
+
+class AclQueryBuilder {
+
+  public $select;
+  public $from;
+  public $where;
+  public $orderBy;
+  public $groupBy;
+  public $limit;
+  public $offset;
+  public $_fromPrefix;
+  public $_joinPrefix;
+  public $_joinParams;
+  public $isInstances;
+  public $isNotInstances;
+
+  public function __construct() {
+    $this->select = [];
+    $this->from = null; // (ACL | tableName)
+    $this->where = [];
+    $this->orderBy = null;
+    $this->groupBy = null;
+    $this->limit = null;
+    $this->offset = null;
+    $this->_fromPrefix = 't'; // prefix for this->from - default 't'
+    $this->_joinPrefix = []; // prefix => from (Acl | tableName)
+    $this->_joinParams = []; // prefx => params
+    $this->isInstances = [];
+    $this->isNotInstances = [];
+  }
+
+  public function select($propertyName) { // TODO: ogc:propertyName, *, xpath, @instances, etc...
+    if (!empty($propertyName) && !in_array($propertyName, $this->select)) $this->select[] = $propertyName;
+    return $this;
+  }
+
+  public function from($from, $prefix = 't') {
+    DBG::log([
+      'from instanceof Core_AclBase' => ($from instanceof Core_AclBase),
+      'from instanceof AntAclBase' => ($from instanceof AntAclBase),
+    ], 'array', "\$from class(".get_class($from).")");
+    if ($this->from) throw new Exception("Duplicate FROM");
+    $this->from = $from;
+    $this->_fromPrefix = $prefix;
+    return $this;
+  }
+
+  public function where($params) {
+    if (!empty($params['rawWhere'])) {
+      $this->where[] = $params['rawWhere'];
+    } else {
+      DBG::log($params, 'array', "Where not supported");
+      throw new Exception("Where not supported");
+    }
+  }
+
+  public function join($join, $prefix, $params) {
+    if (array_key_exists($prefix, $this->_joinPrefix)) throw new Exception("Prefix already used!");
+    $this->_joinPrefix[$prefix] = $join;
+    $this->_joinParams[$prefix] = $params;
+    return $this;
+  }
+
+  public function orderBy($orderBy) { // TODO: ogc: order by ...
+
+    return $this;
+  }
+
+  public function limit($limit) {
+    $this->limit = (int)$limit;
+    return $this;
+  }
+
+  public function offset($offset) {
+    $this->offset = (int)$offset;
+    return $this;
+  }
+
+  public function isInstance($instances) {
+    $this->isInstances = (is_array($instances)) ? $instances : [ $instances ];
+    return $this;
+  }
+
+  public function isNotInstance($instances) {
+    $this->isNotInstances = (is_array($instances)) ? $instances : [ $instances ];
+    return $this;
+  }
+
+  public function _parseJoinParams($params) {
+    if (array_key_exists('rawJoin', $params)) return $params['rawJoin'];
+    throw new Exception("Not implemented JOIN params '".json_encode($params)."'");
+  }
+
+  public function _getTableName($source) {
+    if (is_scalar($source)) return $source;
+    if ($source instanceof Core_AclBase) return $source->getRootTableName();
+    throw new Exception("Not implemented FROM type '".get_class($source)."'");
+  }
+
+  public function execute() {
+    $sql = $this->generateSql();
+    DBG::log((array)$this, 'array', "AclQueryBuilder");
+    return DB::getPDO()->fetchAll($sql);
+  }
+
+  public function generateSql() {
+    if (!$this->from) throw new Exception("Missing FROM");
+    $tableName = $this->_getTableName($this->from);
+    if (!$tableName) throw new Exception("Missing FROM table name");
+
+    $sqlPk = 'ID';
+    if ($this->from instanceof Core_AclBase) $sqlPk = $this->from->getSqlPrimaryKeyField();
+
+    $sqlJoin = [];
+    foreach ($this->isInstances as $k => $ns) {
+      $idInstance = ACL::getInstanceId($ns);
+      $instanceTable = ACL::getInstanceTable($ns);
+      // ->join($instanceTable, 'i', [ 'rawJoin' => "i.pk = t.{$sqlPk} and i.idInstance = {$idInstance}" ])
+      $prefix = "is_inst_{$k}";
+      // $sqlJoin[] = " inner join `{$joinName}` {$prefix} on (
+      //   {$prefix}.pk = {$this->_fromPrefix}.{$sqlPk}
+      //   and {$prefix}.idInstance = {$idInstance}
+      // )";
+      $this->where[] = "{$this->_fromPrefix}.{$sqlPk} in (
+        select {$prefix}.pk
+        from `{$instanceTable}` {$prefix}
+        where {$prefix}.idInstance = {$idInstance}
+      )";
+      DBG::log("{$this->_fromPrefix}.{$sqlPk} in (
+        select {$prefix}.pk
+        from `{$instanceTable}` {$prefix}
+        where {$prefix}.idInstance = {$idInstance}
+      )", 'string', "\$this->where[] =");
+    }
+    foreach ($this->isNotInstances as $k => $ns) {
+      $idInstance = ACL::getInstanceId($ns);
+      $instanceTable = ACL::getInstanceTable($ns);
+      // ->join($instanceTable, 'i', [ 'rawJoin' => "i.pk = t.{$sqlPk} and i.idInstance = {$idInstance}" ])
+      $prefix = "is_inst_{$k}";
+      // $sqlJoin[] = " inner join `{$joinName}` {$prefix} on (
+      //   {$prefix}.pk = {$this->_fromPrefix}.{$sqlPk}
+      //   and {$prefix}.idInstance = {$idInstance}
+      // )";
+      $this->where[] = "{$this->_fromPrefix}.{$sqlPk} not in (
+        select {$prefix}.pk
+        from `{$instanceTable}` {$prefix}
+        where {$prefix}.idInstance = {$idInstance}
+      )";
+    }
+    // join `{$instanceTable}` i on(i.pk = t.{$sqlPk} and i.idInstance = {$idInstance})
+    foreach ($this->_joinPrefix as $prefix => $join) {
+      $joinName = $this->_getTableName($join);
+      $sqlJoin[] = " join `{$joinName}` {$prefix} on(" . $this->_parseJoinParams($this->_joinParams[$prefix]) . ")";
+    }
+
+    $sqlJoin = (!empty($sqlJoin)) ? implode("\n\t", $sqlJoin) : "";
+
+    $sqlWhere = [];
+    foreach ($this->where as $where) {
+      $sqlWhere[] = $where;
+    }
+    $sqlWhere = (!empty($sqlWhere)) ? "where " . implode("\n\t and ", $sqlWhere) : '';
+
+    $limit = ($this->limit < 0) ? 0 : $this->limit;
+    $offset = ($this->offset < 0) ? 0 : $this->offset;
+    $sqlLimit = ($limit > 0) ? "limit {$limit} offset {$offset}" : '';
+
+    $sqlSelect = [];
+    $sqlSelect[] = "{$this->_fromPrefix}.{$sqlPk}";
+    if (in_array('*', $this->select)) $sqlSelect[] = "{$this->_fromPrefix}.*";
+    if (in_array('@instances', $this->select)) {
+      if (!($this->from instanceof Core_AclBase)) throw new Exception("select @instances allowed only for Acl object");
+      $instanceTable = ACL::getInstanceTable($this->from->getNamespace());
+      $sqlSelect[] = "
+        (
+          select GROUP_CONCAT(inst_conf.namespace)
+          from `{$instanceTable}` inst_tbl
+            join `CRM_INSTANCE_CONFIG` inst_conf on (inst_conf.id = inst_tbl.idInstance)
+          where inst_tbl.pk = {$this->_fromPrefix}.{$sqlPk}
+        ) as `@instances`
+      ";
+    }
+    $sqlSelect = (!empty($sqlSelect)) ? implode(", ", $sqlSelect) : "{$this->_fromPrefix}.*";
+    return "
+      select {$sqlSelect}
+      from `{$tableName}` {$this->_fromPrefix}
+        {$sqlJoin}
+      {$sqlWhere}
+      {$sqlLimit}
+    ";
+  }
+
+}

+ 32 - 0
SE/se-lib/AntAclBase.php

@@ -1,6 +1,7 @@
 <?php
 
 Lib::loadClass('ACL');
+Lib::loadClass('Core_AclBase');
 
 /**
 * SE/schema/ant-object/default_db.{rootTableName}/{name}/build.xml
@@ -12,11 +13,13 @@ class AntAclBase extends Core_AclBase {
     $this->_name = '';
     $this->_namespace = '';
     $this->_rootTableName = '';
+    $this->_rootNamespace = '';
     $this->_primaryKey = '';
     $this->_fields = [];
   }
   public function getName() { return $this->_name; }
   public function getNamespace() { return $this->_namespace; }
+  public function getRootNamespace() { return $this->_rootNamespace; }
   public function getSourceName() { return 'default_db'; } // TODO: ?
   public function getRootTableName() { return $this->_rootTableName; }
   public function getPrimaryKeyField() { return $this->_primaryKey; }
@@ -110,9 +113,37 @@ class AntAclBase extends Core_AclBase {
   }
   public function getItems($params = []) {
     DBG::log($params, 'array', "AntAclBase::getItems params");
+
+    // $sql->limit = V::get('limit', 10, $params, 'int');
+    // $sql->offset = V::get('limitstart', 0, $params, 'int');
+    $limit = V::get('limit', 0, $params, 'int');
+    $limit = ($limit < 0) ? 0 : $limit;
+    $offset = V::get('limitstart', 0, $params, 'int');
+    $offset = ($offset < 0) ? 0 : $offset;
+    $sqlLimit = ($limit > 0)
+      ? "limit {$limit} offset {$offset}"
+      : '';
+
     $idInstance = ACL::getInstanceId($this->_namespace);
     $instanceTable = ACL::getInstanceTable($this->_namespace);
     $sqlPrimaryKey = $this->getSqlPrimaryKeyField();
+
+    {
+      $filtrIsInstance = [$this->_namespace];
+      $filtrIsNotInstance = [];
+      if (!empty($params['f_is_instance'])) $filtrIsInstance = $params['f_is_instance'];
+      if (!empty($params['f_is_not_instance'])) $filtrIsNotInstance = $params['f_is_not_instance'];
+    }
+
+    return ACL::query($this)
+      ->isInstance($filtrIsInstance)
+      ->isNotInstance($filtrIsNotInstance)
+      ->select('*') // TODO: fields
+      ->select(!empty($params['@instances']) ? '@instances' : '')
+      // ->join($instanceTable, 'i', [ 'rawJoin' => "i.pk = t.{$sqlPrimaryKey} and i.idInstance = {$idInstance}" ])
+      ->limit($limit)
+      ->offset($offset)
+      ->execute();
     return DB::getPDO()->fetchAll(" -- getItems({$this->_namespace})
       select t.*
       from `{$this->_rootTableName}` t
@@ -142,6 +173,7 @@ class AntAclBase extends Core_AclBase {
     $acl->_name = $conf['name'];
     $acl->_rootTableName = $conf['_rootTableName'];
     $acl->_namespace = $conf['namespace'];
+    $acl->_rootNamespace = str_replace('__x3A__', '/', $conf['nsPrefix']);
     $acl->_fields = $conf['field']; // TODO: lazyLoading - use getFields() in all functions
     $acl->_primaryKey = 'ID'; // $conf['primaryKey'];
 

+ 372 - 0
SE/se-lib/Route/ViewObject.php

@@ -86,6 +86,331 @@ class Route_ViewObject extends Route_ViewTableAjax {
 			$tbl = $this->getTableAjaxWidget($acl);
 			$tbl->setFilterInit($filterInit);
 			if (!empty($forceFilterInit)) $tbl->setForceFilterInit($forceFilterInit);
+			if (V::get('DBG_INST', '', $_GET)) { // TODO: TEST namespace
+				$siblings = ACL::getNamespaceSiblings($namespace);
+				DBG::nicePrint($siblings, '$siblings');
+				$filtrInstance = V::get('f_instance', [], $_POST, 'array');
+				DBG::nicePrint($filtrInstance, '$filtrInstance');
+				$sibling = 'default_db/CRM_PROCES/PROCES_INIT'; DBG::nicePrint(array_merge(['type'=>"radio", 'name'=>"f_instance[{$sibling}]", 'value'=>'YES'], ('YES' === V::get($sibling, '', $filtrInstance)) ? ['checked' => "checked"] : []), "merge {$sibling} YES");
+				$sibling = 'default_db/CRM_PROCES/PROCES_INIT'; DBG::nicePrint(array_merge(['type'=>"radio", 'name'=>"f_instance[{$sibling}]", 'value'=>'NO'], ('NO' === V::get($sibling, '', $filtrInstance)) ? ['checked' => "checked"] : []), "merge {$sibling} NO");
+
+				$_ = array(UI, 'h');
+				echo $_('form', ['method' => "POST", 'style' => "width:600px; border:1px solid #ddd; border-radius:2px"], [
+					$_('div', ['style' => "background-color:#ddd"], "Test Filtr instancji"),
+					$_('div', ['style' => "padding:8px"], array_map(function ($sibling) use ($filtrInstance, $_) {
+						return $_('div', [], [
+							$_('label', ['style' => "margin:0 8px"], [
+								$_('input', array_merge(['type'=>"radio", 'name'=>"f_instance[{$sibling}]", 'value'=>'YES'], ('YES' === V::get($sibling, '', $filtrInstance)) ? ['checked' => "checked"] : [])),
+								" TAK "
+							]),
+							$_('label', ['style' => "margin:0 8px"], [
+								$_('input', array_merge(['type'=>"radio", 'name'=>"f_instance[{$sibling}]", 'value'=>'NO'], ('NO' === V::get($sibling, '', $filtrInstance)) ? ['checked' => "checked"] : [])),
+								" NIE "
+							]),
+							$_('label', ['style' => "margin:0 8px"], [
+								$_('input', ['type'=>"radio", 'name'=>"f_instance[{$sibling}]", 'value'=>'x']),
+								" pomiń "
+							]),
+							// $_('button', ['onClick'=>"this.form.f_instance['{$sibling}'].value = ''; return false"], "odznacz"),
+							$_('button', ['onClick'=>"console.log(this.form.elements['f_instance[{$sibling}]']); this.form.elements['f_instance[{$sibling}]'].value = 'x'; return false"], "odznacz"),
+							" - {$sibling}"
+						]);
+					}, $siblings)),
+					$_('div', [], [
+						$_('input', ['type'=>"hidden", 'name'=>'_route', 'value'=>"ViewObject"]),
+						$_('input', ['type'=>"hidden", 'name'=>'namespace', 'value'=>$namespace]),
+						$_('input', ['type'=>"submit", 'value'=>"Filtruj - TEST"]),
+					]),
+				]);
+				DBG::nicePrint($_POST, '$_POST');
+				{
+					$args = $_POST;
+					$fIsInstance = [];
+					$fIsNotInstance = [];
+					if (!empty($args['f_instance'])) {
+						foreach ($args['f_instance'] as $inst => $selected) {
+							if ('YES' === $selected) {
+								$fIsInstance[] = $inst;
+							} else if ('NO' === $selected) {
+								$fIsNotInstance[] = $inst;
+							}
+						}
+					}
+				}
+				$items = $acl->getItems([
+					// TODO: 'propertyName' => "*,@instance",
+					'f_is_instance' => $fIsInstance,
+					'f_is_not_instance' => $fIsNotInstance,
+					'@instances' => '1',
+					'limit' => 10
+				]);
+				$rootNamespace = $acl->getRootNamespace();
+				DBG::nicePrint($rootNamespace, '$rootNamespace');
+				$jsRenderFunName = 'render_dropdown_instances_' . substr(md5(time()), 0, 6);
+				DBG::nicePrint($jsRenderFunName, '$jsRenderFunName');
+				UI::table([
+					'rows' => array_map(function($row) use ($namespace, $siblings, $rootNamespace, $jsRenderFunName) {
+						return [
+							'ID' => $row['ID'],
+							'DESC' => $row['DESC'],
+							'TYPE' => $row['TYPE'],
+							'instances' => UI::h('div', ['class'=>"p5UI__dropdown-wrap"], [
+								UI::h('button', [
+									// 'onClick' => "p5UI__dropdown(event, this, 'left bottom')",
+									'onClick' => "p5UI__dropdown(event, this, 'left bottom', {$jsRenderFunName}({$row['ID']}))",
+									'class' => "btn btn-xs btn-default p5UI__dropdown-btn"
+								], [
+									'<i class="glyphicon glyphicon-menu-hamburger"></i>',
+									" ustal instancje"
+								]),
+								UI::h('div', ['class' => "p5UI__dropdown-content"]
+									, array_merge(
+										[
+											UI::h('input', ['type' => "text", 'placeholder' => "Search..", 'class' => "p5UI__dropdown-input", 'onkeyup' => "p5_ViewObject_instances_filterInput(this)"], null),
+										]
+										, array_map(function ($sibling) use ($row, $namespace, $rootNamespace) {
+											return UI::h('div', ['label'=>$sibling, 'style'=>"padding:4px 0"], [
+												UI::h('button', array_merge(['class' => "btn btn-xs btn-default",
+													'style' => "margin:0 4px 0 0",
+													'title' => "Ustaw instancje '{$sibling}'",
+													'onClick' => "return p5_ViewObject_instance_set(this, '{$row['ID']}', '{$sibling}', 'yes')"],
+													(in_array($sibling, explode(',', $row['@instances']))) ? ['disabled' => "disabled"] : []
+												), "+"),
+												UI::h('button', ['class' => "btn btn-xs btn-default",
+													'style' => "margin:0 4px 0 0",
+													'title' => "Usuń instancje '{$sibling}'",
+													'onClick' => "return p5_ViewObject_instance_set(this, '{$row['ID']}', '{$sibling}', 'no')"], "-"),
+												UI::h('span', [], substr($sibling, strlen($rootNamespace) + 1)),
+											]);
+										}, $siblings)
+									)
+								)
+							]),
+							'instancesList' => implode(', ', ACL::getFeatureNamespaces($namespace, $row['ID'])),
+						];
+					}, $items)
+				]);
+				echo UI::h('style', ['type' => "text/css"], "
+					.p5UI__dropdown-content { min-width:400px; padding:8px; background-color: #f6f6f6; box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2) }
+					.p5UI__dropdown-content .p5UI__dropdown-item { display:block; color:#000; padding:4px; text-decoration:none }
+					.p5UI__dropdown-content .p5UI__dropdown-item:hover { background-color:#ebebeb }
+				");
+				echo UI::h('script', ['src'=>"static/vendor.js", 'type'=>"text/javascript"]);
+				$jsArgs = [
+					'SET_INSTANCE_URL' => $this->getLink('setInstanceAjax'),
+					'NAMESPACE' => $namespace,
+					'DBG' => DBG::isActive() ? 'true' : 'false',
+					'INITIAL_DROPDOWN_DATA' => json_encode([
+						// 'allowed_instances' => $siblings,
+						'allowed_instances' => array_map(function ($sibling) use ($rootNamespace) {
+							return [
+								'namespace' => $sibling,
+								'label' => substr($sibling, strlen($rootNamespace) + 1)
+							];
+						}, $siblings),
+						'items' => array_map(function($row) use ($namespace, $siblings, $rootNamespace, $jsRenderFunName) {
+							return [
+								'pk' => $row['ID'], // TODO: $primaryKeyField
+								'instances' => explode(',', $row['@instances']),
+							];
+						}, $items)
+					])
+				];
+				echo UI::h('script', [], "
+					var DBG = {$jsArgs['DBG']};
+					var NAMESPACE = '{$jsArgs['NAMESPACE']}'
+					var SET_INSTANCE_URL = '{$jsArgs['SET_INSTANCE_URL']}'
+					var initialData = {$jsArgs['INITIAL_DROPDOWN_DATA']};
+					function {$jsRenderFunName}(pk) {
+						if (!window.p5VendorJs.React) throw 'Missing p5VendorJs React'
+						if (!window.p5VendorJs.ReactDOM) throw 'Missing p5VendorJs ReactDOM'
+						var React = window.p5VendorJs.React
+						var ReactDOM = window.p5VendorJs.ReactDOM
+						var h = React.createElement
+						if(DBG)console.log('F.{$jsRenderFunName}', { pk: pk, initialData: initialData })
+						var _state = {
+							allowed_instances: initialData.allowed_instances,
+							query: '',
+							filter: initialData.allowed_instances,
+							item: initialData.items.filter(function (data) {
+								return data['pk'] == pk
+							}).pop(),
+							waitingSet: [],
+							waitingUnSet: [],
+						}
+						var _btnNode = null
+						var _dropdownNode = null
+						var _update = function (type, payload) {
+							if(DBG)console.log('_update', { pk: pk, type: type, payload: payload, state: _state, btnNode: _btnNode, dropdownNode: _dropdownNode })
+							switch (type) {
+								case 'filter': {
+									if(DBG)console.log('TODO: filter payload:', payload)
+									_state.query = payload
+									_state.filter = _state.allowed_instances.filter(function (inst) {
+										return (-1 !== inst.label.toLowerCase().indexOf(payload.toLowerCase()))
+									})
+								} break;
+								case 'send_set': {
+									_state.waitingSet.push(payload)
+									window.fetch(SET_INSTANCE_URL, {
+										method: 'POST',
+										headers: { 'Content-Type': 'application/json' },
+										credentials: 'same-origin',
+										body: JSON.stringify({
+											namespace: NAMESPACE,
+											primaryKey: _state.item.pk,
+											instance: payload,
+											toConnect: 'yes',
+										})
+									}).then(function (response) {
+										return response.json()
+									}).then(function (response) {
+										p5UI__notifyAjaxCallback(response)
+										if(DBG)console.log('response', response) // TODO: render list
+										_update('set', payload)
+									})
+								} break;
+								case 'send_unset': {
+									_state.waitingUnSet.push(payload)
+									window.fetch(SET_INSTANCE_URL, {
+										method: 'POST',
+										headers: { 'Content-Type': 'application/json' },
+										credentials: 'same-origin',
+										body: JSON.stringify({
+											namespace: NAMESPACE,
+											primaryKey: _state.item.pk,
+											instance: payload,
+											toConnect: 'no',
+										})
+									}).then(function (response) {
+										return response.json()
+									}).then(function (response) {
+										p5UI__notifyAjaxCallback(response)
+										if(DBG)console.log('response', response) // TODO: render list
+										_update('unset', payload)
+									})
+								} break;
+								case 'set': {
+									_state.item.instances.push(payload)
+									_state.waitingSet = _state.waitingSet.filter(function (inst) {
+										return inst !== payload
+									})
+								} break; // TODO: sync url
+								case 'unset': {
+									_state.item.instances = _state.item.instances.filter(function (inst) {
+										return inst !== payload
+									})
+									_state.waitingUnSet = _state.waitingSet.filter(function (inst) {
+										return inst !== payload
+									})
+								} break; // TODO: sync url
+							}
+							_render()
+						}
+
+						var btnToggleInstance = function (props) {
+							if (-1 === _state.item.instances.indexOf(props.namespace)) {
+								if (-1 === _state.waitingSet.indexOf(props.namespace)) {
+									return h('button', { style: { margin: '0 6px 0 0' },
+										className: 'btn btn-xs btn-default',
+										onClick: function () { _update('send_set', props.namespace) }
+									}, 'ustaw')
+								} else {
+									return h('button', { style: { margin: '0 6px 0 0' },
+										className: 'btn btn-xs btn-default disabled',
+									}, 'ustaw...')
+								}
+							} else {
+								if (-1 === _state.waitingUnSet.indexOf(props.namespace)) {
+									return h('button', { style: { margin: '0 6px 0 0' },
+										className: 'btn btn-xs btn-default',
+										onClick: function () { _update('send_unset', props.namespace) }
+									}, 'usuń')
+								} else {
+									return h('button', { style: { margin: '0 6px 0 0' },
+										className: 'btn btn-xs btn-default disabled',
+									}, 'usuń...')
+								}
+							}
+						}
+						var _render = function () {
+							if(DBG)console.log('_render', { pk: pk, state: _state, btnNode: _btnNode, dropdownNode: _dropdownNode })
+							if (!_dropdownNode) throw 'Missing dropdownNode'
+							ReactDOM.render(
+								h('div', {}, [].concat(
+										h('input', { type: 'text',
+											placeholder: 'Szukaj...',
+											className: 'p5UI__dropdown-input',
+											onChange: function (e) { _update('filter', e.target.value); },
+											autoFocus: true,
+											value: _state.query,
+										})
+									).concat(
+										_state.filter.map(function (inst) {
+											return h('div', { className: 'p5UI__dropdown-item', style: {padding: '4px 0'} }, [
+												btnToggleInstance({ namespace: inst.namespace }),
+												inst.label,
+											])
+										})
+									)
+								),
+								_dropdownNode
+							);
+							setTimeout(function () {
+								if (_dropdownNode.firstChild && _dropdownNode.firstChild.firstChild)
+									_dropdownNode.firstChild.firstChild.focus()
+							}, 100)
+						}
+						return function (btnNode, dropdownNode) {
+							_btnNode = btnNode
+							_dropdownNode = dropdownNode
+							if(DBG)console.log('F.{$jsRenderFunName}', { pk: pk, initialData: initialData, btnNode: btnNode, dropdownNode: dropdownNode, state: _state })
+							_render()
+						}
+					}
+				");
+				echo UI::h('script', [], "
+					var SET_INSTANCE_URL = '{$jsArgs['SET_INSTANCE_URL']}'
+					var NAMESPACE = '{$jsArgs['NAMESPACE']}'
+					function p5_ViewObject_instance_set(n, pk, sibling, toConnect) {
+						console.log('p5_ViewObject_instance_set pk('+pk+'), sibling('+sibling+'), toConnect('+toConnect+'), n', n);
+						window.fetch(SET_INSTANCE_URL, {
+							method: 'POST',
+							headers: { 'Content-Type': 'application/json' },
+							credentials: 'same-origin',
+							body: JSON.stringify({
+								namespace: NAMESPACE,
+								primaryKey: pk,
+								instance: sibling,
+								toConnect: toConnect,
+							})
+						}).then(function (response) {
+							return response.json()
+						}).then(function (response) {
+							p5UI__notifyAjaxCallback(response)
+							console.log(response) // TODO: render list
+						})
+					}
+				");
+				echo UI::h('script', [], "
+					function p5_ViewObject_instances_filterInput(n) {
+						var input, filter, ul, li, a, i, div;
+						input = n // .id-myInput
+						filter = input.value.toUpperCase()
+						div = n.parentNode // .id-myDropdown
+						a = div.getElementsByTagName('div')
+						for (i = 0; i < a.length; i++) {
+							if (a[i].getAttribute('label') && a[i].getAttribute('label').toUpperCase().indexOf(filter) > -1) {
+								a[i].style.display = 'block'
+							} else {
+								a[i].style.display = 'none'
+							}
+						}
+					}
+				");
+				echo '<hr style="margin-top:300px">';
+				exit;
+			}
 			echo $tbl->render();
 
 			if (DBG::isActive() && V::get('DBG_ACL', '', $_GET)) {// test load perms
@@ -194,6 +519,53 @@ class Route_ViewObject extends Route_ViewTableAjax {
 		UI::dol();
 	}
 
+	public function setInstanceAjaxAction() {
+		Response::sendTryCatchJson(array($this, 'setInstanceAjax'), $args = 'JSON_FROM_REQUEST_BODY');
+	}
+	public function setInstanceAjax($args) {
+		// namespace => default_db/CRM_PROCES/PROCES_INIT
+		// primaryKey => 6
+		// instance => default_db/CRM_PROCES/PROCES_TREE
+		// toConnect => yes
+		if (empty($args['namespace'])) throw new Exception("Missing namespace");
+		if (empty($args['primaryKey'])) throw new Exception("Missing primaryKey");
+		if (empty($args['instance'])) throw new Exception("Missing instance");
+		if (empty($args['toConnect'])) throw new Exception("Missing toConnect");
+		if (!in_array($args['toConnect'], ['yes', 'no'])) throw new Exception("Wrong param toConnect");
+		$idInstance = ACL::getInstanceId($args['instance']);
+		$instanceTable = ACL::getInstanceTable($args['namespace']);
+		switch ($args['toConnect']) {
+			case 'yes': {
+				// TODO: _HIST info - waiting
+				$ret = DB::getPDO()->execSql("
+					insert into `{$instanceTable}` (`pk`, `idInstance`)
+						values ( :pk , :idInstance )
+				", [
+					'pk' => $args['primaryKey'],
+					'idInstance' => $idInstance,
+				]);
+				// TODO: _HIST info - mark as done
+			} break;
+			case 'no': {
+				// TODO: _HIST info - waiting
+				$ret = DB::getPDO()->execSql("
+					delete from `{$instanceTable}`
+					where `pk` = :pk
+						and `idInstance` = :idInstance
+				", [
+					'pk' => $args['primaryKey'],
+					'idInstance' => $idInstance,
+				]);
+				// TODO: _HIST info - mark as done
+			} break;
+		}
+		return [
+			'type' => 'success',
+			'msg' => "Wprowadzono zmiany ({$ret})",
+			// TODO: 'data' => all instances for pk
+		];
+	}
+
 	public function rmUserTableFilterAjaxAction() {
 		Response::sendTryCatchJson(array($this, 'rmUserTableFilterAjax'), $args = 'JSON_FROM_REQUEST_BODY');
 	}

+ 3 - 2
SE/se-lib/tmpl/_layout_gora.php

@@ -157,7 +157,7 @@ if (typeof Object.assign != 'function') {
 	.p5UI__dropdown-input { border-box:box-sizing; width:100% }
 </style>
 <script>
-function p5UI__dropdown(e, n, position) {// @position = ('right top', 'left top', 'left bottom', 'right bottom')
+function p5UI__dropdown(e, n, position, render) {// @position = ('right top', 'left top', 'left bottom', 'right bottom')
 	// e.stopPropagation()
 	// e.preventDefault()
 	var btnNode = n
@@ -181,6 +181,8 @@ function p5UI__dropdown(e, n, position) {// @position = ('right top', 'left top'
 			document.addEventListener('click', node.p5UI__onClickOutsideCallback);
 			// document.addEventListener('keydown', this._closeModalKorespInfoIfHitEscape);
 		}
+		if ('function' === typeof render) render(n, node)
+		// node.firstChild.focus() // TODO: find input
 	} else {
 		if ('function' === typeof node.p5UI__unbindClickOutsideCallback) {
 			node.p5UI__unbindClickOutsideCallback()
@@ -188,7 +190,6 @@ function p5UI__dropdown(e, n, position) {// @position = ('right top', 'left top'
 			delete node.p5UI__unbindClickOutsideCallback
 		}
 	}
-	node.firstChild.focus() // TODO: find input
 }
 function p5UI__dropdown__unbindClickOutsideCallback(n) {
 	var node = n