mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 12:59:18 +08:00
fix(web): align main nav defaults
Default integrations to the model provider section and route the main nav entry there. Hide cloud-only workspace credits and upgrade actions outside cloud edition. Add the repo-local karpathy-guidelines skill.
This commit is contained in:
parent
1e0fe6c939
commit
baaae00fa8
33
.agents/skills/karpathy-guidelines/SKILL.md
Normal file
33
.agents/skills/karpathy-guidelines/SKILL.md
Normal file
@ -0,0 +1,33 @@
|
||||
---
|
||||
name: karpathy-guidelines
|
||||
description: Lightweight coding guardrails for making focused, simple, and verifiable changes in this repo. Use for all coding work.
|
||||
---
|
||||
|
||||
# Karpathy Guidelines
|
||||
|
||||
Use this skill whenever you touch code in this repository.
|
||||
|
||||
## Principles
|
||||
|
||||
- Keep the change small and directly tied to the user request.
|
||||
- Prefer the simplest implementation that fits the existing codebase.
|
||||
- Read the nearby code first, then match its patterns.
|
||||
- Avoid unrelated refactors, broad rewrites, or style churn.
|
||||
- Preserve existing behavior unless the user explicitly asked to change it.
|
||||
- Treat regressions as a signal to narrow the change, not to add workaround layers.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Inspect the current implementation and tests around the change.
|
||||
2. Make the smallest coherent edit.
|
||||
3. Add or update focused tests when the behavior changes or the risk is non-trivial.
|
||||
4. Run the narrowest relevant verification first.
|
||||
5. Report exactly what was verified and anything left unverified.
|
||||
|
||||
## Review Checklist
|
||||
|
||||
- Does this change solve the stated problem without expanding scope?
|
||||
- Did it preserve existing route/component/data-flow semantics?
|
||||
- Are new abstractions justified by real complexity?
|
||||
- Are tests focused on the behavior that could regress?
|
||||
- Are unrelated files and generated artifacts left alone?
|
||||
@ -74,6 +74,14 @@ vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => (path: string) => `https://docs.dify.ai${path}`,
|
||||
}))
|
||||
|
||||
vi.mock('@/config', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/config')>()
|
||||
return {
|
||||
...actual,
|
||||
IS_CLOUD_EDITION: true,
|
||||
}
|
||||
})
|
||||
|
||||
const mockPush = vi.fn()
|
||||
const mockSetShowPricingModal = vi.fn()
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
@ -198,7 +206,7 @@ describe('MainNav', () => {
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.home/ })).toHaveAttribute('href', '/explore/apps')
|
||||
expect(screen.getByRole('link', { name: /common.menus.apps/ })).toHaveAttribute('href', '/apps')
|
||||
expect(screen.getByRole('link', { name: /common.menus.datasets/ })).toHaveAttribute('href', '/datasets')
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.integrations/ })).toHaveAttribute('href', '/tools')
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.integrations/ })).toHaveAttribute('href', '/tools?section=provider')
|
||||
expect(screen.getByRole('link', { name: /common.mainNav.marketplace/ })).toHaveAttribute('href', '/plugins')
|
||||
})
|
||||
|
||||
|
||||
@ -0,0 +1,110 @@
|
||||
import type { AppContextValue } from '@/context/app-context'
|
||||
import type { ModalContextState } from '@/context/modal-context'
|
||||
import type { ProviderContextState } from '@/context/provider-context'
|
||||
import { screen } from '@testing-library/react'
|
||||
import { renderWithSystemFeatures } from '@/__tests__/utils/mock-system-features'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useWorkspacesContext } from '@/context/workspace-context'
|
||||
import WorkspaceCard from '../workspace-card'
|
||||
|
||||
vi.mock('@/config', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/config')>()
|
||||
return {
|
||||
...actual,
|
||||
IS_CLOUD_EDITION: false,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/workspace-context', () => ({
|
||||
useWorkspacesContext: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
switchWorkspace: vi.fn(),
|
||||
}))
|
||||
|
||||
const appContextValue: AppContextValue = {
|
||||
userProfile: {
|
||||
id: 'user-1',
|
||||
name: 'Evan Z',
|
||||
email: 'evan@example.com',
|
||||
avatar: '',
|
||||
avatar_url: '',
|
||||
is_password_set: true,
|
||||
},
|
||||
mutateUserProfile: vi.fn(),
|
||||
currentWorkspace: {
|
||||
id: 'workspace-1',
|
||||
name: 'Solar Studio',
|
||||
plan: Plan.sandbox,
|
||||
status: 'normal',
|
||||
created_at: 0,
|
||||
role: 'owner',
|
||||
providers: [],
|
||||
trial_credits: 10000,
|
||||
trial_credits_used: 2500,
|
||||
next_credit_reset_date: 0,
|
||||
},
|
||||
isCurrentWorkspaceManager: true,
|
||||
isCurrentWorkspaceOwner: true,
|
||||
isCurrentWorkspaceEditor: true,
|
||||
isCurrentWorkspaceDatasetOperator: false,
|
||||
mutateCurrentWorkspace: vi.fn(),
|
||||
langGeniusVersionInfo: {
|
||||
current_env: 'testing',
|
||||
current_version: '1.0.0',
|
||||
latest_version: '1.0.0',
|
||||
release_date: '',
|
||||
release_notes: '',
|
||||
version: '1.0.0',
|
||||
can_auto_update: false,
|
||||
},
|
||||
useSelector: vi.fn(),
|
||||
isLoadingCurrentWorkspace: false,
|
||||
isValidatingCurrentWorkspace: false,
|
||||
}
|
||||
|
||||
describe('WorkspaceCard', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(useAppContext).mockReturnValue(appContextValue)
|
||||
vi.mocked(useProviderContext).mockReturnValue({
|
||||
enableBilling: true,
|
||||
isEducationAccount: false,
|
||||
isFetchedPlan: true,
|
||||
plan: { type: Plan.sandbox },
|
||||
} as ProviderContextState)
|
||||
vi.mocked(useModalContext).mockReturnValue({
|
||||
setShowPricingModal: vi.fn(),
|
||||
setShowAccountSettingModal: vi.fn(),
|
||||
} as unknown as ModalContextState)
|
||||
vi.mocked(useWorkspacesContext).mockReturnValue({
|
||||
workspaces: [
|
||||
{ id: 'workspace-1', name: 'Solar Studio', plan: Plan.sandbox, status: 'normal', created_at: 0, current: true },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it('hides cloud-only credits and upgrade actions outside cloud edition', () => {
|
||||
renderWithSystemFeatures(<WorkspaceCard />)
|
||||
|
||||
expect(screen.getByRole('button', { name: 'common.mainNav.workspace.openMenu' })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: /common\.mainNav\.workspace\.credits/ })).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('billing.upgradeBtn.encourageShort')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -7,6 +7,7 @@ import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { IS_CLOUD_EDITION } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
@ -113,34 +114,36 @@ const WorkspaceCard = () => {
|
||||
</div>
|
||||
<span aria-hidden className="i-ri-expand-up-down-line h-4 w-4 shrink-0 text-text-tertiary" />
|
||||
</button>
|
||||
<div className="flex items-center justify-center gap-1.5 border-t border-divider-subtle py-2 pr-2.5 pl-2">
|
||||
<button
|
||||
type="button"
|
||||
className="flex min-w-0 flex-1 items-center gap-0.5 px-1 text-left text-text-tertiary transition-colors hover:text-text-secondary"
|
||||
aria-label={t('mainNav.workspace.credits', { ns: 'common', count: formattedCredits })}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
|
||||
}}
|
||||
>
|
||||
<span className="i-custom-vender-main-nav-credits h-3 w-3 shrink-0" aria-hidden />
|
||||
<span className="truncate system-xs-medium" title={formattedCredits}>{formattedCredits}</span>
|
||||
<span className="shrink-0 system-xs-regular">{t('mainNav.workspace.creditsUnit', { ns: 'common' })}</span>
|
||||
</button>
|
||||
{enableBilling && (
|
||||
{IS_CLOUD_EDITION && (
|
||||
<div className="flex items-center justify-center gap-1.5 border-t border-divider-subtle py-2 pr-2.5 pl-2">
|
||||
<button
|
||||
type="button"
|
||||
className="max-w-[120px] shrink-0 truncate px-1 system-xs-semibold-uppercase text-saas-dify-blue-accessible transition-colors hover:text-saas-dify-blue-static-hover"
|
||||
title={t('upgradeBtn.encourageShort', { ns: 'billing' })}
|
||||
className="flex min-w-0 flex-1 items-center gap-0.5 px-1 text-left text-text-tertiary transition-colors hover:text-text-secondary"
|
||||
aria-label={t('mainNav.workspace.credits', { ns: 'common', count: formattedCredits })}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handlePlanClick()
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
|
||||
}}
|
||||
>
|
||||
{t('upgradeBtn.encourageShort', { ns: 'billing' })}
|
||||
<span className="i-custom-vender-main-nav-credits h-3 w-3 shrink-0" aria-hidden />
|
||||
<span className="truncate system-xs-medium" title={formattedCredits}>{formattedCredits}</span>
|
||||
<span className="shrink-0 system-xs-regular">{t('mainNav.workspace.creditsUnit', { ns: 'common' })}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{enableBilling && (
|
||||
<button
|
||||
type="button"
|
||||
className="max-w-[120px] shrink-0 truncate px-1 system-xs-semibold-uppercase text-saas-dify-blue-accessible transition-colors hover:text-saas-dify-blue-static-hover"
|
||||
title={t('upgradeBtn.encourageShort', { ns: 'billing' })}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handlePlanClick()
|
||||
}}
|
||||
>
|
||||
{t('upgradeBtn.encourageShort', { ns: 'billing' })}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{open && (
|
||||
<div className="absolute top-0 right-0 left-0 z-20 flex flex-col overflow-hidden rounded-xl border border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg backdrop-blur-[5px]">
|
||||
|
||||
@ -53,7 +53,7 @@ const MainNav = ({
|
||||
activeIcon: 'i-custom-vender-main-nav-knowledge-active',
|
||||
},
|
||||
{
|
||||
href: '/tools',
|
||||
href: '/tools?section=provider',
|
||||
label: t('mainNav.integrations', { ns: 'common' }),
|
||||
active: path => path.startsWith('/tools'),
|
||||
icon: 'i-custom-vender-main-nav-integrations',
|
||||
|
||||
@ -45,6 +45,13 @@ describe('IntegrationsPage', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('defaults to the model provider section when no query is provided', () => {
|
||||
renderIntegrationsPage()
|
||||
|
||||
expect(screen.getByTestId('model-provider-page')).toBeInTheDocument()
|
||||
expect(screen.getAllByText('common.settings.provider')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('renders the model provider section from the section query', () => {
|
||||
renderIntegrationsPage({ section: 'provider' })
|
||||
|
||||
|
||||
@ -129,7 +129,7 @@ export default function IntegrationsPage() {
|
||||
const { t } = useTranslation()
|
||||
const [sectionParam] = useQueryState('section', parseAsIntegrationSection)
|
||||
const [categoryParam] = useQueryState('category', parseAsToolCategory)
|
||||
const section = sectionParam ?? sectionByToolCategory[categoryParam ?? 'builtin']
|
||||
const section = sectionParam ?? (categoryParam ? sectionByToolCategory[categoryParam] : 'provider')
|
||||
const [providerSearchText, setProviderSearchText] = useState('')
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
|
||||
const providerItem = useMemo<NavItem>(() => ({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user