feat: Add user preference for history item preview line limit
This commit introduces a new user setting allowing you to define the maximum number of lines displayed in the preview for text or code clipboard history items. Key changes: - Added a "History Item Preview Line Limit" setting in `ClipboardHistorySettings.tsx` (defaulting to 5 lines), stored in `settingsStore.ts`. - The `historyPreviewLineLimit` setting is now passed as a prop to `ClipboardHistoryRow.tsx` and `ClipboardHistoryQuickPasteRow.tsx` from their parent pages (`ClipboardHistoryPage.tsx` and `ClipboardHistoryQuickPastePage.tsx` respectively). - Modified `ClipboardHistoryRow.tsx` and `ClipboardHistoryQuickPasteRow.tsx` to truncate the displayed text or code preview based on the `historyPreviewLineLimit` prop when the item is not expanded. - The "Show all / Show less" indicators (+X lines / +Y chars) have been updated to accurately reflect the truncation based on the new line limit. - Added comprehensive unit tests for both `ClipboardHistoryRow.tsx` and `ClipboardHistoryQuickPasteRow.tsx` using Vitest and React Testing Library to ensure the new functionality works as expected across various scenarios (different limits, content lengths, text vs. code, expanded state).
This commit is contained in:
parent
1a1b58a528
commit
bfb5821078
@ -0,0 +1,292 @@
|
||||
import { render, screen, within } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { ClipboardHistoryQuickPasteRow } from './ClipboardHistoryQuickPasteRow'
|
||||
import { ClipboardHistoryItem } from '~/types/history'
|
||||
|
||||
// Mock i18n
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { count?: number }) => {
|
||||
if (key === 'lines') return options?.count === 1 ? 'line' : 'lines'
|
||||
if (key === 'chars') return options?.count === 1 ? 'char' : 'chars'
|
||||
if (key === 'Show all') return 'Show all'
|
||||
if (key === 'show less') return 'show less'
|
||||
return key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock Lucide icons
|
||||
vi.mock('lucide-react', async () => {
|
||||
const actual = await vi.importActual<typeof import('lucide-react')>('lucide-react')
|
||||
return {
|
||||
...actual,
|
||||
WrapIcon: () => <div data-testid="wrap-icon" />,
|
||||
NoWrapIcon: () => <div data-testid="nowrap-icon" />,
|
||||
Star: () => <div data-testid="star-icon" />,
|
||||
Dot: () => <div data-testid="dot-icon" />,
|
||||
Check: () => <div data-testid="check-icon" />,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('~/components/atoms/image/image-with-fallback-on-error', () => ({
|
||||
__esModule: true,
|
||||
default: ({ src }: { src: string }) => <img src={src} alt="mocked image" />,
|
||||
}))
|
||||
|
||||
vi.mock('~/components/atoms/link-card/link-card', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="link-card-mock">Link Card</div>,
|
||||
}))
|
||||
|
||||
vi.mock('~/components/video-player/YoutubeEmbed', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="youtube-embed-mock">Youtube Embed</div>,
|
||||
}))
|
||||
|
||||
|
||||
const createMockClipboardItem = (
|
||||
id: string,
|
||||
value: string,
|
||||
valuePreview?: string,
|
||||
detectedLanguage?: string,
|
||||
isLink?: boolean,
|
||||
isImage?: boolean,
|
||||
isImageData?: boolean,
|
||||
isVideo?: boolean
|
||||
): ClipboardHistoryItem => {
|
||||
const lines = value.split('\n')
|
||||
const previewLines = valuePreview?.split('\n') ?? lines
|
||||
return {
|
||||
historyId: id,
|
||||
value,
|
||||
valuePreview: valuePreview ?? value, // QuickPasteRow uses `value` directly for text sometimes, but preview logic relies on this structure
|
||||
valueLines: lines.length,
|
||||
valueMorePreviewLines: previewLines.length < lines.length ? lines.length - previewLines.length : 0,
|
||||
valueMorePreviewChars: valuePreview && value.length > valuePreview.length ? value.length - valuePreview.length : 0,
|
||||
detectedLanguage,
|
||||
isLink: isLink ?? false,
|
||||
isImage: isImage ?? false,
|
||||
isImageData: isImageData ?? false,
|
||||
isVideo: isVideo ?? false,
|
||||
isFavorite: false,
|
||||
isPinned: false,
|
||||
updatedAt: Date.now(),
|
||||
createdAt: Date.now(),
|
||||
copiedFromApp: 'test-app',
|
||||
historyOptions: null,
|
||||
options: null,
|
||||
arrLinks: [],
|
||||
hasEmoji: false,
|
||||
hasMaskedWords: false,
|
||||
isMasked: false,
|
||||
imageHeight: null,
|
||||
imageWidth: null,
|
||||
imageDataUrl: null,
|
||||
linkMetadata: null,
|
||||
timeAgo: 'just now',
|
||||
timeAgoShort: 'now',
|
||||
showTimeAgo: false,
|
||||
pinnedOrderNumber: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
isDark: false,
|
||||
showSelectHistoryItems: false,
|
||||
setBrokenImageItem: vi.fn(),
|
||||
setSelectHistoryItem: vi.fn(),
|
||||
onCopy: vi.fn(),
|
||||
onCopyPaste: vi.fn(),
|
||||
setKeyboardSelected: vi.fn(),
|
||||
setExpanded: vi.fn(),
|
||||
setWrapText: vi.fn(),
|
||||
setSavingItem: vi.fn(),
|
||||
setLargeViewItemId: vi.fn(),
|
||||
invalidateClipboardHistoryQuery: vi.fn(),
|
||||
isExpanded: false,
|
||||
isWrapText: false,
|
||||
isKeyboardSelected: false, // Important for QuickPasteRow
|
||||
}
|
||||
|
||||
describe('ClipboardHistoryQuickPasteRow', () => {
|
||||
// Text content tests
|
||||
describe('Text Content Preview', () => {
|
||||
it('renders default preview lines when historyPreviewLineLimit is not provided', () => {
|
||||
const item = createMockClipboardItem('1', 'line1\nline2\nline3\nline4\nline5\nline6', 'line1\nline2\nline3\nline4\nline5')
|
||||
render(<ClipboardHistoryQuickPasteRow {...defaultProps} clipboard={item} />)
|
||||
expect(screen.getByText(/line1/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/line5/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/line6/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders limited lines when historyPreviewLineLimit is set (e.g., 3)', () => {
|
||||
const item = createMockClipboardItem('1', 'line1\nline2\nline3\nline4\nline5')
|
||||
render(<ClipboardHistoryQuickPasteRow {...defaultProps} clipboard={item} historyPreviewLineLimit={3} />)
|
||||
expect(screen.getByText(/line1/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/line3/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/line4/)).not.toBeInTheDocument()
|
||||
expect(screen.getByText(/\.\.\./)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Show all/)).toHaveTextContent('+2 lines')
|
||||
})
|
||||
|
||||
it('renders all lines if content lines are fewer than historyPreviewLineLimit', () => {
|
||||
const item = createMockClipboardItem('1', 'line1\nline2')
|
||||
render(<ClipboardHistoryQuickPasteRow {...defaultProps} clipboard={item} historyPreviewLineLimit={3} />)
|
||||
expect(screen.getByText(/line1/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/line2/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/\.\.\./)).not.toBeInTheDocument()
|
||||
expect(screen.queryByText(/Show all/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders all lines if content lines are equal to historyPreviewLineLimit', () => {
|
||||
const item = createMockClipboardItem('1', 'line1\nline2\nline3')
|
||||
render(<ClipboardHistoryQuickPasteRow {...defaultProps} clipboard={item} historyPreviewLineLimit={3} />)
|
||||
expect(screen.getByText(/line1/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/line3/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/\.\.\./)).not.toBeInTheDocument()
|
||||
expect(screen.queryByText(/Show all/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders full preview when historyPreviewLineLimit is 0', () => {
|
||||
const item = createMockClipboardItem('1', 'line1\nline2\nline3\nline4\nline5\nline6')
|
||||
render(<ClipboardHistoryQuickPasteRow {...defaultProps} clipboard={item} historyPreviewLineLimit={0} />)
|
||||
expect(screen.getByText(/line1/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/line6/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/\.\.\./)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders full content when isExpanded is true, ignoring historyPreviewLineLimit', () => {
|
||||
const item = createMockClipboardItem('1', 'line1\nline2\nline3\nline4\nline5')
|
||||
render(
|
||||
<ClipboardHistoryQuickPasteRow
|
||||
{...defaultProps}
|
||||
clipboard={item}
|
||||
historyPreviewLineLimit={2}
|
||||
isExpanded={true}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText(/line1/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/line5/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/\.\.\./)).not.toBeInTheDocument()
|
||||
expect(screen.getByText(/show less/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Code content tests
|
||||
describe('Code Content Preview (Highlight component)', () => {
|
||||
const codeItem = (lines: number) => createMockClipboardItem(
|
||||
'code1',
|
||||
Array.from({ length: lines }, (_, i) => `const line${i + 1} = ${i + 1};`).join('\n'),
|
||||
undefined,
|
||||
'javascript'
|
||||
)
|
||||
|
||||
it('renders limited lines for code when historyPreviewLineLimit is set (e.g., 3)', () => {
|
||||
render(<ClipboardHistoryQuickPasteRow {...defaultProps} clipboard={codeItem(5)} historyPreviewLineLimit={3} />)
|
||||
expect(screen.getByText(/const line1 = 1;/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/const line3 = 3;/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/const line4 = 4;/)).not.toBeInTheDocument()
|
||||
const lines = screen.getAllByText(/const line/i)
|
||||
expect(lines.length).toBe(3)
|
||||
expect(within(lines[2].closest('div')!).getByText('...')).toBeInTheDocument()
|
||||
expect(screen.getByText(/Show all/)).toHaveTextContent('+2 lines')
|
||||
})
|
||||
|
||||
it('renders all lines for code if content lines are fewer than historyPreviewLineLimit', () => {
|
||||
render(<ClipboardHistoryQuickPasteRow {...defaultProps} clipboard={codeItem(2)} historyPreviewLineLimit={3} />)
|
||||
expect(screen.getByText(/const line1 = 1;/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/const line2 = 2;/)).toBeInTheDocument()
|
||||
const lines = screen.getAllByText(/const line/i)
|
||||
expect(lines.length).toBe(2)
|
||||
lines.forEach(line => {
|
||||
expect(within(line.closest('div')!).queryByText('...')).not.toBeInTheDocument()
|
||||
})
|
||||
expect(screen.queryByText(/Show all/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders full code preview when historyPreviewLineLimit is 0', () => {
|
||||
render(<ClipboardHistoryQuickPasteRow {...defaultProps} clipboard={codeItem(6)} historyPreviewLineLimit={0} />)
|
||||
expect(screen.getByText(/const line1 = 1;/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/const line6 = 6;/)).toBeInTheDocument()
|
||||
const lines = screen.getAllByText(/const line/i)
|
||||
expect(lines.length).toBe(6)
|
||||
lines.forEach(line => {
|
||||
expect(within(line.closest('div')!).queryByText('...')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('renders full code content when isExpanded is true, ignoring historyPreviewLineLimit', () => {
|
||||
render(
|
||||
<ClipboardHistoryQuickPasteRow
|
||||
{...defaultProps}
|
||||
clipboard={codeItem(5)}
|
||||
historyPreviewLineLimit={2}
|
||||
isExpanded={true}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText(/const line1 = 1;/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/const line5 = 5;/)).toBeInTheDocument()
|
||||
const lines = screen.getAllByText(/const line/i)
|
||||
expect(lines.length).toBe(5)
|
||||
lines.forEach(line => {
|
||||
expect(within(line.closest('div')!).queryByText('...')).not.toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByText(/show less/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Show all / Show less button text', () => {
|
||||
it('shows correct "+X lines" when truncated by line limit (text)', () => {
|
||||
const item = createMockClipboardItem('txt1', 'l1\nl2\nl3\nl4\nl5')
|
||||
render(<ClipboardHistoryQuickPasteRow {...defaultProps} clipboard={item} historyPreviewLineLimit={3} />)
|
||||
expect(screen.getByText(/Show all/)).toHaveTextContent('+2 lines')
|
||||
})
|
||||
|
||||
it('shows correct "+X lines" when truncated by line limit (code)', () => {
|
||||
const item = createMockClipboardItem('code1', 'c1\nc2\nc3\nc4\nc5', undefined, 'javascript')
|
||||
render(<ClipboardHistoryQuickPasteRow {...defaultProps} clipboard={item} historyPreviewLineLimit={2} />)
|
||||
expect(screen.getByText(/Show all/)).toHaveTextContent('+3 lines')
|
||||
})
|
||||
|
||||
it('shows correct "+X chars" when truncated by char limit (line limit is 0 or not applicable)', () => {
|
||||
const longLine = 'a'.repeat(100)
|
||||
const previewChar = 'a'.repeat(50)
|
||||
const item = createMockClipboardItem('char1', `${longLine}\nline2`, `${previewChar}\nline2`)
|
||||
item.valueMorePreviewChars = 50
|
||||
item.valueMorePreviewLines = 0
|
||||
|
||||
render(<ClipboardHistoryQuickPasteRow {...defaultProps} clipboard={item} historyPreviewLineLimit={0} />)
|
||||
const showAllButton = screen.getByText(/Show all/)
|
||||
expect(showAllButton).toBeInTheDocument()
|
||||
expect(showAllButton).toHaveTextContent('+50 chars')
|
||||
})
|
||||
|
||||
it('prioritizes line limit message over char limit message when both could apply', () => {
|
||||
const longText = Array.from({ length: 5 }, (_, i) => `line${i+1} ` + 'char'.repeat(10)).join('\n');
|
||||
const previewText = "line1 " + 'char'.repeat(5);
|
||||
const item = createMockClipboardItem('combo1', longText, previewText);
|
||||
item.valueMorePreviewLines = 4;
|
||||
item.valueMorePreviewChars = (("line1 " + 'char'.repeat(10)).length - previewText.length) +
|
||||
(4 * ("lineX " + 'char'.repeat(10)).length);
|
||||
|
||||
render(<ClipboardHistoryQuickPasteRow {...defaultProps} clipboard={item} historyPreviewLineLimit={2} />);
|
||||
expect(screen.getByText(/Show all/)).toHaveTextContent('+3 lines');
|
||||
expect(screen.queryByText(/chars/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
it('shows "show less" when expanded', () => {
|
||||
const item = createMockClipboardItem('any', 'l1\nl2\nl3')
|
||||
render(<ClipboardHistoryQuickPasteRow {...defaultProps} clipboard={item} isExpanded={true} />)
|
||||
expect(screen.getByText(/show less/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Basic test to confirm the component renders
|
||||
it('ClipboardHistoryQuickPasteRow renders', () => {
|
||||
const mockItem = createMockClipboardItem('id1', 'Test content quick paste')
|
||||
render(<ClipboardHistoryQuickPasteRow {...defaultProps} clipboard={mockItem} />)
|
||||
expect(screen.getByText('Test content quick paste')).toBeInTheDocument()
|
||||
})
|
@ -103,6 +103,7 @@ interface ClipboardHistoryQuickPasteRowProps {
|
||||
setHistoryFilters?: Dispatch<SetStateAction<string[]>>
|
||||
setAppFilters?: Dispatch<SetStateAction<string[]>>
|
||||
isSingleClickToCopyPaste?: boolean
|
||||
historyPreviewLineLimit?: number
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
@ -155,6 +156,7 @@ export function ClipboardHistoryQuickPasteRowComponent({
|
||||
isDragPreview = false,
|
||||
setRowHeight = () => {},
|
||||
isSingleClickToCopyPaste = false,
|
||||
historyPreviewLineLimit = 5,
|
||||
}: ClipboardHistoryQuickPasteRowProps) {
|
||||
const { t } = useTranslation()
|
||||
const rowRef = useRef<HTMLDivElement>(null)
|
||||
@ -541,12 +543,21 @@ export function ClipboardHistoryQuickPasteRowComponent({
|
||||
language={clipboard.detectedLanguage}
|
||||
>
|
||||
{({ className, style, tokens, getLineProps, getTokenProps }) => {
|
||||
const limitedTokens =
|
||||
historyPreviewLineLimit && historyPreviewLineLimit > 0 && !isExpanded
|
||||
? tokens.slice(0, historyPreviewLineLimit)
|
||||
: tokens
|
||||
const remainingLines = tokens.length - limitedTokens.length
|
||||
return (
|
||||
<code className={`${className}`} style={style}>
|
||||
{tokens.map((line, i) => {
|
||||
const isLastLine =
|
||||
i === tokens.length - 1 &&
|
||||
clipboard.valueMorePreviewLines &&
|
||||
{limitedTokens.map((line, i) => {
|
||||
const isLastLineOfPreview = i === limitedTokens.length - 1
|
||||
const isActuallyLastLineOfAllTokens = i === tokens.length - 1
|
||||
|
||||
const showEllipsis =
|
||||
isLastLineOfPreview &&
|
||||
!isActuallyLastLineOfAllTokens &&
|
||||
remainingLines > 0 &&
|
||||
!isExpanded
|
||||
return (
|
||||
<div
|
||||
@ -567,7 +578,7 @@ export function ClipboardHistoryQuickPasteRowComponent({
|
||||
: highlightMatchedText(token.content, searchTerm)}
|
||||
</span>
|
||||
))}
|
||||
{isLastLine && <span className="select-none">...</span>}
|
||||
{showEllipsis && <span className="select-none">...</span>}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@ -575,7 +586,7 @@ export function ClipboardHistoryQuickPasteRowComponent({
|
||||
)
|
||||
}}
|
||||
</Highlight>
|
||||
{clipboard.valueMorePreviewLines && (
|
||||
{(remainingLines > 0 || clipboard.valueMorePreviewLines) && !isExpanded && (
|
||||
<Box className="select-none"> {'\u00A0'} </Box>
|
||||
)}
|
||||
</Box>
|
||||
@ -610,28 +621,38 @@ export function ClipboardHistoryQuickPasteRowComponent({
|
||||
{searchTerm
|
||||
? highlightMatchedText(textValue, searchTerm)
|
||||
: hyperlinkText(textValue, clipboard.arrLinks)}
|
||||
{clipboard.valueMorePreviewChars && (
|
||||
{clipboard.valueMorePreviewChars && !isExpanded && (
|
||||
<Box className="select-none"> {'\u00A0'} </Box>
|
||||
)}
|
||||
</code>
|
||||
) : (
|
||||
<code className="justify-start cursor-pointer">
|
||||
{searchTerm
|
||||
? highlightWithPreviewMatchedText(textValue ?? '', searchTerm)
|
||||
: hyperlinkTextWithPreview({
|
||||
previewLinkCard: !hasLinkCard && isLinkCardPreviewEnabled,
|
||||
isPreviewError: hasClipboardHistoryURLErrors,
|
||||
value: clipboard.valuePreview ?? '',
|
||||
links: clipboard.arrLinks,
|
||||
itemId: null,
|
||||
historyId: clipboard.historyId,
|
||||
})}
|
||||
{clipboard.valueMorePreviewChars && (
|
||||
<>
|
||||
<span className="select-none">...</span>
|
||||
<Box className="select-none"> {'\u00A0'} </Box>
|
||||
</>
|
||||
)}
|
||||
<code className="justify-start cursor-pointer whitespace-pre">
|
||||
{hyperlinkTextWithPreview({
|
||||
previewLinkCard: !hasLinkCard && isLinkCardPreviewEnabled,
|
||||
isPreviewError: hasClipboardHistoryURLErrors,
|
||||
value:
|
||||
historyPreviewLineLimit && historyPreviewLineLimit > 0
|
||||
? textValue
|
||||
.split('\n')
|
||||
.slice(0, historyPreviewLineLimit)
|
||||
.join('\n')
|
||||
: clipboard.valuePreview ?? '',
|
||||
links: clipboard.arrLinks,
|
||||
itemId: null,
|
||||
historyId: clipboard.historyId,
|
||||
searchTerm: searchTerm,
|
||||
})}
|
||||
{(historyPreviewLineLimit &&
|
||||
historyPreviewLineLimit > 0 &&
|
||||
textValue.split('\n').length > historyPreviewLineLimit
|
||||
? true
|
||||
: clipboard.valueMorePreviewChars) &&
|
||||
!isExpanded && (
|
||||
<>
|
||||
<span className="select-none">...</span>
|
||||
<Box className="select-none"> {'\u00A0'} </Box>
|
||||
</>
|
||||
)}
|
||||
{isMp3 && (
|
||||
<PlayButton
|
||||
src={textValue}
|
||||
@ -647,7 +668,14 @@ export function ClipboardHistoryQuickPasteRowComponent({
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{(clipboard.valueMorePreviewLines || clipboard.valueMorePreviewChars) && (
|
||||
{((historyPreviewLineLimit &&
|
||||
historyPreviewLineLimit > 0 &&
|
||||
!isExpanded &&
|
||||
(textValue.split('\n').length > historyPreviewLineLimit ||
|
||||
(clipboard.detectedLanguage &&
|
||||
clipboard.valuePreview &&
|
||||
clipboard.value.split('\n').length > historyPreviewLineLimit))) ||
|
||||
clipboard.valueMorePreviewChars) && (
|
||||
<Box
|
||||
className={`absolute left-1 bottom-1 flex flex-row items-center rounded mb-[2px] pl-0.5 ${bgToolsPanel}`}
|
||||
>
|
||||
@ -666,17 +694,30 @@ export function ClipboardHistoryQuickPasteRowComponent({
|
||||
sideOffset={10}
|
||||
>
|
||||
{!isExpanded ? (
|
||||
clipboard?.valueMorePreviewChars ? (
|
||||
historyPreviewLineLimit &&
|
||||
historyPreviewLineLimit > 0 &&
|
||||
(textValue.split('\n').length > historyPreviewLineLimit ||
|
||||
(clipboard.detectedLanguage &&
|
||||
clipboard.valuePreview &&
|
||||
clipboard.value.split('\n').length > historyPreviewLineLimit)) ? (
|
||||
<>
|
||||
+
|
||||
{clipboard.detectedLanguage
|
||||
? clipboard.value.split('\n').length - historyPreviewLineLimit
|
||||
: textValue.split('\n').length - historyPreviewLineLimit}{' '}
|
||||
{t('lines', { ns: 'common' })}
|
||||
</>
|
||||
) : clipboard?.valueMorePreviewChars ? (
|
||||
<>
|
||||
+{clipboard.valueMorePreviewChars}{' '}
|
||||
{t('chars', { ns: 'common' })}
|
||||
</>
|
||||
) : (
|
||||
) : clipboard?.valueMorePreviewLines ? (
|
||||
<>
|
||||
+{clipboard.valueMorePreviewLines}{' '}
|
||||
{t('lines', { ns: 'common' })}
|
||||
</>
|
||||
)
|
||||
) : null
|
||||
) : (
|
||||
<>- {t('show less', { ns: 'common' })}</>
|
||||
)}
|
||||
|
@ -0,0 +1,306 @@
|
||||
import { render, screen, within } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ClipboardHistoryRow } from './ClipboardHistoryRow'
|
||||
import { ClipboardHistoryItem } from '~/types/history'
|
||||
|
||||
// Mock i18n
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { count?: number }) => {
|
||||
if (key === 'lines') return options?.count === 1 ? 'line' : 'lines'
|
||||
if (key === 'chars') return options?.count === 1 ? 'char' : 'chars'
|
||||
if (key === 'Show all') return 'Show all'
|
||||
if (key === 'show less') return 'show less'
|
||||
return key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock Lucide icons
|
||||
vi.mock('lucide-react', async () => {
|
||||
const actual = await vi.importActual<typeof import('lucide-react')>('lucide-react')
|
||||
return {
|
||||
...actual,
|
||||
WrapIcon: () => <div data-testid="wrap-icon" />,
|
||||
NoWrapIcon: () => <div data-testid="nowrap-icon" />,
|
||||
Star: () => <div data-testid="star-icon" />,
|
||||
Dot: () => <div data-testid="dot-icon" />,
|
||||
Clipboard: () => <div data-testid="clipboard-icon" />,
|
||||
ClipboardPaste: () => <div data-testid="clipboard-paste-icon" />,
|
||||
Grip: () => <div data-testid="grip-icon" />,
|
||||
MoreVertical: () => <div data-testid="more-vertical-icon" />,
|
||||
MoveUp: () => <div data-testid="move-up-icon" />,
|
||||
MoveDown: () => <div data-testid="move-down-icon" />,
|
||||
Check: () => <div data-testid="check-icon" />,
|
||||
ArrowDownToLine: () => <div data-testid="arrow-down-to-line-icon" />,
|
||||
X: () => <div data-testid="x-icon" />,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('~/components/atoms/image/image-with-fallback-on-error', () => ({
|
||||
__esModule: true,
|
||||
default: ({ src }: { src: string }) => <img src={src} alt="mocked image" />,
|
||||
}))
|
||||
|
||||
vi.mock('~/components/atoms/link-card/link-card', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="link-card-mock">Link Card</div>,
|
||||
}))
|
||||
|
||||
vi.mock('~/components/video-player/YoutubeEmbed', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="youtube-embed-mock">Youtube Embed</div>,
|
||||
}))
|
||||
|
||||
const createMockClipboardItem = (
|
||||
id: string,
|
||||
value: string,
|
||||
valuePreview?: string,
|
||||
detectedLanguage?: string,
|
||||
isLink?: boolean,
|
||||
isImage?: boolean,
|
||||
isImageData?: boolean,
|
||||
isVideo?: boolean
|
||||
): ClipboardHistoryItem => {
|
||||
const lines = value.split('\n')
|
||||
const previewLines = valuePreview?.split('\n') ?? lines
|
||||
return {
|
||||
historyId: id,
|
||||
value,
|
||||
valuePreview: valuePreview ?? value,
|
||||
valueLines: lines.length,
|
||||
valueMorePreviewLines: previewLines.length < lines.length ? lines.length - previewLines.length : 0,
|
||||
valueMorePreviewChars: valuePreview && value.length > valuePreview.length ? value.length - valuePreview.length : 0,
|
||||
detectedLanguage,
|
||||
isLink: isLink ?? false,
|
||||
isImage: isImage ?? false,
|
||||
isImageData: isImageData ?? false,
|
||||
isVideo: isVideo ?? false,
|
||||
isFavorite: false,
|
||||
isPinned: false,
|
||||
updatedAt: Date.now(),
|
||||
createdAt: Date.now(),
|
||||
copiedFromApp: 'test-app',
|
||||
historyOptions: null,
|
||||
options: null,
|
||||
arrLinks: [],
|
||||
hasEmoji: false,
|
||||
hasMaskedWords: false,
|
||||
isMasked: false,
|
||||
imageHeight: null,
|
||||
imageWidth: null,
|
||||
imageDataUrl: null,
|
||||
linkMetadata: null,
|
||||
timeAgo: 'just now',
|
||||
timeAgoShort: 'now',
|
||||
showTimeAgo: false,
|
||||
pinnedOrderNumber: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
isDark: false,
|
||||
showSelectHistoryItems: false,
|
||||
setBrokenImageItem: vi.fn(),
|
||||
setSelectHistoryItem: vi.fn(),
|
||||
onCopy: vi.fn(),
|
||||
onCopyPaste: vi.fn(),
|
||||
setExpanded: vi.fn(),
|
||||
setWrapText: vi.fn(),
|
||||
setSavingItem: vi.fn(),
|
||||
setLargeViewItemId: vi.fn(),
|
||||
invalidateClipboardHistoryQuery: vi.fn(),
|
||||
isExpanded: false,
|
||||
isWrapText: false,
|
||||
isKeyboardSelected: false,
|
||||
}
|
||||
|
||||
describe('ClipboardHistoryRow', () => {
|
||||
// Text content tests
|
||||
describe('Text Content Preview', () => {
|
||||
it('renders default preview lines when historyPreviewLineLimit is not provided', () => {
|
||||
const item = createMockClipboardItem('1', 'line1\nline2\nline3\nline4\nline5\nline6', 'line1\nline2\nline3\nline4\nline5')
|
||||
render(<ClipboardHistoryRow {...defaultProps} clipboard={item} />)
|
||||
expect(screen.getByText(/line1/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/line5/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/line6/)).not.toBeInTheDocument()
|
||||
// Assuming default char limit might show '...' or line count based on original component logic
|
||||
// This test focuses on line limit overriding, so exact char limit assertion isn't primary here.
|
||||
})
|
||||
|
||||
it('renders limited lines when historyPreviewLineLimit is set (e.g., 3)', () => {
|
||||
const item = createMockClipboardItem('1', 'line1\nline2\nline3\nline4\nline5')
|
||||
render(<ClipboardHistoryRow {...defaultProps} clipboard={item} historyPreviewLineLimit={3} />)
|
||||
expect(screen.getByText(/line1/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/line3/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/line4/)).not.toBeInTheDocument()
|
||||
expect(screen.getByText(/\.\.\./)).toBeInTheDocument() // For the ellipsis
|
||||
expect(screen.getByText(/Show all/)).toHaveTextContent('+2 lines')
|
||||
})
|
||||
|
||||
it('renders all lines if content lines are fewer than historyPreviewLineLimit', () => {
|
||||
const item = createMockClipboardItem('1', 'line1\nline2')
|
||||
render(<ClipboardHistoryRow {...defaultProps} clipboard={item} historyPreviewLineLimit={3} />)
|
||||
expect(screen.getByText(/line1/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/line2/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/\.\.\./)).not.toBeInTheDocument()
|
||||
expect(screen.queryByText(/Show all/)).not.toBeInTheDocument() // No "Show all" if not truncated
|
||||
})
|
||||
|
||||
it('renders all lines if content lines are equal to historyPreviewLineLimit', () => {
|
||||
const item = createMockClipboardItem('1', 'line1\nline2\nline3')
|
||||
render(<ClipboardHistoryRow {...defaultProps} clipboard={item} historyPreviewLineLimit={3} />)
|
||||
expect(screen.getByText(/line1/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/line3/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/\.\.\./)).not.toBeInTheDocument()
|
||||
expect(screen.queryByText(/Show all/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders full preview when historyPreviewLineLimit is 0', () => {
|
||||
const item = createMockClipboardItem('1', 'line1\nline2\nline3\nline4\nline5\nline6')
|
||||
render(<ClipboardHistoryRow {...defaultProps} clipboard={item} historyPreviewLineLimit={0} />)
|
||||
expect(screen.getByText(/line1/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/line6/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/\.\.\./)).not.toBeInTheDocument() // No line-based ellipsis
|
||||
// It might still show ellipsis due to character limit if that's separate logic
|
||||
})
|
||||
|
||||
it('renders full content when isExpanded is true, ignoring historyPreviewLineLimit', () => {
|
||||
const item = createMockClipboardItem('1', 'line1\nline2\nline3\nline4\nline5')
|
||||
render(
|
||||
<ClipboardHistoryRow
|
||||
{...defaultProps}
|
||||
clipboard={item}
|
||||
historyPreviewLineLimit={2}
|
||||
isExpanded={true}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText(/line1/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/line5/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/\.\.\./)).not.toBeInTheDocument()
|
||||
expect(screen.getByText(/show less/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Code content tests
|
||||
describe('Code Content Preview (Highlight component)', () => {
|
||||
const codeItem = (lines: number) => createMockClipboardItem(
|
||||
'code1',
|
||||
Array.from({ length: lines }, (_, i) => `const line${i + 1} = ${i + 1};`).join('\n'),
|
||||
undefined, // let valuePreview be same as value initially
|
||||
'javascript'
|
||||
)
|
||||
|
||||
it('renders limited lines for code when historyPreviewLineLimit is set (e.g., 3)', () => {
|
||||
render(<ClipboardHistoryRow {...defaultProps} clipboard={codeItem(5)} historyPreviewLineLimit={3} />)
|
||||
expect(screen.getByText(/const line1 = 1;/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/const line3 = 3;/)).toBeInTheDocument()
|
||||
expect(screen.queryByText(/const line4 = 4;/)).not.toBeInTheDocument()
|
||||
// Highlight component renders each line in a div, check for ellipsis in the last visible line
|
||||
const lines = screen.getAllByText(/const line/i)
|
||||
expect(lines.length).toBe(3)
|
||||
expect(within(lines[2].closest('div')!).getByText('...')).toBeInTheDocument()
|
||||
expect(screen.getByText(/Show all/)).toHaveTextContent('+2 lines')
|
||||
})
|
||||
|
||||
it('renders all lines for code if content lines are fewer than historyPreviewLineLimit', () => {
|
||||
render(<ClipboardHistoryRow {...defaultProps} clipboard={codeItem(2)} historyPreviewLineLimit={3} />)
|
||||
expect(screen.getByText(/const line1 = 1;/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/const line2 = 2;/)).toBeInTheDocument()
|
||||
const lines = screen.getAllByText(/const line/i)
|
||||
expect(lines.length).toBe(2)
|
||||
lines.forEach(line => {
|
||||
expect(within(line.closest('div')!).queryByText('...')).not.toBeInTheDocument()
|
||||
})
|
||||
expect(screen.queryByText(/Show all/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders full code preview when historyPreviewLineLimit is 0', () => {
|
||||
render(<ClipboardHistoryRow {...defaultProps} clipboard={codeItem(6)} historyPreviewLineLimit={0} />)
|
||||
expect(screen.getByText(/const line1 = 1;/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/const line6 = 6;/)).toBeInTheDocument()
|
||||
const lines = screen.getAllByText(/const line/i)
|
||||
expect(lines.length).toBe(6)
|
||||
lines.forEach(line => {
|
||||
expect(within(line.closest('div')!).queryByText('...')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('renders full code content when isExpanded is true, ignoring historyPreviewLineLimit', () => {
|
||||
render(
|
||||
<ClipboardHistoryRow
|
||||
{...defaultProps}
|
||||
clipboard={codeItem(5)}
|
||||
historyPreviewLineLimit={2}
|
||||
isExpanded={true}
|
||||
/>
|
||||
)
|
||||
expect(screen.getByText(/const line1 = 1;/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/const line5 = 5;/)).toBeInTheDocument()
|
||||
const lines = screen.getAllByText(/const line/i)
|
||||
expect(lines.length).toBe(5)
|
||||
lines.forEach(line => {
|
||||
expect(within(line.closest('div')!).queryByText('...')).not.toBeInTheDocument()
|
||||
})
|
||||
expect(screen.getByText(/show less/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Show all / Show less button text', () => {
|
||||
it('shows correct "+X lines" when truncated by line limit (text)', () => {
|
||||
const item = createMockClipboardItem('txt1', 'l1\nl2\nl3\nl4\nl5')
|
||||
render(<ClipboardHistoryRow {...defaultProps} clipboard={item} historyPreviewLineLimit={3} />)
|
||||
expect(screen.getByText(/Show all/)).toHaveTextContent('+2 lines')
|
||||
})
|
||||
|
||||
it('shows correct "+X lines" when truncated by line limit (code)', () => {
|
||||
const item = createMockClipboardItem('code1', 'c1\nc2\nc3\nc4\nc5', undefined, 'javascript')
|
||||
render(<ClipboardHistoryRow {...defaultProps} clipboard={item} historyPreviewLineLimit={2} />)
|
||||
expect(screen.getByText(/Show all/)).toHaveTextContent('+3 lines')
|
||||
})
|
||||
|
||||
it('shows correct "+X chars" when truncated by char limit (line limit is 0 or not applicable)', () => {
|
||||
const longLine = 'a'.repeat(100)
|
||||
const previewChar = 'a'.repeat(50)
|
||||
const item = createMockClipboardItem('char1', `${longLine}\nline2`, `${previewChar}\nline2`)
|
||||
item.valueMorePreviewChars = 50 // Manually set for this test case
|
||||
item.valueMorePreviewLines = 0
|
||||
|
||||
render(<ClipboardHistoryRow {...defaultProps} clipboard={item} historyPreviewLineLimit={0} />)
|
||||
// Check if the "Show all" button exists and then check its content
|
||||
const showAllButton = screen.getByText(/Show all/)
|
||||
expect(showAllButton).toBeInTheDocument()
|
||||
expect(showAllButton).toHaveTextContent('+50 chars')
|
||||
})
|
||||
|
||||
it('prioritizes line limit message over char limit message when both could apply', () => {
|
||||
const longText = Array.from({ length: 5 }, (_, i) => `line${i+1} ` + 'char'.repeat(10)).join('\n'); // 5 lines, each long
|
||||
// Preview is only 1 line, and that line is also char limited
|
||||
const previewText = "line1 " + 'char'.repeat(5);
|
||||
|
||||
const item = createMockClipboardItem('combo1', longText, previewText);
|
||||
// Simulate that the original logic determined these char/line differences based on `valuePreview`
|
||||
item.valueMorePreviewLines = 4; // 5 total - 1 previewed = 4 more lines
|
||||
item.valueMorePreviewChars = (("line1 " + 'char'.repeat(10)).length - previewText.length) +
|
||||
(4 * ("lineX " + 'char'.repeat(10)).length); // remaining chars
|
||||
|
||||
render(<ClipboardHistoryRow {...defaultProps} clipboard={item} historyPreviewLineLimit={2} />);
|
||||
// With historyPreviewLineLimit={2}, it should show "+3 lines" (5 total - 2 displayed)
|
||||
expect(screen.getByText(/Show all/)).toHaveTextContent('+3 lines');
|
||||
expect(screen.queryByText(/chars/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "show less" when expanded', () => {
|
||||
const item = createMockClipboardItem('any', 'l1\nl2\nl3')
|
||||
render(<ClipboardHistoryRow {...defaultProps} clipboard={item} isExpanded={true} />)
|
||||
expect(screen.getByText(/show less/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Basic test to confirm the component renders
|
||||
it('ClipboardHistoryRow renders', () => {
|
||||
const mockItem = createMockClipboardItem('id1', 'Test content')
|
||||
render(<ClipboardHistoryRow {...defaultProps} clipboard={mockItem} />)
|
||||
expect(screen.getByText('Test content')).toBeInTheDocument()
|
||||
})
|
@ -122,6 +122,7 @@ interface ClipboardHistoryRowProps {
|
||||
setHistoryFilters?: Dispatch<SetStateAction<string[]>>
|
||||
setAppFilters?: Dispatch<SetStateAction<string[]>>
|
||||
isSingleClickToCopyPaste?: boolean
|
||||
historyPreviewLineLimit?: number
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
@ -180,6 +181,7 @@ export function ClipboardHistoryRowComponent({
|
||||
setHistoryFilters = () => {},
|
||||
setAppFilters = () => {},
|
||||
isSingleClickToCopyPaste = false,
|
||||
historyPreviewLineLimit = 5,
|
||||
}: ClipboardHistoryRowProps) {
|
||||
const { t } = useTranslation()
|
||||
const rowRef = useRef<HTMLDivElement>(null)
|
||||
@ -618,13 +620,23 @@ export function ClipboardHistoryRowComponent({
|
||||
language={clipboard.detectedLanguage}
|
||||
>
|
||||
{({ className, style, tokens, getLineProps, getTokenProps }) => {
|
||||
const limitedTokens =
|
||||
historyPreviewLineLimit && historyPreviewLineLimit > 0 && !isExpanded
|
||||
? tokens.slice(0, historyPreviewLineLimit)
|
||||
: tokens
|
||||
const remainingLines = tokens.length - limitedTokens.length
|
||||
return (
|
||||
<code className={`${className}`} style={style}>
|
||||
{tokens.map((line, i) => {
|
||||
const isLastLine =
|
||||
i === tokens.length - 1 &&
|
||||
clipboard.valueMorePreviewLines &&
|
||||
{limitedTokens.map((line, i) => {
|
||||
const isLastLineOfPreview = i === limitedTokens.length - 1
|
||||
const isActuallyLastLineOfAllTokens = i === tokens.length - 1
|
||||
|
||||
const showEllipsis =
|
||||
isLastLineOfPreview &&
|
||||
!isActuallyLastLineOfAllTokens &&
|
||||
remainingLines > 0 &&
|
||||
!isExpanded
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
@ -649,7 +661,7 @@ export function ClipboardHistoryRowComponent({
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
{isLastLine && (
|
||||
{showEllipsis && (
|
||||
<span className="select-none">...</span>
|
||||
)}
|
||||
</div>
|
||||
@ -659,7 +671,7 @@ export function ClipboardHistoryRowComponent({
|
||||
)
|
||||
}}
|
||||
</Highlight>
|
||||
{clipboard.valueMorePreviewLines && (
|
||||
{(remainingLines > 0 || clipboard.valueMorePreviewLines) && !isExpanded && (
|
||||
<Box className="select-none"> {'\u00A0'} </Box>
|
||||
)}
|
||||
</Box>
|
||||
@ -694,31 +706,38 @@ export function ClipboardHistoryRowComponent({
|
||||
{searchTerm
|
||||
? highlightMatchedText(stringValue, searchTerm)
|
||||
: hyperlinkText(stringValue, clipboard.arrLinks)}
|
||||
{clipboard.valueMorePreviewChars && (
|
||||
{clipboard.valueMorePreviewChars && !isExpanded && (
|
||||
<Box className="select-none"> {'\u00A0'} </Box>
|
||||
)}
|
||||
</code>
|
||||
) : (
|
||||
<code className="justify-start cursor-pointer whitespace-pre">
|
||||
{searchTerm
|
||||
? highlightWithPreviewMatchedText(
|
||||
stringValue ?? '',
|
||||
searchTerm
|
||||
)
|
||||
: hyperlinkTextWithPreview({
|
||||
previewLinkCard: !hasLinkCard && isLinkCardPreviewEnabled,
|
||||
isPreviewError: hasClipboardHistoryURLErrors,
|
||||
value: clipboard.valuePreview ?? '',
|
||||
links: clipboard.arrLinks,
|
||||
itemId: null,
|
||||
historyId: clipboard.historyId,
|
||||
})}
|
||||
{clipboard.valueMorePreviewChars && (
|
||||
<>
|
||||
<span className="select-none">...</span>
|
||||
<Box className="select-none"> {'\u00A0'} </Box>
|
||||
</>
|
||||
)}
|
||||
{hyperlinkTextWithPreview({
|
||||
previewLinkCard: !hasLinkCard && isLinkCardPreviewEnabled,
|
||||
isPreviewError: hasClipboardHistoryURLErrors,
|
||||
value:
|
||||
historyPreviewLineLimit && historyPreviewLineLimit > 0
|
||||
? stringValue
|
||||
.split('\n')
|
||||
.slice(0, historyPreviewLineLimit)
|
||||
.join('\n')
|
||||
: clipboard.valuePreview ?? '',
|
||||
links: clipboard.arrLinks,
|
||||
itemId: null,
|
||||
historyId: clipboard.historyId,
|
||||
searchTerm: searchTerm,
|
||||
})}
|
||||
{(historyPreviewLineLimit &&
|
||||
historyPreviewLineLimit > 0 &&
|
||||
stringValue.split('\n').length > historyPreviewLineLimit
|
||||
? true
|
||||
: clipboard.valueMorePreviewChars) &&
|
||||
!isExpanded && (
|
||||
<>
|
||||
<span className="select-none">...</span>
|
||||
<Box className="select-none"> {'\u00A0'} </Box>
|
||||
</>
|
||||
)}
|
||||
{isMp3 && (
|
||||
<PlayButton
|
||||
src={stringValue}
|
||||
@ -734,7 +753,13 @@ export function ClipboardHistoryRowComponent({
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
{(clipboard.valueMorePreviewLines ||
|
||||
{((historyPreviewLineLimit &&
|
||||
historyPreviewLineLimit > 0 &&
|
||||
!isExpanded &&
|
||||
(stringValue.split('\n').length > historyPreviewLineLimit ||
|
||||
(clipboard.detectedLanguage &&
|
||||
clipboard.valuePreview &&
|
||||
clipboard.value.split('\n').length > historyPreviewLineLimit))) ||
|
||||
clipboard.valueMorePreviewChars) && (
|
||||
<Box
|
||||
className={`absolute left-1 bottom-1 flex flex-row items-center rounded mb-[2px] pl-0.5 ${bgToolsPanel}`}
|
||||
@ -754,17 +779,33 @@ export function ClipboardHistoryRowComponent({
|
||||
sideOffset={10}
|
||||
>
|
||||
{!isExpanded ? (
|
||||
clipboard?.valueMorePreviewChars ? (
|
||||
historyPreviewLineLimit &&
|
||||
historyPreviewLineLimit > 0 &&
|
||||
(stringValue.split('\n').length > historyPreviewLineLimit ||
|
||||
(clipboard.detectedLanguage &&
|
||||
clipboard.valuePreview &&
|
||||
clipboard.value.split('\n').length >
|
||||
historyPreviewLineLimit)) ? (
|
||||
<>
|
||||
+
|
||||
{clipboard.detectedLanguage
|
||||
? clipboard.value.split('\n').length -
|
||||
historyPreviewLineLimit
|
||||
: stringValue.split('\n').length -
|
||||
historyPreviewLineLimit}{' '}
|
||||
{t('lines', { ns: 'common' })}
|
||||
</>
|
||||
) : clipboard?.valueMorePreviewChars ? (
|
||||
<>
|
||||
+{clipboard.valueMorePreviewChars}{' '}
|
||||
{t('chars', { ns: 'common' })}
|
||||
</>
|
||||
) : (
|
||||
) : clipboard?.valueMorePreviewLines ? (
|
||||
<>
|
||||
+{clipboard.valueMorePreviewLines}{' '}
|
||||
{t('lines', { ns: 'common' })}
|
||||
</>
|
||||
)
|
||||
) : null
|
||||
) : (
|
||||
<>- {t('show less', { ns: 'common' })}</>
|
||||
)}
|
||||
|
@ -243,6 +243,7 @@ export default function ClipboardHistoryPage() {
|
||||
isSimplifiedLayout,
|
||||
isSavedClipsPanelVisibleOnly,
|
||||
isSingleClickToCopyPaste,
|
||||
historyPreviewLineLimit,
|
||||
} = useAtomValue(settingsStoreAtom)
|
||||
|
||||
const { t } = useTranslation()
|
||||
@ -1522,6 +1523,7 @@ export default function ClipboardHistoryPage() {
|
||||
isSingleClickToCopyPaste={
|
||||
isSingleClickToCopyPaste
|
||||
}
|
||||
historyPreviewLineLimit={historyPreviewLineLimit}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
@ -2148,6 +2150,7 @@ export default function ClipboardHistoryPage() {
|
||||
isSingleClickToCopyPaste={
|
||||
isSingleClickToCopyPaste
|
||||
}
|
||||
historyPreviewLineLimit={historyPreviewLineLimit}
|
||||
index={index}
|
||||
style={style}
|
||||
/>
|
||||
@ -2208,6 +2211,7 @@ export default function ClipboardHistoryPage() {
|
||||
activeDragId.toString().split('::pinned')[0]
|
||||
})}
|
||||
isSingleClickToCopyPaste={isSingleClickToCopyPaste}
|
||||
historyPreviewLineLimit={historyPreviewLineLimit}
|
||||
/>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
|
@ -123,6 +123,7 @@ export default function ClipboardHistoryQuickPastePage() {
|
||||
isQuickPasteAutoClose,
|
||||
isSingleClickToCopyPaste,
|
||||
isSingleClickToCopyPasteQuickWindow,
|
||||
historyPreviewLineLimit,
|
||||
} = useAtomValue(settingsStoreAtom)
|
||||
|
||||
const [historyFilters, setHistoryFilters] = useState<string[]>([])
|
||||
@ -849,6 +850,7 @@ export default function ClipboardHistoryQuickPastePage() {
|
||||
isSingleClickToCopyPaste ||
|
||||
isSingleClickToCopyPasteQuickWindow
|
||||
}
|
||||
historyPreviewLineLimit={historyPreviewLineLimit}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@ -1048,6 +1050,7 @@ export default function ClipboardHistoryQuickPastePage() {
|
||||
isSingleClickToCopyPaste ||
|
||||
isSingleClickToCopyPasteQuickWindow
|
||||
}
|
||||
historyPreviewLineLimit={historyPreviewLineLimit}
|
||||
index={index}
|
||||
style={style}
|
||||
/>
|
||||
|
@ -84,8 +84,10 @@ export default function ClipboardHistorySettings() {
|
||||
isHistoryEnabled,
|
||||
clipTextMinLength,
|
||||
clipTextMaxLength,
|
||||
historyPreviewLineLimit,
|
||||
setClipTextMinLength,
|
||||
setClipTextMaxLength,
|
||||
setHistoryPreviewLineLimit,
|
||||
setIsHistoryEnabled,
|
||||
isImageCaptureDisabled,
|
||||
setIsImageCaptureDisabled,
|
||||
@ -357,6 +359,64 @@ export default function ClipboardHistorySettings() {
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
<Box className="max-w-xl mt-4 animate-in fade-in">
|
||||
<Card>
|
||||
<CardHeader className="pb-1">
|
||||
<CardTitle className="animate-in fade-in text-md font-medium w-full">
|
||||
{t('History Item Preview Line Limit', {
|
||||
ns: 'settings',
|
||||
})}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Text className="text-sm text-muted-foreground">
|
||||
{t(
|
||||
'Set the maximum number of lines to display in the preview of a history item. Setting it to 0 means unlimited.',
|
||||
{
|
||||
ns: 'settings',
|
||||
}
|
||||
)}
|
||||
</Text>
|
||||
<Flex className="w-full gap-10 my-4 items-start justify-start">
|
||||
<InputField
|
||||
className="text-md !w-36"
|
||||
type="number"
|
||||
step="1"
|
||||
min={0}
|
||||
small
|
||||
label={t('Line limit', { ns: 'common' })}
|
||||
value={historyPreviewLineLimit}
|
||||
onBlur={() => {
|
||||
if (historyPreviewLineLimit < 0) {
|
||||
setHistoryPreviewLineLimit(0)
|
||||
}
|
||||
}}
|
||||
onChange={e => {
|
||||
const value = e.target.value
|
||||
if (value === '') {
|
||||
setHistoryPreviewLineLimit(0)
|
||||
} else {
|
||||
const number = parseInt(value)
|
||||
setHistoryPreviewLineLimit(number)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={historyPreviewLineLimit === 5}
|
||||
onClick={() => {
|
||||
setHistoryPreviewLineLimit(5)
|
||||
}}
|
||||
className="text-sm bg-slate-200 dark:bg-slate-700 dark:text-slate-200 mt-1"
|
||||
>
|
||||
{t('Reset', { ns: 'common' })}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
<Box className="max-w-xl animate-in fade-in mt-4">
|
||||
<Card
|
||||
className={`${
|
||||
|
@ -89,6 +89,7 @@ type Settings = {
|
||||
isScreenLockPassCodeRequireOnStart: boolean
|
||||
clipTextMinLength: number
|
||||
clipTextMaxLength: number
|
||||
historyPreviewLineLimit: number
|
||||
isImageCaptureDisabled: boolean
|
||||
isMenuItemCopyOnlyEnabled: boolean
|
||||
isNoteIconsEnabled: boolean
|
||||
@ -213,6 +214,7 @@ export interface SettingsStoreState {
|
||||
initSettings: (settings: Settings) => void
|
||||
setClipTextMinLength: (width: number) => void
|
||||
setClipTextMaxLength: (height: number) => void
|
||||
setHistoryPreviewLineLimit: (limit: number) => void
|
||||
setProtectedCollections: (ids: string[]) => void
|
||||
setHasPinProtectedCollections: (hasPinProtectedCollections: boolean) => Promise<void>
|
||||
}
|
||||
@ -280,6 +282,7 @@ const initialState: SettingsStoreState & Settings = {
|
||||
isFirstRunAfterUpdate: false,
|
||||
clipTextMinLength: 0,
|
||||
clipTextMaxLength: 5000,
|
||||
historyPreviewLineLimit: 5,
|
||||
isImageCaptureDisabled: false,
|
||||
isMenuItemCopyOnlyEnabled: false,
|
||||
isNoteIconsEnabled: true,
|
||||
@ -353,6 +356,7 @@ const initialState: SettingsStoreState & Settings = {
|
||||
setIsShowNavBarItemsOnHoverOnly: () => {},
|
||||
setClipTextMinLength: () => {},
|
||||
setClipTextMaxLength: () => {},
|
||||
setHistoryPreviewLineLimit: () => {},
|
||||
setIsImageCaptureDisabled: () => {},
|
||||
setIsMenuItemCopyOnlyEnabled: () => {},
|
||||
setIsNoteIconsEnabled: () => {},
|
||||
@ -681,6 +685,9 @@ export const settingsStore = createStore<SettingsStoreState & Settings>()((set,
|
||||
setClipTextMaxLength: async (length: number) => {
|
||||
return get().updateSetting('clipTextMaxLength', length)
|
||||
},
|
||||
setHistoryPreviewLineLimit: async (limit: number) => {
|
||||
return get().updateSetting('historyPreviewLineLimit', limit)
|
||||
},
|
||||
setIsImageCaptureDisabled: async (isEnabled: boolean) => {
|
||||
return get().updateSetting('isImageCaptureDisabled', isEnabled)
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user