Skip to content

Commit

Permalink
Field Grouping in layout.json (#157)
Browse files Browse the repository at this point in the history
* add lint support for field grouping in layout.json

* refactor common schema

* include variable items defined under groupbox

* add test for linting variables in groupbox

* fix incorrect default snippet for groupbox

* add groupbox to valid all-fields.json

* add more valid and invalid fields and enums examples

* add field completion tests for GroupBox items and json-schema defaultSnippets test

* push up tests in layout.test.ts some are failing

* Update packages/analyticsdx-template-lint/src/linter.ts

Co-authored-by: Greg Smith <[email protected]>

* fix test for variable in groupbox go to definition

* fix completion tests for groupbox var

* fix code completion test for groupbox var

* fix failing test in ui.test.ts due to new variable introduced

* fix broken test for layout.jso in templateLinter

* add object type to avoid strict mode warning

* Delete jsconfig.json

* Delete .forceignore

---------

Co-authored-by: Greg Smith <[email protected]>
  • Loading branch information
xshenwork and smithgp authored Aug 21, 2023
1 parent 5869fb2 commit 38e1d98
Show file tree
Hide file tree
Showing 18 changed files with 530 additions and 109 deletions.
2 changes: 1 addition & 1 deletion contributing/tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ would for NPM modules.

For vscode extension packages, place your strict unit tests in the `test/unit` directory of your package and create an
npm script in your package.json like `"test:unit": "./node_modules/.bin/_mocha --recursive out/test/unit"`
for running the tests. Check out the `"test:unit"` scripts in extensions/analyticsdx-vscode-templates/package.json file
for running the tests. Check out the `"test:unit"` scripts in `extensions/analyticsdx-vscode-templates/package.json` file
to see examples of how to configure code coverage reporting when running the tests.

These tests should not require a VS Code instance or a Salesforce server connection, and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ export class LayoutVariableCompletionItemProviderDelegate extends VariableRefCom
// TODO: make these more specific to the layout type (e.g. only 'center' if SingleColumn)
(location.matches(['pages', '*', 'layout', 'center', 'items', '*', 'name']) ||
location.matches(['pages', '*', 'layout', 'right', 'items', '*', 'name']) ||
location.matches(['pages', '*', 'layout', 'left', 'items', '*', 'name']))
location.matches(['pages', '*', 'layout', 'left', 'items', '*', 'name']) ||
location.matches(['pages', '*', 'layout', 'center', 'items', '*', 'items', '*', 'name']) ||
location.matches(['pages', '*', 'layout', 'right', 'items', '*', 'items', '*', 'name']) ||
location.matches(['pages', '*', 'layout', 'left', 'items', '*', 'items', '*', 'name']))
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ export class LayoutVariableDefinitionProvider extends VariableRefDefinitionProvi
// TODO: make these more specific to the layout type (e.g. only 'center' if SingleColumn)
(location.matches(['pages', '*', 'layout', 'center', 'items', '*', 'name']) ||
location.matches(['pages', '*', 'layout', 'right', 'items', '*', 'name']) ||
location.matches(['pages', '*', 'layout', 'left', 'items', '*', 'name']))
location.matches(['pages', '*', 'layout', 'left', 'items', '*', 'name']) ||
location.matches(['pages', '*', 'layout', 'center', 'items', '*', 'items', '*', 'name']) ||
location.matches(['pages', '*', 'layout', 'right', 'items', '*', 'items', '*', 'name']) ||
location.matches(['pages', '*', 'layout', 'left', 'items', '*', 'items', '*', 'name']))
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ describe('layout-schema.json hookup', () => {
// these types should see some available completions in the item (plus visibility)
{ type: 'Variable', expected: ['name'] },
{ type: 'Image', expected: ['image'] },
{ type: 'Text', expected: ['text'] }
{ type: 'Text', expected: ['text'] },
{ type: 'GroupBox', expected: ['text', 'description', 'items'] }
].forEach(({ type, expected }) => {
it(type, async () => {
const layoutEditor = await createTemplateWithItemType(type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,23 @@ describe('TemplateEditorManager configures layoutDefinition', () => {
expect.fail("Expected to find '[' after '\"items\":'");
}
position = scan.end.translate({ characterDelta: 1 });
await verifyCompletionsContain(
doc,
position,
'New Image item',
'New Text item',
'New Variable item',
'New Groupbox item'
);

// go to just after the [ in items[3] (a GroupBox item type) "items"
node = findNodeAtLocation(tree!, ['pages', 0, 'layout', 'center', 'items', 3, 'items']);
expect(node, 'pages[0].layout.center.items[3].items').to.not.be.undefined;
scan = scanLinesUntil(doc, ch => ch === '[', doc.positionAt(node!.offset));
if (scan.ch !== '[') {
expect.fail("Expected to find '[' after '\"items[3].items\":'");
}
position = scan.end.translate({ characterDelta: 1 });
await verifyCompletionsContain(doc, position, 'New Image item', 'New Text item', 'New Variable item');

// go right after the [ in "displayMessages"
Expand Down Expand Up @@ -286,8 +303,8 @@ describe('TemplateEditorManager configures layoutDefinition', () => {
it('go to definition support for variable names', async () => {
const uri = uriFromTestRoot(waveTemplatesUriPath, 'BadVariables', 'layout.json');
const [doc] = await openFile(uri, true);
// we should see the 2 warnings about the bad var types
await waitForDiagnostics(uri, d => d && d.length >= 2);
// we should see the 4 warnings about the bad var types
await waitForDiagnostics(uri, d => d && d.length >= 4);
await waitForTemplateEditorManagerHas(await getTemplateEditorManager(), uriDirname(uri), true);

const position = findPositionByJsonPath(doc, ['pages', 0, 'layout', 'center', 'items', 0, 'name']);
Expand All @@ -300,13 +317,35 @@ describe('TemplateEditorManager configures layoutDefinition', () => {
expect(locations[0].uri.fsPath, 'location path').to.equal(
vscode.Uri.joinPath(uriDirname(uri), 'variables.json').fsPath
);

// Go to definition for variable defined in groupbox
const groupBoxVarPosition = findPositionByJsonPath(doc, [
'pages',
0,
'layout',
'center',
'items',
4,
'items',
0,
'name'
]);
expect(groupBoxVarPosition, 'pages[0].layout.center.items[4].items[0].name').to.not.be.undefined;

const groupBoxVarlocations = await getDefinitionLocations(uri, groupBoxVarPosition!.translate(undefined, 1));
if (groupBoxVarlocations.length !== 1) {
expect.fail('Expected 1 location, got:\n' + JSON.stringify(groupBoxVarlocations, undefined, 2));
}
expect(groupBoxVarlocations[0].uri.fsPath, 'location path').to.equal(
vscode.Uri.joinPath(uriDirname(uri), 'variables.json').fsPath
);
});

it('code completions for variable names', async () => {
const uri = uriFromTestRoot(waveTemplatesUriPath, 'BadVariables', 'layout.json');
const [doc] = await openFile(uri, true);
// we should see the 2 warnings about the bad var types
await waitForDiagnostics(uri, d => d && d.length >= 2);
// we should see the 4 warnings about the bad var types
await waitForDiagnostics(uri, d => d && d.length >= 4);
await waitForTemplateEditorManagerHas(await getTemplateEditorManager(), uriDirname(uri), true);

const position = findPositionByJsonPath(doc, ['pages', 0, 'layout', 'center', 'items', 0, 'name']);
Expand All @@ -316,25 +355,35 @@ describe('TemplateEditorManager configures layoutDefinition', () => {
doc,
position!,
'"DatasetAnyFieldTypeVar"',
'"DateTimeTypeGroupBoxVar"',
'"DateTimeTypeVar"',
'"ObjectTypeGroupBoxVar"',
'"ObjectTypeVar"',
'"StringArrayVar"',
'"StringTypeVar"'
)
).sort(compareCompletionItems);
if (completions.length !== 5) {
expect.fail('Expected 5 completions, got: ' + completions.map(i => i.label).join(', '));
if (completions.length !== 7) {
expect.fail('Expected 7 completions, got: ' + completions.map(i => i.label).join(', '));
}
// check some more stuff on the completion items
[
{
detail: '(DatasetAnyFieldType) A dataset any field variable',
docs: "This can't be put in a non-vfpage page"
},
{
detail: '(DateTimeType) A datetime variable for groupbox',
docs: "This can't be put in a non-vfpage page"
},
{
detail: '(DateTimeType) A datetime variable',
docs: "This can't be put in a non-vfpage page"
},
{
detail: '(ObjectType) An object variable for groupbox',
docs: "This can't be put in a non-vfpage page"
},
{
detail: '(ObjectType) An object variable',
docs: "This can't be put in a non-vfpage page"
Expand All @@ -353,6 +402,72 @@ describe('TemplateEditorManager configures layoutDefinition', () => {
expect(item.detail, `${item.label} details`).to.equal(detail);
expect(item.documentation, `${item.label} documentation`).to.equal(docs);
});

// Check for variable code completitons inside groupBox
const groupBoxPosition = findPositionByJsonPath(doc, [
'pages',
0,
'layout',
'center',
'items',
4,
'items',
0,
'name'
]);
expect(groupBoxPosition, 'pages[0].layout.center.items[4].items[0].name').to.not.be.undefined;
const groupBoxCompletions = (
await verifyCompletionsContain(
doc,
groupBoxPosition!,
'"DatasetAnyFieldTypeVar"',
'"DateTimeTypeGroupBoxVar"',
'"DateTimeTypeVar"',
'"ObjectTypeGroupBoxVar"',
'"ObjectTypeVar"',
'"StringArrayVar"',
'"StringTypeVar"'
)
).sort(compareCompletionItems);
if (groupBoxCompletions.length !== 7) {
expect.fail('Expected 7 completions, got: ' + groupBoxCompletions.map(i => i.label).join(', '));
}
// check some more stuff on the completion items
[
{
detail: '(DatasetAnyFieldType) A dataset any field variable',
docs: "This can't be put in a non-vfpage page"
},
{
detail: '(DateTimeType) A datetime variable for groupbox',
docs: "This can't be put in a non-vfpage page"
},
{
detail: '(DateTimeType) A datetime variable',
docs: "This can't be put in a non-vfpage page"
},
{
detail: '(ObjectType) An object variable for groupbox',
docs: "This can't be put in a non-vfpage page"
},
{
detail: '(ObjectType) An object variable',
docs: "This can't be put in a non-vfpage page"
},
{
detail: '(StringType[])',
docs: undefined
},
{
detail: '(StringType) A string variable',
docs: 'String variable description'
}
].forEach(({ detail, docs }, i) => {
const item = groupBoxCompletions[i];
expect(item.kind, `${item.label} kind`).to.equal(vscode.CompletionItemKind.Variable);
expect(item.detail, `${item.label} details`).to.equal(detail);
expect(item.documentation, `${item.label} documentation`).to.equal(docs);
});
});

it('quick fixes on bad variable names', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,25 +295,35 @@ describe('TemplateEditorManager configures uiDefinition', () => {
doc,
position!,
'"DatasetAnyFieldTypeVar"',
'"DateTimeTypeGroupBoxVar"',
'"DateTimeTypeVar"',
'"ObjectTypeGroupBoxVar"',
'"ObjectTypeVar"',
'"StringArrayVar"',
'"StringTypeVar"'
)
).sort(compareCompletionItems);
if (completions.length !== 5) {
expect.fail('Expected 5 completions, got: ' + completions.map(i => i.label).join(', '));
if (completions.length !== 7) {
expect.fail('Expected 7 completions, got: ' + completions.map(i => i.label).join(', '));
}
// check some more stuff on the completion items
[
{
detail: '(DatasetAnyFieldType) A dataset any field variable',
docs: "This can't be put in a non-vfpage page"
},
{
detail: '(DateTimeType) A datetime variable for groupbox',
docs: "This can't be put in a non-vfpage page"
},
{
detail: '(DateTimeType) A datetime variable',
docs: "This can't be put in a non-vfpage page"
},
{
detail: '(ObjectType) An object variable for groupbox',
docs: "This can't be put in a non-vfpage page"
},
{
detail: '(ObjectType) An object variable',
docs: "This can't be put in a non-vfpage page"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,14 +162,14 @@ describe('TemplateLinterManager lints layout.json', () => {

const [doc] = await openFile(uriFromTestRoot(waveTemplatesUriPath, 'BadVariables', 'layout.json'));
const diagnostics = (
await waitForDiagnostics(doc.uri, d => d && d.filter(varFilter).length >= 2, 'initial diagnostics on layout.json')
await waitForDiagnostics(doc.uri, d => d && d.filter(varFilter).length >= 4, 'initial diagnostics on layout.json')
)
.filter(varFilter)
.sort(sortDiagnostics);
if (diagnostics.length !== 2) {
expect.fail('Expected 2 initial diagnostics, got:\n' + JSON.stringify(diagnostics, undefined, 2));
if (diagnostics.length !== 4) {
expect.fail('Expected 4 initial diagnostics, got:\n' + JSON.stringify(diagnostics, undefined, 2));
}
// make sure we get the 2 expected errors
// make sure we get the 2 expected errors for variables not under a groupbox
['DateTimeType', 'ObjectType'].forEach((type, i) => {
const diagnostic = diagnostics[i];
expect(diagnostic, `diagnostics[${i}]`).to.be.not.undefined;
Expand All @@ -181,5 +181,21 @@ describe('TemplateLinterManager lints layout.json', () => {
`pages[0].layout.center.items[${i}].name`
);
});

// make sure we get the 2 expected errors for variables under groupbox
['DateTimeType', 'ObjectType'].forEach((type, i) => {
const groupBoxVarIndex = i + 2;
const diagnostic = diagnostics[groupBoxVarIndex];
expect(diagnostic, `diagnostics[${groupBoxVarIndex}]`).to.be.not.undefined;
expect(diagnostic.message, `diagnostics[${groupBoxVarIndex}].message`).to.equal(
`${type} variable '${type}GroupBoxVar' is not supported in layout pages`
);
expect(diagnostic.code, `diagnostics[${groupBoxVarIndex}].code`).to.equal(
ERRORS.LAYOUT_PAGE_UNSUPPORTED_VARIABLE
);
expect(jsonpathFrom(diagnostic), `diagnostics[${groupBoxVarIndex}].jsonpath`).to.equal(
`pages[0].layout.center.items[4].items[${i}].name`
);
});
});
});
24 changes: 17 additions & 7 deletions packages/analyticsdx-template-lint/src/linter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,27 @@ function findAllItemsForLayoutDefinition(layoutJson: JsonNode): JsonNode[] {
*/
function findAllVariableNamesForLayoutDefinition(layoutJson: JsonNode): Array<{ name: string; nameNode: JsonNode }> {
return findAllItemsForLayoutDefinition(layoutJson).reduce((items, item) => {
const type = findJsonPrimitiveAttributeValue(item, 'type')[0];
if (type === 'Variable') {
const [name, nameNode] = findJsonPrimitiveAttributeValue(item, 'name');
if (typeof name === 'string' && nameNode) {
items.push({ name, nameNode });
}
}
const variableItems = findAllVariableItemsForLayoutItem(item);
items.push(...variableItems);
return items;
}, [] as Array<{ name: string; nameNode: JsonNode }>);
}

function findAllVariableItemsForLayoutItem(item: JsonNode): Array<{ name: string; nameNode: JsonNode }> {
const type = findJsonPrimitiveAttributeValue(item, 'type')[0];
if (type === 'Variable') {
const [name, nameNode] = findJsonPrimitiveAttributeValue(item, 'name');
if (typeof name === 'string' && nameNode) {
return [{ name, nameNode }];
}
} else if (type === 'GroupBox') {
const [nodes, node] = findJsonArrayAttributeValue(item, 'items');
const childrenVariableItems = nodes?.flatMap(n => findAllVariableItemsForLayoutItem(n));
return childrenVariableItems ? childrenVariableItems : [];
}
return [];
}

export type TemplateLinterUri = {
toString(): string;
};
Expand Down
Loading

0 comments on commit 38e1d98

Please sign in to comment.