Merge pull request #345 from getrebuild/h5app-beta

H5app beta
This commit is contained in:
devezhao 2021-06-08 23:03:38 +08:00 committed by GitHub
commit 48d68a2318
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 256 additions and 175 deletions

View file

@ -1,25 +1,23 @@
# include nginx-rebuild.conf
server {
server_name YOUR_DOMAIN;
listen 80;
# HTTPS
#listen 443 ssl http2;
#ssl_certificate ssl.crt;
#ssl_certificate_key ssl.key;
# PROXY
proxy_redirect http:// $scheme://;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location / {
proxy_pass http://localhost:18080;
etag on;
}
location /assets {
proxy_pass http://localhost:18080/assets;
expires 90d;
}
#location /h5-app {
# alias /data/rebuild/h5-app;
# etag on;
#}
}
server_name YOUR_DOMAIN;
listen 80;
# HTTPS
#listen 443 ssl http2;
#ssl_certificate /path/to/ssl.crt;
#ssl_certificate_key /path/to/ssl.key;
# PROXY
proxy_redirect http:// $scheme://;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
location / {
proxy_pass http://127.0.0.1:18080;
etag on;
}
location /h5app {
alias /path/to/public/h5app;
try_files $uri $uri/ /h5app/index.html;
etag on;
}
}

4
.gitignore vendored
View file

@ -42,3 +42,7 @@ node
.DS_Store
test.*
# Build
**/public/h5app/

2
@rbv

@ -1 +1 @@
Subproject commit fa267c9c6721819f19208a3afd0f6dff6b14f620
Subproject commit 28379ae7d7f3d76b203f889f877ec7b6deb690b7

View file

@ -452,7 +452,7 @@ public class FormsBuilder extends FormsManager {
.append(" where ")
.append(entity.getPrimaryField().getName())
.append(" = ?");
return Application.getQueryFactory().createQuery(sql.toString(), user).setParameter(1, id).record();
return Application.createQuery(sql.toString(), user).setParameter(1, id).record();
}
/**

View file

@ -15,6 +15,8 @@ import com.rebuild.core.UserContextHolder;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.metadata.MetadataHelper;
import com.rebuild.core.metadata.easymeta.EasyMetaFactory;
import com.rebuild.core.privileges.OperationDeniedException;
import com.rebuild.core.privileges.UserHelper;
import com.rebuild.core.privileges.UserService;
import com.rebuild.core.service.BaseService;
import com.rebuild.core.service.DataSpecificationNoRollbackException;
@ -252,6 +254,10 @@ public class ApprovalStepService extends BaseService {
final ID opUser = UserContextHolder.getUser();
final ApprovalState useState = isRevoke ? ApprovalState.REVOKED : ApprovalState.CANCELED;
if (isRevoke && !UserHelper.isAdmin(opUser)) {
throw new OperationDeniedException(Language.L("仅管理员可撤销审批"));
}
Record step = EntityHelper.forNew(EntityHelper.RobotApprovalStep, opUser);
step.setID("recordId", recordId);
step.setID("approvalId", approvalId == null ? APPROVAL_NOID : approvalId);

View file

@ -73,7 +73,7 @@ public abstract class BulkOperator extends HeavyTask<Integer> {
entity.getPrimaryField().getName(), entity.getName(), sqlWhere);
// NOTE 注意没有分页
Query query = Application.getQueryFactory().createQuery(sql, context.getOpUser());
Query query = Application.createQuery(sql, context.getOpUser());
Object[][] array = QueryHelper.readArray(query);
Set<ID> ids = new HashSet<>();
for (Object[] o : array) {

View file

@ -53,7 +53,7 @@ public class ProjectConfigService extends BaseConfigurationService implements Ad
@Override
public int delete(ID projectId) {
Object[] count = Application.createQuery(
Object[] count = Application.createQueryNoFilter(
"select count(taskId) from ProjectTask where projectId = ?")
.setParameter(1, projectId)
.unique();
@ -77,9 +77,12 @@ public class ProjectConfigService extends BaseConfigurationService implements Ad
// 使用模板
if (useTemplate == TEMPLATE_DEFAULT) {
ID id1 = createPlan(project.getPrimary(), Language.L("待处理"), 1000, ProjectPlanConfigService.FLOW_STATUS_START, null);
ID id2 = createPlan(project.getPrimary(), Language.L("进行中"), 2000, ProjectPlanConfigService.FLOW_STATUS_PROCESSING, null);
ID id3 = createPlan(project.getPrimary(), Language.L("已完成"), 3000, ProjectPlanConfigService.FLOW_STATUS_END, new ID[]{id1, id2});
ID id1 = createPlan(project.getPrimary(),
Language.L("待处理"), 1000, ProjectPlanConfigService.FLOW_STATUS_START, null);
ID id2 = createPlan(project.getPrimary(),
Language.L("进行中"), 2000, ProjectPlanConfigService.FLOW_STATUS_PROCESSING, null);
ID id3 = createPlan(project.getPrimary(),
Language.L("已完成"), 3000, ProjectPlanConfigService.FLOW_STATUS_END, new ID[]{id1, id2});
updateFlowNexts(id1, new ID[]{id2, id3});
updateFlowNexts(id2, new ID[]{id1, id3});
}

View file

@ -16,6 +16,7 @@ import com.rebuild.core.UserContextHolder;
import com.rebuild.core.configuration.ConfigBean;
import com.rebuild.core.metadata.EntityHelper;
import com.rebuild.core.privileges.OperationDeniedException;
import com.rebuild.core.service.DataSpecificationException;
import com.rebuild.core.service.notification.Message;
import com.rebuild.core.service.notification.MessageBuilder;
import com.rebuild.core.support.i18n.Language;
@ -50,9 +51,16 @@ public class ProjectTaskService extends BaseTaskService {
final ID user = UserContextHolder.getUser();
checkInMembers(user, record.getID("projectId"));
record.setLong("taskNumber", getNextTaskNumber(record.getID("projectId")));
applyFlowStatue(record);
record.setInt("seq", getNextSeqViaMidValue(record.getID("projectPlanId")));
ID projectId = record.getID("projectId");
ID projectPlanId = record.getID("projectPlanId");
ConfigBean p = ProjectManager.instance.getPlanOfProject(projectPlanId, projectId);
if (p.getInteger("flowStatus") == ProjectPlanConfigService.FLOW_STATUS_PROCESSING) {
throw new DataSpecificationException(Language.L("该任务面板不可新建任务"));
}
record.setLong("taskNumber", getNextTaskNumber(projectId));
applyFlowStatus(record);
record.setInt("seq", getNextSeqViaMidValue(projectPlanId));
record = super.create(record);
@ -68,17 +76,26 @@ public class ProjectTaskService extends BaseTaskService {
checkInMembers(user, record.getPrimary());
// 自动完成
int flowStatus = applyFlowStatue(record);
int newFlowStatus = applyFlowStatus(record);
if (flowStatus == ProjectPlanConfigService.FLOW_STATUS_END) {
if (newFlowStatus == ProjectPlanConfigService.FLOW_STATUS_END) {
record.setDate("endTime", CalendarUtils.now());
} else if (record.hasValue("status")) {
int status = record.getInt("status");
// 处理完成时间
if (status == 0) {
record.setNull("endTime");
record.setInt("seq", getSeqInStatus(record.getPrimary(), false));
} else {
// 检查工作流
Object[] flowStatus = Application.getQueryFactory()
.uniqueNoFilter(record.getPrimary(), "projectPlanId.flowStatus");
if ((int) flowStatus[0] == ProjectPlanConfigService.FLOW_STATUS_PROCESSING) {
throw new DataSpecificationException(Language.L("该任务面板不可完成任务"));
}
record.setDate("endTime", CalendarUtils.now());
record.setInt("seq", getSeqInStatus(record.getPrimary(), true));
}
@ -141,13 +158,13 @@ public class ProjectTaskService extends BaseTaskService {
*/
synchronized
private int getSeqInStatus(ID taskId, boolean desc) {
Object[] taskStatus = Application.createQuery(
Object[] taskStatus = Application.createQueryNoFilter(
"select status,projectPlanId from ProjectTask where taskId = ?")
.setParameter(1, taskId)
.unique();
if (taskStatus == null) return 1;
Object[] seq = Application.createQuery(
Object[] seq = Application.createQueryNoFilter(
"select " + (desc ? "max" : "min") + "(seq) from ProjectTask where status = ? and projectPlanId = ?")
.setParameter(1, taskStatus[0])
.setParameter(2, taskStatus[1])
@ -162,7 +179,7 @@ public class ProjectTaskService extends BaseTaskService {
*
* @param newOrUpdate
*/
private int applyFlowStatue(Record newOrUpdate) {
private int applyFlowStatus(Record newOrUpdate) {
if (newOrUpdate.hasValue("projectPlanId")) {
ConfigBean c = ProjectManager.instance.getPlanOfProject(
newOrUpdate.getID("projectPlanId"), newOrUpdate.getID("projectId"));
@ -178,11 +195,10 @@ public class ProjectTaskService extends BaseTaskService {
return -1;
}
/**
* @param taskId
*/
private void sendNotification(ID taskId) {
Object[] task = Application.getQueryFactory().uniqueNoFilter(taskId, "executor", "taskName");
if (task[0] == null) return;
String msg = Language.L("有一个新任务分派给你") + " \n> " + task[1];
Application.getNotifications().send(
MessageBuilder.createMessage((ID) task[0], msg, Message.TYPE_PROJECT, taskId));

View file

@ -40,6 +40,7 @@ public class QueryFactory {
/**
* @param ajql
* @return
* @see #createQueryNoFilter(String)
*/
public Query createQuery(String ajql) {
return createQuery(ajql, UserContextHolder.getUser());
@ -54,14 +55,6 @@ public class QueryFactory {
return createQuery(ajql, Application.getPrivilegesManager().createQueryFilter(user));
}
/**
* @param ajql
* @return
*/
public Query createQueryNoFilter(String ajql) {
return createQuery(ajql, RoleBaseQueryFilter.ALLOWED);
}
/**
* @param ajql
* @param filter
@ -76,11 +69,25 @@ public class QueryFactory {
}
/**
* @param sql
* 1.无权限实体查询无权限实体使用 #createQuery 除管理员外将查不到数据
* 2.有权限实体查询单不应用角色权限
*
* @param ajql
* @return
* @see #createQuery(String)
*/
public Query createQueryNoFilter(String ajql) {
return createQuery(ajql, RoleBaseQueryFilter.ALLOWED);
}
/**
* 原生 SQL 查询
*
* @param rawSql
* @return
*/
public NativeQuery createNativeQuery(String sql) {
return aPMFactory.createNativeQuery(sql)
public NativeQuery createNativeQuery(String rawSql) {
return aPMFactory.createNativeQuery(rawSql)
.setTimeout(QUERY_TIMEOUT)
.setSlowLoggerTime(SLOW_LOGGER_TIME);
}
@ -129,11 +136,6 @@ public class QueryFactory {
return createQueryNoFilter(sql).setParameter(1, recordId).unique();
}
/**
* @param recordId
* @param fields
* @return
*/
private String buildUniqueSql(ID recordId, String... fields) {
Assert.notNull(recordId, "[recordId] cannot be null");

View file

@ -117,7 +117,7 @@ public class FieldWriteback extends FieldAggregation {
} else {
String sql = String.format("select %s from %s where %s = ?",
targetEntity.getPrimaryField().getName(), targetFieldEntity[1], targetFieldEntity[0]);
Object[][] array = Application.getQueryFactory().createQueryNoFilter(sql)
Object[][] array = Application.createQueryNoFilter(sql)
.setParameter(1, operatingContext.getAnyRecord().getPrimary())
.array();

View file

@ -127,7 +127,7 @@ public class BatchOperatorQuery extends SetUser {
int pageNo = queryData.getIntValue("pageNo");
int pageSize = queryData.getIntValue("pageSize");
Object[][] array = Application.getQueryFactory().createQuery(sql, getUser())
Object[][] array = Application.createQuery(sql, getUser())
.setLimit(pageSize, pageNo * pageSize - pageSize)
.setTimeout(60)
.array();

View file

@ -66,11 +66,11 @@ public class DataListBuilderImpl implements DataListBuilder {
public JSON getJSONResult() {
int totalRows = 0;
if (queryParser.isNeedReload()) {
Object[] count = Application.getQueryFactory().createQuery(queryParser.toCountSql(), user).unique();
Object[] count = Application.createQuery(queryParser.toCountSql(), user).unique();
totalRows = ObjectUtils.toInt(count[0]);
}
Query query = Application.getQueryFactory().createQuery(queryParser.toSql(), user);
Query query = Application.createQuery(queryParser.toSql(), user);
int[] limits = queryParser.getSqlLimit();
Object[][] data = query.setLimit(limits[0], limits[1]).array();

View file

@ -43,6 +43,10 @@ public class AppUtils {
public static final String SK_LOCALE = WebUtils.KEY_PREFIX + ".LOCALE";
public static final String CK_LOCALE = "rb.locale";
// RbMob
public static final String HF_CLIENT = "X-Client";
public static final String HF_LOCALE = "X-ClientLocale";
/**
* @return
* @see BootApplication#getContextPath()
@ -106,6 +110,9 @@ public class AppUtils {
*/
public static String getReuqestLocale(HttpServletRequest request) {
String locale = (String) ServletUtils.getSessionAttribute(request, SK_LOCALE);
if (locale == null) {
locale = StringUtils.defaultIfBlank(request.getHeader(HF_LOCALE), null);
}
if (locale == null) {
locale = RebuildConfiguration.get(ConfigurationItem.DefaultLanguage);
}
@ -171,7 +178,7 @@ public class AppUtils {
* @return
*/
public static boolean isRbMobile(HttpServletRequest request) {
String UA = request.getHeader("X-Client");
String UA = request.getHeader(HF_CLIENT);
return UA != null && UA.startsWith("RB/Mobile-");
}

View file

@ -133,7 +133,7 @@ public class HttpUtils {
* @throws IOException
*/
public static File readBinary(String url) throws IOException {
File tmp = RebuildConfiguration.getFileOfTemp("download." + UUID.randomUUID().toString());
File tmp = RebuildConfiguration.getFileOfTemp("download." + UUID.randomUUID());
boolean success = readBinary(url, tmp, Collections.singletonMap(HttpHeaders.USER_AGENT, RB_UA));
return success && tmp.exists() ? tmp : null;
}

View file

@ -32,6 +32,7 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.thymeleaf.exceptions.TemplateInputException;
@ -92,9 +93,16 @@ public class RebuildWebConfigurer implements WebMvcConfigurer, ErrorViewResolver
.excludePathPatterns("/gw/api/**")
.excludePathPatterns("/language/**")
.excludePathPatterns("/assets/**")
.excludePathPatterns("/h5app/**")
.excludePathPatterns("/*.txt");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/h5app/**")
.addResourceLocations("classpath:/public/h5app/");
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
// URL 参数
@ -113,11 +121,6 @@ public class RebuildWebConfigurer implements WebMvcConfigurer, ErrorViewResolver
return createError(request, (Exception) request.getAttribute(ServletUtils.ERROR_EXCEPTION), status, model);
}
/**
* @param request
* @param ex
* @return
*/
private ModelAndView createError(HttpServletRequest request, Exception ex, HttpStatus status, Map<String, Object> model) {
// IGNORED
if (request.getRequestURI().contains("/assets/")) return null;
@ -164,12 +167,12 @@ public class RebuildWebConfigurer implements WebMvcConfigurer, ErrorViewResolver
}
/**
* 获取请求/引用地址
* 获取请求+引用地址
*
* @param request
* @return
*/
static String getRequestUrls(HttpServletRequest request) {
protected static String getRequestUrls(HttpServletRequest request) {
String reqUrl = request.getRequestURL().toString();
String refUrl = ServletUtils.getReferer(request);

View file

@ -154,7 +154,7 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
if (requestEntry.isHtmlRequest()) {
sendRedirect(response, "/user/login", requestUri);
} else {
ServletUtils.writeJson(response, RespBody.error(HttpStatus.FORBIDDEN.value()).toJSONString());
ServletUtils.writeJson(response, RespBody.error(HttpStatus.UNAUTHORIZED.value()).toJSONString());
}
return false;
@ -189,43 +189,45 @@ public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallSt
* @param request
* @param response
* @return
* @see AppUtils#getReuqestLocale(HttpServletRequest)
*/
private String detectLocale(HttpServletRequest request, HttpServletResponse response) {
String rbmobLocale = request.getHeader(AppUtils.HF_LOCALE);
if (rbmobLocale != null) return rbmobLocale;
// 0. Session
String locale = (String) ServletUtils.getSessionAttribute(request, AppUtils.SK_LOCALE);
String havingLocale = (String) ServletUtils.getSessionAttribute(request, AppUtils.SK_LOCALE);
String urlLocale = request.getParameter("locale");
if (StringUtils.isNotBlank(urlLocale) && !urlLocale.equals(locale)) {
if (StringUtils.isNotBlank(urlLocale) && !urlLocale.equals(havingLocale)) {
urlLocale = Application.getLanguage().available(urlLocale);
if (urlLocale != null) {
locale = urlLocale;
havingLocale = urlLocale;
ServletUtils.setSessionAttribute(request, AppUtils.SK_LOCALE, locale);
ServletUtils.addCookie(response, AppUtils.CK_LOCALE, locale,
ServletUtils.setSessionAttribute(request, AppUtils.SK_LOCALE, havingLocale);
ServletUtils.addCookie(response, AppUtils.CK_LOCALE, havingLocale,
CommonsCache.TS_DAY * 90, null, StringUtils.defaultIfBlank(AppUtils.getContextPath(), "/"));
if (Application.devMode()) {
Application.getLanguage().refresh();
}
if (Application.devMode()) Application.getLanguage().refresh();
}
}
if (locale != null) return locale;
if (havingLocale != null) return havingLocale;
// 1. Cookie
locale = ServletUtils.readCookie(request, AppUtils.CK_LOCALE);
if (locale == null) {
havingLocale = ServletUtils.readCookie(request, AppUtils.CK_LOCALE);
if (havingLocale == null) {
// 2. User-Local
locale = request.getLocale().toString();
havingLocale = request.getLocale().toString();
}
// 3. Default
if ((locale = Application.getLanguage().available(locale)) == null) {
locale = RebuildConfiguration.get(ConfigurationItem.DefaultLanguage);
if ((havingLocale = Application.getLanguage().available(havingLocale)) == null) {
havingLocale = RebuildConfiguration.get(ConfigurationItem.DefaultLanguage);
}
ServletUtils.setSessionAttribute(request, AppUtils.SK_LOCALE, locale);
return locale;
ServletUtils.setSessionAttribute(request, AppUtils.SK_LOCALE, havingLocale);
return havingLocale;
}
/**

View file

@ -11,18 +11,21 @@ import cn.devezhao.commons.CodecUtils;
import cn.devezhao.commons.ObjectUtils;
import cn.devezhao.persist4j.engine.ID;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.rebuild.api.RespBody;
import com.rebuild.core.Application;
import com.rebuild.core.configuration.ConfigBean;
import com.rebuild.core.configuration.ConfigurationException;
import com.rebuild.core.service.project.ProjectManager;
import com.rebuild.core.support.i18n.Language;
import com.rebuild.utils.AppUtils;
import com.rebuild.utils.JSONUtils;
import com.rebuild.web.BaseController;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
@ -37,7 +40,7 @@ import java.util.regex.Pattern;
* @author devezhao
* @since 2020/6/29
*/
@Controller
@RestController
@RequestMapping("/project/")
public class ProjectController extends BaseController {
@ -80,6 +83,31 @@ public class ProjectController extends BaseController {
return mv;
}
@GetMapping("{projectId}/details")
public RespBody getPlans(@PathVariable String projectId, HttpServletRequest request) throws IOException {
final ID user = getRequestUser(request);
JSONObject details;
try {
ConfigBean p = ProjectManager.instance.getProject(ID.valueOf(projectId), user);
details = JSONUtils.toJSONObject(
new String[] { "projectName", "isMember" },
new Object[] { p.getString("projectName"), p.get("members", Set.class).contains(user) });
} catch (ConfigurationException ex) {
return RespBody.error(ex.getLocalizedMessage(), 403);
}
ConfigBean[] plans = ProjectManager.instance.getPlansOfProject(ID.valueOf(projectId));
JSONArray array = new JSONArray();
for (ConfigBean cb : plans) {
array.add(cb.toJSON());
}
details.put("projectPlans", array);
return RespBody.ok(details);
}
/**
* @see com.rebuild.web.general.ListAndViewRedirection
*/

View file

@ -70,18 +70,18 @@ public class ProjectTaskController extends BaseController {
return null;
}
ConfigBean cfg = ProjectManager.instance.getProjectByTask(taskId2, user);
ConfigBean project = ProjectManager.instance.getProjectByTask(taskId2, user);
ModelAndView mv = createModelAndView("/project/task-view");
mv.getModel().put("id", taskId2.toLiteral());
mv.getModel().put("projectIcon", cfg.getString("iconName"));
mv.getModel().put("isMember", cfg.get("members", Set.class).contains(user));
mv.getModel().put("isManageable", ProjectHelper.isManageable(taskId2, user));
mv.getModel().put("projectIcon", project.getString("iconName"));
mv.getModel().put("isMember", project.get("members", Set.class).contains(user));
return mv;
}
@RequestMapping("tasks/list")
public JSON taskList(@IdParam(name = "plan") ID planId, HttpServletRequest request) {
final ID user = getRequestUser(request);
String queryWhere = "projectPlanId = ?";
// 关键词搜索
@ -125,7 +125,7 @@ public class ProjectTaskController extends BaseController {
JSONArray alist = new JSONArray();
for (Object[] o : tasks) {
alist.add(formatTask(o, true));
alist.add(formatTask(o, user));
}
return JSONUtils.toJSONObject(
@ -139,19 +139,19 @@ public class ProjectTaskController extends BaseController {
"select " + BASE_FIELDS + " from ProjectTask where taskId = ?")
.setParameter(1, taskId)
.unique();
return formatTask(task, true);
return formatTask(task, null);
}
@GetMapping("tasks/details")
public JSON taskDetails(@IdParam(name = "task") ID taskId) {
public JSON taskDetails(@IdParam(name = "task") ID taskId, HttpServletRequest request) {
final ID user = getRequestUser(request);
Object[] task = Application.createQueryNoFilter(
"select " + BASE_FIELDS + ",projectId,description,attachments,relatedRecord from ProjectTask where taskId = ?")
"select " + BASE_FIELDS + ",description,attachments,relatedRecord from ProjectTask where taskId = ?")
.setParameter(1, taskId)
.unique();
JSONObject details = formatTask(task, true);
JSONObject details = formatTask(task, user);
// 状态面板
details.put("projectId", task[11]);
details.put("description", task[12]);
String attachments = (String) task[13];
details.put("attachments", JSON.parseArray(attachments));
@ -168,24 +168,33 @@ public class ProjectTaskController extends BaseController {
}
private static final String BASE_FIELDS =
"projectId.projectCode,taskNumber,taskId,taskName,createdOn,deadline,executor,status,seq,priority,endTime";
"projectId,projectPlanId,taskNumber,taskId,taskName,createdOn,deadline,executor,status,seq,priority,endTime";
private JSONObject formatTask(Object[] o, boolean appendTags) {
String taskNumber = o[1].toString();
if (StringUtils.isNotBlank((String) o[0])) taskNumber = o[0] + "-" + taskNumber;
private JSONObject formatTask(Object[] o, ID user) {
final ConfigBean project = ProjectManager.instance.getProject((ID) o[0], user);
String createdOn = I18nUtils.formatDate((Date) o[4]);
String deadline = I18nUtils.formatDate((Date) o[5]);
String endTime = I18nUtils.formatDate((Date) o[10]);
String taskNumber = String.format("%s-%s", project.getString("projectCode"), o[2]);
String createdOn = I18nUtils.formatDate((Date) o[5]);
String deadline = I18nUtils.formatDate((Date) o[6]);
String endTime = I18nUtils.formatDate((Date) o[11]);
Object[] executor = o[6] == null ? null : new Object[]{o[6], UserHelper.getName((ID) o[6])};
Object[] executor = o[7] == null ? null : new Object[]{o[7], UserHelper.getName((ID) o[7])};
JSONObject data = JSONUtils.toJSONObject(
new String[] { "id", "taskNumber", "taskName", "createdOn", "deadline", "executor", "status", "seq", "priority", "endTime" },
new Object[] { o[2], taskNumber, o[3], createdOn, deadline, executor, o[7], o[8], o[9], endTime });
new String[] { "id", "taskNumber", "taskName", "createdOn", "deadline", "executor", "status", "seq", "priority", "endTime", "projectId" },
new Object[] { o[3], taskNumber, o[4], createdOn, deadline, executor, o[8], o[9], o[10], endTime, o[0] });
// 标签
data.put("tags", TaskTagController.getTaskTags((ID) o[3]));
if (appendTags) {
data.put("tags", TaskTagController.getTaskTags((ID) o[2]));
if (user != null) {
// 项目信息
ConfigBean plan = ProjectManager.instance.getPlanOfProject((ID) o[1], (ID) o[0]);
data.put("planName", String.format("%s (%s)",
project.getString("projectName"), plan.getString("planName")));
data.put("planFlow", plan.getInteger("flowStatus"));
// 权限
data.put("projectMember", project.get("members", Set.class).contains(user));
data.put("isManageable", ProjectHelper.isManageable((ID) o[3], user));
}
return data;
@ -199,8 +208,7 @@ public class ProjectTaskController extends BaseController {
return sort;
}
// -- for EntityView
// for View of Entity
@GetMapping("alist")
public RespBody getProjectAndPlans(HttpServletRequest request) {
final ID user = getRequestUser(request);
@ -230,6 +238,8 @@ public class ProjectTaskController extends BaseController {
@IdParam(name = "task", required = false) ID taskId,
HttpServletRequest request) {
Assert.isTrue(relatedId != null || taskId != null, Language.L("无效请求参数"));
final ID user = getRequestUser(request);
String queryWhere = String.format("relatedRecord = '%s'", relatedId);
// 关键词搜索
@ -248,30 +258,16 @@ public class ProjectTaskController extends BaseController {
queryWhere = String.format("taskId = '%s'", taskId);
}
String querySql = "select " + BASE_FIELDS + ",projectPlanId,projectId from ProjectTask where " + queryWhere;
String querySql = "select " + BASE_FIELDS + " from ProjectTask where " + queryWhere;
Object[][] tasks = Application.createQueryNoFilter(querySql)
.setLimit(pageSize, pageNo * pageSize - pageSize)
.array();
JSONArray alist = new JSONArray();
JSONArray array = new JSONArray();
for (Object[] o : tasks) {
JSONObject formatted = formatTask(o, false);
formatted.put("taskNumber", String.format("%s-%d", o[0], o[1] ));
Object executor = o[6] == null ? null : new Object[] { o[6], UserHelper.getName((ID) o[6]) };
formatted.put("executor", executor);
ID projectPlanId = (ID) o[11];
ID projectId = (ID) o[12];
ConfigBean project = ProjectManager.instance.getProject(projectId, null);
ConfigBean plan = ProjectManager.instance.getPlanOfProject(projectPlanId, projectId);
formatted.put("planName", String.format("%s (%s)",
project.getString("projectName"), plan.getString("planName")));
formatted.put("planFlow", plan.getInteger("flowStatus"));
alist.add(formatted);
array.add(formatTask(o, user));
}
return alist;
return array;
}
}

View file

@ -106,9 +106,9 @@ public class TaskTagController extends BaseController {
*/
static JSONArray getTaskTags(ID taskId) {
Object[][] tags = Application.createQueryNoFilter(
"select tagId.tagName,tagId.color,relationId from ProjectTaskTagRelation where taskId = ? order by createdOn")
"select tagId.tagName,tagId.color,relationId,tagId from ProjectTaskTagRelation where taskId = ? order by createdOn")
.setParameter(1, taskId)
.array();
return JSONUtils.toJSONObjectArray(new String[] { "name", "color" , "rid" }, tags);
return JSONUtils.toJSONObjectArray(new String[] { "name", "color" , "rid", "id" }, tags);
}
}

View file

@ -24,7 +24,7 @@ spring:
multipart.enabled: false
web:
resources:
static-locations: classpath:/web/assets
static-locations: classpath:/web/assets/
mvc:
static-path-pattern: /assets/**
thymeleaf:

View file

@ -1157,7 +1157,7 @@
"注册信息已提交,审批结果将通过邮件通知你":"注册信息已提交,审批结果将通过邮件通知你",
"注册成功":"注册成功",
"注册验证码":"注册验证码",
"注销登录":"注销登录",
"退出":"退出",
"流程结束":"流程结束",
"流程设计":"流程设计",
"测试发送":"测试发送",

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="minimal-ui, width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no" />
<title>REBUILD</title>
</head>
<body>
<i>Free version does not support H5 App feature. <a href="https://getrebuild.com/docs/rbv-features" target="_blank">(View details)</a></i>
</body>
</html>

View file

@ -29,7 +29,7 @@
<div class="user-id" th:data-user="${user.getId()}">[[${user.getEmail() ?: bundle.L('邮箱未设置')}]]</div>
</div>
<a class="dropdown-item" th:href="@{/settings/user}"><i class="icon zmdi zmdi-account-box"></i>[[${bundle.L('个人设置')}]]</a>
<a class="dropdown-item" th:href="@{/user/logout}"><i class="icon zmdi zmdi-power"></i>[[${bundle.L('注销登录')}]]</a>
<a class="dropdown-item" th:href="@{/user/logout}"><i class="icon zmdi zmdi-power"></i>[[${bundle.L('退出')}]]</a>
<div class="use-theme">
<div>[[${bundle.L('选择主题')}]] <sup class="rbv"></sup></div>
<ul class="list-inline list-unstyled mt-1">

View file

@ -11364,20 +11364,14 @@ canvas {
color: #404040;
border-radius: 99px;
background: #f7f7f7;
background: rgba(255, 255, 255, 0.8);
width: 320px;
border-width: 0;
padding: 4px 15px;
text-align: left;
transition: background 0.2s;
}
.rb-top-header .search-input-gs:focus {
background: rgba(255, 255, 255, 0.98);
}
.rb-top-header .search-input-gs::placeholder {
color: rgba(0, 0, 0, 0.4);
color: #aaa;
text-align: center;
}

View file

@ -3781,11 +3781,11 @@ a.select-lang:hover {
}
.use-theme ul li a.theme-dark {
background-color: #112034 !important;
background-color: #2d333b !important;
}
.use-theme ul li a.theme-blue {
background-color: #4285f4 !important;
background-color: #4873c0 !important;
}
.use-theme ul li a.theme-green {

View file

@ -18,7 +18,7 @@ let __TaskContent
let __TaskComment
$(document).ready(() => {
renderRbcomp(<TaskForm id={wpc.taskId} editable={wpc.isMember} isManageable={wpc.isManageable} />, 'task-contents', function () {
renderRbcomp(<TaskForm id={wpc.taskId} editable={wpc.isMember} />, 'task-contents', function () {
__TaskContent = this
})
if (wpc.isMember) {
@ -45,7 +45,7 @@ class TaskForm extends React.Component {
<div className="col-10">
<ValueTaskName taskName={this.state.taskName} $$$parent={this} />
</div>
{this.props.editable && this.props.isManageable && (
{this.props.editable && this.state.isManageable && (
<div className="col-2 text-right">
<button className="btn btn-secondary" style={{ minWidth: 80, marginTop: 2 }} data-toggle="dropdown">
{$L('操作')} <i className="icon zmdi zmdi-more-vert"></i>
@ -63,7 +63,7 @@ class TaskForm extends React.Component {
<i className="icon zmdi zmdi-square-o" /> {$L('状态')}
</label>
<div className="col-12 col-sm-9">
<ValueStatus status={this.state.status} $$$parent={this} />
<ValueStatus status={this.state.status} readonly={this.state.planFlow === 2} $$$parent={this} />
</div>
</div>
<div className="form-group row">
@ -256,7 +256,7 @@ class ValueStatus extends ValueComp {
<div className="form-control-plaintext">
<span className="status-checkbox">
<label className="custom-control custom-checkbox custom-control-inline" onClick={(e) => $stopEvent(e)}>
<input className="custom-control-input" type="checkbox" ref={(c) => (this._status = c)} onChange={(e) => this._handleChangeStatus(e)} />
<input className="custom-control-input" type="checkbox" disabled={this.props.readonly} ref={(c) => (this._status = c)} onChange={(e) => this._handleChangeStatus(e)} />
<span className="custom-control-label">{this.state.status > 0 ? $L('已完成') : $L('未完成')}</span>
</label>
</span>
@ -644,7 +644,7 @@ class ValueTags extends ValueComp {
</div>
</span>
) : (
tags.length === 0 && <div className="form-control-plaintext text-muted">{$L('无')}</div>
tags.length === 0 && <span className="text-muted" style={{ display: 'inline-block', paddingTop: 5 }}>{$L('无')}</span>
)}
</div>
)

View file

@ -234,9 +234,11 @@ class ApprovalUsersForm extends RbFormHandler {
)}
</div>
) : (
<div className="form-group">
<RbAlertBox message={$L('当前审批流程无可用审批人')} />
</div>
!this.state.isLastStep && (
<div className="form-group">
<RbAlertBox message={$L('当前审批流程无可用审批人')} />
</div>
)
)}
{ccHas && (
<div className="form-group">
@ -505,7 +507,7 @@ class ApprovalStepViewer extends React.Component {
{!this.state.steps && <RbSpinner fully={true} />}
<ul className="timeline approved-steps">
{(this.state.steps || []).map((item, idx) => {
return idx === 0 ? this.renderSubmitter(item, idx) : this.renderApprovers(item, idx, stateLast)
return idx === 0 ? this.renderSubmitter(item) : this.renderApprover(item, stateLast)
})}
{stateLast >= 10 && (
<li className="timeline-item last">
@ -520,10 +522,10 @@ class ApprovalStepViewer extends React.Component {
)
}
renderSubmitter(s, idx) {
renderSubmitter(s) {
return (
<li className="timeline-item state0" key={`step-${idx}`}>
{this.__formatTime(s.createdOn)}
<li className="timeline-item state0" key={`step-${$random()}`}>
{this._formatTime(s.createdOn)}
<div className="timeline-content">
<div className="timeline-avatar">
<img src={`${rb.baseUrl}/account/user-avatar/${s.submitter}`} alt="Avatar" />
@ -545,8 +547,7 @@ class ApprovalStepViewer extends React.Component {
)
}
renderApprovers(s, idx, lastState) {
const kp = 'step-' + idx + '-'
renderApprover(s, stateLast) {
const sss = []
let nodeState = 0
if (s[0].signMode === 'OR') {
@ -559,11 +560,11 @@ class ApprovalStepViewer extends React.Component {
const approverName = item.approver === rb.currentUser ? $L('你') : item.approverName
let aMsg = $L('等待 %s 审批', approverName)
if (item.state >= 10) aMsg = $L(' %s %s', approverName, STATE_NAMES[item.state])
if ((nodeState >= 10 || lastState >= 10) && item.state < 10) aMsg = `${approverName} ${$L('未进行审批')}`
if ((nodeState >= 10 || stateLast >= 10) && item.state < 10) aMsg = `${approverName} ${$L('未进行审批')}`
sss.push(
<li className={'timeline-item state' + item.state} key={kp + sss.length}>
{this.__formatTime(item.approvedTime || item.createdOn)}
<li className={'timeline-item state' + item.state} key={`step-${$random()}`}>
{this._formatTime(item.approvedTime || item.createdOn)}
<div className="timeline-content">
<div className="timeline-avatar">
<img src={`${rb.baseUrl}/account/user-avatar/${item.approver}`} alt="Avatar" />
@ -585,13 +586,13 @@ class ApprovalStepViewer extends React.Component {
const sm = s[0].signMode
const clazz = sm === 'OR' || sm === 'AND' ? 'joint' : 'no-joint'
return (
<div key={kp} className={clazz} _title={sm === 'OR' ? $L('或签') : sm === 'AND' ? $L('会签') : null}>
<div className={clazz} _title={sm === 'OR' ? $L('或签') : sm === 'AND' ? $L('会签') : null}>
{sss}
</div>
)
}
__formatTime(time) {
_formatTime(time) {
time = time.split(' ')
return (
<div className="timeline-date">
@ -608,7 +609,9 @@ class ApprovalStepViewer extends React.Component {
RbHighbar.create($L('未查询到流程详情'))
this.hide()
this.__noStepFound = true
} else this.setState({ steps: res.data })
} else {
this.setState({ steps: res.data })
}
})
}
@ -617,7 +620,9 @@ class ApprovalStepViewer extends React.Component {
if (this.__noStepFound === true) {
RbHighbar.create($L('未查询到流程详情'))
this.hide()
} else $(this._dlg).modal({ show: true, keyboard: true })
} else {
$(this._dlg).modal({ show: true, keyboard: true })
}
}
}

View file

@ -123,7 +123,7 @@ class LightTaskList extends RelatedList {
<div className="row header-title">
<div className="col-7 title">
<label className="custom-control custom-control-sm custom-checkbox custom-control-inline">
<input className="custom-control-input" type="checkbox" defaultChecked={item.status > 0} disabled={item.planFlow === 2} onClick={() => this._toggleStatus(item)} />
<input className="custom-control-input" type="checkbox" defaultChecked={item.status > 0} disabled={item.planFlow === 2 || !item.projectMember} onClick={() => this._toggleStatus(item)} />
<span className="custom-control-label"></span>
</label>
<a href={`${rb.baseUrl}/app/list-and-view?id=${item.id}`} target="_blank" title={$L('打开')}>

View file

@ -10,9 +10,10 @@
<link rel="stylesheet" type="text/css" th:href="@{/assets/css/rb-base.css}" />
<title>REBUILD</title>
<style type="text/css">
.zmdi.err404,
.zmdi.err403,
.zmdi.err400,
.zmdi.err401,
.zmdi.err403,
.zmdi.err404,
.zmdi.err600 {
color: #4285f4 !important;
}

View file

@ -26,7 +26,6 @@
type: 'TaskView',
taskId: '[[${id}]]',
isMember: '[[${isMember}]]' === 'true',
isManageable: '[[${isManageable}]]' === 'true',
}
</script>
<script th:src="@{/assets/js/project/task-view.js}" type="text/babel"></script>

View file

@ -25,7 +25,7 @@ import org.junit.jupiter.api.Test;
public class QueryFactoryTest extends TestSupport {
@Test
public void testBaseQuery() {
void testBaseQuery() {
String sql = "select loginName from User";
Filter filter = Application.getPrivilegesManager().createQueryFilter(SIMPLE_USER);
Object[][] array = Application.getQueryFactory().createQuery(sql, filter).array();
@ -33,7 +33,7 @@ public class QueryFactoryTest extends TestSupport {
}
@Test
public void testQueryAllDT() {
void testQueryAllDT() {
Entity allDT = MetadataHelper.getEntity(TestAllFields);
StringBuilder sql = new StringBuilder("select ");
for (Field f : allDT.getFields()) {
@ -51,8 +51,14 @@ public class QueryFactoryTest extends TestSupport {
}
@Test
public void testNoUser() {
void testNoUser() {
Assertions.assertThrows(AccessDeniedException.class,
() -> Application.getQueryFactory().createQuery("select loginName from User").array());
}
@Test
void createNativeQuery() {
Object[] x = Application.getQueryFactory().createNativeQuery("select current_date").unique();
System.out.println(x[0]);
}
}