mirror of https://github.com/langgenius/dify.git
Merge branch 'main' into feat/hitl-frontend
This commit is contained in:
commit
0357530468
|
|
@ -480,4 +480,4 @@ const useButtonState = () => {
|
|||
### Related Skills
|
||||
|
||||
- `frontend-testing` - For testing refactored components
|
||||
- `web/testing/testing.md` - Testing specification
|
||||
- `web/docs/test.md` - Testing specification
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ description: Generate Vitest + React Testing Library tests for Dify frontend com
|
|||
|
||||
This skill enables Claude to generate high-quality, comprehensive frontend tests for the Dify project following established conventions and best practices.
|
||||
|
||||
> **⚠️ Authoritative Source**: This skill is derived from `web/testing/testing.md`. Use Vitest mock/timer APIs (`vi.*`).
|
||||
> **⚠️ Authoritative Source**: This skill is derived from `web/docs/test.md`. Use Vitest mock/timer APIs (`vi.*`).
|
||||
|
||||
## When to Apply This Skill
|
||||
|
||||
|
|
@ -309,7 +309,7 @@ For more detailed information, refer to:
|
|||
|
||||
### Primary Specification (MUST follow)
|
||||
|
||||
- **`web/testing/testing.md`** - The canonical testing specification. This skill is derived from this document.
|
||||
- **`web/docs/test.md`** - The canonical testing specification. This skill is derived from this document.
|
||||
|
||||
### Reference Examples in Codebase
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ This guide defines the workflow for generating tests, especially for complex com
|
|||
|
||||
## Scope Clarification
|
||||
|
||||
This guide addresses **multi-file workflow** (how to process multiple test files). For coverage requirements within a single test file, see `web/testing/testing.md` § Coverage Goals.
|
||||
This guide addresses **multi-file workflow** (how to process multiple test files). For coverage requirements within a single test file, see `web/docs/test.md` § Coverage Goals.
|
||||
|
||||
| Scope | Rule |
|
||||
|-------|------|
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ jobs:
|
|||
OPENDAL_FS_ROOT: /tmp/dify-storage
|
||||
run: |
|
||||
uv run --project api pytest \
|
||||
-n auto \
|
||||
--timeout "${PYTEST_TIMEOUT:-180}" \
|
||||
api/tests/integration_tests/workflow \
|
||||
api/tests/integration_tests/tools \
|
||||
|
|
|
|||
33
AGENTS.md
33
AGENTS.md
|
|
@ -7,7 +7,7 @@ Dify is an open-source platform for developing LLM applications with an intuitiv
|
|||
The codebase is split into:
|
||||
|
||||
- **Backend API** (`/api`): Python Flask application organized with Domain-Driven Design
|
||||
- **Frontend Web** (`/web`): Next.js 15 application using TypeScript and React 19
|
||||
- **Frontend Web** (`/web`): Next.js application using TypeScript and React
|
||||
- **Docker deployment** (`/docker`): Containerized deployment configurations
|
||||
|
||||
## Backend Workflow
|
||||
|
|
@ -18,36 +18,7 @@ The codebase is split into:
|
|||
|
||||
## Frontend Workflow
|
||||
|
||||
```bash
|
||||
cd web
|
||||
pnpm lint:fix
|
||||
pnpm type-check:tsgo
|
||||
pnpm test
|
||||
```
|
||||
|
||||
### Frontend Linting
|
||||
|
||||
ESLint is used for frontend code quality. Available commands:
|
||||
|
||||
```bash
|
||||
# Lint all files (report only)
|
||||
pnpm lint
|
||||
|
||||
# Lint and auto-fix issues
|
||||
pnpm lint:fix
|
||||
|
||||
# Lint specific files or directories
|
||||
pnpm lint:fix app/components/base/button/
|
||||
pnpm lint:fix app/components/base/button/index.tsx
|
||||
|
||||
# Lint quietly (errors only, no warnings)
|
||||
pnpm lint:quiet
|
||||
|
||||
# Check code complexity
|
||||
pnpm lint:complexity
|
||||
```
|
||||
|
||||
**Important**: Always run `pnpm lint:fix` before committing. The pre-commit hook runs `lint-staged` which only lints staged files.
|
||||
- Read `web/AGENTS.md` for details
|
||||
|
||||
## Testing & Quality Practices
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ How we prioritize:
|
|||
|
||||
For setting up the frontend service, please refer to our comprehensive [guide](https://github.com/langgenius/dify/blob/main/web/README.md) in the `web/README.md` file. This document provides detailed instructions to help you set up the frontend environment properly.
|
||||
|
||||
**Testing**: All React components must have comprehensive test coverage. See [web/testing/testing.md](https://github.com/langgenius/dify/blob/main/web/testing/testing.md) for the canonical frontend testing guidelines and follow every requirement described there.
|
||||
**Testing**: All React components must have comprehensive test coverage. See [web/docs/test.md](https://github.com/langgenius/dify/blob/main/web/docs/test.md) for the canonical frontend testing guidelines and follow every requirement described there.
|
||||
|
||||
#### Backend
|
||||
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -80,7 +80,7 @@ test:
|
|||
echo "Target: $(TARGET_TESTS)"; \
|
||||
uv run --project api --dev pytest $(TARGET_TESTS); \
|
||||
else \
|
||||
uv run --project api --dev dev/pytest/pytest_unit_tests.sh; \
|
||||
PYTEST_XDIST_ARGS="-n auto" uv run --project api --dev dev/pytest/pytest_unit_tests.sh; \
|
||||
fi
|
||||
@echo "✅ Tests complete"
|
||||
|
||||
|
|
|
|||
|
|
@ -175,6 +175,7 @@ dev = [
|
|||
# "locust>=2.40.4", # Temporarily removed due to compatibility issues. Uncomment when resolved.
|
||||
"sseclient-py>=1.8.0",
|
||||
"pytest-timeout>=2.4.0",
|
||||
"pytest-xdist>=3.8.0",
|
||||
]
|
||||
|
||||
############################################################
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from unittest.mock import MagicMock, patch
|
|||
|
||||
import pytest
|
||||
from flask import Flask
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
# Getting the absolute path of the current file's directory
|
||||
ABS_PATH = os.path.dirname(os.path.abspath(__file__))
|
||||
|
|
@ -36,6 +37,7 @@ import sys
|
|||
|
||||
sys.path.insert(0, PROJECT_DIR)
|
||||
|
||||
from core.db.session_factory import configure_session_factory, session_factory
|
||||
from extensions import ext_redis
|
||||
|
||||
|
||||
|
|
@ -102,3 +104,18 @@ def reset_secret_key():
|
|||
yield
|
||||
finally:
|
||||
dify_config.SECRET_KEY = original
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def _unit_test_engine():
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
yield engine
|
||||
engine.dispose()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _configure_session_factory(_unit_test_engine):
|
||||
try:
|
||||
session_factory.get_session_maker()
|
||||
except RuntimeError:
|
||||
configure_session_factory(_unit_test_engine, expire_on_commit=False)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,13 @@ def _load_app_module():
|
|||
|
||||
def schema_model(self, name, schema):
|
||||
self.models[name] = schema
|
||||
return schema
|
||||
|
||||
def model(self, name, model_dict=None, **kwargs):
|
||||
"""Register a model with the namespace (flask-restx compatibility)."""
|
||||
if model_dict is not None:
|
||||
self.models[name] = model_dict
|
||||
return model_dict
|
||||
|
||||
def _decorator(self, obj):
|
||||
return obj
|
||||
|
|
|
|||
24
api/uv.lock
24
api/uv.lock
|
|
@ -1479,6 +1479,7 @@ dev = [
|
|||
{ name = "pytest-env" },
|
||||
{ name = "pytest-mock" },
|
||||
{ name = "pytest-timeout" },
|
||||
{ name = "pytest-xdist" },
|
||||
{ name = "ruff" },
|
||||
{ name = "scipy-stubs" },
|
||||
{ name = "sseclient-py" },
|
||||
|
|
@ -1678,6 +1679,7 @@ dev = [
|
|||
{ name = "pytest-env", specifier = "~=1.1.3" },
|
||||
{ name = "pytest-mock", specifier = "~=3.14.0" },
|
||||
{ name = "pytest-timeout", specifier = ">=2.4.0" },
|
||||
{ name = "pytest-xdist", specifier = ">=3.8.0" },
|
||||
{ name = "ruff", specifier = "~=0.14.0" },
|
||||
{ name = "scipy-stubs", specifier = ">=1.15.3.0" },
|
||||
{ name = "sseclient-py", specifier = ">=1.8.0" },
|
||||
|
|
@ -1896,6 +1898,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/19/d8/2a1c638d9e0aa7e269269a1a1bf423ddd94267f1a01bbe3ad03432b67dd4/eval_type_backport-0.3.0-py3-none-any.whl", hash = "sha256:975a10a0fe333c8b6260d7fdb637698c9a16c3a9e3b6eb943fee6a6f67a37fe8", size = 6061, upload-time = "2025-11-13T20:56:49.499Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "execnet"
|
||||
version = "2.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "faker"
|
||||
version = "38.2.0"
|
||||
|
|
@ -5141,6 +5152,19 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-xdist"
|
||||
version = "3.8.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "execnet" },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-calamine"
|
||||
version = "0.5.4"
|
||||
|
|
|
|||
|
|
@ -5,6 +5,12 @@ SCRIPT_DIR="$(dirname "$(realpath "$0")")"
|
|||
cd "$SCRIPT_DIR/../.."
|
||||
|
||||
PYTEST_TIMEOUT="${PYTEST_TIMEOUT:-20}"
|
||||
PYTEST_XDIST_ARGS="${PYTEST_XDIST_ARGS:--n auto}"
|
||||
|
||||
# libs
|
||||
pytest --timeout "${PYTEST_TIMEOUT}" api/tests/unit_tests
|
||||
# Run most tests in parallel (excluding controllers which have import conflicts with xdist)
|
||||
# Controller tests have module-level side effects (Flask route registration) that cause
|
||||
# race conditions when imported concurrently by multiple pytest-xdist workers.
|
||||
pytest --timeout "${PYTEST_TIMEOUT}" ${PYTEST_XDIST_ARGS} api/tests/unit_tests --ignore=api/tests/unit_tests/controllers
|
||||
|
||||
# Run controller tests sequentially to avoid import race conditions
|
||||
pytest --timeout "${PYTEST_TIMEOUT}" api/tests/unit_tests/controllers
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
## Frontend Workflow
|
||||
|
||||
- Refer to the `./docs/test.md` and `./docs/lint.md` for detailed frontend workflow instructions.
|
||||
|
||||
## Automated Test Generation
|
||||
|
||||
- Use `web/testing/testing.md` as the canonical instruction set for generating frontend automated tests.
|
||||
- Use `./docs/test.md` as the canonical instruction set for generating frontend automated tests.
|
||||
- When proposing or saving tests, re-read that document and follow every requirement.
|
||||
- All frontend tests MUST also comply with the `frontend-testing` skill. Treat the skill as a mandatory constraint, not optional guidance.
|
||||
|
|
|
|||
|
|
@ -107,6 +107,8 @@ Open [http://localhost:6006](http://localhost:6006) with your browser to see the
|
|||
|
||||
If your IDE is VSCode, rename `web/.vscode/settings.example.json` to `web/.vscode/settings.json` for lint code setting.
|
||||
|
||||
Then follow the [Lint Documentation](./docs/lint.md) to lint the code.
|
||||
|
||||
## Test
|
||||
|
||||
We use [Vitest](https://vitest.dev/) and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) for Unit Testing.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'
|
|||
import { cn } from '@/utils/classnames'
|
||||
import Button from '../base/button'
|
||||
import Tooltip from '../base/tooltip'
|
||||
import { getKeyboardKeyNameBySystem } from '../workflow/utils'
|
||||
import ShortcutsName from '../workflow/shortcuts-name'
|
||||
|
||||
type TooltipContentProps = {
|
||||
expand: boolean
|
||||
|
|
@ -20,18 +20,7 @@ const TooltipContent = ({
|
|||
return (
|
||||
<div className="flex items-center gap-x-1">
|
||||
<span className="system-xs-medium px-0.5 text-text-secondary">{expand ? t('sidebar.collapseSidebar', { ns: 'layout' }) : t('sidebar.expandSidebar', { ns: 'layout' })}</span>
|
||||
<div className="flex items-center gap-x-0.5">
|
||||
{
|
||||
TOGGLE_SHORTCUT.map(key => (
|
||||
<span
|
||||
key={key}
|
||||
className="system-kbd inline-flex items-center justify-center rounded-[4px] bg-components-kbd-bg-gray px-1 text-text-tertiary"
|
||||
>
|
||||
{getKeyboardKeyNameBySystem(key)}
|
||||
</span>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<ShortcutsName keys={TOGGLE_SHORTCUT} textColor="secondary" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ import Divider from '../../base/divider'
|
|||
import Loading from '../../base/loading'
|
||||
import Toast from '../../base/toast'
|
||||
import Tooltip from '../../base/tooltip'
|
||||
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '../../workflow/utils'
|
||||
import ShortcutsName from '../../workflow/shortcuts-name'
|
||||
import { getKeyboardKeyCodeBySystem } from '../../workflow/utils'
|
||||
import AccessControl from '../app-access-control'
|
||||
import PublishWithMultipleModel from './publish-with-multiple-model'
|
||||
import SuggestedAction from './suggested-action'
|
||||
|
|
@ -342,13 +343,7 @@ const AppPublisher = ({
|
|||
: (
|
||||
<div className="flex gap-1">
|
||||
<span>{t('common.publishUpdate', { ns: 'workflow' })}</span>
|
||||
<div className="flex gap-0.5">
|
||||
{PUBLISH_SHORTCUT.map(key => (
|
||||
<span key={key} className="system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white text-text-primary-on-surface">
|
||||
{getKeyboardKeyNameBySystem(key)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<ShortcutsName keys={PUBLISH_SHORTCUT} bgColor="white" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client'
|
||||
|
||||
import type { AppIconSelection } from '../../base/app-icon-picker'
|
||||
import { RiArrowRightLine, RiArrowRightSLine, RiCommandLine, RiCornerDownLeftLine, RiExchange2Fill } from '@remixicon/react'
|
||||
import { RiArrowRightLine, RiArrowRightSLine, RiExchange2Fill } from '@remixicon/react'
|
||||
|
||||
import { useDebounceFn, useKeyPress } from 'ahooks'
|
||||
import Image from 'next/image'
|
||||
|
|
@ -29,6 +29,7 @@ import { getRedirection } from '@/utils/app-redirection'
|
|||
import { cn } from '@/utils/classnames'
|
||||
import { basePath } from '@/utils/var'
|
||||
import AppIconPicker from '../../base/app-icon-picker'
|
||||
import ShortcutsName from '../../workflow/shortcuts-name'
|
||||
|
||||
type CreateAppProps = {
|
||||
onSuccess: () => void
|
||||
|
|
@ -269,10 +270,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
|||
<Button onClick={onClose}>{t('newApp.Cancel', { ns: 'app' })}</Button>
|
||||
<Button disabled={isAppsFull || !name} className="gap-1" variant="primary" onClick={handleCreateApp}>
|
||||
<span>{t('newApp.Create', { ns: 'app' })}</span>
|
||||
<div className="flex gap-0.5">
|
||||
<RiCommandLine size={14} className="system-kbd rounded-sm bg-components-kbd-bg-white p-0.5" />
|
||||
<RiCornerDownLeftLine size={14} className="system-kbd rounded-sm bg-components-kbd-bg-white p-0.5" />
|
||||
</div>
|
||||
<ShortcutsName keys={['ctrl', '↵']} bgColor="white" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
'use client'
|
||||
|
||||
import type { MouseEventHandler } from 'react'
|
||||
import { RiCloseLine, RiCommandLine, RiCornerDownLeftLine } from '@remixicon/react'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useDebounceFn, useKeyPress } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
|
@ -28,6 +28,7 @@ import {
|
|||
} from '@/service/apps'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import ShortcutsName from '../../workflow/shortcuts-name'
|
||||
import Uploader from './uploader'
|
||||
|
||||
type CreateFromDSLModalProps = {
|
||||
|
|
@ -298,10 +299,7 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
|||
className="gap-1"
|
||||
>
|
||||
<span>{t('newApp.Create', { ns: 'app' })}</span>
|
||||
<div className="flex gap-0.5">
|
||||
<RiCommandLine size={14} className="system-kbd rounded-sm bg-components-kbd-bg-white p-0.5" />
|
||||
<RiCornerDownLeftLine size={14} className="system-kbd rounded-sm bg-components-kbd-bg-white p-0.5" />
|
||||
</div>
|
||||
<ShortcutsName keys={['ctrl', '↵']} bgColor="white" />
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import * as React from 'react'
|
|||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
|
||||
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
||||
import { getKeyboardKeyCodeBySystem } from '@/app/components/workflow/utils'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
import { useDocumentContext } from '../../context'
|
||||
|
||||
|
|
@ -54,7 +55,7 @@ const ActionButtons: FC<IActionButtonsProps> = ({
|
|||
>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<span className="system-sm-medium text-components-button-secondary-text">{t('operation.cancel', { ns: 'common' })}</span>
|
||||
<span className="system-kbd rounded-[4px] bg-components-kbd-bg-gray px-[1px] text-text-tertiary">ESC</span>
|
||||
<ShortcutsName keys={['ESC']} textColor="secondary" />
|
||||
</div>
|
||||
</Button>
|
||||
{(isParentChildParagraphMode && actionType === 'edit' && !isChildChunk && showRegenerationButton)
|
||||
|
|
@ -76,10 +77,7 @@ const ActionButtons: FC<IActionButtonsProps> = ({
|
|||
>
|
||||
<div className="flex items-center gap-x-1">
|
||||
<span className="text-components-button-primary-text">{t('operation.save', { ns: 'common' })}</span>
|
||||
<div className="flex items-center gap-x-0.5">
|
||||
<span className="system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white capitalize text-text-primary-on-surface">{getKeyboardKeyNameBySystem('ctrl')}</span>
|
||||
<span className="system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white text-text-primary-on-surface">S</span>
|
||||
</div>
|
||||
<ShortcutsName keys={['ctrl', 'S']} bgColor="white" />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client'
|
||||
import type { AppIconType } from '@/types/app'
|
||||
import { RiCloseLine, RiCommandLine, RiCornerDownLeftLine } from '@remixicon/react'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useDebounceFn, useKeyPress } from 'ahooks'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
|
|
@ -17,6 +17,7 @@ import AppsFull from '@/app/components/billing/apps-full-in-dialog'
|
|||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import AppIconPicker from '../../base/app-icon-picker'
|
||||
import ShortcutsName from '../../workflow/shortcuts-name'
|
||||
|
||||
export type CreateAppModalProps = {
|
||||
show: boolean
|
||||
|
|
@ -198,10 +199,7 @@ const CreateAppModal = ({
|
|||
onClick={handleSubmit}
|
||||
>
|
||||
<span>{!isEditModal ? t('operation.create', { ns: 'common' }) : t('operation.save', { ns: 'common' })}</span>
|
||||
<div className="flex gap-0.5">
|
||||
<RiCommandLine size={14} className="system-kbd rounded-sm bg-components-kbd-bg-white p-0.5" />
|
||||
<RiCornerDownLeftLine size={14} className="system-kbd rounded-sm bg-components-kbd-bg-white p-0.5" />
|
||||
</div>
|
||||
<ShortcutsName keys={['ctrl', '↵']} bgColor="white" />
|
||||
</Button>
|
||||
<Button className="w-24" onClick={onHide}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import { getKeyboardKeyCodeBySystem, isEventTargetInputArea, isMac } from '@/app/components/workflow/utils/common'
|
||||
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
||||
import { getKeyboardKeyCodeBySystem, isEventTargetInputArea } from '@/app/components/workflow/utils/common'
|
||||
import { selectWorkflowNode } from '@/app/components/workflow/utils/node-navigation'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import InstallFromMarketplace from '../plugins/install-plugin/install-from-marketplace'
|
||||
|
|
@ -356,14 +357,7 @@ const GotoAnything: FC<Props> = ({
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-text-quaternary">
|
||||
<span className="system-kbd rounded bg-gray-200 px-1 py-[2px] font-mono text-gray-700 dark:bg-gray-800 dark:text-gray-100">
|
||||
{isMac() ? '⌘' : 'Ctrl'}
|
||||
</span>
|
||||
<span className="system-kbd ml-1 rounded bg-gray-200 px-1 py-[2px] font-mono text-gray-700 dark:bg-gray-800 dark:text-gray-100">
|
||||
K
|
||||
</span>
|
||||
</div>
|
||||
<ShortcutsName keys={['ctrl', 'K']} textColor="secondary" />
|
||||
</div>
|
||||
|
||||
<Command.List className="h-[240px] overflow-y-auto">
|
||||
|
|
|
|||
|
|
@ -28,11 +28,12 @@ import { useToastContext } from '@/app/components/base/toast'
|
|||
import {
|
||||
useChecklistBeforePublish,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '@/app/components/workflow/store'
|
||||
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
|
||||
import { getKeyboardKeyCodeBySystem } from '@/app/components/workflow/utils'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
|
|
@ -253,13 +254,7 @@ const Popup = () => {
|
|||
: (
|
||||
<div className="flex gap-1">
|
||||
<span>{t('common.publishUpdate', { ns: 'workflow' })}</span>
|
||||
<div className="flex gap-0.5">
|
||||
{PUBLISH_SHORTCUT.map(key => (
|
||||
<span key={key} className="system-kbd h-4 w-4 rounded-[4px] bg-components-kbd-bg-white text-text-primary-on-surface">
|
||||
{getKeyboardKeyNameBySystem(key)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<ShortcutsName keys={PUBLISH_SHORTCUT} bgColor="white" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import { useCallback } from 'react'
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { StopCircle } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { useWorkflowRun, useWorkflowStartRun } from '@/app/components/workflow/hooks'
|
||||
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import { getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
|
||||
import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
|
@ -78,14 +78,7 @@ const RunMode = ({
|
|||
)}
|
||||
{
|
||||
!isDisabled && (
|
||||
<div className="system-kbd flex items-center gap-x-0.5 text-text-tertiary">
|
||||
<div className="flex size-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray">
|
||||
{getKeyboardKeyNameBySystem('alt')}
|
||||
</div>
|
||||
<div className="flex size-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray">
|
||||
R
|
||||
</div>
|
||||
</div>
|
||||
<ShortcutsName keys={['alt', 'R']} textColor="secondary" />
|
||||
)
|
||||
}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import StartNodeSelectionPanel from './start-node-selection-panel'
|
||||
|
||||
|
|
@ -75,9 +76,7 @@ const WorkflowOnboardingModal: FC<WorkflowOnboardingModalProps> = ({
|
|||
{isShow && (
|
||||
<div className="body-xs-regular pointer-events-none fixed left-1/2 top-1/2 z-[70] flex -translate-x-1/2 translate-y-[165px] items-center gap-1 text-text-quaternary">
|
||||
<span>{t('onboarding.escTip.press', { ns: 'workflow' })}</span>
|
||||
<kbd className="system-kbd inline-flex h-4 min-w-4 items-center justify-center rounded bg-components-kbd-bg-gray px-1 text-text-tertiary">
|
||||
{t('onboarding.escTip.key', { ns: 'workflow' })}
|
||||
</kbd>
|
||||
<ShortcutsName keys={[t('onboarding.escTip.key', { ns: 'workflow' })]} textColor="secondary" />
|
||||
<span>{t('onboarding.escTip.toDismiss', { ns: 'workflow' })}</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ import { trackEvent } from '@/app/components/base/amplitude'
|
|||
import { StopCircle } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useWorkflowRun, useWorkflowRunValidation, useWorkflowStartRun } from '@/app/components/workflow/hooks'
|
||||
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import { getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
|
||||
import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
|
@ -136,14 +136,7 @@ const RunMode = ({
|
|||
>
|
||||
<RiPlayLargeLine className="mr-1 size-4" />
|
||||
{text ?? t('common.run', { ns: 'workflow' })}
|
||||
<div className="system-kbd flex items-center gap-x-0.5 text-text-tertiary">
|
||||
<div className="flex size-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray">
|
||||
{getKeyboardKeyNameBySystem('alt')}
|
||||
</div>
|
||||
<div className="flex size-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray">
|
||||
R
|
||||
</div>
|
||||
</div>
|
||||
<ShortcutsName keys={['alt', 'R']} textColor="secondary" />
|
||||
</div>
|
||||
</TestRunMenu>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import useTheme from '@/hooks/use-theme'
|
|||
import { cn } from '@/utils/classnames'
|
||||
import Button from '../../base/button'
|
||||
import Tooltip from '../../base/tooltip'
|
||||
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '../utils'
|
||||
import ShortcutsName from '../shortcuts-name'
|
||||
import { getKeyboardKeyCodeBySystem } from '../utils'
|
||||
|
||||
type VersionHistoryButtonProps = {
|
||||
onClick: () => Promise<unknown> | unknown
|
||||
|
|
@ -23,16 +24,7 @@ const PopupContent = React.memo(() => {
|
|||
<div className="system-xs-medium px-0.5 text-text-secondary">
|
||||
{t('common.versionHistory', { ns: 'workflow' })}
|
||||
</div>
|
||||
<div className="flex items-center gap-x-0.5">
|
||||
{VERSION_HISTORY_SHORTCUT.map(key => (
|
||||
<span
|
||||
key={key}
|
||||
className="system-kbd rounded-[4px] bg-components-kbd-bg-white px-[1px] text-text-tertiary"
|
||||
>
|
||||
{getKeyboardKeyNameBySystem(key)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<ShortcutsName keys={VERSION_HISTORY_SHORTCUT} bgColor="gray" textColor="secondary" />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import { useKeyPress } from 'ahooks'
|
|||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
|
||||
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
||||
import { getKeyboardKeyCodeBySystem } from '@/app/components/workflow/utils'
|
||||
|
||||
type AdvancedActionsProps = {
|
||||
isConfirmDisabled: boolean
|
||||
|
|
@ -11,15 +12,6 @@ type AdvancedActionsProps = {
|
|||
onConfirm: () => void
|
||||
}
|
||||
|
||||
const Key = (props: { keyName: string }) => {
|
||||
const { keyName } = props
|
||||
return (
|
||||
<kbd className="system-kbd flex h-4 min-w-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-white px-px text-text-primary-on-surface">
|
||||
{keyName}
|
||||
</kbd>
|
||||
)
|
||||
}
|
||||
|
||||
const AdvancedActions: FC<AdvancedActionsProps> = ({
|
||||
isConfirmDisabled,
|
||||
onCancel,
|
||||
|
|
@ -48,10 +40,7 @@ const AdvancedActions: FC<AdvancedActionsProps> = ({
|
|||
onClick={onConfirm}
|
||||
>
|
||||
<span>{t('operation.confirm', { ns: 'common' })}</span>
|
||||
<div className="flex items-center gap-x-0.5">
|
||||
<Key keyName={getKeyboardKeyNameBySystem('ctrl')} />
|
||||
<Key keyName="⏎" />
|
||||
</div>
|
||||
<ShortcutsName keys={['ctrl', '⏎']} bgColor="white" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ type ShortcutsNameProps = {
|
|||
keys: string[]
|
||||
className?: string
|
||||
textColor?: 'default' | 'secondary'
|
||||
bgColor?: 'gray' | 'white'
|
||||
}
|
||||
const ShortcutsName = ({
|
||||
keys,
|
||||
className,
|
||||
textColor = 'default',
|
||||
bgColor = 'gray',
|
||||
}: ShortcutsNameProps) => {
|
||||
return (
|
||||
<div className={cn(
|
||||
|
|
@ -23,7 +25,9 @@ const ShortcutsName = ({
|
|||
<div
|
||||
key={key}
|
||||
className={cn(
|
||||
'system-kbd flex h-4 min-w-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray capitalize',
|
||||
'system-kbd flex h-4 min-w-4 items-center justify-center rounded-[4px] px-1 capitalize',
|
||||
bgColor === 'gray' && 'bg-components-kbd-bg-gray',
|
||||
bgColor === 'white' && 'bg-components-kbd-bg-white text-text-primary-on-surface',
|
||||
textColor === 'secondary' && 'text-text-tertiary',
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
# Lint Guide
|
||||
|
||||
We use ESLint and Typescript to maintain code quality and consistency across the project.
|
||||
|
||||
## ESLint
|
||||
|
||||
### Common Flags
|
||||
|
||||
**File/folder targeting**: Append paths to lint specific files or directories.
|
||||
|
||||
```sh
|
||||
pnpm eslint [options] file.js [file.js] [dir]
|
||||
```
|
||||
|
||||
**`--cache`**: Caches lint results for faster subsequent runs. Keep this enabled by default; only disable when you encounter unexpected lint results.
|
||||
|
||||
**`--concurrency`**: Enables multi-threaded linting. Use `--concurrency=auto` or experiment with specific numbers to find the optimal setting for your machine. Keep this enabled when linting multiple files.
|
||||
|
||||
- [ESLint multi-thread linting blog post](https://eslint.org/blog/2025/08/multithread-linting/)
|
||||
|
||||
**`--fix`**: Automatically fixes auto-fixable rule violations. Always review the diff before committing to ensure no unintended changes.
|
||||
|
||||
**`--quiet`**: Suppresses warnings and only shows errors. Useful when you want to reduce noise from existing issues.
|
||||
|
||||
**`--suppress-all`**: Temporarily suppresses error-level violations and records them, allowing CI to pass. Treat this as an escape hatch—fix these errors when time permits.
|
||||
|
||||
**`--prune-suppressions`**: Removes outdated suppressions after you've fixed the underlying errors.
|
||||
|
||||
- [ESLint bulk suppressions blog post](https://eslint.org/blog/2025/04/introducing-bulk-suppressions/)
|
||||
|
||||
### Type-Aware Linting
|
||||
|
||||
Some ESLint rules require type information, such as [no-leaked-conditional-rendering](https://www.eslint-react.xyz/docs/rules/no-leaked-conditional-rendering). However, [typed linting via typescript-eslint](https://typescript-eslint.io/getting-started/typed-linting) is too slow for practical use, so we use [TSSLint](https://github.com/johnsoncodehk/tsslint) instead.
|
||||
|
||||
```sh
|
||||
pnpm lint:tss
|
||||
```
|
||||
|
||||
This command lints the entire project and is intended for final verification before committing or pushing changes.
|
||||
|
||||
## Type Check
|
||||
|
||||
You should be able to see suggestions from TypeScript in your editor for all open files.
|
||||
|
||||
However, it can be useful to run the TypeScript 7 command-line (tsgo) to type check all files:
|
||||
|
||||
```sh
|
||||
pnpm type-check:tsgo
|
||||
```
|
||||
|
||||
Prefer using `tsgo` for type checking as it is significantly faster than the standard TypeScript compiler. Only fall back to `pnpm type-check` (which uses `tsc`) if you encounter unexpected results.
|
||||
|
|
@ -360,11 +360,11 @@ describe('ComponentName', () => {
|
|||
let mockPortalOpenState = false
|
||||
|
||||
vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
|
||||
PortalToFollowElem: ({ children, open, ...props }: any) => {
|
||||
PortalToFollowElem: ({ children, open, ...props }) => {
|
||||
mockPortalOpenState = open || false // Update shared state
|
||||
return <div data-open={open}>{children}</div>
|
||||
},
|
||||
PortalToFollowElemContent: ({ children }: any) => {
|
||||
PortalToFollowElemContent: ({ children }) => {
|
||||
// ✅ Matches actual: returns null when open is false
|
||||
if (!mockPortalOpenState)
|
||||
return null
|
||||
|
|
@ -4371,11 +4371,6 @@
|
|||
"count": 10
|
||||
}
|
||||
},
|
||||
"testing/testing.md": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"types/app.ts": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
|
|
|
|||
|
|
@ -337,7 +337,7 @@ Test file under review:
|
|||
${testPath}
|
||||
|
||||
Checklist (ensure every item is addressed in your review):
|
||||
- Confirm the tests satisfy all requirements listed above and in web/testing/TESTING.md.
|
||||
- Confirm the tests satisfy all requirements listed above and in web/docs/test.md.
|
||||
- Verify Arrange → Act → Assert structure, mocks, and cleanup follow project conventions.
|
||||
- Ensure all detected component features (state, effects, routing, API, events, etc.) are exercised, including edge cases and error paths.
|
||||
- Check coverage of prop variations, null/undefined inputs, and high-priority workflows implied by usage score.
|
||||
|
|
@ -382,7 +382,7 @@ Examples:
|
|||
# Review existing test
|
||||
pnpm analyze-component app/components/base/button/index.tsx --review
|
||||
|
||||
For complete testing guidelines, see: web/testing/testing.md
|
||||
For complete testing guidelines, see: web/docs/test.md
|
||||
`)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue