diff --git a/projects/cdk/schematics/ng-update/v4/steps/migrate-templates.ts b/projects/cdk/schematics/ng-update/v4/steps/migrate-templates.ts index 1725156bbe01..113c6ec81bab 100644 --- a/projects/cdk/schematics/ng-update/v4/steps/migrate-templates.ts +++ b/projects/cdk/schematics/ng-update/v4/steps/migrate-templates.ts @@ -33,6 +33,7 @@ import { migrateAvatar, migrateBadge, migrateBadgedContent, + migrateButtonAppearance, migrateCheckbox, migrateExpandable, migrateFocusable, @@ -105,6 +106,7 @@ export function migrateTemplates(fileSystem: DevkitFileSystem, options: TuiSchem migrateProgressSegmented, migrateThumbnailCard, migrateOverscroll, + migrateButtonAppearance, migrateLabel, ] as const; diff --git a/projects/cdk/schematics/ng-update/v4/steps/templates/index.ts b/projects/cdk/schematics/ng-update/v4/steps/templates/index.ts index 30d7ef96619e..725e7ba814d6 100644 --- a/projects/cdk/schematics/ng-update/v4/steps/templates/index.ts +++ b/projects/cdk/schematics/ng-update/v4/steps/templates/index.ts @@ -1,6 +1,7 @@ export * from './migrate-avatar'; export * from './migrate-badge'; export * from './migrate-badged-content'; +export * from './migrate-button-appearance'; export * from './migrate-expandable'; export * from './migrate-focusable'; export * from './migrate-label'; diff --git a/projects/cdk/schematics/ng-update/v4/steps/templates/migrate-button-appearance.ts b/projects/cdk/schematics/ng-update/v4/steps/templates/migrate-button-appearance.ts new file mode 100644 index 000000000000..b0c067e370f0 --- /dev/null +++ b/projects/cdk/schematics/ng-update/v4/steps/templates/migrate-button-appearance.ts @@ -0,0 +1,92 @@ +import type {UpdateRecorder} from '@angular-devkit/schematics'; +import type {DevkitFileSystem} from 'ng-morph'; +import type {ElementLocation} from 'parse5/dist/common/token'; +import type {Element} from 'parse5/dist/tree-adapters/default'; + +import {findElementsWithDirective} from '../../../../utils/templates/elements'; +import { + getTemplateFromTemplateResource, + getTemplateOffset, +} from '../../../../utils/templates/template-resource'; +import type {TemplateResource} from '../../../interfaces'; +import {removeAttrs} from '../utils/remove-attrs'; + +const tuiButtonSelectors = ['tuiButton', 'tuiIconButton']; + +const appearanceInputName = 'appearance'; +const appearanceInputNameDict = { + [appearanceInputName]: true, + [`[${appearanceInputName}]`]: true, +} as const; + +export function migrateButtonAppearance({ + resource, + recorder, + fileSystem, +}: { + fileSystem: DevkitFileSystem; + recorder: UpdateRecorder; + resource: TemplateResource; +}): void { + const template = getTemplateFromTemplateResource(resource, fileSystem); + const templateOffset = getTemplateOffset(resource); + + const elements = tuiButtonSelectors.flatMap((selector) => + findElementsWithDirective(template, selector).filter( + ({sourceCodeLocation, attrs}) => + !!sourceCodeLocation && + attrs.some(({name}) => appearanceInputNameDict[name]), + ), + ); + + if (!elements.length) { + return; + } + + const whiteBlockValue = 'whiteblock-active'; + + elements.forEach(({attrs, sourceCodeLocation}: Element) => { + const whiteBlockActiveAttr = attrs.find( + ({value, name}) => + appearanceInputNameDict[name] && + (value === whiteBlockValue || value === `'${whiteBlockValue}'`), + ); + + if (whiteBlockActiveAttr) { + removeAttrs( + [whiteBlockActiveAttr], + sourceCodeLocation as ElementLocation, + recorder, + templateOffset, + ); + + const {startOffset} = sourceCodeLocation?.attrs?.[ + whiteBlockActiveAttr.name + ] || {startOffset: 0, endOffset: 0}; + + recorder.insertLeft( + startOffset + templateOffset, + ` ${appearanceInputName}="whiteblock"`, + ); + recorder.insertLeft(startOffset + templateOffset, ' data-mode="checked"'); + } + }); + + const elementWithConditionAppearance = elements.find(({attrs}: Element) => + attrs.some( + ({name, value}) => + name === `[${appearanceInputName}]` && !value.trim().startsWith("'"), + ), + ); + + if (elementWithConditionAppearance) { + addTodo(recorder, templateOffset); + } +} + +function addTodo(recorder: UpdateRecorder, templateOffset: number): void { + recorder.insertRight( + templateOffset, + '\n', + ); +} diff --git a/projects/cdk/schematics/ng-update/v4/tests/schematic-migrate-button-appearance.spec.ts b/projects/cdk/schematics/ng-update/v4/tests/schematic-migrate-button-appearance.spec.ts new file mode 100644 index 000000000000..01abbb261331 --- /dev/null +++ b/projects/cdk/schematics/ng-update/v4/tests/schematic-migrate-button-appearance.spec.ts @@ -0,0 +1,143 @@ +import {join} from 'node:path'; + +import {HostTree} from '@angular-devkit/schematics'; +import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing'; +import type {TuiSchema} from '@taiga-ui/cdk/schematics/ng-add/schema'; +import { + createProject, + createSourceFile, + resetActiveProject, + saveActiveProject, + setActiveProject, +} from 'ng-morph'; + +import {createAngularJson} from '../../../utils/create-angular-json'; + +const collectionPath = join(__dirname, '../../../migration.json'); + +const COMPONENT = ` +import {TuiButton} from '@taiga-ui/core/components/button'; + +@Component({ + standalone: true, + templateUrl: './test.template.html', + imports: [TuiButton] +}) +class TestComponent {} +`; + +const COMPONENT_WITH_CONDITION = ` +import {TuiButton} from '@taiga-ui/core/components/button'; + +@Component({ + standalone: true, + templateUrl: './test-with-condition.template.html', + imports: [TuiButton] +}) +class TestWithConditionComponent { + get appearance() {return 'flat'}; +} +`; + +const TEMPLATE_BEFORE = ` + + + + + + + + + +`; + +const TEMPLATE_WITH_CONDITION_BEFORE = ` + +`; + +const TEMPLATE_AFTER = ` + + + + + + + + + +`; + +const TEMPLATE_WITH_CONDITION_AFTER = ` + + +`; + +describe('ng-update', () => { + let host: UnitTestTree; + let runner: SchematicTestRunner; + + beforeEach(() => { + host = new UnitTestTree(new HostTree()); + runner = new SchematicTestRunner('schematics', collectionPath); + + setActiveProject(createProject(host)); + + createMainFiles(); + + saveActiveProject(); + }); + + it('should migrate button appearance in a template', async () => { + const tree = await runner.runSchematic( + 'updateToV4', + {'skip-logs': process.env['TUI_CI'] === 'true'} as Partial, + host, + ); + + expect(tree.readContent('test/app/test.template.html')).toEqual(TEMPLATE_AFTER); + }); + + it('should migrate button appearance with condition in a template', async () => { + const tree = await runner.runSchematic( + 'updateToV4', + {'skip-logs': process.env['TUI_CI'] === 'true'} as Partial, + host, + ); + + expect(tree.readContent('test/app/test-with-condition.template.html')).toEqual( + TEMPLATE_WITH_CONDITION_AFTER, + ); + }); + + afterEach(() => { + resetActiveProject(); + }); +}); + +function createMainFiles(): void { + createSourceFile('test/app/test.component.ts', COMPONENT); + createSourceFile('test/app/test.template.html', TEMPLATE_BEFORE); + + createSourceFile( + 'test/app/test-with-condition.component.ts', + COMPONENT_WITH_CONDITION, + ); + createSourceFile( + 'test/app/test-with-condition.template.html', + TEMPLATE_WITH_CONDITION_BEFORE, + ); + + createAngularJson(); + createSourceFile( + 'package.json', + '{"dependencies": {"@angular/core": "~13.0.0", "@taiga-ui/addon-commerce": "~3.42.0"}}', + ); +}