mirror of
https://github.com/langgenius/dify.git
synced 2026-01-18 12:59:58 +00:00
Compare commits
3 Commits
feature/ta
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b66bbc35a | ||
|
|
77366f33a4 | ||
|
|
e3b0918dd9 |
10
.github/workflows/style.yml
vendored
10
.github/workflows/style.yml
vendored
@@ -106,8 +106,9 @@ jobs:
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
working-directory: ./web
|
||||
run: |
|
||||
pnpm run lint:report
|
||||
continue-on-error: true
|
||||
pnpm run lint:ci
|
||||
# pnpm run lint:report
|
||||
# continue-on-error: true
|
||||
|
||||
# - name: Annotate Code
|
||||
# if: steps.changed-files.outputs.any_changed == 'true' && github.event_name == 'pull_request'
|
||||
@@ -126,11 +127,6 @@ jobs:
|
||||
working-directory: ./web
|
||||
run: pnpm run knip
|
||||
|
||||
- name: Web build check
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
working-directory: ./web
|
||||
run: pnpm run build
|
||||
|
||||
superlinter:
|
||||
name: SuperLinter
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
45
.github/workflows/web-tests.yml
vendored
45
.github/workflows/web-tests.yml
vendored
@@ -366,3 +366,48 @@ jobs:
|
||||
path: web/coverage
|
||||
retention-days: 30
|
||||
if-no-files-found: error
|
||||
|
||||
web-build:
|
||||
name: Web Build
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./web
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v47
|
||||
with:
|
||||
files: |
|
||||
web/**
|
||||
.github/workflows/web-tests.yml
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
package_json_file: web/package.json
|
||||
run_install: false
|
||||
|
||||
- name: Setup NodeJS
|
||||
uses: actions/setup-node@v6
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
cache-dependency-path: ./web/pnpm-lock.yaml
|
||||
|
||||
- name: Web dependencies
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
working-directory: ./web
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Web build check
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
working-directory: ./web
|
||||
run: pnpm run build
|
||||
|
||||
56
web/__mocks__/zustand.ts
Normal file
56
web/__mocks__/zustand.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type * as ZustandExportedTypes from 'zustand'
|
||||
import { act } from '@testing-library/react'
|
||||
|
||||
export * from 'zustand'
|
||||
|
||||
const { create: actualCreate, createStore: actualCreateStore }
|
||||
// eslint-disable-next-line antfu/no-top-level-await
|
||||
= await vi.importActual<typeof ZustandExportedTypes>('zustand')
|
||||
|
||||
export const storeResetFns = new Set<() => void>()
|
||||
|
||||
const createUncurried = <T>(
|
||||
stateCreator: ZustandExportedTypes.StateCreator<T>,
|
||||
) => {
|
||||
const store = actualCreate(stateCreator)
|
||||
const initialState = store.getInitialState()
|
||||
storeResetFns.add(() => {
|
||||
store.setState(initialState, true)
|
||||
})
|
||||
return store
|
||||
}
|
||||
|
||||
export const create = (<T>(
|
||||
stateCreator: ZustandExportedTypes.StateCreator<T>,
|
||||
) => {
|
||||
return typeof stateCreator === 'function'
|
||||
? createUncurried(stateCreator)
|
||||
: createUncurried
|
||||
}) as typeof ZustandExportedTypes.create
|
||||
|
||||
const createStoreUncurried = <T>(
|
||||
stateCreator: ZustandExportedTypes.StateCreator<T>,
|
||||
) => {
|
||||
const store = actualCreateStore(stateCreator)
|
||||
const initialState = store.getInitialState()
|
||||
storeResetFns.add(() => {
|
||||
store.setState(initialState, true)
|
||||
})
|
||||
return store
|
||||
}
|
||||
|
||||
export const createStore = (<T>(
|
||||
stateCreator: ZustandExportedTypes.StateCreator<T>,
|
||||
) => {
|
||||
return typeof stateCreator === 'function'
|
||||
? createStoreUncurried(stateCreator)
|
||||
: createStoreUncurried
|
||||
}) as typeof ZustandExportedTypes.createStore
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
storeResetFns.forEach((resetFn) => {
|
||||
resetFn()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -3,9 +3,7 @@ import type { App } from '@/types/app'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import useAccessControlStore from '@/context/access-control-store'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { AccessMode, SubjectType } from '@/models/access-control'
|
||||
import { defaultSystemFeatures } from '@/types/feature'
|
||||
import Toast from '../../base/toast'
|
||||
import AccessControlDialog from './access-control-dialog'
|
||||
import AccessControlItem from './access-control-item'
|
||||
@@ -105,22 +103,6 @@ const memberSubject: Subject = {
|
||||
accountData: baseMember,
|
||||
} as Subject
|
||||
|
||||
const resetAccessControlStore = () => {
|
||||
useAccessControlStore.setState({
|
||||
appId: '',
|
||||
specificGroups: [],
|
||||
specificMembers: [],
|
||||
currentMenu: AccessMode.SPECIFIC_GROUPS_MEMBERS,
|
||||
selectedGroupsForBreadcrumb: [],
|
||||
})
|
||||
}
|
||||
|
||||
const resetGlobalStore = () => {
|
||||
useGlobalPublicStore.setState({
|
||||
systemFeatures: defaultSystemFeatures,
|
||||
})
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
class MockIntersectionObserver {
|
||||
observe = vi.fn(() => undefined)
|
||||
@@ -132,9 +114,6 @@ beforeAll(() => {
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
resetAccessControlStore()
|
||||
resetGlobalStore()
|
||||
mockMutateAsync.mockResolvedValue(undefined)
|
||||
mockUseUpdateAccessMode.mockReturnValue({
|
||||
isPending: false,
|
||||
|
||||
@@ -189,6 +189,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
{isFetchingNextPage && <Loading />}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -21,15 +21,15 @@ export const AppCardSkeleton = React.memo(({ count = 6 }: AppCardSkeletonProps)
|
||||
>
|
||||
<SkeletonContainer className="h-full">
|
||||
<SkeletonRow>
|
||||
<SkeletonRectangle className="h-10 w-10 rounded-lg" />
|
||||
<SkeletonRectangle className="h-10 w-10 animate-pulse rounded-lg" />
|
||||
<div className="flex flex-1 flex-col gap-1">
|
||||
<SkeletonRectangle className="h-4 w-2/3" />
|
||||
<SkeletonRectangle className="h-3 w-1/3" />
|
||||
<SkeletonRectangle className="h-4 w-2/3 animate-pulse" />
|
||||
<SkeletonRectangle className="h-3 w-1/3 animate-pulse" />
|
||||
</div>
|
||||
</SkeletonRow>
|
||||
<div className="mt-4 flex flex-col gap-2">
|
||||
<SkeletonRectangle className="h-3 w-full" />
|
||||
<SkeletonRectangle className="h-3 w-4/5" />
|
||||
<SkeletonRectangle className="h-3 w-full animate-pulse" />
|
||||
<SkeletonRectangle className="h-3 w-4/5 animate-pulse" />
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
</div>
|
||||
|
||||
@@ -248,6 +248,9 @@ const List = () => {
|
||||
// No apps - show empty state
|
||||
return <Empty />
|
||||
})()}
|
||||
{isFetchingNextPage && (
|
||||
<AppCardSkeleton count={3} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isCurrentWorkspaceEditor && (
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { cn } from '@/utils/classnames'
|
||||
import './style.css'
|
||||
|
||||
type ILoadingProps = {
|
||||
type?: 'area' | 'app'
|
||||
className?: string
|
||||
}
|
||||
const Loading = (
|
||||
{ type = 'area' }: ILoadingProps = { type: 'area' },
|
||||
) => {
|
||||
|
||||
const Loading = (props?: ILoadingProps) => {
|
||||
const { type = 'area', className } = props || {}
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex w-full items-center justify-center ${type === 'app' ? 'h-full' : ''}`}
|
||||
className={cn(
|
||||
'flex w-full items-center justify-center',
|
||||
type === 'app' && 'h-full',
|
||||
className,
|
||||
)}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-label={t('loading', { ns: 'appApi' })}
|
||||
@@ -37,4 +41,5 @@ const Loading = (
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Loading
|
||||
|
||||
@@ -16,7 +16,6 @@ import { Theme } from '@/types/app'
|
||||
import SVGRenderer from '../svg-gallery' // Assumes svg-gallery.tsx is in /base directory
|
||||
|
||||
const Flowchart = dynamic(() => import('@/app/components/base/mermaid'), { ssr: false })
|
||||
const QuadrantMatrix = dynamic(() => import('@/app/components/base/quadrant-matrix'), { ssr: false })
|
||||
|
||||
// Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD
|
||||
const capitalizationLanguageNameMap: Record<string, string> = {
|
||||
@@ -41,7 +40,6 @@ const capitalizationLanguageNameMap: Record<string, string> = {
|
||||
latex: 'Latex',
|
||||
svg: 'SVG',
|
||||
abc: 'ABC',
|
||||
quadrant: 'Quadrant',
|
||||
}
|
||||
const getCorrectCapitalizationLanguageName = (language: string) => {
|
||||
if (!language)
|
||||
@@ -411,12 +409,6 @@ const CodeBlock: any = memo(({ inline, className, children = '', ...props }: any
|
||||
<MarkdownMusic children={content} />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
case 'quadrant':
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<QuadrantMatrix content={content} />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<SyntaxHighlighter
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { QuadrantData } from './types'
|
||||
import { RiExpandDiagonalLine } from '@remixicon/react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import FullScreenModal from '@/app/components/base/fullscreen-modal'
|
||||
import QuadrantCard from './quadrant-card'
|
||||
import { isValidQuadrantData, QUADRANT_CONFIGS } from './types'
|
||||
|
||||
type QuadrantMatrixProps = {
|
||||
content: string
|
||||
}
|
||||
|
||||
const QuadrantMatrix: FC<QuadrantMatrixProps> = ({ content }) => {
|
||||
const { t } = useTranslation()
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
|
||||
const parsedData = useMemo<QuadrantData | null>(() => {
|
||||
try {
|
||||
const trimmed = content.trim()
|
||||
const data = JSON.parse(trimmed)
|
||||
|
||||
if (!isValidQuadrantData(data))
|
||||
return null
|
||||
|
||||
return data
|
||||
}
|
||||
catch {
|
||||
return null
|
||||
}
|
||||
}, [content])
|
||||
|
||||
const handleExpand = useCallback(() => {
|
||||
setIsExpanded(true)
|
||||
}, [])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setIsExpanded(false)
|
||||
}, [])
|
||||
|
||||
if (!parsedData) {
|
||||
return (
|
||||
<div className="flex items-center justify-center rounded-xl bg-components-panel-bg-blur p-8">
|
||||
<div className="text-center text-text-secondary">
|
||||
<div className="system-md-semibold mb-2">{t('quadrantMatrix.invalidData', { ns: 'app' })}</div>
|
||||
<div className="text-sm text-text-tertiary">
|
||||
{t('quadrantMatrix.invalidDataDesc', { ns: 'app' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const totalTasks
|
||||
= parsedData.q1.length
|
||||
+ parsedData.q2.length
|
||||
+ parsedData.q3.length
|
||||
+ parsedData.q4.length
|
||||
|
||||
// Shared grid content component
|
||||
const renderGrid = (expanded: boolean) => (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* Row 1: Q1 (Do First), Q2 (Schedule) */}
|
||||
<QuadrantCard
|
||||
config={QUADRANT_CONFIGS.q1}
|
||||
tasks={parsedData.q1}
|
||||
expanded={expanded}
|
||||
/>
|
||||
<QuadrantCard
|
||||
config={QUADRANT_CONFIGS.q2}
|
||||
tasks={parsedData.q2}
|
||||
expanded={expanded}
|
||||
/>
|
||||
|
||||
{/* Row 2: Q3 (Delegate), Q4 (Don't Do) */}
|
||||
<QuadrantCard
|
||||
config={QUADRANT_CONFIGS.q3}
|
||||
tasks={parsedData.q3}
|
||||
expanded={expanded}
|
||||
/>
|
||||
<QuadrantCard
|
||||
config={QUADRANT_CONFIGS.q4}
|
||||
tasks={parsedData.q4}
|
||||
expanded={expanded}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-full overflow-hidden rounded-xl bg-components-panel-bg-blur p-4">
|
||||
{/* Header */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="system-md-semibold text-text-primary">
|
||||
{t('quadrantMatrix.title', { ns: 'app' })}
|
||||
</div>
|
||||
<div className="text-xs text-text-tertiary">
|
||||
{t('quadrantMatrix.taskCount', { ns: 'app', count: totalTasks })}
|
||||
</div>
|
||||
</div>
|
||||
{/* Legend + Expand Button */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-3 text-[11px] text-text-quaternary">
|
||||
<span>{t('quadrantMatrix.legend.importance', { ns: 'app' })}</span>
|
||||
<span>{t('quadrantMatrix.legend.urgency', { ns: 'app' })}</span>
|
||||
</div>
|
||||
<ActionButton onClick={handleExpand}>
|
||||
<RiExpandDiagonalLine className="h-4 w-4" />
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2x2 Grid */}
|
||||
{renderGrid(false)}
|
||||
</div>
|
||||
|
||||
{/* Fullscreen Modal */}
|
||||
<FullScreenModal
|
||||
open={isExpanded}
|
||||
onClose={handleClose}
|
||||
closable
|
||||
>
|
||||
<div className="flex h-full flex-col p-6">
|
||||
{/* Modal Header */}
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-xl font-semibold text-text-primary">
|
||||
{t('quadrantMatrix.title', { ns: 'app' })}
|
||||
</div>
|
||||
<div className="text-sm text-text-tertiary">
|
||||
{t('quadrantMatrix.taskCount', { ns: 'app', count: totalTasks })}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm text-text-quaternary">
|
||||
<span>{t('quadrantMatrix.legend.importance', { ns: 'app' })}</span>
|
||||
<span>{t('quadrantMatrix.legend.urgency', { ns: 'app' })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Grid */}
|
||||
<div className="min-h-0 flex-1">
|
||||
{renderGrid(true)}
|
||||
</div>
|
||||
</div>
|
||||
</FullScreenModal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default QuadrantMatrix
|
||||
@@ -1,102 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { QuadrantConfig, Task } from './types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import TaskItem from './task-item'
|
||||
|
||||
type QuadrantCardProps = {
|
||||
config: QuadrantConfig
|
||||
tasks: Task[]
|
||||
expanded?: boolean
|
||||
maxDisplay?: number
|
||||
}
|
||||
|
||||
const QuadrantCard: FC<QuadrantCardProps> = ({
|
||||
config,
|
||||
tasks,
|
||||
expanded = false,
|
||||
maxDisplay = 3,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { number, titleKey, subtitleKey, bgClass, borderClass, titleClass } = config
|
||||
const displayLimit = expanded ? Infinity : maxDisplay
|
||||
const displayTasks = tasks.slice(0, displayLimit)
|
||||
const remainingCount = Math.max(0, tasks.length - displayLimit)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex min-w-0 flex-col rounded-xl border p-3',
|
||||
bgClass,
|
||||
borderClass,
|
||||
expanded ? 'min-h-[280px]' : 'min-h-[200px]',
|
||||
)}
|
||||
>
|
||||
{/* Header with numbered circle */}
|
||||
<div className="mb-2 shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Numbered circle */}
|
||||
<span className={cn(
|
||||
'flex h-5 w-5 items-center justify-center rounded-full border text-xs font-semibold',
|
||||
borderClass,
|
||||
titleClass,
|
||||
)}
|
||||
>
|
||||
{number}
|
||||
</span>
|
||||
<span className={cn('system-sm-semibold', titleClass)}>{t(titleKey, { ns: 'app' })}</span>
|
||||
{tasks.length > 0 && (
|
||||
<span className="bg-components-badge-bg-gray rounded-full px-1.5 py-0.5 text-[10px] font-medium text-text-tertiary">
|
||||
{tasks.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[11px] text-text-tertiary">{t(subtitleKey, { ns: 'app' })}</div>
|
||||
</div>
|
||||
|
||||
{/* Task List */}
|
||||
<div className={cn(
|
||||
'flex min-h-0 flex-1 flex-col gap-2',
|
||||
expanded && 'overflow-y-auto',
|
||||
)}
|
||||
>
|
||||
{displayTasks.length > 0
|
||||
? (
|
||||
displayTasks.map((task) => {
|
||||
const taskKey = [
|
||||
task.name,
|
||||
task.deadline ?? 'no-deadline',
|
||||
task.importance_score,
|
||||
task.urgency_score,
|
||||
task.description ?? '',
|
||||
task.action_advice ?? '',
|
||||
].join('|')
|
||||
|
||||
return (
|
||||
<TaskItem
|
||||
key={taskKey}
|
||||
task={task}
|
||||
expanded={expanded}
|
||||
/>
|
||||
)
|
||||
})
|
||||
)
|
||||
: (
|
||||
<div className="flex flex-1 items-center justify-center text-xs text-text-quaternary">
|
||||
{t('quadrantMatrix.noTasks', { ns: 'app' })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* More indicator (only in non-expanded mode) */}
|
||||
{!expanded && remainingCount > 0 && (
|
||||
<div className="mt-2 shrink-0 text-center text-[11px] text-text-tertiary">
|
||||
{t('quadrantMatrix.more', { ns: 'app', count: remainingCount })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default QuadrantCard
|
||||
@@ -1,88 +0,0 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { Task } from './types'
|
||||
import { RiCalendarLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type TaskItemProps = {
|
||||
task: Task
|
||||
expanded?: boolean
|
||||
showScores?: boolean
|
||||
}
|
||||
|
||||
const TaskItem: FC<TaskItemProps> = ({ task, expanded = false, showScores = true }) => {
|
||||
const { t } = useTranslation()
|
||||
const { name, description, deadline, importance_score, urgency_score, action_advice } = task
|
||||
|
||||
return (
|
||||
<div className="group min-w-0 rounded-lg bg-components-panel-bg p-2.5 shadow-xs transition-all hover:shadow-sm">
|
||||
{/* Header: Task Name + Scores */}
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
'system-sm-medium min-w-0 flex-1 text-text-primary',
|
||||
!expanded && 'truncate',
|
||||
)}
|
||||
title={name}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
{showScores && (
|
||||
<div className="flex shrink-0 items-center gap-1 text-[10px] font-medium">
|
||||
<span className="text-text-accent">
|
||||
I:
|
||||
{importance_score}
|
||||
</span>
|
||||
<span className="text-text-warning">
|
||||
U:
|
||||
{urgency_score}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
{description && (
|
||||
<div className={cn(
|
||||
'mt-1 text-xs text-text-tertiary',
|
||||
!expanded && 'line-clamp-2',
|
||||
)}
|
||||
>
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Deadline Badge */}
|
||||
{deadline && (
|
||||
<div className="mt-1.5">
|
||||
<span className="bg-components-badge-bg-gray inline-flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] text-text-tertiary">
|
||||
<RiCalendarLine className="h-3 w-3" />
|
||||
<span>
|
||||
{t('quadrantMatrix.deadline', { ns: 'app' })}
|
||||
{' '}
|
||||
{deadline}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Advice */}
|
||||
{action_advice && (
|
||||
<div className="mt-2 border-t border-divider-subtle pt-2">
|
||||
<p
|
||||
className={cn(
|
||||
'text-xs italic text-text-quaternary',
|
||||
!expanded && 'line-clamp-2',
|
||||
)}
|
||||
title={!expanded ? action_advice : undefined}
|
||||
>
|
||||
{action_advice}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TaskItem
|
||||
@@ -1,92 +0,0 @@
|
||||
/**
|
||||
* Type definitions for Eisenhower Matrix (Task Quadrant) visualization
|
||||
*/
|
||||
import type { I18nKeysWithPrefix } from '@/types/i18n'
|
||||
|
||||
export type Task = {
|
||||
name: string
|
||||
description?: string
|
||||
deadline?: string // YYYY-MM-DD format
|
||||
importance_score: number // 0-100, based on goal alignment and long-term value
|
||||
urgency_score: number // 0-100, based on deadline pressure and delay penalty
|
||||
action_advice?: string // Suggested action for this task
|
||||
}
|
||||
|
||||
export type QuadrantData = {
|
||||
q1: Task[] // Urgent & Important - Do First
|
||||
q2: Task[] // Not Urgent & Important - Schedule
|
||||
q3: Task[] // Urgent & Not Important - Delegate
|
||||
q4: Task[] // Not Urgent & Not Important - Don't Do
|
||||
}
|
||||
|
||||
type QuadrantKeyBase = I18nKeysWithPrefix<'app', 'quadrantMatrix.q'>
|
||||
type QuadrantTitleKey = Extract<QuadrantKeyBase, `${string}.title`>
|
||||
type QuadrantSubtitleKey = Extract<QuadrantKeyBase, `${string}.subtitle`>
|
||||
|
||||
export type QuadrantConfig = {
|
||||
key: 'q1' | 'q2' | 'q3' | 'q4'
|
||||
number: number
|
||||
titleKey: QuadrantTitleKey // i18n key for title
|
||||
subtitleKey: QuadrantSubtitleKey // i18n key for subtitle
|
||||
bgClass: string
|
||||
borderClass: string
|
||||
titleClass: string
|
||||
}
|
||||
|
||||
// Layout based on Eisenhower Matrix:
|
||||
// Q1 (Do First) - top-left, Q2 (Schedule) - top-right
|
||||
// Q3 (Delegate) - bottom-left, Q4 (Don't Do) - bottom-right
|
||||
export const QUADRANT_CONFIGS: Record<string, QuadrantConfig> = {
|
||||
q1: {
|
||||
key: 'q1',
|
||||
number: 1,
|
||||
titleKey: 'quadrantMatrix.q1.title',
|
||||
subtitleKey: 'quadrantMatrix.q1.subtitle',
|
||||
bgClass: 'bg-state-destructive-hover',
|
||||
borderClass: 'border-state-destructive-border',
|
||||
titleClass: 'text-text-destructive',
|
||||
},
|
||||
q2: {
|
||||
key: 'q2',
|
||||
number: 2,
|
||||
titleKey: 'quadrantMatrix.q2.title',
|
||||
subtitleKey: 'quadrantMatrix.q2.subtitle',
|
||||
bgClass: 'bg-state-accent-hover',
|
||||
borderClass: 'border-state-accent-border',
|
||||
titleClass: 'text-text-accent',
|
||||
},
|
||||
q3: {
|
||||
key: 'q3',
|
||||
number: 3,
|
||||
titleKey: 'quadrantMatrix.q3.title',
|
||||
subtitleKey: 'quadrantMatrix.q3.subtitle',
|
||||
bgClass: 'bg-state-warning-hover',
|
||||
borderClass: 'border-state-warning-border',
|
||||
titleClass: 'text-text-warning',
|
||||
},
|
||||
q4: {
|
||||
key: 'q4',
|
||||
number: 4,
|
||||
titleKey: 'quadrantMatrix.q4.title',
|
||||
subtitleKey: 'quadrantMatrix.q4.subtitle',
|
||||
bgClass: 'bg-components-panel-on-panel-item-bg',
|
||||
borderClass: 'border-divider-regular',
|
||||
titleClass: 'text-text-tertiary',
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if the data structure matches QuadrantData interface
|
||||
*/
|
||||
export function isValidQuadrantData(data: unknown): data is QuadrantData {
|
||||
if (typeof data !== 'object' || data === null)
|
||||
return false
|
||||
|
||||
const d = data as Record<string, unknown>
|
||||
return (
|
||||
Array.isArray(d.q1)
|
||||
&& Array.isArray(d.q2)
|
||||
&& Array.isArray(d.q3)
|
||||
&& Array.isArray(d.q4)
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { useDatasetList, useInvalidDatasetList } from '@/service/knowledge/use-dataset'
|
||||
import DatasetCard from './dataset-card'
|
||||
@@ -25,6 +26,7 @@ const Datasets = ({
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetching,
|
||||
isFetchingNextPage,
|
||||
} = useDatasetList({
|
||||
initialPage: 1,
|
||||
tag_ids: tags,
|
||||
@@ -60,6 +62,7 @@ const Datasets = ({
|
||||
{datasetList?.pages.map(({ data: datasets }) => datasets.map(dataset => (
|
||||
<DatasetCard key={dataset.id} dataset={dataset} onSuccess={invalidDatasetList} />),
|
||||
))}
|
||||
{isFetchingNextPage && <Loading />}
|
||||
<div ref={anchorRef} className="h-0" />
|
||||
</nav>
|
||||
</>
|
||||
|
||||
@@ -33,6 +33,7 @@ const AppNav = () => {
|
||||
data: appsData,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
refetch,
|
||||
} = useInfiniteAppList({
|
||||
page: 1,
|
||||
@@ -111,6 +112,7 @@ const AppNav = () => {
|
||||
createText={t('menus.newApp', { ns: 'common' })}
|
||||
onCreate={openModal}
|
||||
onLoadMore={handleLoadMore}
|
||||
isLoadingMore={isFetchingNextPage}
|
||||
/>
|
||||
<CreateAppModal
|
||||
show={showNewAppDialog}
|
||||
|
||||
@@ -23,6 +23,7 @@ const DatasetNav = () => {
|
||||
data: datasetList,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useDatasetList({
|
||||
initialPage: 1,
|
||||
limit: 30,
|
||||
@@ -93,6 +94,7 @@ const DatasetNav = () => {
|
||||
createText={t('menus.newDataset', { ns: 'common' })}
|
||||
onCreate={() => router.push(createRoute)}
|
||||
onLoadMore={handleLoadMore}
|
||||
isLoadingMore={isFetchingNextPage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ const Nav = ({
|
||||
createText,
|
||||
onCreate,
|
||||
onLoadMore,
|
||||
isLoadingMore,
|
||||
isApp,
|
||||
}: INavProps) => {
|
||||
const setAppDetail = useAppStore(state => state.setAppDetail)
|
||||
@@ -81,6 +82,7 @@ const Nav = ({
|
||||
createText={createText}
|
||||
onCreate={onCreate}
|
||||
onLoadMore={onLoadMore}
|
||||
isLoadingMore={isLoadingMore}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { AppTypeIcon } from '@/app/components/app/type-selector'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { FileArrow01, FilePlus01, FilePlus02 } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
@@ -34,9 +35,10 @@ export type INavSelectorProps = {
|
||||
isApp?: boolean
|
||||
onCreate: (state: string) => void
|
||||
onLoadMore?: () => void
|
||||
isLoadingMore?: boolean
|
||||
}
|
||||
|
||||
const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onLoadMore }: INavSelectorProps) => {
|
||||
const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onLoadMore, isLoadingMore }: INavSelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
@@ -106,6 +108,11 @@ const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onL
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
{isLoadingMore && (
|
||||
<div className="flex justify-center py-2">
|
||||
<Loading />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{!isApp && isCurrentWorkspaceEditor && (
|
||||
<MenuItem as="div" className="w-full p-1">
|
||||
|
||||
@@ -19,6 +19,7 @@ const ListWrapper = ({
|
||||
marketplaceCollections,
|
||||
marketplaceCollectionPluginsMap,
|
||||
isLoading,
|
||||
isFetchingNextPage,
|
||||
page,
|
||||
} = useMarketplaceData()
|
||||
|
||||
@@ -53,6 +54,11 @@ const ListWrapper = ({
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
isFetchingNextPage && (
|
||||
<Loading className="my-3" />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export function useMarketplaceData() {
|
||||
}, [isSearchMode, searchPluginText, activePluginType, filterPluginTags, sort])
|
||||
|
||||
const pluginsQuery = useMarketplacePlugins(queryParams)
|
||||
const { hasNextPage, fetchNextPage, isFetching } = pluginsQuery
|
||||
const { hasNextPage, fetchNextPage, isFetching, isFetchingNextPage } = pluginsQuery
|
||||
|
||||
const handlePageChange = useCallback(() => {
|
||||
if (hasNextPage && !isFetching)
|
||||
@@ -50,5 +50,6 @@ export function useMarketplaceData() {
|
||||
pluginsTotal: pluginsQuery.data?.pages[0]?.total,
|
||||
page: pluginsQuery.data?.pages.length || 1,
|
||||
isLoading: collectionsQuery.isLoading || pluginsQuery.isLoading,
|
||||
isFetchingNextPage,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,17 +144,6 @@ describe('constant.ts - Type Definitions', () => {
|
||||
|
||||
// ==================== store.ts Tests ====================
|
||||
describe('store.ts - Zustand Store', () => {
|
||||
beforeEach(() => {
|
||||
// Reset store to initial state
|
||||
const { setState } = useStore
|
||||
setState({
|
||||
tagList: [],
|
||||
categoryList: [],
|
||||
showTagManagementModal: false,
|
||||
showCategoryManagementModal: false,
|
||||
})
|
||||
})
|
||||
|
||||
describe('Initial State', () => {
|
||||
it('should have empty tagList initially', () => {
|
||||
const { result } = renderHook(() => useStore(state => state.tagList))
|
||||
|
||||
@@ -5,11 +5,11 @@ import { useDebounceFn } from 'ahooks'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { renderI18nObject } from '@/i18n-config'
|
||||
import { useInstalledLatestVersion, useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins'
|
||||
import Loading from '../../base/loading'
|
||||
import { PluginSource } from '../types'
|
||||
import { usePluginPageContext } from './context'
|
||||
import Empty from './empty'
|
||||
@@ -107,12 +107,17 @@ const PluginsPanel = () => {
|
||||
<div className="w-full">
|
||||
<List pluginList={filteredList || []} />
|
||||
</div>
|
||||
{!isLastPage && !isFetching && (
|
||||
<Button onClick={loadNextPage}>
|
||||
{t('common.loadMore', { ns: 'workflow' })}
|
||||
</Button>
|
||||
{!isLastPage && (
|
||||
<div className="flex justify-center py-4">
|
||||
{isFetching
|
||||
? <Loading className="size-8" />
|
||||
: (
|
||||
<Button onClick={loadNextPage}>
|
||||
{t('common.loadMore', { ns: 'workflow' })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isFetching && <div className="system-md-semibold text-text-secondary">{t('detail.loading', { ns: 'appLog' })}</div>}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
|
||||
@@ -134,13 +134,6 @@ describe('BUILTIN_TOOLS_ARRAY', () => {
|
||||
// Store Tests
|
||||
// ================================
|
||||
describe('useReadmePanelStore', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Reset store state before each test
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail()
|
||||
})
|
||||
|
||||
describe('Initial State', () => {
|
||||
it('should have undefined currentPluginDetail initially', () => {
|
||||
const { currentPluginDetail } = useReadmePanelStore.getState()
|
||||
@@ -228,13 +221,6 @@ describe('useReadmePanelStore', () => {
|
||||
// ReadmeEntrance Component Tests
|
||||
// ================================
|
||||
describe('ReadmeEntrance', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Reset store state
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail()
|
||||
})
|
||||
|
||||
// ================================
|
||||
// Rendering Tests
|
||||
// ================================
|
||||
@@ -417,11 +403,6 @@ describe('ReadmeEntrance', () => {
|
||||
// ================================
|
||||
describe('ReadmePanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Reset store state
|
||||
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
|
||||
setCurrentPluginDetail()
|
||||
// Reset mock
|
||||
mockUsePluginReadme.mockReturnValue({
|
||||
data: null,
|
||||
isLoading: false,
|
||||
|
||||
5107
web/eslint-suppressions.json
Normal file
5107
web/eslint-suppressions.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -9,30 +9,15 @@ import difyI18n from './eslint-rules/index.js'
|
||||
export default antfu(
|
||||
{
|
||||
react: {
|
||||
reactCompiler: true,
|
||||
overrides: {
|
||||
'react/no-context-provider': 'off',
|
||||
'react/no-forward-ref': 'off',
|
||||
'react/no-use-context': 'off',
|
||||
'react/prefer-namespace-import': 'error',
|
||||
|
||||
// React Compiler rules
|
||||
// Set to warn for gradual adoption
|
||||
'react-hooks/config': 'warn',
|
||||
'react-hooks/error-boundaries': 'warn',
|
||||
'react-hooks/component-hook-factories': 'warn',
|
||||
'react-hooks/gating': 'warn',
|
||||
'react-hooks/globals': 'warn',
|
||||
'react-hooks/immutability': 'warn',
|
||||
'react-hooks/preserve-manual-memoization': 'warn',
|
||||
'react-hooks/purity': 'warn',
|
||||
'react-hooks/refs': 'warn',
|
||||
// prefer react-hooks-extra/no-direct-set-state-in-use-effect
|
||||
'react-hooks/set-state-in-effect': 'off',
|
||||
'react-hooks/set-state-in-render': 'warn',
|
||||
'react-hooks/static-components': 'warn',
|
||||
'react-hooks/unsupported-syntax': 'warn',
|
||||
'react-hooks/use-memo': 'warn',
|
||||
'react-hooks/incompatible-library': 'warn',
|
||||
'react-hooks-extra/no-direct-set-state-in-use-effect': 'error',
|
||||
},
|
||||
},
|
||||
nextjs: true,
|
||||
@@ -40,7 +25,7 @@ export default antfu(
|
||||
typescript: {
|
||||
overrides: {
|
||||
'ts/consistent-type-definitions': ['error', 'type'],
|
||||
'ts/no-explicit-any': 'warn',
|
||||
'ts/no-explicit-any': 'error',
|
||||
},
|
||||
},
|
||||
test: {
|
||||
@@ -54,6 +39,11 @@ export default antfu(
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'node/prefer-global/process': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
settings: {
|
||||
@@ -62,32 +52,6 @@ export default antfu(
|
||||
},
|
||||
},
|
||||
},
|
||||
// downgrade some rules from error to warn for gradual adoption
|
||||
// we should fix these in following pull requests
|
||||
{
|
||||
// @keep-sorted
|
||||
rules: {
|
||||
'next/inline-script-id': 'warn',
|
||||
'no-console': 'warn',
|
||||
'no-irregular-whitespace': 'warn',
|
||||
'node/prefer-global/buffer': 'warn',
|
||||
'node/prefer-global/process': 'warn',
|
||||
'react/no-create-ref': 'warn',
|
||||
'react/no-missing-key': 'warn',
|
||||
'react/no-nested-component-definitions': 'warn',
|
||||
'regexp/no-dupe-disjunctions': 'warn',
|
||||
'regexp/no-super-linear-backtracking': 'warn',
|
||||
'regexp/no-unused-capturing-group': 'warn',
|
||||
'regexp/no-useless-assertions': 'warn',
|
||||
'regexp/no-useless-quantifier': 'warn',
|
||||
'style/multiline-ternary': 'warn',
|
||||
'test/no-identical-title': 'warn',
|
||||
'test/prefer-hooks-in-order': 'warn',
|
||||
'ts/no-empty-object-type': 'warn',
|
||||
'unicorn/prefer-number-properties': 'warn',
|
||||
'unused-imports/no-unused-vars': 'warn',
|
||||
},
|
||||
},
|
||||
storybook.configs['flat/recommended'],
|
||||
...pluginQuery.configs['flat/recommended'],
|
||||
// sonar
|
||||
@@ -178,19 +142,19 @@ export default antfu(
|
||||
},
|
||||
},
|
||||
// dify i18n namespace migration
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
ignores: ['eslint-rules/**', 'i18n/**', 'i18n-config/**'],
|
||||
plugins: {
|
||||
'dify-i18n': difyI18n,
|
||||
},
|
||||
rules: {
|
||||
// 'dify-i18n/no-as-any-in-t': ['error', { mode: 'all' }],
|
||||
'dify-i18n/no-as-any-in-t': 'error',
|
||||
// 'dify-i18n/no-legacy-namespace-prefix': 'error',
|
||||
// 'dify-i18n/require-ns-option': 'error',
|
||||
},
|
||||
},
|
||||
// {
|
||||
// files: ['**/*.ts', '**/*.tsx'],
|
||||
// ignores: ['eslint-rules/**', 'i18n/**', 'i18n-config/**'],
|
||||
// plugins: {
|
||||
// 'dify-i18n': difyI18n,
|
||||
// },
|
||||
// rules: {
|
||||
// // 'dify-i18n/no-as-any-in-t': ['error', { mode: 'all' }],
|
||||
// 'dify-i18n/no-as-any-in-t': 'error',
|
||||
// // 'dify-i18n/no-legacy-namespace-prefix': 'error',
|
||||
// // 'dify-i18n/require-ns-option': 'error',
|
||||
// },
|
||||
// },
|
||||
// i18n JSON validation rules
|
||||
{
|
||||
files: ['i18n/**/*.json'],
|
||||
|
||||
@@ -196,24 +196,6 @@
|
||||
"publishApp.notSet": "Not set",
|
||||
"publishApp.notSetDesc": "Currently nobody can access the web app. Please set permissions.",
|
||||
"publishApp.title": "Who can access web app",
|
||||
"quadrantMatrix.deadline": "DDL:",
|
||||
"quadrantMatrix.invalidData": "Invalid Quadrant Data",
|
||||
"quadrantMatrix.invalidDataDesc": "Expected JSON format with q1, q2, q3, q4 arrays",
|
||||
"quadrantMatrix.legend.importance": "I = Importance",
|
||||
"quadrantMatrix.legend.urgency": "U = Urgency",
|
||||
"quadrantMatrix.more": "+{{count}} more",
|
||||
"quadrantMatrix.noTasks": "No tasks",
|
||||
"quadrantMatrix.q1.subtitle": "Urgent & Important",
|
||||
"quadrantMatrix.q1.title": "Do First",
|
||||
"quadrantMatrix.q2.subtitle": "Important & Not Urgent",
|
||||
"quadrantMatrix.q2.title": "Schedule",
|
||||
"quadrantMatrix.q3.subtitle": "Urgent & Not Important",
|
||||
"quadrantMatrix.q3.title": "Delegate",
|
||||
"quadrantMatrix.q4.subtitle": "Not Urgent & Not Important",
|
||||
"quadrantMatrix.q4.title": "Don't Do",
|
||||
"quadrantMatrix.taskCount_one": "{{count}} task prioritized",
|
||||
"quadrantMatrix.taskCount_other": "{{count}} tasks prioritized",
|
||||
"quadrantMatrix.title": "Eisenhower Matrix",
|
||||
"removeOriginal": "Delete the original app",
|
||||
"roadmap": "See our roadmap",
|
||||
"showMyCreatedAppsOnly": "Created by me",
|
||||
|
||||
@@ -196,24 +196,6 @@
|
||||
"publishApp.notSet": "未设置",
|
||||
"publishApp.notSetDesc": "当前任何人都无法访问 Web 应用。请设置访问权限。",
|
||||
"publishApp.title": "谁可以访问 web 应用",
|
||||
"quadrantMatrix.deadline": "截止:",
|
||||
"quadrantMatrix.invalidData": "无效的象限数据",
|
||||
"quadrantMatrix.invalidDataDesc": "需要包含 q1, q2, q3, q4 数组的 JSON 格式",
|
||||
"quadrantMatrix.legend.importance": "I = 重要性",
|
||||
"quadrantMatrix.legend.urgency": "U = 紧急性",
|
||||
"quadrantMatrix.more": "+{{count}} 更多",
|
||||
"quadrantMatrix.noTasks": "暂无任务",
|
||||
"quadrantMatrix.q1.subtitle": "紧急且重要",
|
||||
"quadrantMatrix.q1.title": "立即执行",
|
||||
"quadrantMatrix.q2.subtitle": "重要但不紧急",
|
||||
"quadrantMatrix.q2.title": "计划安排",
|
||||
"quadrantMatrix.q3.subtitle": "紧急但不重要",
|
||||
"quadrantMatrix.q3.title": "委派他人",
|
||||
"quadrantMatrix.q4.subtitle": "不紧急也不重要",
|
||||
"quadrantMatrix.q4.title": "不要做",
|
||||
"quadrantMatrix.taskCount_one": "{{count}} 个任务已排序",
|
||||
"quadrantMatrix.taskCount_other": "{{count}} 个任务已排序",
|
||||
"quadrantMatrix.title": "艾森豪威尔矩阵",
|
||||
"removeOriginal": "删除原应用",
|
||||
"roadmap": "产品路线图",
|
||||
"showMyCreatedAppsOnly": "我创建的",
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"build:docker": "next build && node scripts/optimize-standalone.js",
|
||||
"start": "node ./scripts/copy-and-start.mjs",
|
||||
"lint": "eslint --cache --cache-location node_modules/.cache/eslint/.eslint-cache",
|
||||
"lint:ci": "pnpm lint --concurrency 3",
|
||||
"lint:fix": "pnpm lint --fix",
|
||||
"lint:quiet": "pnpm lint --quiet",
|
||||
"lint:complexity": "pnpm lint --rule 'complexity: [error, {max: 15}]' --quiet",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
"target": "es2015",
|
||||
"target": "es2022",
|
||||
"jsx": "preserve",
|
||||
"lib": [
|
||||
"dom",
|
||||
|
||||
@@ -85,6 +85,10 @@ afterEach(() => {
|
||||
// mock next/image to avoid width/height requirements for data URLs
|
||||
vi.mock('next/image')
|
||||
|
||||
// mock zustand - auto-resets all stores after each test
|
||||
// Based on official Zustand testing guide: https://zustand.docs.pmnd.rs/guides/testing
|
||||
vi.mock('zustand')
|
||||
|
||||
// mock react-i18next
|
||||
vi.mock('react-i18next', async () => {
|
||||
const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next')
|
||||
|
||||
Reference in New Issue
Block a user