Skip to content

Commit 3b35dc1

Browse files
Merge pull request #23 from Acumatica/bugifx/qp-include-field-validation-wip
Bugifx/qp include field validation
2 parents c6e6b11 + 81360ba commit 3b35dc1

51 files changed

Lines changed: 2519 additions & 291 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

acumate-plugin/src/providers/html-completion-provider.ts

Lines changed: 107 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,18 @@ import {
44
getRelatedTsFiles,
55
loadClassInfosFromFiles,
66
CollectedClassInfo,
7-
createClassInfoLookup,
87
resolveViewBinding,
98
ClassPropertyInfo,
10-
filterScreenLikeClasses,
119
collectActionProperties,
1210
extractConfigPropertyNames,
13-
filterClassesBySource,
1411
} from '../utils';
1512
import {
1613
parseDocumentDom,
1714
findNodeAtOffset,
1815
elevateToElementNode,
1916
getAttributeContext,
2017
findParentViewName,
18+
isActionStateBindTag,
2119
} from './html-shared';
2220
import {
2321
ClientControlMetadata,
@@ -27,6 +25,16 @@ import { getScreenTemplates } from '../services/screen-template-service';
2725
import { getIncludeMetadata, IncludeMetadata } from '../services/include-service';
2826
import { BackendFieldMetadata, normalizeMetaName } from '../backend-metadata-utils';
2927
import { loadBackendFieldsForView } from './html-backend-utils';
28+
import { getBaseScreenDocument } from '../services/screen-html-service';
29+
import {
30+
HtmlIncludeFieldContext,
31+
createHtmlFieldMetadataContext,
32+
getFieldPropertiesFromViews,
33+
getIncludeFieldContext,
34+
getParentOrSelectorViewName,
35+
getViewNameFromCustomizationSelectors,
36+
resolveTemplateValue,
37+
} from '../services/html-field-context-service';
3038

3139
// Registers completions so HTML view bindings stay in sync with PX metadata.
3240
export function registerHtmlCompletionProvider(context: vscode.ExtensionContext) {
@@ -103,13 +111,8 @@ export class HtmlCompletionProvider implements vscode.CompletionItemProvider {
103111
return undefined;
104112
}
105113

106-
const relevantClassInfos = filterClassesBySource(classInfos, tsFilePaths);
107-
if (!relevantClassInfos.length) {
108-
return undefined;
109-
}
110-
111-
const classInfoLookup = createClassInfoLookup(classInfos);
112-
const screenClasses = filterScreenLikeClasses(relevantClassInfos);
114+
const metadataContext = createHtmlFieldMetadataContext(classInfos, tsFilePaths);
115+
const { classInfoLookup, screenClasses } = metadataContext;
113116
// Completions are sourced from the same metadata as validation/definitions to keep behavior consistent.
114117

115118
if (attributeContext.attributeName === 'view.bind') {
@@ -121,7 +124,9 @@ export class HtmlCompletionProvider implements vscode.CompletionItemProvider {
121124
}
122125

123126
if (attributeContext.attributeName === 'state.bind') {
124-
return this.createActionCompletions(screenClasses);
127+
return isActionStateBindTag(attributeContext.tagName)
128+
? this.createActionCompletions(screenClasses, classInfoLookup, elementNode, attributeContext.value)
129+
: this.createControlStateCompletions(attributeContext.value, screenClasses, classInfoLookup);
125130
}
126131

127132
if (attributeContext.attributeName === 'control-state.bind' && attributeContext.tagName === 'qp-field') {
@@ -130,7 +135,35 @@ export class HtmlCompletionProvider implements vscode.CompletionItemProvider {
130135

131136
if (attributeContext.attributeName === 'name' && attributeContext.tagName === 'field') {
132137
// Field completions are scoped to the PXView resolved from the surrounding markup.
133-
const viewName = findParentViewName(elementNode);
138+
const includeContext = getIncludeFieldContext({
139+
documentPath: document.uri.fsPath,
140+
elementNode,
141+
hostTsFilePaths: tsFilePaths,
142+
workspaceRoots: vscode.workspace.workspaceFolders?.map(folder => folder.uri.fsPath),
143+
});
144+
if (includeContext) {
145+
const hostSelectorViewName = resolveTemplateValue(
146+
getViewNameFromCustomizationSelectors(elementNode, includeContext.templateDocument),
147+
includeContext.parameterValues
148+
);
149+
const hostSelectorResolution = hostSelectorViewName
150+
? resolveViewBinding(hostSelectorViewName, screenClasses, classInfoLookup)
151+
: undefined;
152+
if (hostSelectorResolution?.viewClass) {
153+
const backendFields = await loadBackendFieldsForView(hostSelectorViewName!, screenClasses);
154+
return this.createFieldCompletions(hostSelectorResolution.viewClass.properties, backendFields);
155+
}
156+
157+
const items = this.createIncludeFieldNameCompletions(elementNode, includeContext);
158+
if (items?.length) {
159+
return items;
160+
}
161+
}
162+
163+
const viewName = getParentOrSelectorViewName(
164+
elementNode,
165+
getBaseScreenDocument(document.uri.fsPath)
166+
);
134167
if (!viewName) {
135168
return undefined;
136169
}
@@ -366,6 +399,22 @@ export class HtmlCompletionProvider implements vscode.CompletionItemProvider {
366399
return items.length ? items : undefined;
367400
}
368401

402+
private createIncludeFieldNameCompletions(
403+
elementNode: any,
404+
context: HtmlIncludeFieldContext
405+
): vscode.CompletionItem[] | undefined {
406+
const selectorViewName = getViewNameFromCustomizationSelectors(elementNode, context.templateDocument);
407+
const viewName = resolveTemplateValue(selectorViewName, context.parameterValues);
408+
if (viewName) {
409+
const resolution = resolveViewBinding(viewName, context.screenClasses, context.classInfoLookup);
410+
const viewClass = resolution?.viewClass;
411+
return viewClass ? this.createFieldCompletions(viewClass.properties) : undefined;
412+
}
413+
414+
const fields = getFieldPropertiesFromViews(context);
415+
return fields.size ? this.createFieldCompletions(fields) : undefined;
416+
}
417+
369418
private isInsideDataFeed(node: any): boolean {
370419
let current = node?.parent ?? node?.parentNode;
371420
while (current) {
@@ -590,14 +639,58 @@ export class HtmlCompletionProvider implements vscode.CompletionItemProvider {
590639
return items;
591640
}
592641

593-
private createActionCompletions(screenClasses: CollectedClassInfo[]): vscode.CompletionItem[] {
642+
private createActionCompletions(
643+
screenClasses: CollectedClassInfo[],
644+
classInfoLookup: Map<string, CollectedClassInfo>,
645+
elementNode: any,
646+
currentValue?: string
647+
): vscode.CompletionItem[] {
648+
const normalizedPrefix = (currentValue ?? '').trim().toLowerCase();
594649
const actionMap = collectActionProperties(screenClasses);
595650
const items: vscode.CompletionItem[] = [];
596-
actionMap.forEach((property, name) => {
651+
const seen = new Set<string>();
652+
const addAction = (name: string, property: ClassPropertyInfo) => {
653+
if (seen.has(name)) {
654+
return;
655+
}
656+
if (normalizedPrefix && !name.toLowerCase().startsWith(normalizedPrefix)) {
657+
return;
658+
}
659+
seen.add(name);
597660
const item = new vscode.CompletionItem(name, vscode.CompletionItemKind.Function);
598661
item.detail = property.typeName ?? 'PXActionState';
599662
items.push(item);
663+
};
664+
665+
actionMap.forEach((property, name) => {
666+
addAction(name, property);
600667
});
668+
669+
const viewName = findParentViewName(elementNode);
670+
const viewClass = viewName
671+
? resolveViewBinding(viewName, screenClasses, classInfoLookup)?.viewClass
672+
: undefined;
673+
viewClass?.properties.forEach((property, name) => {
674+
if (property.kind === 'action') {
675+
addAction(name, property);
676+
}
677+
});
678+
679+
for (const screenClass of screenClasses) {
680+
for (const [propertyName, property] of screenClass.properties) {
681+
if (property.kind !== 'view' && property.kind !== 'viewCollection') {
682+
continue;
683+
}
684+
685+
const viewClass = resolveViewBinding(propertyName, screenClasses, classInfoLookup)?.viewClass;
686+
viewClass?.properties.forEach((viewProperty, actionName) => {
687+
if (viewProperty.kind === 'action') {
688+
addAction(`${propertyName}.${actionName}`, viewProperty);
689+
}
690+
});
691+
}
692+
}
693+
601694
return items;
602695
}
603696

0 commit comments

Comments
 (0)