From 52d02b132e3dffa547891bd3984ab9a7f2c4deb2 Mon Sep 17 00:00:00 2001 From: yyh Date: Mon, 2 Mar 2026 19:56:15 +0800 Subject: [PATCH] feat(web): tighten overlay migration lint governance - narrow overlay-migration ignore scope to explicit legacy base file allowlist - replace directory-level react-refresh disable with allowExportNames for base UI primitives - extract long lint constants into eslint.constants.mjs for config readability - add overlay migration guide and link it from lint docs - refactor dropdown-menu internal popup helper to avoid react-refresh false positives --- .../base/ui/dropdown-menu/index.tsx | 40 +++++------ web/docs/lint.md | 2 + web/docs/overlay-migration.md | 50 +++++++++++++ web/eslint.config.mjs | 9 ++- web/eslint.constants.mjs | 72 +++++++++++++++++++ 5 files changed, 147 insertions(+), 26 deletions(-) create mode 100644 web/docs/overlay-migration.md create mode 100644 web/eslint.constants.mjs diff --git a/web/app/components/base/ui/dropdown-menu/index.tsx b/web/app/components/base/ui/dropdown-menu/index.tsx index 0c3effadc4..68d52f320e 100644 --- a/web/app/components/base/ui/dropdown-menu/index.tsx +++ b/web/app/components/base/ui/dropdown-menu/index.tsx @@ -35,7 +35,7 @@ type DropdownMenuPopupProps = Required - {children} - - ) + return renderDropdownMenuPopup({ + children, + placement, + sideOffset, + alignOffset, + className, + popupClassName, + }) } type DropdownMenuSubTriggerProps = React.ComponentPropsWithoutRef & { @@ -128,17 +125,14 @@ export function DropdownMenuSubContent({ className, popupClassName, }: DropdownMenuSubContentProps) { - return ( - - {children} - - ) + return renderDropdownMenuPopup({ + children, + placement, + sideOffset, + alignOffset, + className, + popupClassName, + }) } type DropdownMenuItemProps = React.ComponentPropsWithoutRef & { diff --git a/web/docs/lint.md b/web/docs/lint.md index a0ec9d58ad..1105d4af08 100644 --- a/web/docs/lint.md +++ b/web/docs/lint.md @@ -43,6 +43,8 @@ This command lints the entire project and is intended for final verification bef If a new rule causes many existing code errors or automatic fixes generate too many diffs, do not use the `--fix` option for automatic fixes. You can introduce the rule first, then use the `--suppress-all` option to temporarily suppress these errors, and gradually fix them in subsequent changes. +For overlay migration policy and cleanup phases, see [Overlay Migration Guide](./overlay-migration.md). + ## Type Check You should be able to see suggestions from TypeScript in your editor for all open files. diff --git a/web/docs/overlay-migration.md b/web/docs/overlay-migration.md new file mode 100644 index 0000000000..6f6fd22b89 --- /dev/null +++ b/web/docs/overlay-migration.md @@ -0,0 +1,50 @@ +# Overlay Migration Guide + +This document tracks the migration away from legacy `portal-to-follow-elem` APIs. + +## Scope + +- Deprecated API: `@/app/components/base/portal-to-follow-elem` +- Replacement primitives: + - `@/app/components/base/ui/tooltip` + - `@/app/components/base/ui/dropdown-menu` + - `@/app/components/base/ui/popover` + - `@/app/components/base/ui/dialog` + - `@/app/components/base/ui/select` +- Tracking issue: https://github.com/langgenius/dify/issues/32767 + +## ESLint policy + +- `no-restricted-imports` blocks new usage of `portal-to-follow-elem`. +- The rule is enabled for normal source files and test files are excluded. +- Legacy `app/components/base/*` callers are temporarily allowlisted in ESLint config. +- New files must not be added to the allowlist without migration owner approval. + +## Migration phases + +1. Business/UI features outside `app/components/base/**` + - Migrate old calls to semantic primitives. + - Keep `eslint-suppressions.json` stable or shrinking. +2. Legacy base components in allowlist + - Migrate allowlisted base callers gradually. + - Remove migrated files from allowlist immediately. +3. Cleanup + - Remove remaining suppressions for `no-restricted-imports`. + - Remove legacy `portal-to-follow-elem` implementation. + +## Suppression maintenance + +- After each migration batch, run: + +```sh +pnpm eslint --prune-suppressions --pass-on-unpruned-suppressions +``` + +- Never increase suppressions to bypass new code. +- Prefer direct migration over adding suppression entries. + +## React Refresh policy for base UI primitives + +- We keep primitive aliases (for example `DropdownMenu = Menu.Root`) in the same module. +- To avoid IDE noise, `react-refresh/only-export-components` is configured with explicit `allowExportNames` for the base UI primitive surface. +- Do not use file-level `eslint-disable` comments for this policy. diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index 2293cdc595..0c3f740f5a 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -6,6 +6,7 @@ import hyoban from 'eslint-plugin-hyoban' import sonar from 'eslint-plugin-sonarjs' import storybook from 'eslint-plugin-storybook' import dify from './eslint-rules/index.js' +import { BASE_UI_PRIMITIVE_EXPORT_NAMES, OVERLAY_MIGRATION_LEGACY_BASE_FILES } from './eslint.constants.mjs' // Enable Tailwind CSS IntelliSense mode for ESLint runs // See: tailwind-css-plugin.ts @@ -147,17 +148,19 @@ export default antfu( }, { name: 'dify/base-ui-primitives', - files: ['app/components/base/ui/**/*.ts', 'app/components/base/ui/**/*.tsx'], + files: ['app/components/base/ui/**/*.tsx'], rules: { - 'react-refresh/only-export-components': 'off', + 'react-refresh/only-export-components': ['error', { + allowExportNames: BASE_UI_PRIMITIVE_EXPORT_NAMES, + }], }, }, { name: 'dify/overlay-migration', files: [GLOB_TS, GLOB_TSX], ignores: [ - 'app/components/base/**', ...GLOB_TESTS, + ...OVERLAY_MIGRATION_LEGACY_BASE_FILES, ], rules: { 'no-restricted-imports': ['error', { diff --git a/web/eslint.constants.mjs b/web/eslint.constants.mjs new file mode 100644 index 0000000000..c880d3b97b --- /dev/null +++ b/web/eslint.constants.mjs @@ -0,0 +1,72 @@ +export const BASE_UI_PRIMITIVE_EXPORT_NAMES = [ + 'Dialog', + 'DialogClose', + 'DialogContent', + 'DialogDescription', + 'DialogTitle', + 'DialogTrigger', + 'DropdownMenu', + 'DropdownMenuCheckboxItem', + 'DropdownMenuCheckboxItemIndicator', + 'DropdownMenuContent', + 'DropdownMenuGroup', + 'DropdownMenuGroupLabel', + 'DropdownMenuItem', + 'DropdownMenuPortal', + 'DropdownMenuRadioGroup', + 'DropdownMenuRadioItem', + 'DropdownMenuRadioItemIndicator', + 'DropdownMenuSeparator', + 'DropdownMenuSub', + 'DropdownMenuSubContent', + 'DropdownMenuSubTrigger', + 'DropdownMenuTrigger', + 'Popover', + 'PopoverClose', + 'PopoverContent', + 'PopoverDescription', + 'PopoverTitle', + 'PopoverTrigger', + 'Select', + 'SelectContent', + 'SelectGroup', + 'SelectGroupLabel', + 'SelectItem', + 'SelectSeparator', + 'SelectTrigger', + 'SelectValue', + 'Tooltip', + 'TooltipContent', + 'TooltipProvider', + 'TooltipTrigger', +] + +export const OVERLAY_MIGRATION_LEGACY_BASE_FILES = [ + 'app/components/base/chat/chat-with-history/header/mobile-operation-dropdown.tsx', + 'app/components/base/chat/chat-with-history/header/operation.tsx', + 'app/components/base/chat/chat-with-history/inputs-form/view-form-dropdown.tsx', + 'app/components/base/chat/chat-with-history/sidebar/operation.tsx', + 'app/components/base/chat/chat/citation/popup.tsx', + 'app/components/base/chat/chat/citation/progress-tooltip.tsx', + 'app/components/base/chat/chat/citation/tooltip.tsx', + 'app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx', + 'app/components/base/chip/index.tsx', + 'app/components/base/date-and-time-picker/date-picker/index.tsx', + 'app/components/base/date-and-time-picker/time-picker/index.tsx', + 'app/components/base/dropdown/index.tsx', + 'app/components/base/features/new-feature-panel/file-upload/setting-modal.tsx', + 'app/components/base/features/new-feature-panel/text-to-speech/voice-settings.tsx', + 'app/components/base/file-uploader/file-from-link-or-local/index.tsx', + 'app/components/base/image-uploader/chat-image-uploader.tsx', + 'app/components/base/image-uploader/text-generation-image-uploader.tsx', + 'app/components/base/modal/modal.tsx', + 'app/components/base/prompt-editor/plugins/context-block/component.tsx', + 'app/components/base/prompt-editor/plugins/history-block/component.tsx', + 'app/components/base/select/custom.tsx', + 'app/components/base/select/index.tsx', + 'app/components/base/select/pure.tsx', + 'app/components/base/sort/index.tsx', + 'app/components/base/tag-management/filter.tsx', + 'app/components/base/theme-selector.tsx', + 'app/components/base/tooltip/index.tsx', +]