@svadmin/core 0.61.0 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/core",
3
- "version": "0.61.0",
3
+ "version": "0.62.0",
4
4
  "author": "zuohuadong",
5
5
  "repository": {
6
6
  "type": "git",
@@ -171,9 +171,6 @@ const baseLocales: Record<string, Record<string, string>> = {
171
171
  'common.batchDeleteFailed': '批量删除失败,未删除 {count} 条记录',
172
172
  'common.selectedItems': '已选择 {count} 项',
173
173
  'common.clearSelection': '清除选择',
174
- 'common.density': '表格密度',
175
- 'common.densityCompact': '紧凑',
176
- 'common.densityComfortable': '宽松',
177
174
  'common.filterTags': '已选条件',
178
175
  'common.clearAllFilters': '清除全部',
179
176
  'common.pageUnit': '页',
@@ -249,6 +246,7 @@ const baseLocales: Record<string, Record<string, string>> = {
249
246
  'common.goToSite': '返回工作台',
250
247
  'common.clear': '清除',
251
248
  'common.menu': '菜单',
249
+ 'common.mainNavigation': '主导航',
252
250
  'common.darkMode': '暗色模式',
253
251
  'common.lightMode': '亮色模式',
254
252
  'common.detail': '详情',
@@ -1025,9 +1023,6 @@ const baseLocales: Record<string, Record<string, string>> = {
1025
1023
  'common.batchDeleteFailed': 'Batch delete failed; {count} records were not deleted',
1026
1024
  'common.selectedItems': '{count} selected',
1027
1025
  'common.clearSelection': 'Clear selection',
1028
- 'common.density': 'Table density',
1029
- 'common.densityCompact': 'Compact',
1030
- 'common.densityComfortable': 'Comfortable',
1031
1026
  'common.filterTags': 'Active filters',
1032
1027
  'common.clearAllFilters': 'Clear all',
1033
1028
  'common.pageUnit': 'Page',
@@ -1103,6 +1098,7 @@ const baseLocales: Record<string, Record<string, string>> = {
1103
1098
  'common.goToSite': 'Go to Workspace',
1104
1099
  'common.clear': 'Clear',
1105
1100
  'common.menu': 'Menu',
1101
+ 'common.mainNavigation': 'Main navigation',
1106
1102
  'common.darkMode': 'Dark mode',
1107
1103
  'common.lightMode': 'Light mode',
1108
1104
  'common.detail': 'Detail',
@@ -27,8 +27,16 @@ export function buildProductPagePlan(surface: ProductSurface, plan: ProductPlan)
27
27
  .filter(id => id !== surface.primaryActionId)
28
28
  .map(id => actionById.get(id))
29
29
  .filter((action): action is ProductAction => action !== undefined);
30
+ // ProductSurface currently declares section titles separately from its
31
+ // resource list. Keep resource declarations in one section until the plan
32
+ // schema can express per-section ownership; repeating every resource in
33
+ // every section makes generated pages look like duplicate data.
30
34
  const sections = surface.sections.length
31
- ? surface.sections.map((title, index) => ({ id: `${surface.id}.section.${index + 1}`, title, resourceIds: surface.resourceIds }))
35
+ ? surface.sections.map((title, index) => ({
36
+ id: `${surface.id}.section.${index + 1}`,
37
+ title,
38
+ resourceIds: index === 0 ? surface.resourceIds : [],
39
+ }))
32
40
  : [{ id: `${surface.id}.content`, title: surface.name, resourceIds: surface.resourceIds }];
33
41
  return {
34
42
  id: surface.id,
@@ -37,6 +37,40 @@ export function reviewProductPlan(plan: ProductPlan): ProductReviewReport {
37
37
  const surfaceIds = new Set(plan.surfaces.map(surface => surface.id));
38
38
  const actionIds = new Set(plan.actions.map(action => action.id));
39
39
 
40
+ const runtimeNames = new Map<string, string>();
41
+ for (const resource of plan.resources) {
42
+ const runtimeName = resource.runtimeName ?? resource.name;
43
+ const previous = runtimeNames.get(runtimeName);
44
+ if (previous !== undefined) {
45
+ addIssue(issues, {
46
+ code: 'resource.duplicate-runtime-name',
47
+ severity: 'error',
48
+ message: `Resources "${resource.label}" and "${previous}" resolve to the same runtime name "${runtimeName}".`,
49
+ path: `resources.${resource.id}.runtimeName`,
50
+ suggestion: 'Give every generated resource route a unique runtime name.',
51
+ });
52
+ } else {
53
+ runtimeNames.set(runtimeName, resource.label);
54
+ }
55
+ }
56
+
57
+ const surfaceRoutes = new Map<string, string>();
58
+ for (const surface of plan.surfaces) {
59
+ const route = surface.route ?? `/workflows/${surface.id}`;
60
+ const previous = surfaceRoutes.get(route);
61
+ if (previous !== undefined) {
62
+ addIssue(issues, {
63
+ code: 'surface.duplicate-route',
64
+ severity: 'error',
65
+ message: `Surfaces "${surface.name}" and "${previous}" resolve to the same route "${route}".`,
66
+ path: `surfaces.${surface.id}.route`,
67
+ suggestion: 'Give every generated surface a unique route.',
68
+ });
69
+ } else {
70
+ surfaceRoutes.set(route, surface.name);
71
+ }
72
+ }
73
+
40
74
  for (const workflow of plan.workflows) {
41
75
  if (!workflow.actorRoleIds.length) addIssue(issues, { code: 'workflow.missing-actor', severity: 'error', message: `Workflow "${workflow.name}" has no actor role.`, path: `workflows.${workflow.id}.actorRoleIds` });
42
76
  for (const roleId of workflow.actorRoleIds) if (!has(roleIds, roleId)) addIssue(issues, { code: 'workflow.unknown-role', severity: 'error', message: `Workflow "${workflow.name}" references unknown role "${roleId}".`, path: `workflows.${workflow.id}.actorRoleIds` });
@@ -46,12 +80,34 @@ export function reviewProductPlan(plan: ProductPlan): ProductReviewReport {
46
80
  for (const resource of plan.resources) {
47
81
  if (!resource.workflowIds.length && !resource.internal) addIssue(issues, { code: 'resource.no-workflow', severity: 'warning', message: `Resource "${resource.label}" is not attached to a workflow.`, path: `resources.${resource.id}.workflowIds`, suggestion: 'Make it contextual/internal or attach it to a real task.' });
48
82
  for (const workflowId of resource.workflowIds) if (!has(workflowIds, workflowId)) addIssue(issues, { code: 'resource.unknown-workflow', severity: 'error', message: `Resource "${resource.label}" references unknown workflow "${workflowId}".`, path: `resources.${resource.id}.workflowIds` });
83
+ for (const roleId of resource.roleIds) if (!has(roleIds, roleId)) addIssue(issues, { code: 'resource.unknown-role', severity: 'error', message: `Resource "${resource.label}" references unknown role "${roleId}".`, path: `resources.${resource.id}.roleIds` });
84
+ resource.fields.forEach(field => {
85
+ for (const roleId of field.readableBy) if (!has(roleIds, roleId)) addIssue(issues, { code: 'field.unknown-role', severity: 'error', message: `Field "${field.label}" references unknown readable role "${roleId}".`, path: `resources.${resource.id}.fields.${field.id}.readableBy` });
86
+ for (const roleId of field.writableBy) if (!has(roleIds, roleId)) addIssue(issues, { code: 'field.unknown-role', severity: 'error', message: `Field "${field.label}" references unknown writable role "${roleId}".`, path: `resources.${resource.id}.fields.${field.id}.writableBy` });
87
+ for (const workflowId of field.usedByWorkflowIds) if (!has(workflowIds, workflowId)) addIssue(issues, { code: 'field.unknown-workflow', severity: 'error', message: `Field "${field.label}" references unknown workflow "${workflowId}".`, path: `resources.${resource.id}.fields.${field.id}.usedByWorkflowIds` });
88
+ });
49
89
  }
50
90
 
51
91
  for (const surface of plan.surfaces) {
52
- reviewSurface(surface, { workflowIds, actionIds, resourceIds }, issues);
92
+ reviewSurface(surface, { workflowIds, actionIds, resourceIds, roleIds, surfaceIds }, issues);
53
93
  }
54
94
 
95
+ const visitingSurfaces = new Set<string>();
96
+ const visitedSurfaces = new Set<string>();
97
+ const visitSurface = (id: string): void => {
98
+ if (visitingSurfaces.has(id)) {
99
+ addIssue(issues, { code: 'surface.cycle', severity: 'error', message: `Surface hierarchy contains a cycle at "${id}".`, path: `surfaces.${id}.parentSurfaceId` });
100
+ return;
101
+ }
102
+ if (visitedSurfaces.has(id)) return;
103
+ visitingSurfaces.add(id);
104
+ const parentId = plan.surfaces.find(surface => surface.id === id)?.parentSurfaceId;
105
+ if (parentId !== undefined && surfaceIds.has(parentId)) visitSurface(parentId);
106
+ visitingSurfaces.delete(id);
107
+ visitedSurfaces.add(id);
108
+ };
109
+ for (const surface of plan.surfaces) visitSurface(surface.id);
110
+
55
111
  const menuById = new Map(plan.menu.map(item => [item.id, item] as const));
56
112
  for (const item of plan.menu) {
57
113
  if (item.parentId !== undefined && !menuById.has(item.parentId)) {
@@ -78,11 +134,14 @@ export function reviewProductPlan(plan: ProductPlan): ProductReviewReport {
78
134
  if (!item.workflowIds.length && !item.resourceId && !item.surfaceId) addIssue(issues, { code: 'menu.no-business-target', severity: 'warning', message: `Menu item "${item.label}" has no workflow, resource or surface target.`, path: `menu.${item.id}`, suggestion: 'Keep it only for an intentional external/system entry.' });
79
135
  if (item.surfaceId && !has(surfaceIds, item.surfaceId)) addIssue(issues, { code: 'menu.unknown-surface', severity: 'error', message: `Menu item "${item.label}" references unknown surface.`, path: `menu.${item.id}.surfaceId` });
80
136
  if (item.resourceId && !has(resourceIds, item.resourceId)) addIssue(issues, { code: 'menu.unknown-resource', severity: 'error', message: `Menu item "${item.label}" references unknown resource.`, path: `menu.${item.id}.resourceId` });
137
+ for (const workflowId of item.workflowIds) if (!has(workflowIds, workflowId)) addIssue(issues, { code: 'menu.unknown-workflow', severity: 'error', message: `Menu item "${item.label}" references unknown workflow "${workflowId}".`, path: `menu.${item.id}.workflowIds` });
138
+ for (const roleId of item.roleIds) if (!has(roleIds, roleId)) addIssue(issues, { code: 'menu.unknown-role', severity: 'error', message: `Menu item "${item.label}" references unknown role "${roleId}".`, path: `menu.${item.id}.roleIds` });
81
139
  if (!item.roleIds.length && item.placement !== 'hidden') addIssue(issues, { code: 'menu.public-entry', severity: 'info', message: `Menu item "${item.label}" is public at the UI hint layer.`, path: `menu.${item.id}.roleIds`, suggestion: 'Keep server authorization independent from menu visibility.' });
82
140
  }
83
141
 
84
142
  for (const action of plan.actions) {
85
143
  if (!has(workflowIds, action.workflowId)) addIssue(issues, { code: 'action.unknown-workflow', severity: 'error', message: `Action "${action.label}" references unknown workflow.`, path: `actions.${action.id}.workflowId` });
144
+ for (const roleId of action.roleIds) if (!has(roleIds, roleId)) addIssue(issues, { code: 'action.unknown-role', severity: 'error', message: `Action "${action.label}" references unknown role "${roleId}".`, path: `actions.${action.id}.roleIds` });
86
145
  if (!action.roleIds.length) addIssue(issues, { code: 'action.missing-roles', severity: 'warning', message: `Action "${action.label}" has no role visibility contract.`, path: `actions.${action.id}.roleIds` });
87
146
  }
88
147
 
@@ -110,8 +169,10 @@ export function reviewProductPlan(plan: ProductPlan): ProductReviewReport {
110
169
  };
111
170
  }
112
171
 
113
- function reviewSurface(surface: ProductSurface, refs: { workflowIds: ReadonlySet<string>; actionIds: ReadonlySet<string>; resourceIds: ReadonlySet<string> }, issues: ProductReviewIssue[]): void {
172
+ function reviewSurface(surface: ProductSurface, refs: { workflowIds: ReadonlySet<string>; actionIds: ReadonlySet<string>; resourceIds: ReadonlySet<string>; roleIds: ReadonlySet<string>; surfaceIds: ReadonlySet<string> }, issues: ProductReviewIssue[]): void {
114
173
  if (!surface.purpose.trim()) addIssue(issues, { code: 'surface.missing-purpose', severity: 'error', message: `Surface "${surface.name}" has no purpose.`, path: `surfaces.${surface.id}.purpose` });
174
+ for (const roleId of surface.roleIds) if (!refs.roleIds.has(roleId)) addIssue(issues, { code: 'surface.unknown-role', severity: 'error', message: `Surface "${surface.name}" references unknown role "${roleId}".`, path: `surfaces.${surface.id}.roleIds` });
175
+ if (surface.parentSurfaceId !== undefined && !refs.surfaceIds.has(surface.parentSurfaceId)) addIssue(issues, { code: 'surface.unknown-parent', severity: 'error', message: `Surface "${surface.name}" references unknown parent surface "${surface.parentSurfaceId}".`, path: `surfaces.${surface.id}.parentSurfaceId` });
115
176
  if (!surface.workflowIds.length) addIssue(issues, { code: 'surface.no-workflow', severity: 'warning', message: `Surface "${surface.name}" is not attached to a workflow.`, path: `surfaces.${surface.id}.workflowIds` });
116
177
  surface.workflowIds.forEach(id => { if (!refs.workflowIds.has(id)) addIssue(issues, { code: 'surface.unknown-workflow', severity: 'error', message: `Surface "${surface.name}" references unknown workflow "${id}".`, path: `surfaces.${surface.id}.workflowIds` }); });
117
178
  surface.resourceIds.forEach(id => { if (!refs.resourceIds.has(id)) addIssue(issues, { code: 'surface.unknown-resource', severity: 'error', message: `Surface "${surface.name}" references unknown resource "${id}".`, path: `surfaces.${surface.id}.resourceIds` }); });