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 }">`; - 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 }">`; - 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 }">`; + 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 }">`; + 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"">`; - 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">`; - 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(); };