mirror of
https://github.com/langgenius/dify.git
synced 2025-08-13 19:57:36 +00:00
fix: Multiple UI component improvements and code quality enhancements (#23446)
This commit is contained in:
parent
5eb061466f
commit
2cd3fe0dce
@ -0,0 +1,156 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import '@testing-library/jest-dom'
|
||||||
|
import { OpikIconBig } from '@/app/components/base/icons/src/public/tracing'
|
||||||
|
|
||||||
|
// Mock dependencies to isolate the SVG rendering issue
|
||||||
|
jest.mock('react-i18next', () => ({
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: (key: string) => key,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('SVG Attribute Error Reproduction', () => {
|
||||||
|
// Capture console errors
|
||||||
|
const originalError = console.error
|
||||||
|
let errorMessages: string[] = []
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
errorMessages = []
|
||||||
|
console.error = jest.fn((message) => {
|
||||||
|
errorMessages.push(message)
|
||||||
|
originalError(message)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
console.error = originalError
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should reproduce inkscape attribute errors when rendering OpikIconBig', () => {
|
||||||
|
console.log('\n=== TESTING OpikIconBig SVG ATTRIBUTE ERRORS ===')
|
||||||
|
|
||||||
|
// Test multiple renders to check for inconsistency
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
console.log(`\nRender attempt ${i + 1}:`)
|
||||||
|
|
||||||
|
const { unmount } = render(<OpikIconBig />)
|
||||||
|
|
||||||
|
// Check for specific inkscape attribute errors
|
||||||
|
const inkscapeErrors = errorMessages.filter(msg =>
|
||||||
|
typeof msg === 'string' && msg.includes('inkscape'),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (inkscapeErrors.length > 0) {
|
||||||
|
console.log(`Found ${inkscapeErrors.length} inkscape errors:`)
|
||||||
|
inkscapeErrors.forEach((error, index) => {
|
||||||
|
console.log(` ${index + 1}. ${error.substring(0, 100)}...`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log('No inkscape errors found in this render')
|
||||||
|
}
|
||||||
|
|
||||||
|
unmount()
|
||||||
|
|
||||||
|
// Clear errors for next iteration
|
||||||
|
errorMessages = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should analyze the SVG structure causing the errors', () => {
|
||||||
|
console.log('\n=== ANALYZING SVG STRUCTURE ===')
|
||||||
|
|
||||||
|
// Import the JSON data directly
|
||||||
|
const iconData = require('@/app/components/base/icons/src/public/tracing/OpikIconBig.json')
|
||||||
|
|
||||||
|
console.log('Icon structure analysis:')
|
||||||
|
console.log('- Root element:', iconData.icon.name)
|
||||||
|
console.log('- Children count:', iconData.icon.children?.length || 0)
|
||||||
|
|
||||||
|
// Find problematic elements
|
||||||
|
const findProblematicElements = (node: any, path = '') => {
|
||||||
|
const problematicElements: any[] = []
|
||||||
|
|
||||||
|
if (node.name && (node.name.includes(':') || node.name.startsWith('sodipodi'))) {
|
||||||
|
problematicElements.push({
|
||||||
|
path,
|
||||||
|
name: node.name,
|
||||||
|
attributes: Object.keys(node.attributes || {}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check attributes for inkscape/sodipodi properties
|
||||||
|
if (node.attributes) {
|
||||||
|
const problematicAttrs = Object.keys(node.attributes).filter(attr =>
|
||||||
|
attr.startsWith('inkscape:') || attr.startsWith('sodipodi:'),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (problematicAttrs.length > 0) {
|
||||||
|
problematicElements.push({
|
||||||
|
path,
|
||||||
|
name: node.name,
|
||||||
|
problematicAttributes: problematicAttrs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.children) {
|
||||||
|
node.children.forEach((child: any, index: number) => {
|
||||||
|
problematicElements.push(
|
||||||
|
...findProblematicElements(child, `${path}/${node.name}[${index}]`),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return problematicElements
|
||||||
|
}
|
||||||
|
|
||||||
|
const problematicElements = findProblematicElements(iconData.icon, 'root')
|
||||||
|
|
||||||
|
console.log(`\n🚨 Found ${problematicElements.length} problematic elements:`)
|
||||||
|
problematicElements.forEach((element, index) => {
|
||||||
|
console.log(`\n${index + 1}. Element: ${element.name}`)
|
||||||
|
console.log(` Path: ${element.path}`)
|
||||||
|
if (element.problematicAttributes)
|
||||||
|
console.log(` Problematic attributes: ${element.problematicAttributes.join(', ')}`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should test the normalizeAttrs function behavior', () => {
|
||||||
|
console.log('\n=== TESTING normalizeAttrs FUNCTION ===')
|
||||||
|
|
||||||
|
const { normalizeAttrs } = require('@/app/components/base/icons/utils')
|
||||||
|
|
||||||
|
const testAttributes = {
|
||||||
|
'inkscape:showpageshadow': '2',
|
||||||
|
'inkscape:pageopacity': '0.0',
|
||||||
|
'inkscape:pagecheckerboard': '0',
|
||||||
|
'inkscape:deskcolor': '#d1d1d1',
|
||||||
|
'sodipodi:docname': 'opik-icon-big.svg',
|
||||||
|
'xmlns:inkscape': 'https://www.inkscape.org/namespaces/inkscape',
|
||||||
|
'xmlns:sodipodi': 'https://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd',
|
||||||
|
'xmlns:svg': 'https://www.w3.org/2000/svg',
|
||||||
|
'data-name': 'Layer 1',
|
||||||
|
'normal-attr': 'value',
|
||||||
|
'class': 'test-class',
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Input attributes:', Object.keys(testAttributes))
|
||||||
|
|
||||||
|
const normalized = normalizeAttrs(testAttributes)
|
||||||
|
|
||||||
|
console.log('Normalized attributes:', Object.keys(normalized))
|
||||||
|
console.log('Normalized values:', normalized)
|
||||||
|
|
||||||
|
// Check if problematic attributes are still present
|
||||||
|
const problematicKeys = Object.keys(normalized).filter(key =>
|
||||||
|
key.toLowerCase().includes('inkscape') || key.toLowerCase().includes('sodipodi'),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (problematicKeys.length > 0)
|
||||||
|
console.log(`🚨 PROBLEM: Still found problematic attributes: ${problematicKeys.join(', ')}`)
|
||||||
|
else
|
||||||
|
console.log('✅ No problematic attributes found after normalization')
|
||||||
|
})
|
||||||
|
})
|
@ -1,12 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
import React, { useCallback, useRef, useState } from 'react'
|
||||||
import {
|
|
||||||
RiEqualizer2Line,
|
|
||||||
} from '@remixicon/react'
|
|
||||||
import type { PopupProps } from './config-popup'
|
import type { PopupProps } from './config-popup'
|
||||||
import ConfigPopup from './config-popup'
|
import ConfigPopup from './config-popup'
|
||||||
import cn from '@/utils/classnames'
|
|
||||||
import {
|
import {
|
||||||
PortalToFollowElem,
|
PortalToFollowElem,
|
||||||
PortalToFollowElemContent,
|
PortalToFollowElemContent,
|
||||||
@ -17,13 +14,13 @@ type Props = {
|
|||||||
readOnly: boolean
|
readOnly: boolean
|
||||||
className?: string
|
className?: string
|
||||||
hasConfigured: boolean
|
hasConfigured: boolean
|
||||||
controlShowPopup?: number
|
children?: React.ReactNode
|
||||||
} & PopupProps
|
} & PopupProps
|
||||||
|
|
||||||
const ConfigBtn: FC<Props> = ({
|
const ConfigBtn: FC<Props> = ({
|
||||||
className,
|
className,
|
||||||
hasConfigured,
|
hasConfigured,
|
||||||
controlShowPopup,
|
children,
|
||||||
...popupProps
|
...popupProps
|
||||||
}) => {
|
}) => {
|
||||||
const [open, doSetOpen] = useState(false)
|
const [open, doSetOpen] = useState(false)
|
||||||
@ -37,13 +34,6 @@ const ConfigBtn: FC<Props> = ({
|
|||||||
setOpen(!openRef.current)
|
setOpen(!openRef.current)
|
||||||
}, [setOpen])
|
}, [setOpen])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (controlShowPopup)
|
|
||||||
// setOpen(!openRef.current)
|
|
||||||
setOpen(true)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [controlShowPopup])
|
|
||||||
|
|
||||||
if (popupProps.readOnly && !hasConfigured)
|
if (popupProps.readOnly && !hasConfigured)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
@ -52,14 +42,11 @@ const ConfigBtn: FC<Props> = ({
|
|||||||
open={open}
|
open={open}
|
||||||
onOpenChange={setOpen}
|
onOpenChange={setOpen}
|
||||||
placement='bottom-end'
|
placement='bottom-end'
|
||||||
offset={{
|
offset={12}
|
||||||
mainAxis: 12,
|
|
||||||
crossAxis: hasConfigured ? 8 : 49,
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<PortalToFollowElemTrigger onClick={handleTrigger}>
|
<PortalToFollowElemTrigger onClick={handleTrigger}>
|
||||||
<div className={cn(className, 'rounded-md p-1')}>
|
<div className="select-none">
|
||||||
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</PortalToFollowElemTrigger>
|
</PortalToFollowElemTrigger>
|
||||||
<PortalToFollowElemContent className='z-[11]'>
|
<PortalToFollowElemContent className='z-[11]'>
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import React, { useCallback, useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
RiArrowDownDoubleLine,
|
RiArrowDownDoubleLine,
|
||||||
|
RiEqualizer2Line,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation'
|
||||||
@ -180,10 +181,6 @@ const Panel: FC = () => {
|
|||||||
})()
|
})()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const [controlShowPopup, setControlShowPopup] = useState<number>(0)
|
|
||||||
const showPopup = useCallback(() => {
|
|
||||||
setControlShowPopup(Date.now())
|
|
||||||
}, [setControlShowPopup])
|
|
||||||
if (!isLoaded) {
|
if (!isLoaded) {
|
||||||
return (
|
return (
|
||||||
<div className='mb-3 flex items-center justify-between'>
|
<div className='mb-3 flex items-center justify-between'>
|
||||||
@ -196,18 +193,7 @@ const Panel: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex items-center justify-between')}>
|
<div className={cn('flex items-center justify-between')}>
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'flex cursor-pointer items-center rounded-xl border-l-[0.5px] border-t border-effects-highlight bg-background-default-dodge p-2 shadow-xs hover:border-effects-highlight-lightmode-off hover:bg-background-default-lighter',
|
|
||||||
controlShowPopup && 'border-effects-highlight-lightmode-off bg-background-default-lighter',
|
|
||||||
)}
|
|
||||||
onClick={showPopup}
|
|
||||||
>
|
|
||||||
{!inUseTracingProvider && (
|
{!inUseTracingProvider && (
|
||||||
<>
|
|
||||||
<TracingIcon size='md' />
|
|
||||||
<div className='system-sm-semibold mx-2 text-text-secondary'>{t(`${I18N_PREFIX}.title`)}</div>
|
|
||||||
<div className='flex items-center' onClick={e => e.stopPropagation()}>
|
|
||||||
<ConfigButton
|
<ConfigButton
|
||||||
appId={appId}
|
appId={appId}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
@ -225,31 +211,29 @@ const Panel: FC = () => {
|
|||||||
aliyunConfig={aliyunConfig}
|
aliyunConfig={aliyunConfig}
|
||||||
onConfigUpdated={handleTracingConfigUpdated}
|
onConfigUpdated={handleTracingConfigUpdated}
|
||||||
onConfigRemoved={handleTracingConfigRemoved}
|
onConfigRemoved={handleTracingConfigRemoved}
|
||||||
controlShowPopup={controlShowPopup}
|
>
|
||||||
/>
|
<div
|
||||||
|
className={cn(
|
||||||
|
'flex cursor-pointer select-none items-center rounded-xl border-l-[0.5px] border-t border-effects-highlight bg-background-default-dodge p-2 shadow-xs hover:border-effects-highlight-lightmode-off hover:bg-background-default-lighter',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TracingIcon size='md' />
|
||||||
|
<div className='system-sm-semibold mx-2 text-text-secondary'>{t(`${I18N_PREFIX}.title`)}</div>
|
||||||
|
<div className='rounded-md p-1'>
|
||||||
|
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
|
||||||
</div>
|
</div>
|
||||||
<Divider type='vertical' className='h-3.5' />
|
<Divider type='vertical' className='h-3.5' />
|
||||||
<div className='rounded-md p-1'>
|
<div className='rounded-md p-1'>
|
||||||
<RiArrowDownDoubleLine className='h-4 w-4 text-text-tertiary' />
|
<RiArrowDownDoubleLine className='h-4 w-4 text-text-tertiary' />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
|
</ConfigButton>
|
||||||
)}
|
)}
|
||||||
{hasConfiguredTracing && (
|
{hasConfiguredTracing && (
|
||||||
<>
|
|
||||||
<div className='ml-4 mr-1 flex items-center'>
|
|
||||||
<Indicator color={enabled ? 'green' : 'gray'} />
|
|
||||||
<div className='system-xs-semibold-uppercase ml-1.5 text-text-tertiary'>
|
|
||||||
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{InUseProviderIcon && <InUseProviderIcon className='ml-1 h-4' />}
|
|
||||||
<Divider type='vertical' className='h-3.5' />
|
|
||||||
<div className='flex items-center' onClick={e => e.stopPropagation()}>
|
|
||||||
<ConfigButton
|
<ConfigButton
|
||||||
appId={appId}
|
appId={appId}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
hasConfigured
|
hasConfigured
|
||||||
className='ml-2'
|
|
||||||
enabled={enabled}
|
enabled={enabled}
|
||||||
onStatusChange={handleTracingEnabledChange}
|
onStatusChange={handleTracingEnabledChange}
|
||||||
chosenProvider={inUseTracingProvider}
|
chosenProvider={inUseTracingProvider}
|
||||||
@ -263,13 +247,27 @@ const Panel: FC = () => {
|
|||||||
aliyunConfig={aliyunConfig}
|
aliyunConfig={aliyunConfig}
|
||||||
onConfigUpdated={handleTracingConfigUpdated}
|
onConfigUpdated={handleTracingConfigUpdated}
|
||||||
onConfigRemoved={handleTracingConfigRemoved}
|
onConfigRemoved={handleTracingConfigRemoved}
|
||||||
controlShowPopup={controlShowPopup}
|
>
|
||||||
/>
|
<div
|
||||||
</div>
|
className={cn(
|
||||||
</>
|
'flex cursor-pointer select-none items-center rounded-xl border-l-[0.5px] border-t border-effects-highlight bg-background-default-dodge p-2 shadow-xs hover:border-effects-highlight-lightmode-off hover:bg-background-default-lighter',
|
||||||
)}
|
)}
|
||||||
</div >
|
>
|
||||||
</div >
|
<div className='ml-4 mr-1 flex items-center'>
|
||||||
|
<Indicator color={enabled ? 'green' : 'gray'} />
|
||||||
|
<div className='system-xs-semibold-uppercase ml-1.5 text-text-tertiary'>
|
||||||
|
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{InUseProviderIcon && <InUseProviderIcon className='ml-1 h-4' />}
|
||||||
|
<div className='ml-2 rounded-md p-1'>
|
||||||
|
<RiEqualizer2Line className='h-4 w-4 text-text-tertiary' />
|
||||||
|
</div>
|
||||||
|
<Divider type='vertical' className='h-3.5' />
|
||||||
|
</div>
|
||||||
|
</ConfigButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
export default React.memo(Panel)
|
export default React.memo(Panel)
|
||||||
|
@ -36,13 +36,13 @@ const Footer = () => {
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className='shrink-0 grow-0 px-12 py-2 relative'>
|
<footer className='relative shrink-0 grow-0 px-12 py-2'>
|
||||||
<button
|
<button
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
className='absolute top-2 right-2 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full transition-colors duration-200 ease-in-out hover:bg-gray-100 dark:hover:bg-gray-800'
|
className='absolute right-2 top-2 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full transition-colors duration-200 ease-in-out hover:bg-components-main-nav-nav-button-bg-active'
|
||||||
aria-label="Close footer"
|
aria-label="Close footer"
|
||||||
>
|
>
|
||||||
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
|
<RiCloseLine className='h-4 w-4 text-text-tertiary hover:text-text-secondary' />
|
||||||
</button>
|
</button>
|
||||||
<h3 className='text-gradient text-xl font-semibold leading-tight'>{t('app.join')}</h3>
|
<h3 className='text-gradient text-xl font-semibold leading-tight'>{t('app.join')}</h3>
|
||||||
<p className='system-sm-regular mt-1 text-text-tertiary'>{t('app.communityIntro')}</p>
|
<p className='system-sm-regular mt-1 text-text-tertiary'>{t('app.communityIntro')}</p>
|
||||||
|
@ -14,9 +14,26 @@ export type Attrs = {
|
|||||||
|
|
||||||
export function normalizeAttrs(attrs: Attrs = {}): Attrs {
|
export function normalizeAttrs(attrs: Attrs = {}): Attrs {
|
||||||
return Object.keys(attrs).reduce((acc: Attrs, key) => {
|
return Object.keys(attrs).reduce((acc: Attrs, key) => {
|
||||||
|
// Filter out editor metadata attributes before processing
|
||||||
|
if (key.startsWith('inkscape:')
|
||||||
|
|| key.startsWith('sodipodi:')
|
||||||
|
|| key.startsWith('xmlns:inkscape')
|
||||||
|
|| key.startsWith('xmlns:sodipodi')
|
||||||
|
|| key.startsWith('xmlns:svg')
|
||||||
|
|| key === 'data-name')
|
||||||
|
return acc
|
||||||
|
|
||||||
const val = attrs[key]
|
const val = attrs[key]
|
||||||
key = key.replace(/([-]\w)/g, (g: string) => g[1].toUpperCase())
|
key = key.replace(/([-]\w)/g, (g: string) => g[1].toUpperCase())
|
||||||
key = key.replace(/([:]\w)/g, (g: string) => g[1].toUpperCase())
|
key = key.replace(/([:]\w)/g, (g: string) => g[1].toUpperCase())
|
||||||
|
|
||||||
|
// Additional filter after camelCase conversion
|
||||||
|
if (key === 'xmlnsInkscape'
|
||||||
|
|| key === 'xmlnsSodipodi'
|
||||||
|
|| key === 'xmlnsSvg'
|
||||||
|
|| key === 'dataName')
|
||||||
|
return acc
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'class':
|
case 'class':
|
||||||
acc.className = val
|
acc.className = val
|
||||||
|
@ -139,7 +139,10 @@ const TagFilter: FC<TagFilterProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className='border-t-[0.5px] border-divider-regular' />
|
<div className='border-t-[0.5px] border-divider-regular' />
|
||||||
<div className='p-1'>
|
<div className='p-1'>
|
||||||
<div className='flex cursor-pointer items-center gap-2 rounded-lg py-[6px] pl-3 pr-2 hover:bg-state-base-hover' onClick={() => setShowTagManagementModal(true)}>
|
<div className='flex cursor-pointer items-center gap-2 rounded-lg py-[6px] pl-3 pr-2 hover:bg-state-base-hover' onClick={() => {
|
||||||
|
setShowTagManagementModal(true)
|
||||||
|
setOpen(false)
|
||||||
|
}}>
|
||||||
<Tag03 className='h-4 w-4 text-text-tertiary' />
|
<Tag03 className='h-4 w-4 text-text-tertiary' />
|
||||||
<div className='grow truncate text-sm leading-5 text-text-secondary'>
|
<div className='grow truncate text-sm leading-5 text-text-secondary'>
|
||||||
{t('common.tag.manageTags')}
|
{t('common.tag.manageTags')}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user