Merge branch 'master' into develop

This commit is contained in:
RB 2022-11-22 00:07:02 +08:00
commit 2ef3c566f4
21 changed files with 253 additions and 138 deletions

2
@rbv

@ -1 +1 @@
Subproject commit 6d344ca5660799c7d19eea0ad55964c778a8db57 Subproject commit 09da8d287be7103eeb3bfe727cdb75d5e19e3bb1

View file

@ -7,6 +7,7 @@ See LICENSE and COMMERCIAL in the project root for license information.
package com.rebuild.core.configuration.general; package com.rebuild.core.configuration.general;
import cn.devezhao.commons.CalendarUtils;
import cn.devezhao.persist4j.Entity; import cn.devezhao.persist4j.Entity;
import cn.devezhao.persist4j.Field; import cn.devezhao.persist4j.Field;
import cn.devezhao.persist4j.Query; import cn.devezhao.persist4j.Query;
@ -16,59 +17,67 @@ import com.alibaba.fastjson.JSONArray;
import com.rebuild.core.Application; import com.rebuild.core.Application;
import com.rebuild.core.configuration.ConfigBean; import com.rebuild.core.configuration.ConfigBean;
import com.rebuild.core.metadata.easymeta.DisplayType; import com.rebuild.core.metadata.easymeta.DisplayType;
import com.rebuild.core.metadata.easymeta.EasyField;
import com.rebuild.core.metadata.easymeta.EasyMetaFactory; import com.rebuild.core.metadata.easymeta.EasyMetaFactory;
import com.rebuild.core.metadata.impl.EasyEntityConfigProps; import com.rebuild.core.metadata.impl.EasyEntityConfigProps;
import com.rebuild.core.support.general.FieldValueHelper; import com.rebuild.core.support.general.FieldValueHelper;
import com.rebuild.utils.JSONUtils; import com.rebuild.utils.JSONUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang.StringUtils;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.ArrayList; import java.util.*;
import java.util.Comparator;
import java.util.List;
/** /**
* 列表字段分类数据 * 列表字段分类分组数据
* *
* @author ZHAO * @author ZHAO
* @since 07/23/2022 * @since 07/23/2022
*/ */
public class DataListCategory { public class DataListCategory {
public static final DataListCategory instance = new DataListCategory();
private DataListCategory() {
}
/** /**
* @param entity * @param entity
* @param user * @param user
* @return * @return
*/ */
public static JSON datas(Entity entity, ID user) { public JSON datas(Entity entity, ID user) {
final Field categoryField = getFieldOfCategory(entity); final Field categoryField = getFieldOfCategory(entity);
if (categoryField == null) return null; if (categoryField == null) return null;
DisplayType dt = EasyMetaFactory.getDisplayType(categoryField); EasyField easyField = EasyMetaFactory.valueOf(categoryField);
DisplayType dt = easyField.getDisplayType();
List<Object[]> clist = new ArrayList<>(); List<Object[]> dataList = new ArrayList<>();
if (dt == DisplayType.MULTISELECT || dt == DisplayType.PICKLIST) { if (dt == DisplayType.MULTISELECT || dt == DisplayType.PICKLIST) {
ConfigBean[] entries = MultiSelectManager.instance.getPickListRaw(categoryField, true); ConfigBean[] entries = MultiSelectManager.instance.getPickListRaw(categoryField, true);
for (ConfigBean e : entries) { for (ConfigBean e : entries) {
Object id = e.getID("id"); Object id = e.getID("id");
if (dt == DisplayType.MULTISELECT) id = e.getLong("mask"); if (dt == DisplayType.MULTISELECT) id = e.getLong("mask");
dataList.add(new Object[] { e.getString("text"), id });
clist.add(new Object[] { e.getString("text"), id });
} }
} else { } else {
// TODO 考虑支持更多分组字段类型例如日期但要考虑日期格式
String sql; String sql;
if (dt == DisplayType.N2NREFERENCE) { if (dt == DisplayType.N2NREFERENCE) {
sql = MessageFormat.format( sql = MessageFormat.format(
"select distinct referenceId from NreferenceItem where belongEntity = ''{0}'' and belongField = ''{1}''", "select distinct referenceId from NreferenceItem where belongEntity = ''{0}'' and belongField = ''{1}''",
entity.getName(), categoryField.getName()); entity.getName(), categoryField.getName());
} else { } else {
String wrapField = categoryField.getName();
if (dt == DisplayType.DATETIME) {
wrapField = String.format("DATE_FORMAT(%s, '%%Y-%%m-%%d')", wrapField);
}
sql = MessageFormat.format( sql = MessageFormat.format(
"select distinct {0} from {1} where {0} is not null", categoryField.getName(), entity.getName()); "select {0} from {1} where {2} is not null group by {0}",
wrapField, entity.getName(), categoryField.getName());
} }
Query query = user == null Query query = user == null
@ -76,18 +85,38 @@ public class DataListCategory {
: Application.getQueryFactory().createQuery(sql, user); : Application.getQueryFactory().createQuery(sql, user);
Object[][] array = query.array(); Object[][] array = query.array();
String cf = EasyMetaFactory.valueOf(entity).getExtraAttr(EasyEntityConfigProps.ADV_LIST_SHOWCATEGORY);
String[] ff = cf.split(":");
String format = ff.length > 1 ? ff[1] : null;
Set<Object> unique = new HashSet<>();
for (Object[] o : array) { for (Object[] o : array) {
Object id = o[0]; Object id = o[0];
Object label = FieldValueHelper.getLabelNotry((ID) id); Object label;
clist.add(new Object[] { label, id });
}
// TODO 分类数据 code 排序 if (dt == DisplayType.DATE || dt == DisplayType.DATETIME) {
clist.sort(Comparator.comparing(o -> o[0].toString())); format = StringUtils.defaultIfBlank(format, CalendarUtils.UTC_DATE_FORMAT);
if (id instanceof Date) {
label = CalendarUtils.format(format, (Date) id);
} else {
label = id.toString().substring(0, format.length());
}
id = label;
} else {
label = FieldValueHelper.getLabelNotry((ID) id);
}
if (unique.contains(id)) continue;
unique.add(id);
dataList.add(new Object[] { label, id });
}
} }
JSONArray res = new JSONArray(); JSONArray res = new JSONArray();
for (Object[] o : clist) { for (Object[] o : dataList) {
res.add(JSONUtils.toJSONObject( res.add(JSONUtils.toJSONObject(
new String[] { "label", "id", "count" }, new String[] { "label", "id", "count" },
new Object[] { o[0], o[1], 0 } )); new Object[] { o[0], o[1], 0 } ));
@ -102,9 +131,10 @@ public class DataListCategory {
* @param entity * @param entity
* @return * @return
*/ */
public static Field getFieldOfCategory(Entity entity) { public Field getFieldOfCategory(Entity entity) {
String categoryField = EasyMetaFactory.valueOf(entity).getExtraAttr(EasyEntityConfigProps.ADV_LIST_SHOWCATEGORY); String categoryField = EasyMetaFactory.valueOf(entity).getExtraAttr(EasyEntityConfigProps.ADV_LIST_SHOWCATEGORY);
if (StringUtils.isBlank(categoryField) || !entity.containsField(categoryField)) return null; if (categoryField != null) categoryField = categoryField.split(":")[0];
return entity.getField(categoryField); if (categoryField != null && entity.containsField(categoryField)) return entity.getField(categoryField);
return null;
} }
} }

View file

@ -189,19 +189,19 @@ public class ProtocolFilterParser {
*/ */
protected String parseCategory(String entity, String value) { protected String parseCategory(String entity, String value) {
Entity rootEntity = MetadataHelper.getEntity(entity); Entity rootEntity = MetadataHelper.getEntity(entity);
Field classField = DataListCategory.getFieldOfCategory(rootEntity); Field categoryField = DataListCategory.instance.getFieldOfCategory(rootEntity);
if (classField == null) return "(9=9)"; if (categoryField == null) return "(9=9)";
DisplayType dt = EasyMetaFactory.getDisplayType(classField); DisplayType dt = EasyMetaFactory.getDisplayType(categoryField);
if (dt == DisplayType.MULTISELECT) { if (dt == DisplayType.MULTISELECT) {
return String.format("%s && %d", classField.getName(), ObjectUtils.toInt(value)); return String.format("%s && %d", categoryField.getName(), ObjectUtils.toInt(value));
} else if (dt == DisplayType.N2NREFERENCE) { } else if (dt == DisplayType.N2NREFERENCE) {
return String.format( return String.format(
"exists (select recordId from NreferenceItem where ^%s = recordId and belongField = '%s' and referenceId = '%s')", "exists (select recordId from NreferenceItem where ^%s = recordId and belongField = '%s' and referenceId = '%s')",
rootEntity.getPrimaryField().getName(), classField.getName(), StringEscapeUtils.escapeSql(value)); rootEntity.getPrimaryField().getName(), categoryField.getName(), StringEscapeUtils.escapeSql(value));
} else { } else {
return String.format("%s = '%s'", classField.getName(), StringEscapeUtils.escapeSql(value)); return String.format("%s = '%s'", categoryField.getName(), StringEscapeUtils.escapeSql(value));
} }
} }

View file

@ -57,6 +57,7 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
private static final int CODE_STARTING = 600; private static final int CODE_STARTING = 600;
private static final int CODE_DENIEDMSG = 601; private static final int CODE_DENIEDMSG = 601;
@SuppressWarnings("unused")
private static final int CODE_MAINTAIN = 602; private static final int CODE_MAINTAIN = 602;
private static final int CODE_UNSAFE_USE = 603; private static final int CODE_UNSAFE_USE = 603;
@ -86,11 +87,11 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
request.setAttribute(WebConstants.LOCALE, requestEntry.getLocale()); request.setAttribute(WebConstants.LOCALE, requestEntry.getLocale());
request.setAttribute(WebConstants.$BUNDLE, Application.getLanguage().getBundle(requestEntry.getLocale())); request.setAttribute(WebConstants.$BUNDLE, Application.getLanguage().getBundle(requestEntry.getLocale()));
final String requestUrl = requestEntry.getRequestUrl(); final String requestUri = requestEntry.getRequestUri();
// 服务暂不可用 // 服务暂不可用
if (!Application.isReady()) { if (!Application.isReady()) {
final boolean isError = requestUrl.endsWith("/error") || requestUrl.contains("/error/"); final boolean isError = requestUri.endsWith("/error") || requestUri.contains("/error/");
// 已安装 // 已安装
if (checkInstalled()) { if (checkInstalled()) {
@ -104,7 +105,7 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
} }
} }
// 未安装 // 未安装
else if (!(requestUrl.contains("/setup/") || requestUrl.contains("/commons/theme/") || isError)) { else if (!(requestUri.contains("/setup/") || requestUri.contains("/commons/theme/") || isError)) {
sendRedirect(response, "/setup/install", null); sendRedirect(response, "/setup/install", null);
return false; return false;
} else { } else {
@ -120,9 +121,9 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
if (requestUser != null) { if (requestUser != null) {
// 管理中心二次验证 // 管理中心二次验证
if (requestUrl.contains("/admin/") && !AppUtils.isAdminVerified(request)) { if (requestUri.contains("/admin/") && !AppUtils.isAdminVerified(request)) {
if (isHtmlRequest(request)) { if (isHtmlRequest(requestUri, request)) {
sendRedirect(response, "/user/admin-verify", requestEntry.getRequestUri()); sendRedirect(response, "/user/admin-verify", requestEntry.getRequestUriWithQuery());
} else { } else {
response.sendError(HttpStatus.FORBIDDEN.value()); response.sendError(HttpStatus.FORBIDDEN.value());
} }
@ -134,7 +135,7 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
// User // User
request.setAttribute(WebConstants.$USER, Application.getUserStore().getUser(requestUser)); request.setAttribute(WebConstants.$USER, Application.getUserStore().getUser(requestUser));
if (isHtmlRequest(request)) { if (isHtmlRequest(requestUri, request)) {
// Last active // Last active
Application.getSessionStore().storeLastActive(request); Application.getSessionStore().storeLastActive(request);
@ -142,7 +143,7 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
String sidebarCollapsed = ServletUtils.readCookie(request, "rb.sidebarCollapsed"); String sidebarCollapsed = ServletUtils.readCookie(request, "rb.sidebarCollapsed");
String sideCollapsedClazz = BooleanUtils.toBoolean(sidebarCollapsed) ? "rb-collapsible-sidebar-collapsed" : ""; String sideCollapsedClazz = BooleanUtils.toBoolean(sidebarCollapsed) ? "rb-collapsible-sidebar-collapsed" : "";
// Aside collapsed // Aside collapsed
if (!(requestUrl.contains("/admin/") || requestUrl.contains("/setup/"))) { if (!(requestUri.contains("/admin/") || requestUri.contains("/setup/"))) {
String asideCollapsed = ServletUtils.readCookie(request, "rb.asideCollapsed"); String asideCollapsed = ServletUtils.readCookie(request, "rb.asideCollapsed");
if (BooleanUtils.toBoolean(asideCollapsed)) sideCollapsedClazz += " rb-aside-collapsed"; if (BooleanUtils.toBoolean(asideCollapsed)) sideCollapsedClazz += " rb-aside-collapsed";
} }
@ -156,14 +157,14 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
if (RebuildConfiguration.getBool(ConfigurationItem.SecurityEnhanced)) skipCheckSafeUse = false; if (RebuildConfiguration.getBool(ConfigurationItem.SecurityEnhanced)) skipCheckSafeUse = false;
else skipCheckSafeUse = UserHelper.isSuperAdmin(requestUser); else skipCheckSafeUse = UserHelper.isSuperAdmin(requestUser);
} else if (!isIgnoreAuth(requestUrl)) { } else if (!isIgnoreAuth(requestUri)) {
// 独立验证逻辑 // 独立验证逻辑
if (requestUrl.contains("/filex/")) return true; if (requestUri.contains("/filex/")) return true;
log.warn("Unauthorized access {}", RebuildWebConfigurer.getRequestUrls(request)); log.warn("Unauthorized access {}", RebuildWebConfigurer.getRequestUrls(request));
if (isHtmlRequest(request)) { if (isHtmlRequest(requestUri, request)) {
sendRedirect(response, "/user/login", requestEntry.getRequestUri()); sendRedirect(response, "/user/login", requestEntry.getRequestUriWithQuery());
} else { } else {
response.sendError(HttpStatus.UNAUTHORIZED.value()); response.sendError(HttpStatus.UNAUTHORIZED.value());
} }
@ -190,7 +191,7 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
// 打印处理时间 // 打印处理时间
long time = requestEntry == null ? 0 : (System.currentTimeMillis() - requestEntry.getRequestTime()); long time = requestEntry == null ? 0 : (System.currentTimeMillis() - requestEntry.getRequestTime());
if (time > 1000) { if (time > 1500) {
log.warn("Method handle time {} ms. Request URL(s) {}", time, RebuildWebConfigurer.getRequestUrls(request)); log.warn("Method handle time {} ms. Request URL(s) {}", time, RebuildWebConfigurer.getRequestUrls(request));
} }
@ -203,38 +204,38 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
if (rbmobLocale != null) return rbmobLocale; if (rbmobLocale != null) return rbmobLocale;
// 0. Session // 0. Session
String havingLocale = (String) ServletUtils.getSessionAttribute(request, AppUtils.SK_LOCALE); String useLocale = (String) ServletUtils.getSessionAttribute(request, AppUtils.SK_LOCALE);
String urlLocale = request.getParameter("locale"); String urlLocale = request.getParameter("locale");
if (StringUtils.isNotBlank(urlLocale) && !urlLocale.equals(havingLocale)) { if (StringUtils.isNotBlank(urlLocale) && !urlLocale.equals(useLocale)) {
urlLocale = Application.getLanguage().available(urlLocale); urlLocale = Application.getLanguage().available(urlLocale);
if (urlLocale != null) { if (urlLocale != null) {
havingLocale = urlLocale; useLocale = urlLocale;
ServletUtils.setSessionAttribute(request, AppUtils.SK_LOCALE, havingLocale); ServletUtils.setSessionAttribute(request, AppUtils.SK_LOCALE, useLocale);
ServletUtils.addCookie(response, AppUtils.CK_LOCALE, havingLocale, ServletUtils.addCookie(response, AppUtils.CK_LOCALE, useLocale,
CommonsCache.TS_DAY * 90, null, StringUtils.defaultIfBlank(AppUtils.getContextPath(), "/")); CommonsCache.TS_DAY * 90, null, StringUtils.defaultIfBlank(AppUtils.getContextPath(), "/"));
if (Application.devMode()) Application.getLanguage().refresh(); if (Application.devMode()) Application.getLanguage().refresh();
} }
} }
if (havingLocale != null) return havingLocale; if (useLocale != null) return useLocale;
// 1. Cookie // 1. Cookie
havingLocale = ServletUtils.readCookie(request, AppUtils.CK_LOCALE); useLocale = ServletUtils.readCookie(request, AppUtils.CK_LOCALE);
if (havingLocale == null) { if (useLocale == null) {
// 2. User-Local // 2. User-Local
havingLocale = request.getLocale().toString(); useLocale = request.getLocale().toString();
} }
// 3. Default // 3. Default
if ((havingLocale = Application.getLanguage().available(havingLocale)) == null) { if ((useLocale = Application.getLanguage().available(useLocale)) == null) {
havingLocale = RebuildConfiguration.get(ConfigurationItem.DefaultLanguage); useLocale = RebuildConfiguration.get(ConfigurationItem.DefaultLanguage);
} }
ServletUtils.setSessionAttribute(request, AppUtils.SK_LOCALE, havingLocale); ServletUtils.setSessionAttribute(request, AppUtils.SK_LOCALE, useLocale);
return havingLocale; return useLocale;
} }
private boolean isIgnoreAuth(String requestUri) { private boolean isIgnoreAuth(String requestUri) {
@ -242,14 +243,15 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
return true; return true;
} }
requestUri = requestUri.split("\\?")[0];
requestUri = requestUri.replaceFirst(AppUtils.getContextPath(), ""); requestUri = requestUri.replaceFirst(AppUtils.getContextPath(), "");
return requestUri.length() < 3 return requestUri.length() < 3
|| requestUri.endsWith("/error") || requestUri.contains("/error/") || requestUri.endsWith("/error") || requestUri.contains("/error/")
|| requestUri.startsWith("/f/") || requestUri.startsWith("/s/") || requestUri.endsWith("/logout")
|| requestUri.startsWith("/setup/") || requestUri.startsWith("/f/")
|| requestUri.startsWith("/s/")
|| requestUri.startsWith("/gw/") || requestUri.startsWith("/gw/")
|| requestUri.startsWith("/setup/")
|| requestUri.startsWith("/language/") || requestUri.startsWith("/language/")
|| requestUri.startsWith("/filex/access/") || requestUri.startsWith("/filex/access/")
|| requestUri.startsWith("/filex/download/") || requestUri.startsWith("/filex/download/")
@ -259,12 +261,10 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
|| requestUri.startsWith("/commons/barcode/render") || requestUri.startsWith("/commons/barcode/render")
|| requestUri.startsWith("/commons/theme/") || requestUri.startsWith("/commons/theme/")
|| requestUri.startsWith("/account/user-avatar/") || requestUri.startsWith("/account/user-avatar/")
|| requestUri.startsWith("/rbmob/env") || requestUri.startsWith("/rbmob/env");
|| requestUri.endsWith("/logout");
} }
private boolean isHtmlRequest(HttpServletRequest request) { private boolean isHtmlRequest(String requestUri, HttpServletRequest request) {
String requestUri = request.getRequestURI();
if (ServletUtils.isAjaxRequest(request) if (ServletUtils.isAjaxRequest(request)
|| requestUri.contains("/assets/") || requestUri.contains("/assets/")
|| requestUri.contains("/commons/frontjs/") || requestUri.contains("/commons/frontjs/")
@ -285,9 +285,9 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
} }
private void sendRedirect(HttpServletResponse response, String url, String nexturl) throws IOException { private void sendRedirect(HttpServletResponse response, String url, String nexturl) throws IOException {
String fullUrl = AppUtils.getContextPath(url); String redirectUrl = AppUtils.getContextPath(url);
if (nexturl != null) fullUrl += "?nexturl=" + CodecUtils.urlEncode(nexturl); if (nexturl != null) redirectUrl += "?nexturl=" + CodecUtils.urlEncode(nexturl);
response.sendRedirect(fullUrl); response.sendRedirect(redirectUrl);
} }
private void checkSafeUse(String ipAddr, String requestUri) throws DefinedException { private void checkSafeUse(String ipAddr, String requestUri) throws DefinedException {
@ -315,22 +315,21 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
private static class RequestEntry { private static class RequestEntry {
final long requestTime; final long requestTime;
final String requestUri; final String requestUri;
final String requestUrl; final String requestUriWithQuery;
final ID requestUser; final ID requestUser;
final String locale; final String locale;
RequestEntry(HttpServletRequest request, String locale) { RequestEntry(HttpServletRequest request, String locale) {
this.requestTime = System.currentTimeMillis(); this.requestTime = System.currentTimeMillis();
this.requestUri = request.getRequestURI() this.requestUri = request.getRequestURI();
+ (request.getQueryString() != null ? ("?" + request.getQueryString()) : ""); this.requestUriWithQuery = this.requestUri + (request.getQueryString() == null ? "" : "?" + request.getQueryString());
this.requestUrl = request.getRequestURI();
this.requestUser = AppUtils.getRequestUser(request, true); this.requestUser = AppUtils.getRequestUser(request, true);
this.locale = locale; this.locale = locale;
} }
@Override @Override
public String toString() { public String toString() {
return requestUri; return requestUriWithQuery;
} }
} }
} }

View file

@ -73,6 +73,7 @@ public class ConfigurationController extends BaseController {
mv.getModel().put("LicenseType", mv.getModel().put("LicenseType",
auth.getString("authType") + " (" + auth.getString("authObject") + ")"); auth.getString("authType") + " (" + auth.getString("authObject") + ")");
mv.getModel().put("Version", Application.VER); mv.getModel().put("Version", Application.VER);
mv.getModel().put("SN", "***" + License.SN().substring(12));
return mv; return mv;
} }

View file

@ -69,7 +69,7 @@ public class WidgetController extends BaseController implements ShareTo {
@GetMapping("widget-category-data") @GetMapping("widget-category-data")
public RespBody getCategoryData(@PathVariable String entity, HttpServletRequest request) { public RespBody getCategoryData(@PathVariable String entity, HttpServletRequest request) {
JSON data = DataListCategory.datas( JSON data = DataListCategory.instance.datas(
MetadataHelper.getEntity(entity), getRequestUser(request)); MetadataHelper.getEntity(entity), getRequestUser(request));
return RespBody.ok(data); return RespBody.ok(data);
} }

View file

@ -2216,7 +2216,6 @@
"请至少添加 1 个查询字段":"请至少添加 1 个查询字段", "请至少添加 1 个查询字段":"请至少添加 1 个查询字段",
"请填写固定值":"请填写固定值", "请填写固定值":"请填写固定值",
"@%s 退回了你的 %s 审批,请重新审核":"@%s 退回了你的 %s 审批,请重新审核", "@%s 退回了你的 %s 审批,请重新审核":"@%s 退回了你的 %s 审批,请重新审核",
"显示侧栏分类":"显示侧栏分类",
"转换明细记录需选择主记录":"转换明细记录需选择主记录", "转换明细记录需选择主记录":"转换明细记录需选择主记录",
"@%s 驳回了你的 %s 审批,请重新提交":"@%s 驳回了你的 %s 审批,请重新提交", "@%s 驳回了你的 %s 审批,请重新提交":"@%s 驳回了你的 %s 审批,请重新提交",
"如有多个类型请使用逗号或空格分开":"如有多个类型请使用逗号或空格分开", "如有多个类型请使用逗号或空格分开":"如有多个类型请使用逗号或空格分开",
@ -2265,7 +2264,7 @@
"退回至":"退回至", "退回至":"退回至",
"提交模式":"提交模式", "提交模式":"提交模式",
"启用提交模式必须选择审批流程":"启用提交模式必须选择审批流程", "启用提交模式必须选择审批流程":"启用提交模式必须选择审批流程",
"显示侧栏“分类”":"显示侧栏“分类”", "显示侧栏“分组”":"显示侧栏“分组”",
"显示侧栏“常用查询”":"显示侧栏“常用查询”", "显示侧栏“常用查询”":"显示侧栏“常用查询”",
"仅提交不审批。选择的审批流程至少配置一个审批人,否则会提交失败":"仅提交不审批。选择的审批流程至少配置一个审批人,否则会提交失败", "仅提交不审批。选择的审批流程至少配置一个审批人,否则会提交失败":"仅提交不审批。选择的审批流程至少配置一个审批人,否则会提交失败",
"需要先添加 [记录转换](../transforms) 才能在此处选择":"需要先添加 [记录转换](../transforms) 才能在此处选择", "需要先添加 [记录转换](../transforms) 才能在此处选择":"需要先添加 [记录转换](../transforms) 才能在此处选择",
@ -2273,7 +2272,6 @@
"显示侧栏“图表”":"显示侧栏“图表”", "显示侧栏“图表”":"显示侧栏“图表”",
"选择的审批流程至少配置一个审批人":"选择的审批流程至少配置一个审批人", "选择的审批流程至少配置一个审批人":"选择的审批流程至少配置一个审批人",
"需要先添加 [审批流程](../approvals) 才能在此处选择":"需要先添加 [审批流程](../approvals) 才能在此处选择", "需要先添加 [审批流程](../approvals) 才能在此处选择":"需要先添加 [审批流程](../approvals) 才能在此处选择",
"Category":"分类",
"管理员撤销":"管理员撤销", "管理员撤销":"管理员撤销",
"部分功能可能需要商业版才能正常运行":"部分功能可能需要商业版才能正常运行", "部分功能可能需要商业版才能正常运行":"部分功能可能需要商业版才能正常运行",
"默认大小":"默认大小", "默认大小":"默认大小",
@ -2379,5 +2377,8 @@
"APP SECRET 已重置":"APP SECRET 已重置", "APP SECRET 已重置":"APP SECRET 已重置",
"修改 API 秘钥":"修改 API 秘钥", "修改 API 秘钥":"修改 API 秘钥",
"自动合并单元格":"自动合并单元格", "自动合并单元格":"自动合并单元格",
"重置后第三方应用需更换新的 APP SECRET 使用":"重置后第三方应用需更换新的 APP SECRET 使用" "重置后第三方应用需更换新的 APP SECRET 使用":"重置后第三方应用需更换新的 APP SECRET 使用",
"分组":"分组",
"字段格式":"字段格式",
"分组字段":"分组字段"
} }

View file

@ -44,6 +44,12 @@
.form-group .switch-button-xs { .form-group .switch-button-xs {
margin-top: 7px; margin-top: 7px;
} }
.advListShowCategory-set {
border-radius: 2px;
border: 1px solid #eee;
padding: 15px;
margin-top: 10px;
}
</style> </style>
</head> </head>
<body> <body>

View file

@ -247,8 +247,12 @@ const _fieldsMapping = (columns, fields) => {
const $tbody = $('#fieldsMapping tbody').empty() const $tbody = $('#fieldsMapping tbody').empty()
$(columns).each(function (idx, item) { $(columns).each(function (idx, item) {
let L = _LETTERS[idx]
if (idx > 25) L = `A${_LETTERS[idx - 26] || 'X'}` // AA
if (idx > 51) L = `B${_LETTERS[idx - 52] || 'X'}` // BA
const $tr = $(`<tr data-col="${idx}"></tr>`).appendTo($tbody) const $tr = $(`<tr data-col="${idx}"></tr>`).appendTo($tbody)
$(`<td><em>${_LETTERS[idx] || _LETTERS[idx - 26]}1</em> ${item || $L('空')}<i class="zmdi zmdi-arrow-right"></i></td>`).appendTo($tr) $(`<td><em>${L}</em> ${item || $L('空')}<i class="zmdi zmdi-arrow-right"></i></td>`).appendTo($tr)
const $td = $('<td></td>').appendTo($tr) const $td = $('<td></td>').appendTo($tr)
const $clone = $fieldSelect.clone().appendTo($td) const $clone = $fieldSelect.clone().appendTo($td)
$('<td class="pl-3"></td>').appendTo($tr) $('<td class="pl-3"></td>').appendTo($tr)

View file

@ -12,10 +12,10 @@ const _ENTITIES = {
$(document).ready(() => { $(document).ready(() => {
$.get('/commons/metadata/entities?detail=true', (res) => { $.get('/commons/metadata/entities?detail=true', (res) => {
$(res.data).each(function () { res.data && res.data.forEach((item) => (_ENTITIES[item.name] = item.label))
$(`<option value="${this.name}">${this.label}</option>`).appendTo('#belongEntity') for (let name in _ENTITIES) {
_ENTITIES[this.name] = this.label $(`<option value="${name}">${_ENTITIES[name]}</option>`).appendTo('#belongEntity')
}) }
renderRbcomp(<DataList />, 'react-list', function () { renderRbcomp(<DataList />, 'react-list', function () {
RbListPage._RbList = this._List RbListPage._RbList = this._List

View file

@ -12,10 +12,7 @@ const _ENTITIES = {
$(document).ready(() => { $(document).ready(() => {
$.get('/commons/metadata/entities?detail=true', (res) => { $.get('/commons/metadata/entities?detail=true', (res) => {
$(res.data).each(function () { res.data && res.data.forEach((item) => (_ENTITIES[item.name] = item.label))
_ENTITIES[this.name] = this.label
})
for (let name in _ENTITIES) { for (let name in _ENTITIES) {
$(`<option value="${name}">${_ENTITIES[name]}</option>`).appendTo('#belongEntity') $(`<option value="${name}">${_ENTITIES[name]}</option>`).appendTo('#belongEntity')
} }

View file

@ -44,6 +44,10 @@ useEditComp = function (name) {
</select> </select>
) )
} else if ('PasswordPolicy' === name) { } else if ('PasswordPolicy' === name) {
// 借用贵宝地
_toggleImage('.applogo')
_toggleImage('.bgimg')
return ( return (
<select className="form-control form-control-sm"> <select className="form-control form-control-sm">
<option value="1">{$L(' (最低6位无字符类型限制)')}</option> <option value="1">{$L(' (最低6位无字符类型限制)')}</option>
@ -52,10 +56,6 @@ useEditComp = function (name) {
</select> </select>
) )
} else if ('DefaultLanguage' === name) { } else if ('DefaultLanguage' === name) {
// 借用贵宝地
_toggleImage('.applogo')
_toggleImage('.bgimg')
const options = [] const options = []
for (let k in wpc._LANGS) { for (let k in wpc._LANGS) {
options.push( options.push(

View file

@ -162,7 +162,7 @@ class ChartIndex extends BaseChart {
_resize() { _resize() {
const ch = $(this._$chart).height() const ch = $(this._$chart).height()
const zoom = ch > 100 ? (ch > 330 ? 2.831 : 1.3) : 1 const zoom = ch > 100 ? (ch > 330 ? 2 : 1.3) : 1
$(this._$chart).find('strong').css('zoom', zoom) $(this._$chart).find('strong').css('zoom', zoom)
// const $text = $(this._$chart).find('strong') // const $text = $(this._$chart).find('strong')

View file

@ -99,7 +99,8 @@ function modeSave(newOption, next) {
}) })
} }
const CLASS_TYPES = ['PICKLIST', 'MULTISELECT', 'CLASSIFICATION', 'REFERENCE', 'N2NREFERENCE'] // const CATE_TYPES = ['PICKLIST', 'MULTISELECT', 'CLASSIFICATION', 'DATE', 'DATETIME', 'REFERENCE', 'N2NREFERENCE']
const CATE_TYPES = ['PICKLIST', 'MULTISELECT', 'CLASSIFICATION', 'REFERENCE', 'N2NREFERENCE']
// 模式选项 // 模式选项
class DlgMode1Option extends RbFormHandler { class DlgMode1Option extends RbFormHandler {
@ -109,7 +110,7 @@ class DlgMode1Option extends RbFormHandler {
<div className="form"> <div className="form">
<div className="form-group row"> <div className="form-group row">
<label className="col-sm-3 col-form-label text-sm-right">{$L('显示侧栏常用查询')}</label> <label className="col-sm-3 col-form-label text-sm-right">{$L('显示侧栏常用查询')}</label>
<div className="col-sm-7"> <div className="col-sm-9">
<div className="switch-button switch-button-xs"> <div className="switch-button switch-button-xs">
<input type="checkbox" id="advListHideFilters" defaultChecked={wpc.extConfig && !wpc.extConfig.advListHideFilters} /> <input type="checkbox" id="advListHideFilters" defaultChecked={wpc.extConfig && !wpc.extConfig.advListHideFilters} />
<span> <span>
@ -119,8 +120,8 @@ class DlgMode1Option extends RbFormHandler {
</div> </div>
</div> </div>
<div className="form-group row bosskey-show"> <div className="form-group row bosskey-show">
<label className="col-sm-3 col-form-label text-sm-right">{$L('显示侧栏')}</label> <label className="col-sm-3 col-form-label text-sm-right">{$L('显示侧栏')}</label>
<div className="col-sm-7"> <div className="col-sm-9">
<div className="switch-button switch-button-xs"> <div className="switch-button switch-button-xs">
<input type="checkbox" id="advListShowCategory" defaultChecked={wpc.extConfig && wpc.extConfig.advListShowCategory} /> <input type="checkbox" id="advListShowCategory" defaultChecked={wpc.extConfig && wpc.extConfig.advListShowCategory} />
<span> <span>
@ -128,23 +129,41 @@ class DlgMode1Option extends RbFormHandler {
</span> </span>
</div> </div>
<div className="clearfix"></div> <div className="clearfix"></div>
<div className={`J_advListShowCategory mt-2 ${this.state.advListShowCategory ? '' : 'hide'}`}> <div className={`advListShowCategory-set ${this.state.advListShowCategory ? '' : 'hide'}`}>
<select className="form-control form-control-sm"> <div className="row">
{this.state.advListShowCategoryFields && <div className="col-8">
this.state.advListShowCategoryFields.map((item) => { <label className="mb-1">{$L('分组字段')}</label>
return ( <select className="form-control form-control-sm">
<option key={item.name} value={item.name}> {this.state.advListShowCategoryFields &&
{item.label} this.state.advListShowCategoryFields.map((item) => {
</option> return (
) <option key={item.name} value={item.name}>
})} {item.label}
</select> </option>
)
})}
</select>
</div>
<div className={`col-4 pl-0 ${this.state.advListShowCategoryFormats ? '' : 'hide'}`}>
<label className="mb-1">{$L('字段格式')}</label>
<select className="form-control form-control-sm" disabled>
{this.state.advListShowCategoryFormats &&
this.state.advListShowCategoryFormats.map((item) => {
return (
<option key={item[0]} value={item[0]}>
{item[1]}
</option>
)
})}
</select>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<div className="form-group row"> <div className="form-group row">
<label className="col-sm-3 col-form-label text-sm-right">{$L('显示侧栏图表')}</label> <label className="col-sm-3 col-form-label text-sm-right">{$L('显示侧栏图表')}</label>
<div className="col-sm-7"> <div className="col-sm-9">
<div className="switch-button switch-button-xs"> <div className="switch-button switch-button-xs">
<input type="checkbox" id="advListHideCharts" defaultChecked={wpc.extConfig && !wpc.extConfig.advListHideCharts} /> <input type="checkbox" id="advListHideCharts" defaultChecked={wpc.extConfig && !wpc.extConfig.advListHideCharts} />
<span> <span>
@ -155,7 +174,7 @@ class DlgMode1Option extends RbFormHandler {
</div> </div>
<div className="form-group row bosskey-show"> <div className="form-group row bosskey-show">
<label className="col-sm-3 col-form-label text-sm-right">{$L('显示顶部查询面板')}</label> <label className="col-sm-3 col-form-label text-sm-right">{$L('显示顶部查询面板')}</label>
<div className="col-sm-7"> <div className="col-sm-9">
<div className="switch-button switch-button-xs"> <div className="switch-button switch-button-xs">
<input type="checkbox" id="advListFilterPane" defaultChecked={wpc.extConfig && wpc.extConfig.advListFilterPane} /> <input type="checkbox" id="advListFilterPane" defaultChecked={wpc.extConfig && wpc.extConfig.advListFilterPane} />
<span> <span>
@ -165,7 +184,7 @@ class DlgMode1Option extends RbFormHandler {
</div> </div>
</div> </div>
<div className="form-group row footer"> <div className="form-group row footer">
<div className="col-sm-7 offset-sm-3" ref={(c) => (this._btns = c)}> <div className="col-sm-9 offset-sm-3" ref={(c) => (this._btns = c)}>
<button className="btn btn-primary" type="button" onClick={this.save}> <button className="btn btn-primary" type="button" onClick={this.save}>
{$L('确定')} {$L('确定')}
</button> </button>
@ -180,8 +199,9 @@ class DlgMode1Option extends RbFormHandler {
} }
componentDidMount() { componentDidMount() {
let $catFields, $catFormats
const that = this const that = this
let $class2
$('#advListShowCategory').on('change', function () { $('#advListShowCategory').on('change', function () {
if ($val(this)) { if ($val(this)) {
that.setState({ advListShowCategory: true }) that.setState({ advListShowCategory: true })
@ -189,19 +209,63 @@ class DlgMode1Option extends RbFormHandler {
that.setState({ advListShowCategory: null }) that.setState({ advListShowCategory: null })
} }
if (!$class2) { if (!$catFields) {
$class2 = $('.J_advListShowCategory select') $catFields = $('.advListShowCategory-set select:eq(0)')
$catFormats = $('.advListShowCategory-set select:eq(1)')
$.get(`/commons/metadata/fields?entity=${wpc.entityName}&deep=2`, (res) => { $.get(`/commons/metadata/fields?entity=${wpc.entityName}&deep=2`, (res) => {
const _data = [] const _data = []
res.data.forEach((item) => { res.data &&
if (CLASS_TYPES.includes(item.type)) _data.push(item) res.data.forEach((item) => {
}) if (CATE_TYPES.includes(item.type) && !(item.name.includes('owningDept.') || item.name.includes('owningUser.'))) {
_data.push(item)
}
})
// FIELD:[FORMAT]
let set = wpc.extConfig && wpc.extConfig.advListShowCategory ? wpc.extConfig.advListShowCategory : null
if (set) set = set.split(':')
wpc.extConfig.advListShowCategory
that.setState({ advListShowCategoryFields: _data }, () => { that.setState({ advListShowCategoryFields: _data }, () => {
$class2 $catFields
.select2({ placeholder: $L('选择分类字段') }) .select2({
.val((wpc.extConfig && wpc.extConfig.advListShowCategory) || null) placeholder: $L('选择分类字段'),
.trigger('change') allowClear: false,
})
.on('change', () => {
const s = $catFields.val()
const found = _data.find((x) => x.name === s)
let formats
if (found && found.type === 'CLASSIFICATION') {
formats = [
[0, $L('%d 级分类', 1)],
[1, $L('%d 级分类', 2)],
[2, $L('%d 级分类', 3)],
[3, $L('%d 级分类', 4)],
]
} else if (found && (found.type === 'DATE' || found.type === 'DATETIME')) {
formats = [
['yyyy', 'YYYY'],
['yyyy-MM', 'YYYY-MM'],
['yyyy-MM-dd', 'YYYY-MM-DD'],
]
}
that.setState({ advListShowCategoryFormats: formats }, () => {
$catFormats.val(null).trigger('change')
})
})
$catFormats.select2({ placeholder: $L('默认') })
if (set) {
$catFields.val(set[0]).trigger('change')
setTimeout(() => {
if (set[1]) $catFormats.val(set[1]).trigger('change')
}, 200)
}
}) })
}) })
} }
@ -216,10 +280,13 @@ class DlgMode1Option extends RbFormHandler {
const o = { const o = {
advListHideFilters: !$val('#advListHideFilters'), advListHideFilters: !$val('#advListHideFilters'),
advListHideCharts: !$val('#advListHideCharts'), advListHideCharts: !$val('#advListHideCharts'),
advListShowCategory: this.state.advListShowCategory ? $val('.J_advListShowCategory select') : null,
advListFilterPane: $val('#advListFilterPane'), advListFilterPane: $val('#advListFilterPane'),
} }
if (this.state.advListShowCategory) {
o.advListShowCategory = `${$val('.advListShowCategory-set select:eq(0)')}:${$val('.advListShowCategory-set select:eq(1)') || ''}`
}
this.disabled(true) this.disabled(true)
modeSave(o, () => { modeSave(o, () => {
this.hide() this.hide()

View file

@ -112,7 +112,7 @@ class RbFormModal extends React.Component {
const FORM = ( const FORM = (
<RbForm entity={entity} id={id} rawModel={formModel} $$$parent={this}> <RbForm entity={entity} id={id} rawModel={formModel} $$$parent={this}>
{formModel.elements.map((item) => { {formModel.elements.map((item) => {
return detectElement(item) return detectElement(item, entity)
})} })}
</RbForm> </RbForm>
) )
@ -265,8 +265,7 @@ class RbForm extends React.Component {
let _ProTable let _ProTable
if (window._CustomizedForms) { if (window._CustomizedForms) {
_ProTable = window._CustomizedForms.useProTable(this.props.entity, this) _ProTable = window._CustomizedForms.useProTable(this.props.entity, this)
// 不显示 if (_ProTable === false) return null // 不显示
if (_ProTable === false) return null
} }
const that = this const that = this
@ -2402,8 +2401,13 @@ class RbFormDivider extends React.Component {
} }
// 确定元素类型 // 确定元素类型
var detectElement = function (item) { var detectElement = function (item, entity) {
if (!item.key) item.key = 'field-' + (item.field === TYPE_DIVIDER ? $random() : item.field) if (!item.key) item.key = `field-${item.field === TYPE_DIVIDER ? $random() : item.field}`
if (entity && window._CustomizedForms) {
const c = window._CustomizedForms.useFormElement(entity, item)
if (c) return c
}
if (item.type === 'TEXT' || item.type === 'SERIES') { if (item.type === 'TEXT' || item.type === 'SERIES') {
return <RbFormText {...item} /> return <RbFormText {...item} />

View file

@ -278,14 +278,17 @@ var _initNav = function () {
$sub.toggleClass('visible') $sub.toggleClass('visible')
$currsntSubnav = $this $currsntSubnav = $this
$this.find('a').eq(0).tooltip('hide') $this.find('a').eq(0).tooltip('hide')
$('.left-sidebar-scroll').perfectScrollbar('update')
if ($sub.hasClass('visible')) $this.addClass('open') if ($sub.hasClass('visible')) $this.addClass('open')
else $this.removeClass('open') else $this.removeClass('open')
$('.left-sidebar-scroll').perfectScrollbar('update')
}) })
$('.sidebar-elements li.parent .sub-menu').on('click', function (e) { $('.sidebar-elements li.parent .sub-menu').on('click', function (e) {
e.stopPropagation() e.stopPropagation()
}) })
$(document.body).on('click', function () { $(document.body).on('click', function () {
closeSubnav() closeSubnav()
if (isOffcanvas) { if (isOffcanvas) {

View file

@ -58,7 +58,7 @@ class RbViewForm extends React.Component {
{res.data.elements.map((item) => { {res.data.elements.map((item) => {
if (item.field !== TYPE_DIVIDER) viewData[item.field] = item.value if (item.field !== TYPE_DIVIDER) viewData[item.field] = item.value
item.$$$parent = this item.$$$parent = this
return detectViewElement(item) return detectViewElement(item, this.props.entity)
})} })}
</div> </div>
</React.Fragment> </React.Fragment>
@ -163,12 +163,11 @@ class RbViewForm extends React.Component {
} }
} }
const detectViewElement = function (item) { const detectViewElement = function (item, entity) {
if (!window.detectElement) throw 'detectElement undef' if (!window.detectElement) throw 'detectElement undef'
item.onView = true item.onView = true
item.editMode = false item.editMode = false
// item.key = `col-${item.field === TYPE_DIVIDER ? $random() : item.field}` return window.detectElement(item, entity)
return window.detectElement(item)
} }
const _renderError = (message) => { const _renderError = (message) => {

View file

@ -250,10 +250,18 @@ class ContentFieldWriteback extends ActionContentSpec {
} }
}) })
// fix: GitHub#491
let sfLast = $(this._$sourceField).val()
sfLast = sourceFields.find((x) => x.name === sfLast)
this.setState({ targetField: null, sourceFields: sourceFields }, () => { this.setState({ targetField: null, sourceFields: sourceFields }, () => {
if (sourceFields.length > 0) $(this._$sourceField).val(sourceFields[0].name) if (!sfLast && sourceFields.length > 0) sfLast = sourceFields[0]
// 强制销毁后再渲染 // 强制销毁后再渲染
this.setState({ targetField: targetField }) this.setState({ targetField: targetField }, () => {
if (sfLast) $(this._$sourceField).val(sfLast.name)
$(this._$sourceField).trigger('change')
})
}) })
} }

View file

@ -71,11 +71,7 @@
<table class="table table-bordered table-sm table-hover"> <table class="table table-bordered table-sm table-hover">
<tbody> <tbody>
<tr> <tr>
<th width="30%">SN</th> <th width="30%">Version</th>
<td>[[${T(com.rebuild.core.support.License).SN()}]]</td>
</tr>
<tr>
<th>Version</th>
<td>[[${T(com.rebuild.core.Application).VER}]]</td> <td>[[${T(com.rebuild.core.Application).VER}]]</td>
</tr> </tr>
<tr> <tr>

View file

@ -16,7 +16,7 @@
<div class="tab-container"> <div class="tab-container">
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li class="nav-item" th:if="${advListHideFilters != 'true'}"><a class="nav-link" href="#asideFilters" data-toggle="tab">[[${bundle.L('常用查询')}]]</a></li> <li class="nav-item" th:if="${advListHideFilters != 'true'}"><a class="nav-link" href="#asideFilters" data-toggle="tab">[[${bundle.L('常用查询')}]]</a></li>
<li class="nav-item" th:if="${advListShowCategory}"><a class="nav-link J_load-category" href="#asideCategory" data-toggle="tab">[[${bundle.L('Category')}]]</a></li> <li class="nav-item" th:if="${advListShowCategory}"><a class="nav-link J_load-category" href="#asideCategory" data-toggle="tab">[[${bundle.L('分组')}]]</a></li>
<li class="nav-item" th:if="${advListHideCharts != 'true'}"><a class="nav-link J_load-charts" href="#asideWidgets" data-toggle="tab">[[${bundle.L('图表')}]]</a></li> <li class="nav-item" th:if="${advListHideCharts != 'true'}"><a class="nav-link J_load-charts" href="#asideWidgets" data-toggle="tab">[[${bundle.L('图表')}]]</a></li>
</ul> </ul>
<div class="tab-content rb-scroller"> <div class="tab-content rb-scroller">

View file

@ -16,7 +16,7 @@
<div class="tab-container"> <div class="tab-container">
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li class="nav-item" th:if="${advListHideFilters != 'true'}"><a class="nav-link" href="#asideFilters" data-toggle="tab">[[${bundle.L('常用查询')}]]</a></li> <li class="nav-item" th:if="${advListHideFilters != 'true'}"><a class="nav-link" href="#asideFilters" data-toggle="tab">[[${bundle.L('常用查询')}]]</a></li>
<li class="nav-item" th:if="${advListShowCategory}"><a class="nav-link J_load-category" href="#asideCategory" data-toggle="tab">[[${bundle.L('Category')}]]</a></li> <li class="nav-item" th:if="${advListShowCategory}"><a class="nav-link J_load-category" href="#asideCategory" data-toggle="tab">[[${bundle.L('分组')}]]</a></li>
<li class="nav-item" th:if="${advListHideCharts != 'true'}"><a class="nav-link J_load-charts" href="#asideWidgets" data-toggle="tab">[[${bundle.L('图表')}]]</a></li> <li class="nav-item" th:if="${advListHideCharts != 'true'}"><a class="nav-link J_load-charts" href="#asideWidgets" data-toggle="tab">[[${bundle.L('图表')}]]</a></li>
</ul> </ul>
<div class="tab-content rb-scroller"> <div class="tab-content rb-scroller">