Prepare chart business block generation

This commit is contained in:
王鹏
2026-06-20 00:01:34 +08:00
parent 77657b2cc9
commit a8b1769e32
2 changed files with 492 additions and 17 deletions

View File

@@ -6,6 +6,10 @@ import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.domain.GenProject;
@@ -18,8 +22,11 @@ import com.ruoyi.generator.domain.front.dto.block.BusinessBlockConfigField;
import com.ruoyi.generator.domain.front.dto.block.BusinessBlockDefinition;
import com.ruoyi.generator.domain.front.dto.block.BusinessBlockInstance;
import com.ruoyi.generator.domain.front.dto.block.BusinessBlockTemplateFile;
import com.ruoyi.generator.domain.front.dto.block.ChartQueryRenderModel;
import com.ruoyi.generator.domain.front.dto.block.PageBusinessBlockLayout;
import com.ruoyi.generator.service.front.BusinessBlockRegistryService;
import com.ruoyi.generator.util.BusinessActionRenderSupport;
import com.ruoyi.generator.util.ChartBlockRenderSupport;
import com.ruoyi.generator.util.VelocityInitializer;
import com.ruoyi.generator.util.VelocityUtils;
import org.apache.velocity.Template;
@@ -35,10 +42,15 @@ public class BusinessBlockGenerationService
private static final String FRONTEND_PAGE_TEMPLATE_RESOURCE = "qing/index.vue.vm";
private static final String BUSINESS_BLOCK_CATEGORY_PREFIX = "business-block:";
private static final String PAGE_DESIGN_CATEGORY_PREFIX = "page-design:";
private static final Pattern STATUS_VALUE_PATTERN = Pattern.compile(
"([0-9]+)\\s*[=:]?\\s*([^A-Za-z0-9_\\s,;()]+)");
@Autowired
private BusinessBlockRegistryService businessBlockRegistryService;
@Autowired
private ChartBlockRenderSupport chartBlockRenderSupport;
public List<GeneratedBusinessBlockFile> listFiles(GenProject project, String templateType)
{
if (project == null || StringUtils.isEmpty(project.getPageDesigns()))
@@ -46,35 +58,48 @@ public class BusinessBlockGenerationService
return Collections.emptyList();
}
List<GeneratedBusinessBlockFile> files = new ArrayList<GeneratedBusinessBlockFile>();
Set<String> emittedSharedOutputs = new HashSet<String>();
for (FrontProjectPageDesign pageDesign : project.getPageDesigns())
{
if (pageDesign == null || "admin".equals(pageDesign.getPageScope()))
if (pageDesign == null)
{
continue;
}
boolean adminScope = "admin".equals(pageDesign.getPageScope());
if ("frontend".equals(templateType) && adminScope)
{
continue;
}
if ("admin_frontend".equals(templateType) && !adminScope)
{
continue;
}
int index = 1;
if (isFrontendBusinessBlockPage(pageDesign))
if (isBusinessBlockPage(pageDesign))
{
PageBusinessBlockLayout layout = registry().parseLayout(pageDesign.getLayoutJson());
if (BusinessBlockRegistryService.BUSINESS_BLOCK_CANVAS.equals(layout.getCanvas()))
{
if ("frontend".equals(templateType))
if ("frontend".equals(templateType) || "admin_frontend".equals(templateType))
{
files.add(pageFile(pageDesign));
files.add(pageFile(pageDesign, templateType));
}
index = appendBlockFiles(files, project, pageDesign, safeBlocks(layout), templateType, index);
index = appendBlockFiles(files, emittedSharedOutputs, project, pageDesign,
safeBlocks(layout), templateType, index);
}
}
else if ("frontend".equals(templateType) && isFrontendDesignedPage(pageDesign))
{
files.add(frontendPageFile(pageDesign));
}
appendBlockFiles(files, project, pageDesign, embeddedBlocks(pageDesign), templateType, index);
appendBlockFiles(files, emittedSharedOutputs, project, pageDesign,
embeddedBlocks(pageDesign), templateType, index);
}
return files;
}
private int appendBlockFiles(List<GeneratedBusinessBlockFile> files, GenProject project,
private int appendBlockFiles(List<GeneratedBusinessBlockFile> files, Set<String> emittedSharedOutputs,
GenProject project,
FrontProjectPageDesign pageDesign, List<BusinessBlockInstance> instances, String templateType, int startIndex)
{
int index = startIndex;
@@ -89,10 +114,13 @@ public class BusinessBlockGenerationService
{
continue;
}
if ("admin_frontend".equals(templateType) && isChartDefinition(definition))
{
addSharedChartRuntime(files, emittedSharedOutputs);
}
for (BusinessBlockTemplateFile templateFile : definition.getTemplates())
{
String fileTemplateType = templateType(templateFile);
if (!StringUtils.equals(fileTemplateType, templateType))
if (!templateMatches(templateType, templateFile))
{
continue;
}
@@ -103,6 +131,22 @@ public class BusinessBlockGenerationService
return index;
}
private void addSharedChartRuntime(List<GeneratedBusinessBlockFile> files, Set<String> emittedSharedOutputs)
{
String outputPath = "src/utils/chartRuntime.js";
if (!emittedSharedOutputs.add(outputPath))
{
return;
}
GeneratedBusinessBlockFile file = new GeneratedBusinessBlockFile();
file.setCategory("business-block:admin_frontend:shared:chartRuntime");
file.setTemplateType("admin_frontend");
file.setOutputPath(outputPath);
file.setTemplateResource("business-blocks/chart/templates/frontend/chartRuntime.js.vm");
file.setTemplateKey("chartRuntime");
files.add(file);
}
public GeneratedBusinessBlockFile findFile(GenProject project, String templateType, String keyword)
{
if (StringUtils.isEmpty(keyword))
@@ -194,10 +238,14 @@ public class BusinessBlockGenerationService
{
List<BusinessActionDesign> candidates = project == null || StringUtils.isEmpty(project.getBusinessActions())
? Collections.<BusinessActionDesign>emptyList() : project.getBusinessActions();
FrontProjectPageDesign currentPageDesign = context.get("currentPageDesign") instanceof FrontProjectPageDesign
? (FrontProjectPageDesign) context.get("currentPageDesign") : null;
List<BusinessActionDesign> toolbarBusinessActions = filterPageDesignBusinessActions(
candidates, context.get("pageDesignToolbarActionCodes"));
candidates, context.get("pageDesignToolbarActionCodes"), currentPageDesign, "primary",
project == null ? null : project.getTables());
List<BusinessActionDesign> rowBusinessActions = filterPageDesignBusinessActions(
candidates, context.get("pageDesignRowActionCodes"));
candidates, context.get("pageDesignRowActionCodes"), currentPageDesign, "text",
project == null ? null : project.getTables());
List<BusinessActionDesign> allActions = mergeBusinessActions(toolbarBusinessActions, rowBusinessActions);
context.put("pageDesignToolbarBusinessActions", toolbarBusinessActions);
context.put("hasPageDesignToolbarBusinessActions", StringUtils.isNotEmpty(toolbarBusinessActions));
@@ -209,7 +257,8 @@ public class BusinessBlockGenerationService
@SuppressWarnings("unchecked")
private List<BusinessActionDesign> filterPageDesignBusinessActions(List<BusinessActionDesign> candidates,
Object actionCodesObject)
Object actionCodesObject, FrontProjectPageDesign pageDesign, String fallbackButtonType,
List<GenTable> projectTables)
{
if (StringUtils.isEmpty(candidates) || !(actionCodesObject instanceof List))
{
@@ -229,20 +278,82 @@ public class BusinessBlockGenerationService
return Collections.emptyList();
}
List<BusinessActionDesign> result = new ArrayList<BusinessActionDesign>();
Set<String> addedCodes = new HashSet<String>();
for (String requestedCode : requestedCodes)
{
for (BusinessActionDesign candidate : candidates)
{
if (candidate != null && StringUtils.equals(requestedCode, candidate.getCode())
&& !result.contains(candidate))
&& addedCodes.add(candidate.getCode()))
{
result.add(candidate);
result.add(applyBusinessButtonConfig(candidate, pageDesign, fallbackButtonType, projectTables));
}
}
}
return result;
}
private BusinessActionDesign applyBusinessButtonConfig(BusinessActionDesign source, FrontProjectPageDesign pageDesign,
String fallbackButtonType, List<GenTable> projectTables)
{
BusinessActionDesign action = copyBusinessAction(source);
Map<String, Object> config = businessButtonConfig(pageDesign, action.getCode());
action.setButtonLabel(stringConfig(config, "label", action.getName()));
action.setButtonType(stringConfig(config, "type", fallbackButtonType));
action.setButtonIcon(stringConfig(config, "icon", "el-icon-position"));
action.setConfirmMessage(stringConfig(config, "confirmMessage", ""));
action.setSuccessMessage(stringConfig(config, "successMessage",
StringUtils.defaultIfEmpty(action.getName(), action.getCode()) + "成功"));
action.setRefreshTarget(stringConfig(config, "refreshTarget", "list"));
BusinessActionRenderSupport.enrich(action, projectTables);
return action;
}
private BusinessActionDesign copyBusinessAction(BusinessActionDesign source)
{
BusinessActionDesign copy = new BusinessActionDesign();
copy.setCode(source.getCode());
copy.setName(source.getName());
copy.setOwnerTable(source.getOwnerTable());
copy.setMethod(source.getMethod());
copy.setPath(source.getPath());
copy.setTransaction(source.getTransaction());
copy.setRequestFields(source.getRequestFields());
copy.setRules(source.getRules());
copy.setRuleChecks(source.getRuleChecks());
copy.setEffects(source.getEffects());
copy.setUiBindings(source.getUiBindings());
copy.setCurrentUserFields(source.getCurrentUserFields());
copy.setInputFields(source.getInputFields());
return copy;
}
@SuppressWarnings("unchecked")
private Map<String, Object> businessButtonConfig(FrontProjectPageDesign pageDesign, String actionCode)
{
if (pageDesign == null || pageDesign.getLayout() == null || StringUtils.isEmpty(actionCode))
{
return Collections.emptyMap();
}
Object configs = pageDesign.getLayout().get("businessButtons");
if (!(configs instanceof Map))
{
return Collections.emptyMap();
}
Object config = ((Map<String, Object>) configs).get(actionCode);
return config instanceof Map ? (Map<String, Object>) config : Collections.<String, Object>emptyMap();
}
private String stringConfig(Map<String, Object> config, String key, String fallback)
{
if (config == null)
{
return fallback;
}
Object value = config.get(key);
return value == null || StringUtils.isEmpty(String.valueOf(value)) ? fallback : String.valueOf(value);
}
private List<BusinessActionDesign> mergeBusinessActions(List<BusinessActionDesign> first,
List<BusinessActionDesign> second)
{
@@ -265,11 +376,17 @@ public class BusinessBlockGenerationService
}
private GeneratedBusinessBlockFile pageFile(FrontProjectPageDesign pageDesign)
{
return pageFile(pageDesign, "admin".equals(pageDesign == null ? null : pageDesign.getPageScope())
? "admin_frontend" : "frontend");
}
private GeneratedBusinessBlockFile pageFile(FrontProjectPageDesign pageDesign, String templateType)
{
GeneratedBusinessBlockFile file = new GeneratedBusinessBlockFile();
String pageCode = safePageCode(pageDesign);
file.setCategory(BUSINESS_BLOCK_CATEGORY_PREFIX + "frontend:" + pageCode + ":page");
file.setTemplateType("frontend");
file.setCategory(BUSINESS_BLOCK_CATEGORY_PREFIX + templateType + ":" + pageCode + ":page");
file.setTemplateType(templateType);
file.setOutputPath("src/views/pages/" + pageCode + "/index.vue");
file.setTemplateResource(PAGE_TEMPLATE_RESOURCE);
file.setPageCode(pageCode);
@@ -307,7 +424,7 @@ public class BusinessBlockGenerationService
file.setTemplateType(fileTemplateType);
file.setOutputPath(resolveOutputPath(project, pageCode, blockCode, instanceCode, instanceClassName,
templateFile.getOutputPath()));
file.setTemplateResource("business-blocks/" + blockCode + "/" + normalizePath(templateFile.getTemplate()));
file.setTemplateResource(templateResource(blockCode, templateFile.getTemplate()));
file.setPageCode(pageCode);
file.setBlockCode(blockCode);
file.setInstanceCode(instanceCode);
@@ -348,6 +465,16 @@ public class BusinessBlockGenerationService
context.put("instanceClassName", StringUtils.uncapitalize(file.getInstanceClassName()));
context.put("apiBasePath", "/business-blocks/" + file.getInstanceCode());
context.put("apiModulePath", "@/api/business-blocks/" + file.getInstanceCode());
if (isChartDefinition(file.getDefinition()))
{
ChartQueryRenderModel chartQuery = chartRenderSupport().build(
requireTable(project, instance.getDataset().getTable()), file.getDefinition(), instance);
context.put("chartQuery", chartQuery);
context.put("chartType", file.getDefinition().getChartType());
context.put("chartDataset", instance.getDataset());
context.put("chartDisplay", instance.getDisplay());
context.put("apiBasePath", "/admin/business-blocks/" + file.getInstanceCode());
}
context.put("cartPkColumn", pkColumnName(project, renderConfig.get("cartTable")));
context.put("noticePkColumn", pkColumnName(project, renderConfig.get("noticeTable")));
context.put("carouselPkColumn", pkColumnName(project, renderConfig.get("carouselTable")));
@@ -359,6 +486,8 @@ public class BusinessBlockGenerationService
context.put("hasProductImage", StringUtils.isNotEmpty(renderConfig.get("productImage")));
context.put("hasStock", StringUtils.isNotEmpty(renderConfig.get("stock")));
context.put("hasNoticeStatus", StringUtils.isNotEmpty(renderConfig.get("status")));
context.put("noticeStatusActiveValue", activeStatusValue(project, renderConfig.get("noticeTable"),
renderConfig.get("status"), "0"));
context.put("hasNoticePublishTime", StringUtils.isNotEmpty(renderConfig.get("publishTime")));
context.put("hasCarouselLink", StringUtils.isNotEmpty(renderConfig.get("link")));
context.put("hasCarouselSort", StringUtils.isNotEmpty(renderConfig.get("sort")));
@@ -464,6 +593,7 @@ public class BusinessBlockGenerationService
block.put("instanceCode", blockFile.getInstanceCode());
block.put("blockCode", blockFile.getBlockCode());
block.put("name", StringUtils.defaultIfEmpty(instance.getName(), definition.getName()));
block.put("span", instance.getSpan() == null ? Integer.valueOf(12) : instance.getSpan());
blocks.add(block);
index++;
}
@@ -523,12 +653,46 @@ public class BusinessBlockGenerationService
return "backend";
}
private boolean templateMatches(String requestedType, BusinessBlockTemplateFile templateFile)
{
String fileType = templateType(templateFile);
if ("admin_frontend".equals(requestedType))
{
return "frontend".equals(fileType);
}
return StringUtils.equals(requestedType, fileType);
}
private String templateResource(String blockCode, String template)
{
String normalized = normalizePath(template);
if (normalized.startsWith("/"))
{
return normalized.substring(1);
}
return "business-blocks/" + blockCode + "/" + normalized;
}
private boolean isChartDefinition(BusinessBlockDefinition definition)
{
return definition != null && "chart".equals(definition.getKind());
}
private boolean isFrontendBusinessBlockPage(FrontProjectPageDesign pageDesign)
{
if (pageDesign == null || "admin".equals(pageDesign.getPageScope()))
{
return false;
}
return isBusinessBlockPage(pageDesign);
}
private boolean isBusinessBlockPage(FrontProjectPageDesign pageDesign)
{
if (pageDesign == null)
{
return false;
}
if ("business_block".equals(pageDesign.getPageType()))
{
return true;
@@ -662,6 +826,59 @@ public class BusinessBlockGenerationService
return fieldName;
}
private String activeStatusValue(GenProject project, String tableName, String statusColumnName, String defaultValue)
{
GenTableColumn column = findColumn(project, tableName, statusColumnName);
if (column == null || StringUtils.isEmpty(column.getColumnComment()))
{
return defaultValue;
}
Matcher matcher = STATUS_VALUE_PATTERN.matcher(column.getColumnComment());
while (matcher.find())
{
String value = matcher.group(1);
String label = matcher.group(2);
if (isActiveStatusLabel(label) && !isInactiveStatusLabel(label))
{
return value;
}
}
return defaultValue;
}
private boolean isActiveStatusLabel(String label)
{
return StringUtils.containsAny(label, "发布", "启用", "上架", "正常", "可用", "有效", "显示");
}
private boolean isInactiveStatusLabel(String label)
{
return StringUtils.containsAny(label, "未发布", "草稿", "下架", "关闭", "禁用", "停用", "暂停", "删除", "失效",
"不显示");
}
private GenTableColumn findColumn(GenProject project, String tableName, String columnName)
{
GenTable table = findTable(project, tableName);
if (table == null || StringUtils.isEmpty(table.getColumns()) || StringUtils.isEmpty(columnName))
{
return null;
}
for (GenTableColumn column : table.getColumns())
{
if (column == null)
{
continue;
}
if (StringUtils.equals(columnName, column.getColumnName())
|| StringUtils.equals(columnName, column.getJavaField()))
{
return column;
}
}
return null;
}
private GenTable findTable(GenProject project, String tableName)
{
if (project == null || StringUtils.isEmpty(project.getTables()) || StringUtils.isEmpty(tableName))
@@ -678,6 +895,16 @@ public class BusinessBlockGenerationService
return null;
}
private GenTable requireTable(GenProject project, String tableName)
{
GenTable table = findTable(project, tableName);
if (table == null)
{
throw new ServiceException("图表数据表不存在:" + StringUtils.defaultString(tableName));
}
return table;
}
private String relativeImportPath(String fromDir, String targetPath)
{
String[] from = normalizePath(fromDir).split("/");
@@ -756,4 +983,13 @@ public class BusinessBlockGenerationService
}
return businessBlockRegistryService;
}
private ChartBlockRenderSupport chartRenderSupport()
{
if (chartBlockRenderSupport == null)
{
chartBlockRenderSupport = new ChartBlockRenderSupport();
}
return chartBlockRenderSupport;
}
}

View File

@@ -1,10 +1,13 @@
package com.ruoyi.generator.service;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.apache.velocity.VelocityContext;
@@ -15,6 +18,9 @@ import com.ruoyi.generator.domain.GenTable;
import com.ruoyi.generator.domain.GenTableColumn;
import com.ruoyi.generator.domain.GeneratedBusinessBlockFile;
import com.ruoyi.generator.domain.front.FrontProjectPageDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionDesign;
import com.ruoyi.generator.domain.front.dto.BusinessActionEffectDesign;
import com.ruoyi.generator.domain.front.dto.block.ChartQueryRenderModel;
import com.ruoyi.generator.service.front.BusinessBlockRegistryService;
public class BusinessBlockGenerationServiceTest
@@ -72,6 +78,7 @@ public class BusinessBlockGenerationServiceTest
assertTrue(pageContent.contains("<Cart001Block />"));
assertTrue(blockContent.contains("listCart001BlockItems"));
assertTrue(blockContent.contains("@/api/business-blocks/cart_001"));
assertTrue(apiContent.contains("import request from \"@/api/request\""));
assertTrue(apiContent.contains("url: \"/business-blocks/cart_001/\" + id + \"/quantity\""));
assertTrue(apiContent.contains("url: \"/business-blocks/cart_001/\" + id"));
assertTrue(controllerContent.contains("class Cart001BlockController"));
@@ -113,6 +120,8 @@ public class BusinessBlockGenerationServiceTest
GenProject project = businessBlocksProject();
String noticeMapper = service.renderFile(project, findPath(service.listFiles(project, "backend"),
"src/main/resources/mapper/block/Notice001BlockMapper.xml"));
String carouselMapper = service.renderFile(project, findPath(service.listFiles(project, "backend"),
"src/main/resources/mapper/block/Carousel001BlockMapper.xml"));
String carouselComponent = service.renderFile(project, findPath(service.listFiles(project, "frontend"),
"src/views/pages/business_page/blocks/carousel_001/CarouselBlock.vue"));
String orderMapper = service.renderFile(project, findPath(service.listFiles(project, "backend"),
@@ -124,7 +133,10 @@ public class BusinessBlockGenerationServiceTest
assertTrue(noticeMapper.contains("from sys_notice n"));
assertTrue(noticeMapper.contains("n.notice_title as title"));
assertTrue(carouselMapper.contains("(b.status = '1' or b.status = 1)"));
assertTrue(carouselComponent.contains("listCarousel001BlockCarouselItems"));
assertTrue(carouselComponent.contains(":src=\"resolveResourceUrl(item.image)\""));
assertTrue(carouselComponent.contains("return \"/api\" + url"));
assertTrue(orderMapper.contains("insert into shop_order"));
assertTrue(orderMapper.contains("left join shop_product p on i.product_id = p.id"));
assertTrue(orderComponent.contains("this.$message.success(\"订单已提交\")"));
@@ -132,6 +144,33 @@ public class BusinessBlockGenerationServiceTest
assertTrue(masterDetailMapper.contains("where d.book_id = #{masterId}"));
}
@Test
public void renderNoticeMapperUsesPublishedStatusValueFromStatusComment()
{
GenProject project = businessBlocksProject();
findColumn(project, "sys_notice", "status").setColumnComment("状态1发布 0草稿/下架)");
String noticeMapper = service.renderFile(project, findPath(service.listFiles(project, "backend"),
"src/main/resources/mapper/block/Notice001BlockMapper.xml"));
assertTrue(noticeMapper.contains("where (n.status = '1' or n.status = 1)"));
assertFalse(noticeMapper.contains("where (n.status = '0' or n.status = 0)"));
}
@Test
public void renderedBackendBusinessBlockFilesDoNotDependOnRuoYiRuntime()
{
GenProject project = businessBlocksProject();
List<GeneratedBusinessBlockFile> files = service.listFiles(project, "backend");
for (GeneratedBusinessBlockFile file : files)
{
String content = service.renderFile(project, file);
assertFalse(file.getOutputPath(), content.contains("com.ruoyi.common"));
}
}
@Test
public void renderFileRendersEveryBundledBusinessBlockFile()
{
@@ -187,6 +226,48 @@ public class BusinessBlockGenerationServiceTest
assertEquals(Boolean.TRUE, context.get("hasPageDesignEmbeddedBusinessBlocks"));
}
@Test
@SuppressWarnings("unchecked")
public void pageDesignBusinessActionsIncludeRenderMetadata() throws Exception
{
GenProject project = new GenProject();
GenTable book = table(301L, "library_book", "LibraryBook");
GenTableColumn dueDate = column("due_date", "dueDate", "0");
dueDate.setColumnComment("Due date");
dueDate.setHtmlType("datetime");
book.setColumns(Arrays.asList(column("id", "id", "1"), column("user_id", "userId", "0"), dueDate));
project.setTables(Arrays.asList(book));
BusinessActionDesign action = new BusinessActionDesign();
action.setCode("borrow_book");
action.setName("Borrow");
action.setOwnerTable("library_book");
action.setRequestFields(Arrays.asList("id", "user_id", "due_date"));
BusinessActionEffectDesign effect = new BusinessActionEffectDesign();
Map<String, String> values = new LinkedHashMap<String, String>();
values.put("user_id", "${current_user.id}");
effect.setValues(values);
action.setEffects(Arrays.asList(effect));
project.setBusinessActions(Arrays.asList(action));
VelocityContext context = new VelocityContext();
context.put("currentPageDesign", new FrontProjectPageDesign());
context.put("pageDesignToolbarActionCodes", Arrays.asList("business:borrow_book"));
context.put("pageDesignRowActionCodes", Arrays.asList());
Method method = BusinessBlockGenerationService.class.getDeclaredMethod(
"putPageDesignBusinessActions", VelocityContext.class, GenProject.class);
method.setAccessible(true);
method.invoke(service, context, project);
List<BusinessActionDesign> actions =
(List<BusinessActionDesign>) context.get("pageDesignBusinessActions");
assertEquals(Arrays.asList("user_id"), actions.get(0).getCurrentUserFields());
assertEquals("due_date", actions.get(0).getInputFields().get(1).getField());
assertEquals("Due date", actions.get(0).getInputFields().get(1).getLabel());
assertEquals("datetime", actions.get(0).getInputFields().get(1).getHtmlType());
}
@Test
public void findFileMatchesCategoryOrPath()
{
@@ -198,6 +279,71 @@ public class BusinessBlockGenerationServiceTest
assertEquals(file.getCategory(), service.findFile(project, "frontend", file.getOutputPath()).getCategory());
}
@Test
public void chartBlockUsesSharedTemplatesAndAdminApiContext() throws Exception
{
GenProject project = chartProject();
List<GeneratedBusinessBlockFile> files = service.listFiles(project, "admin_frontend");
GeneratedBusinessBlockFile component = findBlockFile(
files, "admin_line_chart_001", "frontendComponent");
assertEquals("business-blocks/chart/templates/frontend/AdminChartBlock.vue.vm",
component.getTemplateResource());
assertEquals(Integer.valueOf(6), component.getInstance().getSpan());
VelocityContext context = new VelocityContext();
Method method = BusinessBlockGenerationService.class.getDeclaredMethod(
"putFileContext", VelocityContext.class, GenProject.class, GeneratedBusinessBlockFile.class);
method.setAccessible(true);
method.invoke(service, context, project, component);
assertEquals("/admin/business-blocks/admin_line_chart_001", context.get("apiBasePath"));
assertEquals("line", context.get("chartType"));
ChartQueryRenderModel chartQuery = (ChartQueryRenderModel) context.get("chartQuery");
assertEquals("DATE_FORMAT(t.create_time, '%Y-%m-%d')", chartQuery.getDimensionSql());
assertTrue(chartQuery.getWhereSql().contains("t.status = #{filter0}"));
}
@Test
public void chartProjectEmitsOneSharedRuntime()
{
List<GeneratedBusinessBlockFile> files = service.listFiles(chartProject(), "admin_frontend");
assertEquals(1, countOutput(files, "src/utils/chartRuntime.js"));
GeneratedBusinessBlockFile runtime = findPath(files, "src/utils/chartRuntime.js");
assertEquals("business-block:admin_frontend:shared:chartRuntime", runtime.getCategory());
assertEquals("business-blocks/chart/templates/frontend/chartRuntime.js.vm",
runtime.getTemplateResource());
}
@Test
public void adminChartGeneratesFrontendAndBackendFiles()
{
assertTrue(hasBlockFile(service.listFiles(chartProject(), "admin_frontend"),
"admin_line_chart_001", "frontendComponent"));
assertTrue(hasBlockFile(service.listFiles(chartProject(), "backend"),
"admin_line_chart_001", "mapperXml"));
}
@Test
@SuppressWarnings("unchecked")
public void chartPageViewModelExposesConfiguredSpan() throws Exception
{
GenProject project = chartProject();
GeneratedBusinessBlockFile page = findPath(service.listFiles(project, "admin_frontend"),
"src/views/pages/sales_dashboard/index.vue");
VelocityContext context = new VelocityContext();
Method method = BusinessBlockGenerationService.class.getDeclaredMethod(
"putFileContext", VelocityContext.class, GenProject.class, GeneratedBusinessBlockFile.class);
method.setAccessible(true);
method.invoke(service, context, project, page);
List<Map<String, Object>> blocks = (List<Map<String, Object>>) context.get("businessBlocks");
assertEquals(Integer.valueOf(6), blocks.get(0).get("span"));
assertEquals(Integer.valueOf(4), blocks.get(1).get("span"));
}
private void setField(String name, Object value) throws Exception
{
Field field = BusinessBlockGenerationService.class.getDeclaredField(name);
@@ -222,6 +368,44 @@ public class BusinessBlockGenerationServiceTest
return null;
}
private GeneratedBusinessBlockFile findBlockFile(List<GeneratedBusinessBlockFile> files,
String instanceCode, String templateKey)
{
for (GeneratedBusinessBlockFile file : files)
{
if (instanceCode.equals(file.getInstanceCode()) && templateKey.equals(file.getTemplateKey()))
{
return file;
}
}
throw new AssertionError("Missing block file " + instanceCode + ":" + templateKey);
}
private boolean hasBlockFile(List<GeneratedBusinessBlockFile> files, String instanceCode, String templateKey)
{
for (GeneratedBusinessBlockFile file : files)
{
if (instanceCode.equals(file.getInstanceCode()) && templateKey.equals(file.getTemplateKey()))
{
return true;
}
}
return false;
}
private int countOutput(List<GeneratedBusinessBlockFile> files, String outputPath)
{
int count = 0;
for (GeneratedBusinessBlockFile file : files)
{
if (outputPath.equals(file.getOutputPath()))
{
count++;
}
}
return count;
}
private void assertRenderedFiles(GenProject project, List<GeneratedBusinessBlockFile> files)
{
for (GeneratedBusinessBlockFile file : files)
@@ -274,6 +458,18 @@ public class BusinessBlockGenerationServiceTest
return project;
}
private GenProject chartProject()
{
GenProject project = new GenProject();
project.setProjectId(13L);
project.setProjectName("ChartBlocks");
project.setProjectFileName("chart-blocks");
project.setPackageName("com.example.demo");
project.setTables(Arrays.asList(orderTable()));
project.setPageDesigns(Arrays.asList(chartPageDesign()));
return project;
}
private FrontProjectPageDesign cartPageDesign()
{
FrontProjectPageDesign design = new FrontProjectPageDesign();
@@ -340,6 +536,30 @@ public class BusinessBlockGenerationServiceTest
return design;
}
private FrontProjectPageDesign chartPageDesign()
{
FrontProjectPageDesign design = new FrontProjectPageDesign();
design.setPageCode("sales_dashboard");
design.setPageName("Sales Dashboard");
design.setPageScope("admin");
design.setPageType("business_block");
design.setRoutePath("/admin/sales-dashboard");
design.setLayoutJson("{\"canvas\":\"business-blocks-v1\",\"blocks\":["
+ "{\"id\":\"admin_line_chart_001\",\"blockCode\":\"admin_line_chart\",\"name\":\"Sales Trend\",\"span\":6,"
+ "\"dataset\":{\"schema\":\"single-table-aggregate-v1\",\"table\":\"shop_order\","
+ "\"dimension\":{\"field\":\"createTime\",\"timeUnit\":\"day\"},"
+ "\"metrics\":[{\"field\":\"totalAmount\",\"aggregate\":\"sum\",\"alias\":\"salesAmount\",\"label\":\"Sales\"}],"
+ "\"filters\":[{\"field\":\"status\",\"operator\":\"eq\",\"value\":\"PAID\"}],"
+ "\"sort\":{\"by\":\"dimension\",\"order\":\"asc\"},\"limit\":100},"
+ "\"display\":{\"title\":\"Sales Trend\",\"smooth\":true}},"
+ "{\"id\":\"admin_metric_chart_001\",\"blockCode\":\"admin_metric_chart\",\"name\":\"Order Count\",\"span\":4,"
+ "\"dataset\":{\"schema\":\"single-table-aggregate-v1\",\"table\":\"shop_order\","
+ "\"metrics\":[{\"field\":\"id\",\"aggregate\":\"count\",\"alias\":\"orderCount\",\"label\":\"Orders\"}],"
+ "\"filters\":[],\"limit\":1},\"display\":{\"title\":\"Order Count\"}}"
+ "]}");
return design;
}
private GenTable cartTable()
{
GenTable table = table(201L, "shop_cart", "ShopCart");
@@ -460,4 +680,23 @@ public class BusinessBlockGenerationServiceTest
column.setIsPk(isPk);
return column;
}
private GenTableColumn findColumn(GenProject project, String tableName, String columnName)
{
for (GenTable table : project.getTables())
{
if (!tableName.equals(table.getTableName()))
{
continue;
}
for (GenTableColumn column : table.getColumns())
{
if (columnName.equals(column.getColumnName()))
{
return column;
}
}
}
throw new AssertionError("Missing column " + tableName + "." + columnName);
}
}