diff --git a/projects/igniteui-angular/migrations/update-20_0_6/index.spec.ts b/projects/igniteui-angular/migrations/update-20_0_6/index.spec.ts
index 31c15ad8b0b..bc1e1f39be6 100644
--- a/projects/igniteui-angular/migrations/update-20_0_6/index.spec.ts
+++ b/projects/igniteui-angular/migrations/update-20_0_6/index.spec.ts
@@ -3,104 +3,101 @@ import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/te
import { setupTestTree } from '../common/setup.spec';
describe('Migration 20.0.6 - Replace filteringOptions.filterable', () => {
- let appTree: UnitTestTree;
- const runner = new SchematicTestRunner(
- 'ig-migrate',
- path.join(__dirname, '../migration-collection.json')
- );
- const migrationName = 'migration-48';
- const makeTemplate = (name: string) => `/testSrc/appPrefix/component/${name}.component.html`;
- const makeScript = (name: string) => `/testSrc/appPrefix/component/${name}.component.ts`;
- const components = ['igx-simple-combo', 'igx-combo'];
-
-
-
- beforeEach(() => {
- appTree = setupTestTree();
- });
-
- it('should replace simple inline filteringOptions.filterable true with default behavior of the simple combo', async () => {
- components.forEach(async component =>{
- const input = `<${component} [filteringOptions]="{ filterable: true }">${component}>`;
- appTree.create(makeTemplate(`${component}-inline-true`), input);
-
- const tree = await runner.runSchematic(migrationName, {}, appTree);
- const output = tree.readContent(makeTemplate(`${component}-inline-true`));
-
- expect(output).not.toContain('[disableFiltering]');
- expect(output).not.toContain('filterable');
- });
- });
-
- it('should handle mixed object literal correctly', async () => {
- components.forEach(async component =>{
- const input = `<${component} [filteringOptions]="{ filterable: false, caseSensitive: true }">${component}>`;
- appTree.create(makeTemplate(`${component}-inline2`), input);
-
- const tree = await runner.runSchematic(migrationName, {}, appTree);
- const output = tree.readContent(makeTemplate(`${component}-inline2`));
-
- expect(output).toContain(`[disableFiltering]="true"`);
- expect(output).toContain(`[filteringOptions]="{ caseSensitive: true }"`);
- });
+ let appTree: UnitTestTree;
+ const runner = new SchematicTestRunner(
+ 'ig-migrate',
+ path.join(__dirname, '../migration-collection.json')
+ );
+ const migrationName = 'migration-48';
+ const makeTemplate = (name: string) => `/testSrc/appPrefix/component/${name}.component.html`;
+ const makeScript = (name: string) => `/testSrc/appPrefix/component/${name}.component.ts`;
+ const components = ['igx-simple-combo', 'igx-combo'];
+
+ const warnMsg =
+ "Manual migration needed: please use 'disableFiltering' instead of filteringOptions.filterable. Since it has been deprecated.";
+
+ beforeEach(() => {
+ appTree = setupTestTree();
+ });
+
+ it('should replace simple inline filteringOptions.filterable true with default behavior of the simple combo', async () => {
+ components.forEach(async component =>{
+ const input = `<${component} [filteringOptions]="{ filterable: true }">${component}>`;
+ appTree.create(makeTemplate(`${component}-inline-true`), input);
+
+ const tree = await runner.runSchematic(migrationName, {}, appTree);
+ const output = tree.readContent(makeTemplate(`${component}-inline-true`));
+
+ expect(output).not.toContain('[disableFiltering]');
+ expect(output).not.toContain('filterable');
+ });
+ });
+
+ it('should handle mixed object literal correctly', async () => {
+ components.forEach(async component =>{
+ const input = `<${component} [filteringOptions]="{ filterable: false, caseSensitive: true }">${component}>`;
+ appTree.create(makeTemplate(`${component}-inline2`), input);
+
+ const tree = await runner.runSchematic(migrationName, {}, appTree);
+ const output = tree.readContent(makeTemplate(`${component}-inline2`));
+
+ expect(output).toContain(`[disableFiltering]="true"`);
+ expect(output).toContain(`[filteringOptions]="{ caseSensitive: true }"`);
+ expect(output).not.toContain('filterable');
});
+ });
- it('should warn on variable reference', async () => {
- components.forEach(async component =>{
- const input = `<${component} [filteringOptions]="filterOpts"">${component}>`;
- const warnMsg = "Manual migration needed: please use 'disableFiltering' instead of filteringOptions.filterable." +
- "Since it has been deprecated.'";
-
- appTree.create(makeTemplate(`${component}-referenceInTsFile`), input);
+ it('should warn on variable reference', async () => {
+ for (const component of components) {
+ const input = `<${component} [filteringOptions]="filterOpts">${component}>`;
- const tree = await runner.runSchematic(migrationName, {}, appTree);
- const output = tree.readContent(makeTemplate(`${component}-referenceInTsFile`));
+ appTree.create(makeTemplate(`${component}-referenceInTsFile`), input);
- expect(output).toContain('[filteringOptions]');
- expect(output).toContain(warnMsg);
- });
- });
+ const tree = await runner.runSchematic(migrationName, {}, appTree);
+ const output = tree.readContent(makeTemplate(`${component}-referenceInTsFile`));
- it('should skip adding new [disableFiltering] if already present on igx-combo', async () => {
- const input = ``;
- appTree.create(makeTemplate('combo-has-disableFiltering'), input);
+ expect(output).toContain('[filteringOptions]');
+ expect(output).toContain(warnMsg);
+ }
+ });
- const tree = await runner.runSchematic(migrationName, {}, appTree);
- const output = tree.readContent(makeTemplate('combo-has-disableFiltering'));
+ it('should skip adding new [disableFiltering] if already present on igx-combo', async () => {
+ const input = ``;
+ appTree.create(makeTemplate('combo-has-disableFiltering'), input);
- const occurrences = (output.match(/\[disableFiltering\]/g) || []).length;
+ const tree = await runner.runSchematic(migrationName, {}, appTree);
+ const output = tree.readContent(makeTemplate('combo-has-disableFiltering'));
- expect(occurrences).toBe(1);
- expect(output).not.toContain('filterable');
- });
+ const occurrences = (output.match(/\[disableFiltering\]/g) || []).length;
- // TS file tests
+ expect(occurrences).toBe(1);
+ expect(output).not.toContain('filterable');
+ });
- it('should insert warning comment before `.filteringOptions.filterable = ...` assignment', async () => {
- const input = `this.igxCombo.filteringOptions.filterable = false;`;
- const expectedComment = "// Manual migration needed: please use 'disableFiltering' instead of filteringOptions.filterable." +
- "Since it has been deprecated.'";
+ // TS file tests
- appTree.create(makeScript('tsWarnOnDirectAssignment'), input);
+ it('should insert warning comment before `.filteringOptions.filterable = ...` assignment', async () => {
+ const input = `this.igxCombo.filteringOptions.filterable = false;`;
+ const expectedComment = `// ${warnMsg}`;
- const tree = await runner.runSchematic(migrationName, {}, appTree);
- const output = tree.readContent(makeScript('tsWarnOnDirectAssignment'));
+ appTree.create(makeScript('tsWarnOnDirectAssignment'), input);
- expect(output).toContain(expectedComment);
- expect(output).toContain('this.igxCombo.filteringOptions.filterable = false;');
- });
+ const tree = await runner.runSchematic(migrationName, {}, appTree);
+ const output = tree.readContent(makeScript('tsWarnOnDirectAssignment'));
- it('should insert warning comment before `.filteringOptions = { ... }` assignment', async () => {
- const input = `this.igxCombo.filteringOptions = { filterable: false, caseSensitive: true };`;
- const expectedComment = "// Manual migration needed: please use 'disableFiltering' instead of filteringOptions.filterable." +
- "Since it has been deprecated.'";
+ expect(output).toContain(expectedComment);
+ expect(output).toContain('this.igxCombo.filteringOptions.filterable = false;');
+ });
- appTree.create(makeScript('tsWarnOnObjectAssignment'), input);
+ it('should insert warning comment before `.filteringOptions = { ... }` assignment', async () => {
+ const input = `this.igxCombo.filteringOptions = { filterable: false, caseSensitive: true };`;
+ const expectedComment = `// ${warnMsg}`;
+ appTree.create(makeScript('tsWarnOnObjectAssignment'), input);
- const tree = await runner.runSchematic(migrationName, {}, appTree);
- const output = tree.readContent(makeScript('tsWarnOnObjectAssignment'));
+ const tree = await runner.runSchematic(migrationName, {}, appTree);
+ const output = tree.readContent(makeScript('tsWarnOnObjectAssignment'));
- expect(output).toContain(expectedComment);
- expect(output).toContain('this.igxCombo.filteringOptions = { filterable: false, caseSensitive: true };');
- });
+ expect(output).toContain(expectedComment);
+ expect(output).toContain('this.igxCombo.filteringOptions = { filterable: false, caseSensitive: true };');
+ });
});
diff --git a/projects/igniteui-angular/migrations/update-20_0_6/index.ts b/projects/igniteui-angular/migrations/update-20_0_6/index.ts
index c4f110500eb..62690a75c77 100644
--- a/projects/igniteui-angular/migrations/update-20_0_6/index.ts
+++ b/projects/igniteui-angular/migrations/update-20_0_6/index.ts
@@ -2,125 +2,134 @@ import { Element } from '@angular/compiler';
import type { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
import { UpdateChanges } from '../common/UpdateChanges';
import {
- FileChange,
- findElementNodes,
- getSourceOffset,
- parseFile
+ FileChange,
+ findElementNodes,
+ getSourceOffset,
+ parseFile
} from '../common/util';
import { nativeImport } from 'igniteui-angular/migrations/common/import-helper.js';
const version = '20.0.6';
export default (): Rule => async (host: Tree, context: SchematicContext) => {
- context.logger.info(
- `Applying migration for Ignite UI for Angular to version ${version}`
- );
-
- const { HtmlParser } = await nativeImport('@angular/compiler') as typeof import('@angular/compiler');
-
- const update = new UpdateChanges(__dirname, host, context);
- const changes = new Map();
- const parser = new HtmlParser();
-
- const warnMsg = "Manual migration needed: please use 'disableFiltering' instead of filteringOptions.filterable." +
- "Since it has been deprecated.'";
-
- const applyChanges = () => {
- for (const [path, fileChanges] of changes.entries()) {
- let content = host.read(path).toString();
- fileChanges.sort((a, b) => b.position - a.position).forEach(c => {
- content = c.apply(content);
- });
- host.overwrite(path, content);
- }
- };
-
- const addChange = (path: string, change: FileChange) => {
- if (!changes.has(path)) {
- changes.set(path, []);
- }
- changes.get(path).push(change);
- };
-
- const COMBO_TAGS = ['igx-simple-combo', 'igx-combo'];
-
- for (const path of update.templateFiles) {
- const nodes = findElementNodes(parseFile(parser, host, path), COMBO_TAGS);
-
- for (const node of nodes) {
- if (!(node instanceof Element)) continue;
+ context.logger.info(
+ `Applying migration for Ignite UI for Angular to version ${version}`
+ );
+
+ const { HtmlParser } = (await nativeImport('@angular/compiler')) as typeof import('@angular/compiler');
+
+ const update = new UpdateChanges(__dirname, host, context);
+ const changes = new Map();
+ const parser = new HtmlParser();
+
+ const warnMsg = "Manual migration needed: please use 'disableFiltering' instead of filteringOptions.filterable. " +
+ "Since it has been deprecated.";
+
+ const applyChanges = () => {
+ for (const [path, fileChanges] of changes.entries()) {
+ let content = host.read(path)!.toString();
+ fileChanges
+ .sort((a, b) => b.position - a.position)
+ .forEach((c) => {
+ content = c.apply(content);
+ });
+ host.overwrite(path, content);
+ }
+ };
- const hasDisableFiltering = node.attrs.some(a => a.name.includes('disableFiltering'));
- const attr = node.attrs.find(a => a.name === '[filteringOptions]');
- if (!attr) continue;
+ const addChange = (path: string, change: FileChange) => {
+ if (!changes.has(path)) {
+ changes.set(path, []);
+ }
+ changes.get(path)!.push(change);
+ };
+
+ const COMBO_TAGS = ['igx-simple-combo', 'igx-combo'];
+
+ for (const path of update.templateFiles) {
+ const root = parseFile(parser, host, path);
+ const nodes = findElementNodes(root, COMBO_TAGS);
+
+ for (const node of nodes) {
+ if (!(node instanceof Element)) continue;
+
+ const hasDisableFiltering = node.attrs.some(a => a.name.includes('disableFiltering'));
+ const attr = node.attrs.find(a => a.name === '[filteringOptions]');
+ if (!attr) continue;
+
+ const offset = getSourceOffset(node);
+ const file = offset.file;
+ const attrVal = (attr.value || '').trim();
+
+ // Handle inline object literals like [filteringOptions]="{...}"
+ if (attrVal.startsWith('{') && attrVal.endsWith('}')) {
+ const inner = attrVal.slice(1, -1);
+ let willDisableFiltering = false;
+
+ let remainingInner = inner.replace(
+ /(^|,)\s*filterable\s*:\s*(true|false)\s*(?=,|$)/i,
+ (_m, leading, val) => {
+ if (/^false$/i.test(val) && !hasDisableFiltering) {
+ willDisableFiltering = true;
+ }
+ return leading ? leading : '';
+ }
+ );
- const attrVal = attr.value.trim();
- const offset = getSourceOffset(node);
- const file = offset.file;
+ remainingInner = remainingInner
+ .replace(/\s*,\s*,\s*/g, ',')
+ .replace(/^\s*,\s*|\s*,\s*$/g, '')
+ .trim();
let replacementText = '';
+ if (willDisableFiltering) {
+ replacementText += `[disableFiltering]="true"`;
+ }
+ if (remainingInner.length > 0) {
+ replacementText += ` [filteringOptions]="{ ${remainingInner} }"`;
+ }
+ replacementText = replacementText.trim();
- if (attrVal.startsWith('{')) {
- // inline object literal
- const normalized = attrVal
- .replace(/'/g, '"')
- .replace(/([{,]\s*)(\w+)\s*:/g, '$1"$2":');
- const parsed = JSON.parse(normalized);
- const filterable = parsed.filterable;
-
- if (filterable === false && !hasDisableFiltering) {
- replacementText += `[disableFiltering]="true"`;
- }
-
- const remaining = { ...parsed };
- delete remaining.filterable;
- const remainingProps = Object.entries(remaining)
- .map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
- .join(', ');
-
- if (remainingProps.length > 0) {
- replacementText += ` [filteringOptions]="{ ${remainingProps} }"`;
- }
+ const attrStart = (attr as any).sourceSpan.start.offset as number;
+ const attrEnd = (attr as any).sourceSpan.end.offset as number;
+ const original = file.content.slice(attrStart, attrEnd);
+ addChange(file.url, new FileChange(attrStart, replacementText, original, 'replace'));
- // Replace whole [filteringOptions] attribute
- const match = node.sourceSpan.toString().match(/\[filteringOptions\]="([^"]+)"/);
- if (match) {
- const attrText = match[0];
- const attrPos = file.content.indexOf(attrText, offset.startTag.start);
- addChange(file.url, new FileChange(attrPos, replacementText, attrText, 'replace'));
- }
- } else {
- // log for manual TS edit
- const comment = `\n\n`;
- addChange(file.url, new FileChange(offset.startTag.end, comment));
- }
+ } else if (attrVal.length > 0) {
+ // log for manual TS edit
+ const comment = `\n\n`;
+ addChange(file.url, new FileChange(offset.startTag.end, comment));
}
}
+ }
- applyChanges();
+ applyChanges();
- for (const path of update.tsFiles) {
- const content = host.read(path).toString();
- const lines = content.split('\n');
- const newLines: string[] = [];
+ for (const path of update.tsFiles) {
+ const buf = host.read(path);
+ if (!buf) continue;
- let modified = false;
+ const content = buf.toString();
+ const lines = content.split('\n');
+ const newLines: string[] = [];
- for (const line of lines) {
- if (
- /\.filteringOptions\.filterable\s*=/.test(line) ||
- /\.filteringOptions\s*=/.test(line)
- ) {
- newLines.push('// ' + warnMsg);
- modified = true;
- }
- newLines.push(line);
- }
+ let modified = false;
- if (modified) {
- host.overwrite(path, newLines.join('\n'));
- }
+ for (const line of lines) {
+ if (
+ /\.\s*filteringOptions\s*\.\s*filterable\s*=/.test(line) ||
+ /\.\s*filteringOptions\s*=\s*{/.test(line)
+ ) {
+ newLines.push(`// ${warnMsg}`);
+ modified = true;
+ }
+ newLines.push(line);
+ }
+
+ if (modified) {
+ host.overwrite(path, newLines.join('\n'));
}
+ }
- update.applyChanges();
+ update.applyChanges();
};