diff --git a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/BusinessBlockGenerationService.java b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/BusinessBlockGenerationService.java index 6f94ee3..28475ad 100644 --- a/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/BusinessBlockGenerationService.java +++ b/RuoYi-Vue/ruoyi-generator/src/main/java/com/ruoyi/generator/service/BusinessBlockGenerationService.java @@ -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 listFiles(GenProject project, String templateType) { if (project == null || StringUtils.isEmpty(project.getPageDesigns())) @@ -46,35 +58,48 @@ public class BusinessBlockGenerationService return Collections.emptyList(); } List files = new ArrayList(); + Set emittedSharedOutputs = new HashSet(); 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 files, GenProject project, + private int appendBlockFiles(List files, Set emittedSharedOutputs, + GenProject project, FrontProjectPageDesign pageDesign, List 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 files, Set 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 candidates = project == null || StringUtils.isEmpty(project.getBusinessActions()) ? Collections.emptyList() : project.getBusinessActions(); + FrontProjectPageDesign currentPageDesign = context.get("currentPageDesign") instanceof FrontProjectPageDesign + ? (FrontProjectPageDesign) context.get("currentPageDesign") : null; List toolbarBusinessActions = filterPageDesignBusinessActions( - candidates, context.get("pageDesignToolbarActionCodes")); + candidates, context.get("pageDesignToolbarActionCodes"), currentPageDesign, "primary", + project == null ? null : project.getTables()); List rowBusinessActions = filterPageDesignBusinessActions( - candidates, context.get("pageDesignRowActionCodes")); + candidates, context.get("pageDesignRowActionCodes"), currentPageDesign, "text", + project == null ? null : project.getTables()); List 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 filterPageDesignBusinessActions(List candidates, - Object actionCodesObject) + Object actionCodesObject, FrontProjectPageDesign pageDesign, String fallbackButtonType, + List projectTables) { if (StringUtils.isEmpty(candidates) || !(actionCodesObject instanceof List)) { @@ -229,20 +278,82 @@ public class BusinessBlockGenerationService return Collections.emptyList(); } List result = new ArrayList(); + Set addedCodes = new HashSet(); 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 projectTables) + { + BusinessActionDesign action = copyBusinessAction(source); + Map 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 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) configs).get(actionCode); + return config instanceof Map ? (Map) config : Collections.emptyMap(); + } + + private String stringConfig(Map 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 mergeBusinessActions(List first, List 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; + } } diff --git a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/BusinessBlockGenerationServiceTest.java b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/BusinessBlockGenerationServiceTest.java index 4d079a9..233ecee 100644 --- a/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/BusinessBlockGenerationServiceTest.java +++ b/RuoYi-Vue/ruoyi-generator/src/test/java/com/ruoyi/generator/service/BusinessBlockGenerationServiceTest.java @@ -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("")); 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 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 values = new LinkedHashMap(); + 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 actions = + (List) 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 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 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> blocks = (List>) 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 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 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 files, String outputPath) + { + int count = 0; + for (GeneratedBusinessBlockFile file : files) + { + if (outputPath.equals(file.getOutputPath())) + { + count++; + } + } + return count; + } + private void assertRenderedFiles(GenProject project, List 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); + } }