Commit eb6651aa authored by doc@bgerp.org's avatar doc@bgerp.org

p12248 Isolation fix, drop-downs close, on.login.open many values.

parent 975b3656
......@@ -8,6 +8,7 @@ CREATE DATABASE IF NOT EXISTS bgerp DEFAULT CHARACTER SET utf8 COLLATE utf8_gene
-- SET GLOBAL validate_password.length = 5;
-- SET GLOBAL validate_password.policy = LOW;
CREATE USER IF NOT EXISTS 'bgerp'@'%' IDENTIFIED WITH mysql_native_password BY 'bgerp';
GRANT SYSTEM_USER ON *.* TO 'bgerp'@'%';
GRANT ALL PRIVILEGES ON bgerp.* TO 'bgerp'@'%';
USE bgerp;
......
A: GRANT SYSTEM_USER in DB creation script.
F: Search with process.isolation=group.
C: Hide all other popup menus or drop-downs on showing one.
A: New getting parameter API, supporting old key names.
A: Multiple values in 'on.login.open' and 'on.login.open.pinned', https://bgerp.ru/doc/3.0/manual/kernel/setup.html#user-action-tree
......@@ -1808,7 +1808,9 @@ public class ProcessDAO extends CommonDAO {
pd.addQuery(SQL_FROM);
pd.addQuery(TABLE_PROCESS);
if ( mode == MODE_USER_CREATED) {
pd.addQuery(getIsolationJoin(user));
if (mode == MODE_USER_CREATED) {
pd.addQuery("WHERE create_user_id=?");
pd.addInt(userId);
pd.addQuery(" AND close_dt is NULL");
......@@ -1826,8 +1828,6 @@ public class ProcessDAO extends CommonDAO {
pd.addQuery("status_dt DESC");
}
pd.addQuery(getIsolationJoin(user));
pd.addQuery(getPageLimit(page));
ResultSet rs = pd.executeQuery();
......
......@@ -19,9 +19,9 @@ public class LoginAction extends BaseAction {
//TODO: Когда будет сделано событие авторизации, сделать его слушателя в LoginEventListener.
User user = form.getUser();
if (user != null) {
String onLoginOpen = user.getConfigMap().get("onLoginOpen");
if (Utils.notBlankString(onLoginOpen))
LoginEventListener.addOnLoginEvent(form.getUserId(), new UrlOpenEvent(onLoginOpen));
String onLoginOpen = user.getConfigMap().getSok("on.login.open", "onLoginOpen");
for (String url : Utils.toList(onLoginOpen))
LoginEventListener.addOnLoginEvent(form.getUserId(), new UrlOpenEvent(url));
}
form.getHttpRequest().setAttribute("l", Localization.getLocalizer());
......
package ru.bgcrm.util;
import org.apache.log4j.Logger;
import ru.bgcrm.model.BGMessageException;
import ru.bgerp.util.Log;
/**
* Конфигурация. Создается один раз и хранится в {@link ParameterMap} до его изменения.
......@@ -10,7 +9,7 @@ import ru.bgcrm.model.BGMessageException;
* Получение объекта через {@link ParameterMap#getConfig(Class)}.
*/
public abstract class Config {
protected static final Logger log = Logger.getLogger(Config.class);
protected static final Log log = Log.getLog();
/**
* Конфигурация создана в режиме валидации, можно выбрасывать {@link BGMessageException}.
......@@ -24,56 +23,4 @@ public abstract class Config {
protected Config(ParameterMap setup, boolean validate) {
this.validate = validate;
}
/*protected static int[] intListToArray(List<Integer> list) {
int[] result = new int[list.size()];
int i = 0;
for (Integer in : list) {
result[i] = (in == null) ? 0 : in;
i++;
}
return result;
}
protected static long[] longListToArray(List<Long> list) {
long[] result = new long[list.size()];
int i = 0;
for (Long in : list) {
result[i] = (in == null) ? 0 : in;
i++;
}
return result;
}
protected static Pattern[] patternListToArray(List<Pattern> list) {
Pattern[] result = new Pattern[list.size()];
int i = 0;
for (Pattern in : list) {
result[i] = in;
i++;
}
return result;
}
@SuppressWarnings("unchecked")
protected static <T> T[] toArray(List<T> list, Class<?> t) {
T[] a = (T[]) java.lang.reflect.Array.newInstance(t, list.size());
int i = 0;
for (T e : list) {
a[i] = e;
i++;
}
return a;
}
protected static String[] stringListToArray(List<String> list) {
String[] result = new String[list.size()];
int i = 0;
for (String in : list) {
result[i] = in;
i++;
}
return result;
}*/
}
......@@ -66,6 +66,43 @@ public abstract class ParameterMap extends AbstractMap<String, String> {
return get((String) key, null);
}
/**
* Retrieve value using actual and old keys.
* @param def default value
* @param validate true - throw an exception on using old keys
* @param keys first element is actual ond, after - old values
* @return
* @throws BGMessageException
*/
public String getSok(String def, boolean validate, String... keys) throws BGMessageException {
String value = get(keys[0]);
if (!Utils.isEmptyString(value))
return value;
for (int i = 1; i < keys.length; i++) {
value = get(keys[i]);
if (!Utils.isEmptyString(value)) {
var message = String.format("Using deprecated config key '%s'", keys[i]);
if (validate)
throw new BGMessageException(message);
log.warn(message);
return value;
}
}
return def;
}
/**
* Calls {@link #getSok(String, boolean, String...)} with def = null and validate = false.
* @param keys
* @return
* @throws BGMessageException
*/
public String getSok(String... keys) throws BGMessageException {
return getSok(null, false, keys);
}
public int getInt(String key, int def) {
try {
final String value = get(key, null);
......@@ -82,57 +119,61 @@ public abstract class ParameterMap extends AbstractMap<String, String> {
return getInt(key, 0);
}
public float getFloat(String key, float def) {
public long getLong(String key, long def) {
try {
final String value = get(key, null);
if (Utils.isEmptyString(value))
return def;
else
return Float.parseFloat(value.trim());
return Long.parseLong(value.trim());
} catch (Exception ex) {
return def;
}
}
public long getLong(String key) {
return getLong(key, 0L);
}
public long getLong(String key, long def) {
public final boolean getBoolean(String key, boolean defaultValue) {
return Utils.parseBoolean(get(key, "").trim(), defaultValue);
}
public BigDecimal getBigDecimal(final String key, final BigDecimal def) {
try {
final String value = get(key, null);
if (Utils.isEmptyString(value))
return def;
else
return Long.parseLong(value.trim());
return new BigDecimal(value.trim());
} catch (Exception ex) {
return def;
}
}
public long getLong(String key) {
return getLong(key, 0L);
}
public double getDouble(String key, double def) {
/** The data type is not needed in business app. */
@Deprecated
public float getFloat(String key, float def) {
try {
final String value = get(key, null);
if (Utils.isEmptyString(value))
return def;
else
return Double.parseDouble(value.trim());
return Float.parseFloat(value.trim());
} catch (Exception ex) {
return def;
}
}
public final boolean getBoolean(String key, boolean defaultValue) {
return Utils.parseBoolean(get(key, "").trim(), defaultValue);
}
public BigDecimal getBigDecimal(final String key, final BigDecimal def) {
/** The data type is not needed in business app. */
@Deprecated
public double getDouble(String key, double def) {
try {
final String value = get(key, null);
if (Utils.isEmptyString(value))
return def;
else
return new BigDecimal(value.trim());
return Double.parseDouble(value.trim());
} catch (Exception ex) {
return def;
}
......
......@@ -242,10 +242,10 @@ CAUTION: Для пользователя с кодом 1 конфигураци
----
# отключение проверки прав
#dontCheckPermission=1
# открытие оснастки после авторизации зафиксированной (в данном примере - поиск)
#on.login.open.pinned=/user/search
# открытие оснастки после авторизации (в данном примере - обработка сообщений)
#onLoginOpen=/user/messageQueue
# открытие оснасток после авторизации зафиксированной (в данном примере - поиск и обработка сообщений), разделитель - запятая
#on.login.open.pinned=/user/search,/user/messageQueue
# открытие оснасток после авторизации (в данном примере - обработка сообщений), разделитель - запятая
#on.login.open=/user/messageQueue
----
[[user-isolation]]
......
......@@ -2,6 +2,7 @@ package ru.bgcrm.util;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
......@@ -39,4 +40,30 @@ public class ParameterMapTest {
assertEquals("value2", map.get("key2"));
}
@Test
public void testSok() throws Exception {
var map = ParameterMap.of("key.old", "1", "key.new", "2");
var value = map.getSok("key.new", "key.old");
assertEquals("2", value);
map = ParameterMap.of("key.old", "1");
value = map.getSok("key.new", "key.old");
assertEquals("1", value);
value = map.getSok("key.wrong1", "key.wrong2");
assertNull(value);
value = map.getSok("default", false, "key.wrong");
assertEquals("default", value);
var thrown = false;
try {
map = ParameterMap.of("key.old", "0");
map.getSok(null, true, "key.new", "key.old");
} catch (BGMessageException e) {
thrown = true;
}
assertTrue(thrown);
}
}
......@@ -36,26 +36,9 @@
<button class="btn-white combo mr1 mb1" id="${uiidMoreButton}">
<div class="text-value">${l.l('Ещё')}</div>
<div class="icon"><img src="/images/arrow-down.png"/></div>
<script>
$(function()
{
var menu = $("#${uiidMoreMenu}").menu();
$("#${uiidMoreButton}").click( function()
{
menu.show().position({
my: "left top",
at: "left bottom",
of: this
});
$(document).one( "click", function() {
menu.hide();
});
return false;
});
$(function () {
$$.ui.menuInit($("#${uiidMoreButton}"), $("#${uiidMoreMenu}"), "left");
})
</script>
</button>
......
<%@ tag body-content="empty" pageEncoding="UTF-8" description="Выпадающий список с возможностью выбора нескольких значений"%>
<%@ tag body-content="empty" pageEncoding="UTF-8" description="Выпадающий список с возможностью выбора нескольких значений"%>
<%@ include file="/WEB-INF/jspf/taglibs.jsp"%>
<%--
Значения можно устанавливать двумя способами:
1)
list - List<IdTitle> элементов
list - List<IdTitle> элементов
map - Map<Integer, IdTitle> элементов
available - List<Integer> допустимых значений
......@@ -46,13 +46,13 @@ styleTextValue / widthTextValue следует использовать, ког
<c:otherwise>
<c:set var="uiid" value="${u:uiid()}"/>
</c:otherwise>
</c:choose>
</c:choose>
<div class="btn-white combo ${styleClass}" id="${uiid}" style="${style}">
<c:if test="${not empty prefixText}">
<div class="text-pref">${prefixText}</div>
</c:if>
</c:if>
<%-- ширину всего элемента можно задавать только шириной этого блока --%>
<div class="text-value" style="${styleTextValue}"></div>
<div class="icon"><img src="/images/cross.png"/></div>
......@@ -61,21 +61,21 @@ styleTextValue / widthTextValue следует использовать, ког
<li class="in-table-cell">
<c:set var="filterCode">
var mask = $(this).val().toLowerCase();
$(this).closest('ul').find('li:gt(0)').each( function()
$(this).closest('ul').find('li:gt(0)').each( function()
{
var content = $(this).text().toLowerCase();
$(this).toggle( content.indexOf( mask ) >= 0 );
});
</c:set>
</c:set>
<div style="width: 100%;"><input type="text" style="width: 100%;" placeholder="Фильтр" onkeyup="${filterCode}"/></div>
<div><div class="btn-white btn-small-selectAll ml05" onclick='uiComboCheckUncheck(this)' title="Выделить все / снять выделение">В</div></div>
</li>
</c:if>
</c:if>
<data><%--
--%>${valuesHtml}<%--
--%><c:choose><%--
--%><c:when test="${empty available}"><%--
--%><c:forEach var="item" items="${list}"><%--
--%><c:forEach var="item" items="${list}"><%--
--%><li><%--
--%><input type="checkbox" name="${paramName}" value="${item.id}" ${u:checkedFromCollection( values, item.id )}/> <%--
--%><span>${item.title}</span><%--
......@@ -87,7 +87,7 @@ styleTextValue / widthTextValue следует использовать, ког
--%><c:when test="${map ne null}"><%--
--%><c:forEach var="availableId" items="${available}"><%--
--%><c:set var="item" value="${map[availableId]}"/><%--
--%><c:if test="${not empty item}"><%--
--%><c:if test="${not empty item}"><%--
--%><li><%--
--%><input type="checkbox" name="${paramName}" value="${item.id}" ${u:checkedFromCollection( values, item.id )}/> <%--
--%><span>${item.title}</span><%--
......@@ -98,7 +98,7 @@ styleTextValue / widthTextValue следует использовать, ког
--%><c:otherwise><%--
--%><c:forEach var="availableId" items="${available}"><%--
--%><c:forEach var="item" items="${list}"><%--
--%><c:if test="${availableId eq item.id}"><%--
--%><c:if test="${availableId eq item.id}"><%--
--%><li><%--
--%><input type="checkbox" name="${paramName}" value="${item.id}" ${u:checkedFromCollection( values, item.id )}/> <%--
--%><span>${item.title}</span><%--
......@@ -110,23 +110,23 @@ styleTextValue / widthTextValue следует использовать, ког
--%></c:choose><%--
--%></c:otherwise>
</c:choose>
</data>
</data>
</ul>
<script>
$(function()
{
{
var $comboDiv = $('#${uiid}');
var $drop = $comboDiv.find('ul.drop');
var updateCurrentTitle = function()
{
var checkedCount = 0;
var titles = "";
$comboDiv.find( "ul.drop li input[type=checkbox]" ).each( function()
{
if( this.checked )
if( this.checked )
{
checkedCount++;
var title = $(this).next().text();
......@@ -137,53 +137,43 @@ styleTextValue / widthTextValue следует использовать, ког
titles += title;
}
});
$comboDiv.find( '.text-value' ).text( "[" + checkedCount + "] " + titles );
${onChange}
};
$comboDiv.find( "ul.drop" ).on( "click", "li input", function( event )
$comboDiv.find( "ul.drop" ).on( "click", "li input", function( event )
{
updateCurrentTitle();
event.stopPropagation();
});
$comboDiv.find( "ul.drop" ).on( "click", "li", function()
{
var input = $(this).find( "input" )[0];
input.checked = !(input.checked);
updateCurrentTitle();
return false;
})
$comboDiv.click( function()
{
$drop.show();
$(document).one( "click", function()
{
$drop.hide();
});
return false;
});
})
$$.ui.dropOnClick($comboDiv, $drop);
$comboDiv.find( "div.icon" ).click( function( event )
{
$comboDiv.find( "ul.drop li input" ).each( function()
{
this.checked = false;
this.checked = false;
});
updateCurrentTitle();
event.stopPropagation();
});
updateCurrentTitle();
})
});
updateCurrentTitle();
})
</script>
</div>
\ No newline at end of file
This diff is collapsed.
......@@ -20,29 +20,6 @@ $$.shell = new function () {
event.preventDefault();
};
const createMenu = function($launcher, $menu, align) {
$menu.menu().hide();
// пустые пункты меню
$menu.find( "a:not([onclick])" ).click(function (event) {
return false;
});
$launcher.click(function () {
$menu.show().position({
my: align + " top",
at: align + " bottom",
of: this
});
$(document).one("click", function () {
$menu.hide();
});
return false;
});
};
const getCommandDiv = function (command, closable) {
var $commandDiv = $("body > #content > div#" + command );
......@@ -492,7 +469,6 @@ $$.shell = new function () {
};
// доступные функции
this.createMenu = createMenu;
this.initBuffer = initBuffer;
this.contentLoad = contentLoad;
this.followLink = followLink;
......
......@@ -10,65 +10,57 @@ $$.ui = new function() {
$element.css("border", originalConfig !== $element.val() ? "1px solid red" : "");
});
};
const comboSingleInit = ($comboDiv, onSelect) => {
const comboSingleInit = ($comboDiv, onSelect) => {
var $drop = $comboDiv.find('ul.drop');
var $hidden = $comboDiv.find( 'input[type=hidden]' );
const updateCurrentTitle = function () {
// по-умолчанию выбирается первый элемент
var $currentLi = $drop.find( 'li:not(.filter):first' );
// если указано значение - то ищется оно
var $currentLi = $drop.find( 'li:not(.filter):first' );
// если указано значение - то ищется оно
var currentValue = $hidden.val();
if (currentValue) {
$currentLi = $();
// Наличие значения в hidden не гарантирует наличия соответствующего
// Наличие значения в hidden не гарантирует наличия соответствующего
// элемента <li>, поэтому берем если нашли сам элемент;
const $foundLi = $drop.find( "li[value='" + currentValue + "']" );
const $foundLi = $drop.find( "li[value='" + currentValue + "']" );
if ($foundLi.length != 0) {
$currentLi = $foundLi;
}
}
var $currentTitle = $currentLi.find('span.title');
if ($currentTitle.length == 0) {
$currentTitle = $currentLi;
$hidden.val($currentLi.attr('value'));
}
$drop.find('li').removeAttr('selected');
$currentLi.attr('selected', '1');
$comboDiv.find('.text-value').html($currentTitle.html());
};
$comboDiv.click(function () {
$drop.show();
$(document).one("click", function () {
$drop.hide();
});
return false;
});
dropOnClick($comboDiv, $drop);
// событие клика вешается через функцию on, чтобы событие срабатывало, если элемент добавился после инициации динамически.
$drop.on('click', 'li:not(.filter)', function () {
$hidden.val( $(this).attr( "value" ) );
updateCurrentTitle();
if (onSelect) {
onSelect(this);
}
$drop.hide();
return false;
});
updateCurrentTitle();
};
......@@ -77,53 +69,105 @@ $$.ui = new function() {
};
const comboCheckUncheck = (object) => {
const $parent = $(object).closest("ul");
const $parent = $(object).closest("ul");
if ($parent.find("input[type=checkbox]:checked").length == 0)
$parent.find("input[type=checkbox]").prop("checked", true);
else
$parent.find("input[type=checkbox]").prop("checked", false);
};
// close all visible drop-downs
const dropsHide = () => {
$(document).find("ul.drop:visible").hide();
};
const dropOnClick = ($comboDiv, $drop) => {
$comboDiv.click(function () {
dropsHide();
menusHide();
$drop.show();
$(document).one("click", function () {
$drop.hide();
});
return false;
});
};
const menuInit = ($launcher, $menu, align) => {
$menu.menu().hide();
// empty menu items
$menu.find( "a:not([onclick])" ).click(function (event) {
return false;
});
$launcher.click(function () {
menusHide();
$$.ui.dropsHide();
$menu.show().position({
my: align + " top",
at: align + " bottom",
of: this
});
$(document).one("click", function () {
$menu.hide();
});
return false;
});
};
// close all visible menus
const menusHide = () => {
$(document).find("ul.ui-menu:visible[role=menu]").menu().hide();
};
const monthDaysSelectInit = ($div) => {
var date = new Date();
var $title = $div.find( "#month" );
var $dayFrom = $div.find( "#dayFrom" );
var $dayTo = $div.find( "#dayTo" );
var $dateFromHidden = $div.find( "#dateFrom" );
var $dateToHidden = $div.find( "#dateTo" );
var dateFrom = $dateFromHidden.val();
if (dateFrom) {
var parts = dateFrom.split('.');
date = new Date(parts[2], parts[1]-1, parts[0]);
}
date.setDate(1);
const update = function() {
$title.text( $.datepicker._defaults.monthNames[date.getMonth()] + " " + date.getFullYear() );