Skip to content

Commit d51e675

Browse files
Merge pull request #15 from Acumatica/improvements-after-meeting
Mass-screen validation, linkCommand auto-complete, API calls de-duplication, re-validation on active document change
2 parents 71a2395 + 0779679 commit d51e675

21 files changed

Lines changed: 1108 additions & 80 deletions

acumate-plugin/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

acumate-plugin/package.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,16 @@
131131
"command": "acumate.dropCache",
132132
"title": "Drop Local Cache",
133133
"category": "AcuMate"
134+
},
135+
{
136+
"command": "acumate.validateScreens",
137+
"title": "Validate Screens (HTML)",
138+
"category": "AcuMate"
139+
},
140+
{
141+
"command": "acumate.validateTypeScriptScreens",
142+
"title": "Validate Screens (TypeScript)",
143+
"category": "AcuMate"
134144
}
135145
],
136146
"menus": {
@@ -164,7 +174,9 @@
164174
"watch": "tsc -watch -p ./",
165175
"pretest": "npm run compile && npm run lint",
166176
"lint": "eslint src",
167-
"test": "vscode-test"
177+
"test": "vscode-test",
178+
"validate:screens": "node ./scripts/validate-screens.js",
179+
"validate:screens:ts": "node ./scripts/validate-ts-screens.js"
168180
},
169181
"devDependencies": {
170182
"@types/mocha": "^10.0.9",

acumate-plugin/readme.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,10 @@ The **AcuMate** extension for Visual Studio Code offers a range of powerful feat
5050
- The same metadata powers TypeScript diagnostics, warning when a `graphType` string does not match any graph returned by the backend so you can catch typos before running the UI.
5151
- The `@featureInstalled("FeatureName")` decorator gains backend-driven IntelliSense as you type the feature name and raises diagnostics when a missing/disabled feature is referenced, helping ensure feature-gated screens follow the site configuration.
5252
- Validates `@linkCommand("ActionName")` decorators on PXFieldState members, ensuring the referenced PXAction exists on the backend graph (case-insensitive comparison).
53+
- While editing a `@linkCommand("...")` decorator, AcuMate now surfaces completion items sourced from the backend action list so you can insert the exact action name without leaving the editor.
5354
- Screen extension TypeScript files (under `.../extensions/...`) automatically reuse the parent screen's `@graphInfo` metadata so backend validations, completions, and linkCommand checks keep working even when the extension file lacks its own decorator.
54-
- Hovering PXView field declarations displays the backend field’s display name, raw name, type, and default control, mirroring the HTML hover experience described below.
55+
- Hovering PXView field declarations displays the backend field’s display name, raw name, type, and default control. Hovering PXView properties shows cache type/name metadata, and hovering PXActionState members surfaces their backend display names so you can confirm bindings at a glance.
56+
- Fields whose names contain a double underscore (e.g., `__CustomField`) are treated as intentionally custom and are skipped by `graphInfo` diagnostics to avoid noise while prototyping.
5557

5658
### HTML Features
5759

@@ -112,10 +114,16 @@ The **AcuMate** extension provides several commands to streamline development ta
112114
| `acumate.watchCurrentScreen` | **Watch Current Screen** | Watches the currently active screen for changes and rebuilds as needed. |
113115
| `acumate.repeatLastBuildCommand` | **Repeat Last Build Command** | Repeats the last executed build command, useful for quick iterations. |
114116
| `acumate.dropCache` | **Drop Local Cache** | Clears the local cache, ensuring that the next build retrieves fresh data from the backend. |
117+
| `acumate.validateScreens` | **Validate Screens (HTML)** | Scans every `.html` under `src/screens` (or a folder you choose), reports progress with a cancellable notification, and logs validator diagnostics to the **AcuMate Validation** output channel without failing on warnings. |
118+
| `acumate.validateTypeScriptScreens`| **Validate Screens (TypeScript)** | Iterates through screen `.ts` files, runs the backend-powered `graphInfo` validator with cancellable progress, and streams any warnings/errors to the **AcuMate Validation** output channel so you can review them without interrupting development. |
115119

116120
### Quality & CI
117121

118122
1. **Automated Tests**
119123
- Run `npm test` locally to compile, lint, and execute the VS Code integration suites (metadata, HTML providers, validator, scaffolding, build commands).
120124
- The GitHub Actions workflow in `.github/workflows/ci.yml` performs `npm ci` + `npm test` for every pull request (regardless of branch) and on pushes to `main`, using a Node 18.x / 20.x matrix to catch regressions before and after merges.
125+
2. **Project Screen Validation**
126+
- Inside VS Code, run **AcuMate: Validate Screens (HTML)** to queue the validator against all HTML files beneath `src/screens` (or any folder you input). A cancellable progress notification tracks the run, and results are aggregated in the **AcuMate Validation** output channel so you can inspect warnings without breaking your workflow.
127+
- For TypeScript coverage, run **AcuMate: Validate Screens (TypeScript)** to traverse the same folder structure, execute `collectGraphInfoDiagnostics` for each screen `.ts`, and summarize backend metadata mismatches in the output channel (requires `acuMate.useBackend = true`). This command is also cancellable so you can stop long-running validations instantly.
128+
- From the CLI, run `npm run validate:screens` (HTML) or `npm run validate:screens:ts` (TypeScript) to execute the same scans headlessly via the VS Code test runner. Override the roots with `SCREEN_VALIDATION_ROOT` or `TS_SCREEN_VALIDATION_ROOT` environment variables when needed. Both scripts log summaries instead of failing on warnings, ensuring automated runs focus on catching crashes.
121129

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const { spawn } = require('child_process');
2+
const path = require('path');
3+
4+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
5+
const repoRoot = path.resolve(__dirname, '..');
6+
7+
const child = spawn(npmCmd, ['test'], {
8+
cwd: repoRoot,
9+
env: {
10+
...process.env,
11+
SCREEN_VALIDATION_ROOT: process.env.SCREEN_VALIDATION_ROOT || 'src/screens'
12+
},
13+
stdio: 'inherit'
14+
});
15+
16+
child.on('exit', code => {
17+
process.exit(code ?? 1);
18+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const { spawn } = require('child_process');
2+
const path = require('path');
3+
4+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
5+
const repoRoot = path.resolve(__dirname, '..');
6+
7+
const child = spawn(npmCmd, ['test'], {
8+
cwd: repoRoot,
9+
env: {
10+
...process.env,
11+
TS_SCREEN_VALIDATION_ROOT: process.env.TS_SCREEN_VALIDATION_ROOT || 'src/screens'
12+
},
13+
stdio: 'inherit'
14+
});
15+
16+
child.on('exit', code => {
17+
process.exit(code ?? 1);
18+
});

acumate-plugin/src/api/layered-data-service.ts

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import { FeatureModel } from "../model/FeatureModel";
88

99
export class LayeredDataService implements IAcuMateApiClient {
1010

11+
private inflightGraphs?: Promise<GraphModel[] | undefined>;
12+
private inflightFeatures?: Promise<FeatureModel[] | undefined>;
13+
private readonly inflightStructures = new Map<string, Promise<GraphStructure | undefined>>();
14+
1115
constructor(private cacheService: CachedDataService, private apiService: AcuMateApiClient) {
1216

1317
}
@@ -18,9 +22,21 @@ export class LayeredDataService implements IAcuMateApiClient {
1822
return cachedResult;
1923
}
2024

21-
const apiResult = await this.apiService.getGraphs();
22-
this.cacheService.store(GraphAPICache, apiResult);
23-
return apiResult;
25+
if (this.inflightGraphs) {
26+
return this.inflightGraphs;
27+
}
28+
29+
this.inflightGraphs = this.apiService
30+
.getGraphs()
31+
.then(result => {
32+
this.cacheService.store(GraphAPICache, result);
33+
return result;
34+
})
35+
.finally(() => {
36+
this.inflightGraphs = undefined;
37+
});
38+
39+
return this.inflightGraphs;
2440

2541
}
2642

@@ -30,9 +46,23 @@ export class LayeredDataService implements IAcuMateApiClient {
3046
return cachedResult;
3147
}
3248

33-
const apiResult = await this.apiService.getGraphStructure(graphName);
34-
this.cacheService.store(GraphAPIStructureCachePrefix + graphName, apiResult);
35-
return apiResult;
49+
const existing = this.inflightStructures.get(graphName);
50+
if (existing) {
51+
return existing;
52+
}
53+
54+
const pending = this.apiService
55+
.getGraphStructure(graphName)
56+
.then(result => {
57+
this.cacheService.store(GraphAPIStructureCachePrefix + graphName, result);
58+
return result;
59+
})
60+
.finally(() => {
61+
this.inflightStructures.delete(graphName);
62+
});
63+
64+
this.inflightStructures.set(graphName, pending);
65+
return pending;
3666
}
3767

3868
async getFeatures(): Promise<FeatureModel[] | undefined> {
@@ -41,8 +71,20 @@ export class LayeredDataService implements IAcuMateApiClient {
4171
return cachedResult;
4272
}
4373

44-
const apiResult = await this.apiService.getFeatures();
45-
this.cacheService.store(FeaturesCache, apiResult);
46-
return apiResult;
74+
if (this.inflightFeatures) {
75+
return this.inflightFeatures;
76+
}
77+
78+
this.inflightFeatures = this.apiService
79+
.getFeatures()
80+
.then(result => {
81+
this.cacheService.store(FeaturesCache, result);
82+
return result;
83+
})
84+
.finally(() => {
85+
this.inflightFeatures = undefined;
86+
});
87+
88+
return this.inflightFeatures;
4789
}
4890
}

acumate-plugin/src/backend-metadata-utils.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { GraphStructure } from './model/graph-structure';
2-
import { Field, View } from './model/view';
2+
import { Action, Field, View } from './model/view';
33

44
export interface BackendFieldMetadata {
55
fieldName: string;
@@ -14,6 +14,12 @@ export interface BackendViewMetadata {
1414
fields: Map<string, BackendFieldMetadata>;
1515
}
1616

17+
export interface BackendActionMetadata {
18+
actionName: string;
19+
normalizedName: string;
20+
action: Action;
21+
}
22+
1723
export function normalizeMetaName(value: string | undefined): string | undefined {
1824
if (typeof value !== 'string') {
1925
return undefined;
@@ -25,14 +31,35 @@ export function normalizeMetaName(value: string | undefined): string | undefined
2531

2632
export function buildBackendActionSet(structure: GraphStructure | undefined): Set<string> {
2733
const actions = new Set<string>();
34+
const map = buildBackendActionMap(structure);
35+
for (const key of map.keys()) {
36+
actions.add(key);
37+
}
38+
return actions;
39+
}
40+
41+
export function buildBackendActionMap(structure: GraphStructure | undefined): Map<string, BackendActionMetadata> {
42+
const actions = new Map<string, BackendActionMetadata>();
2843
if (!structure?.actions) {
2944
return actions;
3045
}
3146

3247
for (const action of structure.actions) {
33-
const normalized = normalizeMetaName(action?.name);
34-
if (normalized) {
35-
actions.add(normalized);
48+
if (!action) {
49+
continue;
50+
}
51+
52+
const normalized = normalizeMetaName(action.name);
53+
if (!normalized) {
54+
continue;
55+
}
56+
57+
if (!actions.has(normalized)) {
58+
actions.set(normalized, {
59+
actionName: action.name ?? normalized,
60+
normalizedName: normalized,
61+
action
62+
});
3663
}
3764
}
3865

acumate-plugin/src/completionItemProviders/ts-completion-provider.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { getAvailableGraphs } from '../services/graph-metadata-service';
1313
import { getAvailableFeatures } from '../services/feature-metadata-service';
1414
import { getGraphTypeLiteralAtPosition } from '../typescript/graph-info-utils';
1515
import { getFeatureInstalledLiteralAtPosition } from '../typescript/feature-installed-utils';
16+
import { getLinkCommandLiteralAtPosition } from '../typescript/link-command-utils';
1617
import { buildBackendViewMap, normalizeMetaName } from '../backend-metadata-utils';
1718

1819
export async function provideTSCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken, context: vscode.CompletionContext): Promise<vscode.CompletionItem[] | undefined> {
@@ -32,6 +33,11 @@ export async function provideTSCompletionItems(document: vscode.TextDocument, po
3233
return featureInstalledCompletions;
3334
}
3435

36+
const linkCommandCompletions = await provideLinkCommandCompletions(document, position, sourceFile, documentText);
37+
if (linkCommandCompletions?.length) {
38+
return linkCommandCompletions;
39+
}
40+
3541
let activeClassName: string | undefined;
3642
let activeClassKind: 'PXScreen' | 'PXView' | undefined;
3743

@@ -279,5 +285,50 @@ async function provideFeatureInstalledCompletions(
279285
items.push(item);
280286
}
281287

288+
return items.length ? items : undefined;
289+
}
290+
291+
async function provideLinkCommandCompletions(
292+
document: vscode.TextDocument,
293+
position: vscode.Position,
294+
sourceFile: ts.SourceFile,
295+
documentText: string
296+
): Promise<vscode.CompletionItem[] | undefined> {
297+
const offset = document.offsetAt(position);
298+
const literalInfo = getLinkCommandLiteralAtPosition(sourceFile, offset);
299+
if (!literalInfo) {
300+
return undefined;
301+
}
302+
303+
const graphName = tryGetGraphType(documentText) ?? tryGetGraphTypeFromExtension(document.fileName);
304+
if (!graphName || !AcuMateContext.ApiService) {
305+
return undefined;
306+
}
307+
308+
const graphStructure = await AcuMateContext.ApiService.getGraphStructure(graphName);
309+
if (!graphStructure?.actions?.length) {
310+
return undefined;
311+
}
312+
313+
const range = getStringContentRange(document, literalInfo.literal);
314+
const items: vscode.CompletionItem[] = [];
315+
for (const action of graphStructure.actions) {
316+
if (!action?.name) {
317+
continue;
318+
}
319+
320+
const item = new vscode.CompletionItem(action.name, vscode.CompletionItemKind.EnumMember);
321+
item.insertText = action.name;
322+
item.range = range;
323+
item.sortText = action.name.toLowerCase();
324+
item.detail = action.displayName ? `${action.name} (${action.displayName})` : action.name;
325+
const docLines = [`PXAction from graph ${graphName}.`];
326+
if (action.displayName) {
327+
docLines.push(`Display name: ${action.displayName}`);
328+
}
329+
item.documentation = new vscode.MarkdownString(docLines.join('\n\n'));
330+
items.push(item);
331+
}
332+
282333
return items.length ? items : undefined;
283334
}

0 commit comments

Comments
 (0)