From c82012ba6d8e31a8d5788006ac4e6a340fc73f4d Mon Sep 17 00:00:00 2001 From: "e.muhamethanov" Date: Thu, 19 Sep 2024 14:57:22 +0300 Subject: [PATCH 1/5] feat(ImageBaseOverlay): remove disableInteractive prop --- .../ImageBaseOverlay/ImageBaseOverlay.test.tsx | 18 ++++++++++-------- .../ImageBaseOverlay/ImageBaseOverlay.tsx | 4 +--- .../ImageBase/ImageBaseOverlay/types.ts | 11 +++-------- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/packages/vkui/src/components/ImageBase/ImageBaseOverlay/ImageBaseOverlay.test.tsx b/packages/vkui/src/components/ImageBase/ImageBaseOverlay/ImageBaseOverlay.test.tsx index 08f207edae..48d39df2de 100644 --- a/packages/vkui/src/components/ImageBase/ImageBaseOverlay/ImageBaseOverlay.test.tsx +++ b/packages/vkui/src/components/ImageBase/ImageBaseOverlay/ImageBaseOverlay.test.tsx @@ -1,17 +1,21 @@ import { act } from 'react'; import { fireEvent, render, screen } from '@testing-library/react'; import { Icon12Add } from '@vkontakte/icons'; +import { noop } from '@vkontakte/vkjs'; import { Button } from '../../../components/Button/Button'; import { baselineComponent, userEvent } from '../../../testing/utils'; import { ImageBaseOverlay, type ImageBaseOverlayProps } from './ImageBaseOverlay'; import styles from './ImageBaseOverlay.module.css'; -const ImageBaseOverlayClickableTest = (props: Omit) => ( +const ImageBaseOverlayClickableTest = ({ + onClick, + ...restProps +}: Omit) => ( @@ -20,7 +24,7 @@ const ImageBaseOverlayClickableTest = (props: Omit, ) => ( - + @@ -60,7 +64,7 @@ describe(ImageBaseOverlay, () => { describe('works as clickable element', () => { it('appears as clickable element', () => { - render(); + render(); const element = screen.getByTestId('overlay'); @@ -72,9 +76,7 @@ describe(ImageBaseOverlay, () => { it('handles onClick prop', () => { const handleClick = jest.fn(); - render( - , - ); + render(); fireEvent.click(screen.getByTestId('overlay')); expect(handleClick).toHaveBeenCalledTimes(1); diff --git a/packages/vkui/src/components/ImageBase/ImageBaseOverlay/ImageBaseOverlay.tsx b/packages/vkui/src/components/ImageBase/ImageBaseOverlay/ImageBaseOverlay.tsx index 5b666cc6ed..6dfc0a4d20 100644 --- a/packages/vkui/src/components/ImageBase/ImageBaseOverlay/ImageBaseOverlay.tsx +++ b/packages/vkui/src/components/ImageBase/ImageBaseOverlay/ImageBaseOverlay.tsx @@ -35,7 +35,6 @@ const ImageBaseOverlayInteractive = ({ children, className, getRootRef, - disableInteractive, overlayShown, ...restProps }: ImageBaseOverlayInteractiveProps & { overlayShown?: boolean }) => { @@ -68,7 +67,6 @@ const ImageBaseOverlayInteractive = ({ const ImageBaseOverlayNonInteractive = ({ className, getRootRef, - disableInteractive, overlayShown: overlayShownProps, ...restProps }: ImageBaseOverlayNonInteractiveProps & { overlayShown?: boolean }) => { @@ -115,7 +113,7 @@ export const ImageBaseOverlay: React.FC = ({ }; // Не делаем деструктуризацию пропа, потому что Typescript не вывозит - if (restProps.disableInteractive) { + if (!restProps.onClick) { return ; } diff --git a/packages/vkui/src/components/ImageBase/ImageBaseOverlay/types.ts b/packages/vkui/src/components/ImageBase/ImageBaseOverlay/types.ts index 08f1847789..c384258620 100644 --- a/packages/vkui/src/components/ImageBase/ImageBaseOverlay/types.ts +++ b/packages/vkui/src/components/ImageBase/ImageBaseOverlay/types.ts @@ -26,9 +26,8 @@ export interface ImageBaseOverlayCommonProps export interface ImageBaseOverlayInteractiveProps extends ImageBaseOverlayCommonProps { /** * Обработчик взаимодействия с элементом. - * Свойство несовместимо с `nonInteractive={true}`. */ - onClick?: React.MouseEventHandler; + onClick: React.MouseEventHandler; /** * Принимает иконку. * @@ -42,17 +41,13 @@ export interface ImageBaseOverlayInteractiveProps extends ImageBaseOverlayCommon * > использовали иконку. */ children: React.ReactElement; - disableInteractive?: false; } export interface ImageBaseOverlayNonInteractiveProps extends ImageBaseOverlayCommonProps { /** - * По умолчанию сам компонент является интерактивным элементом. Передав значение `true`, можно отключить - * такое поведение, что дает возможность передавать отдельные интерактивные элементы в `children`. - * - * TODO [>=7]: для определения интерактивности завязываться на св-во `onClick` + * По умолчанию сам компонент является интерактивным элементом. Передав значение равное `'undefined'` или не передав этот параметр вовсе, можно отключить + * такое поведение, что дает возможность передавать отдельные интерактивные элементы в `children`.* */ - disableInteractive: true; onClick?: undefined; children: React.ReactNode; } From 8ebb07c80b5ece3937ffe077ed7a4529784740c2 Mon Sep 17 00:00:00 2001 From: "e.muhamethanov" Date: Thu, 19 Sep 2024 14:57:54 +0300 Subject: [PATCH 2/5] feat(ImageBaseOverlay): add codemod to transform ImageOverlay components from v6 to v7 --- packages/codemods/src/cli.ts | 2 +- .../image-overlay/image-base.input.tsx | 66 ++++++++ .../image-overlay/image.input.tsx | 66 ++++++++ .../__snapshots__/image-overlay.ts.snap | 101 ++++++++++++ .../transforms/v7/__tests__/image-overlay.ts | 11 ++ .../src/transforms/v7/image-overlay.ts | 150 ++++++++++++++++++ 6 files changed, 395 insertions(+), 1 deletion(-) create mode 100644 packages/codemods/src/transforms/v7/__testfixtures__/image-overlay/image-base.input.tsx create mode 100644 packages/codemods/src/transforms/v7/__testfixtures__/image-overlay/image.input.tsx create mode 100644 packages/codemods/src/transforms/v7/__tests__/__snapshots__/image-overlay.ts.snap create mode 100644 packages/codemods/src/transforms/v7/__tests__/image-overlay.ts create mode 100644 packages/codemods/src/transforms/v7/image-overlay.ts diff --git a/packages/codemods/src/cli.ts b/packages/codemods/src/cli.ts index 829e193be0..f644de2aa5 100644 --- a/packages/codemods/src/cli.ts +++ b/packages/codemods/src/cli.ts @@ -3,8 +3,8 @@ import { Command } from 'commander'; import prompts from 'prompts'; import { autoDetectVKUIVersion } from './autoDetectVKUIVersion'; import getAvailableCodemods from './getAvailableCodemods.js'; -import pkg from '../package.json'; import logger from './helpers/logger.js'; +import pkg from '../package.json'; export interface CliOptions { list: boolean; diff --git a/packages/codemods/src/transforms/v7/__testfixtures__/image-overlay/image-base.input.tsx b/packages/codemods/src/transforms/v7/__testfixtures__/image-overlay/image-base.input.tsx new file mode 100644 index 0000000000..a7e1c43faf --- /dev/null +++ b/packages/codemods/src/transforms/v7/__testfixtures__/image-overlay/image-base.input.tsx @@ -0,0 +1,66 @@ +import { ImageBase } from '@vkontakte/vkui'; +import { Icon12Add } from '@vkontakte/vkui-icons'; +import React from 'react'; + +const App = () => { + const callback = () => {}; + return ( + + {/* test 1: disableInteractive={true} -> remove disableInteractive and onClick */} + + + + + + + {/* test 2: disableInteractive -> remove disableInteractive and onClick */} + + + + + + + {/* test 3: disableInteractive={false} and onClick with Identifier -> remove disableInteractive, don't remove onClick */} + + + + + + + {/* test 4: disableInteractive={false} and onClick with ArrowFunction -> remove disableInteractive, don't remove onClick */} + + callback()} + > + + + + + ); +}; diff --git a/packages/codemods/src/transforms/v7/__testfixtures__/image-overlay/image.input.tsx b/packages/codemods/src/transforms/v7/__testfixtures__/image-overlay/image.input.tsx new file mode 100644 index 0000000000..eb5a01a6e8 --- /dev/null +++ b/packages/codemods/src/transforms/v7/__testfixtures__/image-overlay/image.input.tsx @@ -0,0 +1,66 @@ +import { Image } from '@vkontakte/vkui'; +import { Icon12Add } from '@vkontakte/vkui-icons'; +import React from 'react'; + +const App = () => { + const callback = () => {}; + return ( + + {/* test 1: disableInteractive={true} -> remove disableInteractive and onClick */} + Приложение шторм онлайн + + + + + + {/* test 2: disableInteractive -> remove disableInteractive and onClick */} + Приложение шторм онлайн + + + + + + {/* test 3: disableInteractive={false} and onClick with Identifier -> remove disableInteractive, don't remove onClick */} + Приложение шторм онлайн + + + + + + {/* test 4: disableInteractive={false} and onClick with ArrowFunction -> remove disableInteractive, don't remove onClick */} + Приложение шторм онлайн + callback()} + > + + + + + ); +}; diff --git a/packages/codemods/src/transforms/v7/__tests__/__snapshots__/image-overlay.ts.snap b/packages/codemods/src/transforms/v7/__tests__/__snapshots__/image-overlay.ts.snap new file mode 100644 index 0000000000..8e54830375 --- /dev/null +++ b/packages/codemods/src/transforms/v7/__tests__/__snapshots__/image-overlay.ts.snap @@ -0,0 +1,101 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`image-overlay transforms correctly 1`] = ` +"import { Image } from '@vkontakte/vkui'; +import { Icon12Add } from '@vkontakte/vkui-icons'; +import React from 'react'; + +const App = () => { + const callback = () => {}; + return ( + ( + {/* test 1: disableInteractive={true} -> remove disableInteractive and onClick */} + Приложение шторм онлайн + + + + + {/* test 2: disableInteractive -> remove disableInteractive and onClick */} + Приложение шторм онлайн + + + + + {/* test 3: disableInteractive={false} and onClick with Identifier -> remove disableInteractive, don't remove onClick */} + Приложение шторм онлайн + + + + + {/* test 4: disableInteractive={false} and onClick with ArrowFunction -> remove disableInteractive, don't remove onClick */} + Приложение шторм онлайн + callback()}> + + + + ) + ); +};" +`; + +exports[`image-overlay transforms correctly 2`] = ` +"import { ImageBase } from '@vkontakte/vkui'; +import { Icon12Add } from '@vkontakte/vkui-icons'; +import React from 'react'; + +const App = () => { + const callback = () => {}; + return ( + ( + {/* test 1: disableInteractive={true} -> remove disableInteractive and onClick */} + + + + + + {/* test 2: disableInteractive -> remove disableInteractive and onClick */} + + + + + + {/* test 3: disableInteractive={false} and onClick with Identifier -> remove disableInteractive, don't remove onClick */} + + + + + + {/* test 4: disableInteractive={false} and onClick with ArrowFunction -> remove disableInteractive, don't remove onClick */} + + callback()}> + + + + ) + ); +};" +`; diff --git a/packages/codemods/src/transforms/v7/__tests__/image-overlay.ts b/packages/codemods/src/transforms/v7/__tests__/image-overlay.ts new file mode 100644 index 0000000000..a88a304705 --- /dev/null +++ b/packages/codemods/src/transforms/v7/__tests__/image-overlay.ts @@ -0,0 +1,11 @@ +jest.autoMockOff(); +import { defineSnapshotTestFromFixture } from '../../../testHelpers/testHelper'; + +const name = 'image-overlay'; +const fixtures = ['image', 'image-base'] as const; + +describe(name, () => { + fixtures.forEach((test) => + defineSnapshotTestFromFixture(__dirname, name, global.TRANSFORM_OPTIONS, `${name}/${test}`), + ); +}); diff --git a/packages/codemods/src/transforms/v7/image-overlay.ts b/packages/codemods/src/transforms/v7/image-overlay.ts new file mode 100644 index 0000000000..5294986882 --- /dev/null +++ b/packages/codemods/src/transforms/v7/image-overlay.ts @@ -0,0 +1,150 @@ +import chalk from 'chalk'; +import { API, FileInfo, JSXAttribute, JSXSpreadAttribute } from 'jscodeshift'; +import { getImportInfo } from '../../codemod-helpers'; +import { report } from '../../report'; +import { JSCodeShiftOptions } from '../../types'; + +export const parser = 'tsx'; + +export default function transformer(file: FileInfo, api: API, options: JSCodeShiftOptions) { + const { alias } = options; + const j = api.jscodeshift; + const source = j(file.source); + const { localName: ImageLocalName } = getImportInfo(j, file, 'Image', alias); + const { localName: ImageBaseLocalName } = getImportInfo(j, file, 'ImageBase', alias); + + if (!ImageLocalName && !ImageBaseLocalName) { + return source.toSource(); + } + + const findAttribute = ( + attributes: Array | undefined, + attributeName: string, + ): JSXAttribute | undefined => { + return ( + (attributes?.find((attr) => { + return attr.type === 'JSXAttribute' && attr.name.name === attributeName; + }) as JSXAttribute) || null + ); + }; + + const showReport = (localName: string, additionalMessage: string) => { + report( + api, + `: ${chalk.white.bgBlue(`${localName}.Overlay`)} has been changed. Manual changes required: ${additionalMessage}`, + ); + }; + + const calcDisableInteractiveValue = ( + disableInteractiveAttribute: JSXAttribute, + ): boolean | null => { + if (disableInteractiveAttribute.value?.type === 'BooleanLiteral') { + return disableInteractiveAttribute.value.value; + } else if (disableInteractiveAttribute.value === null) { + return true; + } else if (disableInteractiveAttribute.value?.type === 'JSXExpressionContainer') { + const expression = disableInteractiveAttribute.value.expression; + if (expression.type === 'BooleanLiteral') { + return expression.value; + } + if (expression.type === 'Identifier' && expression.name === 'undefined') { + return false; + } + } + return null; + }; + + const removeAttribute = ( + attributes: Array | undefined, + attribute: JSXAttribute, + ) => { + attributes?.splice(attributes?.indexOf(attribute), 1); + }; + + const handleImageComponent = (localName: string) => { + source + .find(j.JSXElement, { + openingElement: { + name: { + type: 'JSXMemberExpression', + object: { name: localName }, + property: { name: 'Overlay' }, + }, + }, + }) + .forEach((path) => { + const overlay = path.node; + const overlayItemAttributes = overlay.openingElement.attributes; + + const onClickAttribute: JSXAttribute | undefined = findAttribute( + overlayItemAttributes, + 'onClick', + ); + const disableInteractiveAttribute: JSXAttribute | undefined = findAttribute( + overlayItemAttributes, + 'disableInteractive', + ); + + // Кейс, когда disableInteractive не был задан, значит overlay interactive. + // Сейчас у него обязательно должен быть onClick, чтобы не ломать обратную совместимость + + if (!disableInteractiveAttribute) { + // Проверяем наличие onClick, и если его нет, то пользователь должен добавить onClick + if (!onClickAttribute) { + showReport(localName, `need to add ${chalk.white.bgBlue('onClick')} prop`); + } + return; + } + // Рассчитываем значение disableInteractive в boolean + const disableInteractiveValue = calcDisableInteractiveValue(disableInteractiveAttribute); + if (disableInteractiveValue === null) { + // Если у disableInteractive используется сложное выражение + // То пользователь сам должен удалить этот проп, как ему нужно + showReport(localName, `need to remove ${chalk.white.bgBlue('disableInteractive')} prop`); + } + + // Удаляем аттрибут disableInteractive + removeAttribute(overlayItemAttributes, disableInteractiveAttribute); + + if (disableInteractiveValue) { + // Если disableInteractive = true, то все, что нам нужно это удалить атрибут onClick + // Важно: мы можем его спокойно удалить, так как в этом кейсе он может быть только undefined + if (onClickAttribute) { + removeAttribute(overlayItemAttributes, onClickAttribute); + } + return; + } + if (!onClickAttribute) { + // Если disableInteractive = false и onClick пропа нет, то пользователь должен его добавить + showReport(localName, `need to add ${chalk.white.bgBlue('onClick')} prop`); + return; + } + // Если disableInteractive = false и onClick не пустой надо обработать следующие кейс: + // onClick=undefined: надо добавить колбэк + // onClick=identifier: все хорошо,оставляем как есть + // В остальных случаях, надо чтобы пользователь убедился, что onClick устанавливается корректно + if (onClickAttribute.value?.type === 'JSXExpressionContainer') { + const expression = onClickAttribute.value.expression; + if (expression.type === 'Identifier') { + if (expression.name === 'undefined') { + showReport(localName, `need to add ${chalk.white.bgBlue('onClick')} prop`); + } + return; + } + if (expression.type === 'ArrowFunctionExpression') { + return; + } + } + showReport(localName, `validate ${chalk.white.bgBlue('onClick')} prop value`); + }); + }; + + if (ImageLocalName) { + handleImageComponent(ImageLocalName); + } + if (ImageBaseLocalName) { + handleImageComponent(ImageBaseLocalName); + } + + return source.toSource(); +} From dcc9f18453237083649a3d48058b87e354c3d7b6 Mon Sep 17 00:00:00 2001 From: "e.muhamethanov" Date: Fri, 20 Sep 2024 10:36:00 +0300 Subject: [PATCH 3/5] fix: run eslint --- packages/codemods/src/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/codemods/src/cli.ts b/packages/codemods/src/cli.ts index f644de2aa5..829e193be0 100644 --- a/packages/codemods/src/cli.ts +++ b/packages/codemods/src/cli.ts @@ -3,8 +3,8 @@ import { Command } from 'commander'; import prompts from 'prompts'; import { autoDetectVKUIVersion } from './autoDetectVKUIVersion'; import getAvailableCodemods from './getAvailableCodemods.js'; -import logger from './helpers/logger.js'; import pkg from '../package.json'; +import logger from './helpers/logger.js'; export interface CliOptions { list: boolean; From f5e808e34ff57131caa9900cad3f21dd971562a8 Mon Sep 17 00:00:00 2001 From: EldarMuhamethanov <61377022+EldarMuhamethanov@users.noreply.github.com> Date: Fri, 20 Sep 2024 17:24:48 +0300 Subject: [PATCH 4/5] fix: fix comment Co-authored-by: Andrey Medvedev --- .../vkui/src/components/ImageBase/ImageBaseOverlay/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vkui/src/components/ImageBase/ImageBaseOverlay/types.ts b/packages/vkui/src/components/ImageBase/ImageBaseOverlay/types.ts index c384258620..933983c4e6 100644 --- a/packages/vkui/src/components/ImageBase/ImageBaseOverlay/types.ts +++ b/packages/vkui/src/components/ImageBase/ImageBaseOverlay/types.ts @@ -46,7 +46,7 @@ export interface ImageBaseOverlayInteractiveProps extends ImageBaseOverlayCommon export interface ImageBaseOverlayNonInteractiveProps extends ImageBaseOverlayCommonProps { /** * По умолчанию сам компонент является интерактивным элементом. Передав значение равное `'undefined'` или не передав этот параметр вовсе, можно отключить - * такое поведение, что дает возможность передавать отдельные интерактивные элементы в `children`.* + * такое поведение, что дает возможность передавать отдельные интерактивные элементы в `children`. */ onClick?: undefined; children: React.ReactNode; From 7d619092edcd89bf2e9c810f54b73b7662df66a8 Mon Sep 17 00:00:00 2001 From: "e.muhamethanov" Date: Sat, 21 Sep 2024 19:30:14 +0300 Subject: [PATCH 5/5] fix: fix comments --- packages/codemods/src/codemod-helpers.ts | 8 +++++ .../src/transforms/v7/image-overlay.ts | 32 +++++++++++-------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/packages/codemods/src/codemod-helpers.ts b/packages/codemods/src/codemod-helpers.ts index 43f517797e..df831acc86 100644 --- a/packages/codemods/src/codemod-helpers.ts +++ b/packages/codemods/src/codemod-helpers.ts @@ -1,3 +1,4 @@ +import { JSXSpreadAttribute } from 'jscodeshift'; import type { API, Collection, @@ -91,6 +92,13 @@ export function swapBooleanValue( }); } +export const removeAttribute = ( + attributes: Array | undefined, + attribute: JSXAttribute, +) => { + attributes?.splice(attributes?.indexOf(attribute), 1); +}; + interface AttributeManipulatorAPI { keyTo?: string | ((k?: string) => string); reportText?: string | (() => string); diff --git a/packages/codemods/src/transforms/v7/image-overlay.ts b/packages/codemods/src/transforms/v7/image-overlay.ts index 5294986882..ec98fef52f 100644 --- a/packages/codemods/src/transforms/v7/image-overlay.ts +++ b/packages/codemods/src/transforms/v7/image-overlay.ts @@ -1,6 +1,6 @@ import chalk from 'chalk'; import { API, FileInfo, JSXAttribute, JSXSpreadAttribute } from 'jscodeshift'; -import { getImportInfo } from '../../codemod-helpers'; +import { getImportInfo, removeAttribute } from '../../codemod-helpers'; import { report } from '../../report'; import { JSCodeShiftOptions } from '../../types'; @@ -35,6 +35,13 @@ export default function transformer(file: FileInfo, api: API, options: JSCodeShi ); }; + const showDisableInteractivePropReport = (localName: string) => { + showReport( + localName, + `"disableInteractive" has been removed, please use "onClick" if you want to make ${localName}.Overlay interactive`, + ); + }; + const calcDisableInteractiveValue = ( disableInteractiveAttribute: JSXAttribute, ): boolean | null => { @@ -54,13 +61,6 @@ export default function transformer(file: FileInfo, api: API, options: JSCodeShi return null; }; - const removeAttribute = ( - attributes: Array | undefined, - attribute: JSXAttribute, - ) => { - attributes?.splice(attributes?.indexOf(attribute), 1); - }; - const handleImageComponent = (localName: string) => { source .find(j.JSXElement, { @@ -91,7 +91,10 @@ export default function transformer(file: FileInfo, api: API, options: JSCodeShi if (!disableInteractiveAttribute) { // Проверяем наличие onClick, и если его нет, то пользователь должен добавить onClick if (!onClickAttribute) { - showReport(localName, `need to add ${chalk.white.bgBlue('onClick')} prop`); + showReport( + localName, + `If you want to make ${localName}.Overlay interactive please add "onClick" prop`, + ); } return; } @@ -100,7 +103,7 @@ export default function transformer(file: FileInfo, api: API, options: JSCodeShi if (disableInteractiveValue === null) { // Если у disableInteractive используется сложное выражение // То пользователь сам должен удалить этот проп, как ему нужно - showReport(localName, `need to remove ${chalk.white.bgBlue('disableInteractive')} prop`); + showDisableInteractivePropReport(localName); } // Удаляем аттрибут disableInteractive @@ -116,7 +119,7 @@ export default function transformer(file: FileInfo, api: API, options: JSCodeShi } if (!onClickAttribute) { // Если disableInteractive = false и onClick пропа нет, то пользователь должен его добавить - showReport(localName, `need to add ${chalk.white.bgBlue('onClick')} prop`); + showDisableInteractivePropReport(localName); return; } // Если disableInteractive = false и onClick не пустой надо обработать следующие кейс: @@ -127,7 +130,7 @@ export default function transformer(file: FileInfo, api: API, options: JSCodeShi const expression = onClickAttribute.value.expression; if (expression.type === 'Identifier') { if (expression.name === 'undefined') { - showReport(localName, `need to add ${chalk.white.bgBlue('onClick')} prop`); + showDisableInteractivePropReport(localName); } return; } @@ -135,7 +138,10 @@ export default function transformer(file: FileInfo, api: API, options: JSCodeShi return; } } - showReport(localName, `validate ${chalk.white.bgBlue('onClick')} prop value`); + showReport( + localName, + `"disableInteractive" has been removed, please validate that "onClick" prop value not falsy`, + ); }); };