feat: Introduce setting for history item preview line limit
Added a new user preference to control max lines displayed in history item previews, improving customization options
This commit is contained in:
parent
e2972ecdba
commit
c10ec6347e
5
.changeset/red-tools-kick.md
Normal file
5
.changeset/red-tools-kick.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'pastebar-app-ui': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Added: history item preview max lines user setting
|
@ -129,6 +129,7 @@ function App() {
|
|||||||
isCustomDbPathValid: null,
|
isCustomDbPathValid: null,
|
||||||
customDbPathError: null,
|
customDbPathError: null,
|
||||||
dbRelocationInProgress: false,
|
dbRelocationInProgress: false,
|
||||||
|
historyPreviewLineLimit: settings.historyPreviewLineLimit?.valueInt ?? null,
|
||||||
appLastUpdateVersion: settings.appLastUpdateVersion?.valueText,
|
appLastUpdateVersion: settings.appLastUpdateVersion?.valueText,
|
||||||
appLastUpdateDate: settings.appLastUpdateDate?.valueText,
|
appLastUpdateDate: settings.appLastUpdateDate?.valueText,
|
||||||
isHideMacOSDockIcon: settings.isHideMacOSDockIcon?.valueBool,
|
isHideMacOSDockIcon: settings.isHideMacOSDockIcon?.valueBool,
|
||||||
|
@ -49,6 +49,7 @@ function QuickPasteApp() {
|
|||||||
settingsStore.initSettings({
|
settingsStore.initSettings({
|
||||||
appDataDir: '',
|
appDataDir: '',
|
||||||
isSingleClickToCopyPaste: settings.isSingleClickToCopyPaste?.valueBool,
|
isSingleClickToCopyPaste: settings.isSingleClickToCopyPaste?.valueBool,
|
||||||
|
historyPreviewLineLimit: settings.historyPreviewLineLimit?.valueInt ?? null,
|
||||||
appLastUpdateVersion: settings.appLastUpdateVersion?.valueText,
|
appLastUpdateVersion: settings.appLastUpdateVersion?.valueText,
|
||||||
appLastUpdateDate: settings.appLastUpdateDate?.valueText,
|
appLastUpdateDate: settings.appLastUpdateDate?.valueText,
|
||||||
isHideMacOSDockIcon: settings.isHideMacOSDockIcon?.valueBool,
|
isHideMacOSDockIcon: settings.isHideMacOSDockIcon?.valueBool,
|
||||||
|
@ -181,7 +181,7 @@ export const CodeViewer: FC<CodeViewerProps> = ({
|
|||||||
return (
|
return (
|
||||||
<Box className="relative">
|
<Box className="relative">
|
||||||
<OverlayScrollbarsComponent
|
<OverlayScrollbarsComponent
|
||||||
className={`${isShowMore ? 'code-scroll-x pb-2' : ''}`}
|
className={`${isShowMore ? 'code-scroll-x' : ''}`}
|
||||||
options={{
|
options={{
|
||||||
overflow: {
|
overflow: {
|
||||||
x: !isShowMore ? 'hidden' : 'scroll',
|
x: !isShowMore ? 'hidden' : 'scroll',
|
||||||
|
@ -64,3 +64,6 @@ Keep Pinned Items: Angehängte Elemente beibehalten
|
|||||||
Keep Starred Items: Mit Stern markierte Elemente beibehalten
|
Keep Starred Items: Mit Stern markierte Elemente beibehalten
|
||||||
Preserve pinned items when clearing history: Angehängte Elemente beim Löschen des Verlaufs beibehalten
|
Preserve pinned items when clearing history: Angehängte Elemente beim Löschen des Verlaufs beibehalten
|
||||||
Preserve starred items when clearing history: Mit Stern markierte Elemente beim Löschen des Verlaufs beibehalten
|
Preserve starred items when clearing history: Mit Stern markierte Elemente beim Löschen des Verlaufs beibehalten
|
||||||
|
History Item Preview Max Lines: Maximale Vorschauzeilenanzahl für Verlaufselemente
|
||||||
|
Set the maximum number of lines to display in the preview of a history item: Maximale Anzahl der Zeilen für die Vorschau eines Verlaufselements festlegen
|
||||||
|
Preview Max Lines: Vorschauzeilen
|
||||||
|
@ -1,38 +1,53 @@
|
|||||||
App restart required: Restart required
|
App restart required: Restart required
|
||||||
Application Starts with Main Window Hidden: Application Starts with Main Window Hidden
|
Application Starts with Main Window Hidden: Application Starts with Main Window Hidden
|
||||||
|
Auto-close window after action: Auto-close window after action
|
||||||
Boards, saved clips and menu items panel visible: Boards, saved clips and menu items panel visible
|
Boards, saved clips and menu items panel visible: Boards, saved clips and menu items panel visible
|
||||||
Both clipboard history and saved clips panels visible: Both clipboard history and saved clips panels visible
|
Both clipboard history and saved clips panels visible: Both clipboard history and saved clips panels visible
|
||||||
Change: Change
|
Change: Change
|
||||||
|
Click Set/Change button to start recording: Click Set/Change button to start recording
|
||||||
Clipboard history panel visible: Clipboard history panel visible
|
Clipboard history panel visible: Clipboard history panel visible
|
||||||
|
Configure the behavior of the Quick Paste window when selecting items: Configure the behavior of the Quick Paste window when selecting items
|
||||||
|
Configure which items to preserve when clearing clipboard history (both manual and auto-clear operations).: Configure which items to preserve when clearing clipboard history (both manual and auto-clear operations).
|
||||||
Control which panels are visible in the main window: Control which panels are visible in the main window
|
Control which panels are visible in the main window: Control which panels are visible in the main window
|
||||||
|
Copy items only (no auto-paste): Copy items only (no auto-paste)
|
||||||
Copy only from menu items: Copy only from menu items
|
Copy only from menu items: Copy only from menu items
|
||||||
Default Note Icon Type: Default Note Icon Type
|
Default Note Icon Type: Default Note Icon Type
|
||||||
Display navbar items only when the mouse hovers over the navigation bar to minimize visible UI elements: Display navbar items only when the mouse hovers over the navigation bar to minimize visible UI elements
|
Display navbar items only when the mouse hovers over the navigation bar to minimize visible UI elements: Display navbar items only when the mouse hovers over the navigation bar to minimize visible UI elements
|
||||||
Display persistent icons on clips that have notes to improve visual organization and make notes easier to discover.: Display persistent icons on clips that have notes to improve visual organization and make notes easier to discover.
|
Display persistent icons on clips that have notes to improve visual organization and make notes easier to discover.: Display persistent icons on clips that have notes to improve visual organization and make notes easier to discover.
|
||||||
Enable simplified, less boxy layout for a cleaner and more streamlined interface design: Enable simplified, less boxy layout for a cleaner and more streamlined interface design
|
Enable simplified, less boxy layout for a cleaner and more streamlined interface design: Enable simplified, less boxy layout for a cleaner and more streamlined interface design
|
||||||
|
Enable single-click to copy/paste clipboard history items and saved clips instead of requiring double-click.: Enable single-click to copy/paste clipboard history items and saved clips instead of requiring double-click.
|
||||||
Global System OS Hotkeys: Global System OS Hotkeys
|
Global System OS Hotkeys: Global System OS Hotkeys
|
||||||
Hide Collections Navbar: Hide Collections Navbar
|
Hide Collections Navbar: Hide Collections Navbar
|
||||||
Hide collections menu dropdown on the navigation bar: Hide collections menu dropdown on the navigation bar
|
Hide collections menu dropdown on the navigation bar: Hide collections menu dropdown on the navigation bar
|
||||||
Hide collections menu on the navbar: Hide collections menu on the navbar
|
Hide collections menu on the navbar: Hide collections menu on the navbar
|
||||||
Hide the App Dock Icon: Hide the App Dock Icon
|
Hide the App Dock Icon: Hide the App Dock Icon
|
||||||
|
History Item Preview Max Lines: History Item Preview Max Lines
|
||||||
|
'How to set hotkeys:': 'How to set hotkeys:'
|
||||||
|
Keep Items on Clear: Keep Items on Clear
|
||||||
|
Keep Pinned Items: Keep Pinned Items
|
||||||
|
Keep Starred Items: Keep Starred Items
|
||||||
? Keep the main application window hidden when the app restarts. You can reopen it using the menu bar or taskbar menu, or using global hotkeys.
|
? Keep the main application window hidden when the app restarts. You can reopen it using the menu bar or taskbar menu, or using global hotkeys.
|
||||||
: Keep the main application window hidden when the app restarts. You can reopen it using the menu bar or taskbar menu, or using global hotkeys.
|
: Keep the main application window hidden when the app restarts. You can reopen it using the menu bar or taskbar menu, or using global hotkeys.
|
||||||
No keys set: No keys set
|
No keys set: No keys set
|
||||||
'Note: At least one panel must remain visible in main window.': 'Note: At least one panel must remain visible in main window.'
|
'Note: At least one panel must remain visible in main window.': 'Note: At least one panel must remain visible in main window.'
|
||||||
Panel Visibility: Panel Visibility
|
Panel Visibility: Panel Visibility
|
||||||
PasteBar Quick Paste: PasteBar Quick Paste
|
PasteBar Quick Paste: PasteBar Quick Paste
|
||||||
|
Preserve pinned items when clearing history: Preserve pinned items when clearing history
|
||||||
|
Preserve starred items when clearing history: Preserve starred items when clearing history
|
||||||
|
Press Backspace/Delete to clear the hotkey: Press Backspace/Delete to clear the hotkey
|
||||||
|
Press Enter to confirm or Escape to cancel: Press Enter to confirm or Escape to cancel
|
||||||
Press keys: Press keys
|
Press keys: Press keys
|
||||||
|
Press your desired key combination (e.g., Ctrl+Shift+V): Press your desired key combination (e.g., Ctrl+Shift+V)
|
||||||
|
Press your key combination...: Press your key combination...
|
||||||
|
Preview Max Lines: Preview Max Lines
|
||||||
|
Quick Paste Window: Quick Paste Window
|
||||||
|
Quick Paste Window Options: Quick Paste Window Options
|
||||||
|
Recording...: Recording...
|
||||||
? Remove PasteBar app icon from the macOS Dock while keeping the app running in the background. The app remains accessible via the menu bar icon. Requires an app restart to take effect.
|
? Remove PasteBar app icon from the macOS Dock while keeping the app running in the background. The app remains accessible via the menu bar icon. Requires an app restart to take effect.
|
||||||
: Remove PasteBar app icon from the macOS Dock while keeping the app running in the background. The app remains accessible via the menu bar icon. Requires an app restart to take effect.
|
: Remove PasteBar app icon from the macOS Dock while keeping the app running in the background. The app remains accessible via the menu bar icon. Requires an app restart to take effect.
|
||||||
Set: Set
|
Set: Set
|
||||||
Set system OS hotkeys to show/hide the main app window and quick paste window. Supports up to 3-key combinations.: Set system OS hotkeys to show/hide the main app window and quick paste window. Supports up to 3-key combinations.
|
Set system OS hotkeys to show/hide the main app window and quick paste window. Supports up to 3-key combinations.: Set system OS hotkeys to show/hide the main app window and quick paste window. Supports up to 3-key combinations.
|
||||||
'How to set hotkeys:': 'How to set hotkeys:'
|
Set the maximum number of lines to display in the preview of a history item: Set the maximum number of lines to display in the preview of a history item
|
||||||
Click Set/Change button to start recording: Click Set/Change button to start recording
|
|
||||||
Press your desired key combination (e.g., Ctrl+Shift+V): Press your desired key combination (e.g., Ctrl+Shift+V)
|
|
||||||
Press Enter to confirm or Escape to cancel: Press Enter to confirm or Escape to cancel
|
|
||||||
Press Backspace/Delete to clear the hotkey: Press Backspace/Delete to clear the hotkey
|
|
||||||
Press your key combination...: Press your key combination...
|
|
||||||
Recording...: Recording...
|
|
||||||
Show Boards and Clips Panel Only: Show Boards and Clips Panel Only
|
Show Boards and Clips Panel Only: Show Boards and Clips Panel Only
|
||||||
Show Both Panels: Show Both Panels
|
Show Both Panels: Show Both Panels
|
||||||
Show History Panel Only: Show History Panel Only
|
Show History Panel Only: Show History Panel Only
|
||||||
@ -44,15 +59,8 @@ Show navbar elements on hover only: Show navbar elements on hover only
|
|||||||
Show/Hide Main App Window: Show/Hide Main App Window
|
Show/Hide Main App Window: Show/Hide Main App Window
|
||||||
Show/Hide Quick Paste Window: Show/Hide Quick Paste Window
|
Show/Hide Quick Paste Window: Show/Hide Quick Paste Window
|
||||||
Simplified Panel Layout: Simplified Panel Layout
|
Simplified Panel Layout: Simplified Panel Layout
|
||||||
When enabled, the Quick Paste window will automatically close after copying or pasting an item.: When enabled, the Quick Paste window will automatically close after copying or pasting an item.
|
|
||||||
Enable single-click to copy/paste clipboard history items and saved clips instead of requiring double-click.: Enable single-click to copy/paste clipboard history items and saved clips instead of requiring double-click.
|
|
||||||
Single Click Copy/Paste action: Single Click Copy/Paste action
|
|
||||||
Single Click Copy/Paste: Single Click Copy/Paste
|
Single Click Copy/Paste: Single Click Copy/Paste
|
||||||
Quick Paste Window: Quick Paste Window
|
Single Click Copy/Paste action: Single Click Copy/Paste action
|
||||||
Quick Paste Window Options: Quick Paste Window Options
|
|
||||||
Configure the behavior of the Quick Paste window when selecting items: Configure the behavior of the Quick Paste window when selecting items
|
|
||||||
Copy items only (no auto-paste): Copy items only (no auto-paste)
|
|
||||||
Auto-close window after action: Auto-close window after action
|
|
||||||
This sets the default icon type for new clips with notes. You can customize individual clips via the context menu.: This sets the default icon type for new clips with notes. You can customize individual clips via the context menu.
|
This sets the default icon type for new clips with notes. You can customize individual clips via the context menu.: This sets the default icon type for new clips with notes. You can customize individual clips via the context menu.
|
||||||
? When enabled, clicking menu items will only copy content to clipboard instead of auto-pasting. This gives you more control over when and where content is pasted.
|
? When enabled, clicking menu items will only copy content to clipboard instead of auto-pasting. This gives you more control over when and where content is pasted.
|
||||||
: When enabled, clicking menu items will only copy content to clipboard instead of auto-pasting. This gives you more control over when and where content is pasted.
|
: When enabled, clicking menu items will only copy content to clipboard instead of auto-pasting. This gives you more control over when and where content is pasted.
|
||||||
@ -60,9 +68,4 @@ This sets the default icon type for new clips with notes. You can customize indi
|
|||||||
: When enabled, clicking or pressing Enter on items in Quick Paste window will only copy them to clipboard without automatically pasting.
|
: When enabled, clicking or pressing Enter on items in Quick Paste window will only copy them to clipboard without automatically pasting.
|
||||||
? When enabled, single click will copy/paste items in Quick Paste window. If global single click is also enabled, both settings work together.
|
? When enabled, single click will copy/paste items in Quick Paste window. If global single click is also enabled, both settings work together.
|
||||||
: When enabled, single click will copy/paste items in Quick Paste window. If global single click is also enabled, both settings work together.
|
: When enabled, single click will copy/paste items in Quick Paste window. If global single click is also enabled, both settings work together.
|
||||||
Configure which items to preserve when clearing clipboard history (both manual and auto-clear operations).: Configure which items to preserve when clearing clipboard history (both manual and auto-clear operations).
|
When enabled, the Quick Paste window will automatically close after copying or pasting an item.: When enabled, the Quick Paste window will automatically close after copying or pasting an item.
|
||||||
Keep Items on Clear: Keep Items on Clear
|
|
||||||
Keep Pinned Items: Keep Pinned Items
|
|
||||||
Keep Starred Items: Keep Starred Items
|
|
||||||
Preserve pinned items when clearing history: Preserve pinned items when clearing history
|
|
||||||
Preserve starred items when clearing history: Preserve starred items when clearing history
|
|
||||||
|
@ -64,3 +64,6 @@ Keep Pinned Items: Conservar elementos fijados
|
|||||||
Keep Starred Items: Conservar elementos destacados
|
Keep Starred Items: Conservar elementos destacados
|
||||||
Preserve pinned items when clearing history: Conservar elementos fijados al borrar el historial
|
Preserve pinned items when clearing history: Conservar elementos fijados al borrar el historial
|
||||||
Preserve starred items when clearing history: Conservar elementos destacados al borrar el historial
|
Preserve starred items when clearing history: Conservar elementos destacados al borrar el historial
|
||||||
|
History Item Preview Max Lines: Límite máximo de líneas de vista previa de elementos del historial
|
||||||
|
Set the maximum number of lines to display in the preview of a history item: Establecer el número máximo de líneas para mostrar en la vista previa de un elemento del historial
|
||||||
|
Preview Max Lines: Líneas de vista previa
|
||||||
|
@ -64,3 +64,6 @@ Keep Pinned Items: Conserver les éléments épinglés
|
|||||||
Keep Starred Items: Conserver les éléments favoris
|
Keep Starred Items: Conserver les éléments favoris
|
||||||
Preserve pinned items when clearing history: Conserver les éléments épinglés lors de l'effacement de l'historique
|
Preserve pinned items when clearing history: Conserver les éléments épinglés lors de l'effacement de l'historique
|
||||||
Preserve starred items when clearing history: Conserver les éléments favoris lors de l'effacement de l'historique
|
Preserve starred items when clearing history: Conserver les éléments favoris lors de l'effacement de l'historique
|
||||||
|
History Item Preview Max Lines: Limite maximale de lignes d'aperçu des éléments de l'historique
|
||||||
|
Set the maximum number of lines to display in the preview of a history item: Définir le nombre maximal de lignes à afficher dans l'aperçu d'un élément de l'historique
|
||||||
|
Preview Max Lines: Lignes d'aperçu
|
||||||
|
@ -64,3 +64,6 @@ Keep Pinned Items: Conserva elementi fissati
|
|||||||
Keep Starred Items: Conserva elementi preferiti
|
Keep Starred Items: Conserva elementi preferiti
|
||||||
Preserve pinned items when clearing history: Conserva elementi fissati durante la cancellazione della cronologia
|
Preserve pinned items when clearing history: Conserva elementi fissati durante la cancellazione della cronologia
|
||||||
Preserve starred items when clearing history: Conserva elementi preferiti durante la cancellazione della cronologia
|
Preserve starred items when clearing history: Conserva elementi preferiti durante la cancellazione della cronologia
|
||||||
|
History Item Preview Max Lines: Limite massimo di righe di anteprima degli elementi della cronologia
|
||||||
|
Set the maximum number of lines to display in the preview of a history item: Imposta il numero massimo di righe da visualizzare nell'anteprima di un elemento della cronologia
|
||||||
|
Preview Max Lines: Righe di anteprima
|
||||||
|
@ -64,3 +64,6 @@ Keep Pinned Items: Сохранять закрепленные элементы
|
|||||||
Keep Starred Items: Сохранять избранные элементы
|
Keep Starred Items: Сохранять избранные элементы
|
||||||
Preserve pinned items when clearing history: Сохранять закрепленные элементы при очистке истории
|
Preserve pinned items when clearing history: Сохранять закрепленные элементы при очистке истории
|
||||||
Preserve starred items when clearing history: Сохранять избранные элементы при очистке истории
|
Preserve starred items when clearing history: Сохранять избранные элементы при очистке истории
|
||||||
|
History Item Preview Max Lines: Количество строк предварительного просмотра элементов истории
|
||||||
|
Set the maximum number of lines to display in the preview of a history item: Установить максимальное количество строк для отображения в предварительном просмотре элемента истории
|
||||||
|
Preview Max Lines: Максимум строк просмотра
|
||||||
|
@ -64,3 +64,6 @@ Keep Pinned Items: Sabitlenmiş öğeleri koru
|
|||||||
Keep Starred Items: Favorilenmiş öğeleri koru
|
Keep Starred Items: Favorilenmiş öğeleri koru
|
||||||
Preserve pinned items when clearing history: Geçmişi temizlerken sabitlenmiş öğeleri koru
|
Preserve pinned items when clearing history: Geçmişi temizlerken sabitlenmiş öğeleri koru
|
||||||
Preserve starred items when clearing history: Geçmişi temizlerken favorilenmiş öğeleri koru
|
Preserve starred items when clearing history: Geçmişi temizlerken favorilenmiş öğeleri koru
|
||||||
|
History Item Preview Max Lines: Geçmiş öğesi önizleme maksimum satır sayısı
|
||||||
|
Set the maximum number of lines to display in the preview of a history item: Bir geçmiş öğesinin önizlemesinde gösterilecek maksimum satır sayısını ayarla
|
||||||
|
Preview Max Lines: Önizleme Satırları
|
||||||
|
@ -64,3 +64,6 @@ Keep Pinned Items: Зберігати закріплені елементи
|
|||||||
Keep Starred Items: Зберігати обрані елементи
|
Keep Starred Items: Зберігати обрані елементи
|
||||||
Preserve pinned items when clearing history: Зберігати закріплені елементи під час очищення історії
|
Preserve pinned items when clearing history: Зберігати закріплені елементи під час очищення історії
|
||||||
Preserve starred items when clearing history: Зберігати обрані елементи під час очищення історії
|
Preserve starred items when clearing history: Зберігати обрані елементи під час очищення історії
|
||||||
|
History Item Preview Max Lines: Рядки перегляду історії
|
||||||
|
Set the maximum number of lines to display in the preview of a history item: Встановити максимальну кількість рядків для відображення в попередньому перегляді елемента історії
|
||||||
|
Preview Max Lines: Рядки перегляду
|
||||||
|
@ -64,3 +64,6 @@ Keep Pinned Items: 保留已固定项目
|
|||||||
Keep Starred Items: 保留已星标项目
|
Keep Starred Items: 保留已星标项目
|
||||||
Preserve pinned items when clearing history: 清除历史记录时保留已固定项目
|
Preserve pinned items when clearing history: 清除历史记录时保留已固定项目
|
||||||
Preserve starred items when clearing history: 清除历史记录时保留已星标项目
|
Preserve starred items when clearing history: 清除历史记录时保留已星标项目
|
||||||
|
History Item Preview Max Lines: 历史项目预览最大行数
|
||||||
|
Set the maximum number of lines to display in the preview of a history item: 设置历史项目预览中显示的最大行数
|
||||||
|
Preview Max Lines: 预览行数
|
||||||
|
@ -189,7 +189,7 @@ export function ClipboardHistoryLargeViewComponent({
|
|||||||
</Box>
|
</Box>
|
||||||
) : clipboard.detectedLanguage && clipboard.valuePreview ? (
|
) : clipboard.detectedLanguage && clipboard.valuePreview ? (
|
||||||
<Box
|
<Box
|
||||||
className="text-ellipsis self-start text-sm w-full p-1.5 animate-in fade-in"
|
className="text-ellipsis self-start text-sm w-full animate-in fade-in"
|
||||||
key={clipboard.historyId}
|
key={clipboard.historyId}
|
||||||
>
|
>
|
||||||
<Highlight
|
<Highlight
|
||||||
@ -215,34 +215,42 @@ export function ClipboardHistoryLargeViewComponent({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<code className={`${className}`} style={style}>
|
<code className={`${className}`} style={style}>
|
||||||
{tokens.map((line, i) => {
|
{tokens
|
||||||
return (
|
.filter((line, i) => {
|
||||||
<div
|
// Remove last line if it's empty (only contains whitespace or newlines)
|
||||||
key={i}
|
if (i === tokens.length - 1) {
|
||||||
{...getLineProps({ line })}
|
return line.some(token => token.content.trim() !== '')
|
||||||
className={`${
|
}
|
||||||
isWrapText
|
return true
|
||||||
? 'whitespace-pre-wrap'
|
})
|
||||||
: 'whitespace-pre'
|
.map((line, i) => {
|
||||||
} overflow-hidden text-ellipsis`}
|
return (
|
||||||
>
|
<div
|
||||||
{line.map((token, key) => (
|
key={i}
|
||||||
<span
|
{...getLineProps({ line })}
|
||||||
key={key}
|
className={`${
|
||||||
{...getTokenProps({ token })}
|
isWrapText
|
||||||
className="select-text"
|
? 'whitespace-pre-wrap'
|
||||||
>
|
: 'whitespace-pre'
|
||||||
{!searchTerm
|
} overflow-hidden text-ellipsis`}
|
||||||
? token.content
|
>
|
||||||
: highlightMatchedText(
|
{line.map((token, key) => (
|
||||||
token.content,
|
<span
|
||||||
searchTerm
|
key={key}
|
||||||
)}
|
{...getTokenProps({ token })}
|
||||||
</span>
|
className="select-text"
|
||||||
))}
|
>
|
||||||
</div>
|
{!searchTerm
|
||||||
)
|
? token.content
|
||||||
})}
|
: highlightMatchedText(
|
||||||
|
token.content,
|
||||||
|
searchTerm
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</code>
|
</code>
|
||||||
</OverlayScrollbarsComponent>
|
</OverlayScrollbarsComponent>
|
||||||
)
|
)
|
||||||
@ -291,9 +299,6 @@ export function ClipboardHistoryLargeViewComponent({
|
|||||||
{searchTerm
|
{searchTerm
|
||||||
? highlightMatchedText(textValue, searchTerm)
|
? highlightMatchedText(textValue, searchTerm)
|
||||||
: hyperlinkText(textValue, clipboard.arrLinks)}
|
: hyperlinkText(textValue, clipboard.arrLinks)}
|
||||||
{clipboard.valueMorePreviewChars && (
|
|
||||||
<Box className="select-none"> {'\u00A0'} </Box>
|
|
||||||
)}
|
|
||||||
</code>
|
</code>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
@ -1,292 +0,0 @@
|
|||||||
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()
|
|
||||||
})
|
|
@ -13,6 +13,7 @@ import WrapIcon from '~/assets/icons/wrap'
|
|||||||
import { MINUTE_IN_MS } from '~/constants'
|
import { MINUTE_IN_MS } from '~/constants'
|
||||||
import { isEmailNotUrl } from '~/libs/utils'
|
import { isEmailNotUrl } from '~/libs/utils'
|
||||||
import { formatLocale as format } from '~/locales/date-locales'
|
import { formatLocale as format } from '~/locales/date-locales'
|
||||||
|
import { getValuePreview } from '~/pages/components/Dashboard/components/utils'
|
||||||
import { hoveringHistoryRowId, isKeyAltPressed, isKeyCtrlPressed } from '~/store'
|
import { hoveringHistoryRowId, isKeyAltPressed, isKeyCtrlPressed } from '~/store'
|
||||||
import { Check, Dot, Star } from 'lucide-react'
|
import { Check, Dot, Star } from 'lucide-react'
|
||||||
import { Highlight, themes } from 'prism-react-renderer'
|
import { Highlight, themes } from 'prism-react-renderer'
|
||||||
@ -103,7 +104,7 @@ interface ClipboardHistoryQuickPasteRowProps {
|
|||||||
setHistoryFilters?: Dispatch<SetStateAction<string[]>>
|
setHistoryFilters?: Dispatch<SetStateAction<string[]>>
|
||||||
setAppFilters?: Dispatch<SetStateAction<string[]>>
|
setAppFilters?: Dispatch<SetStateAction<string[]>>
|
||||||
isSingleClickToCopyPaste?: boolean
|
isSingleClickToCopyPaste?: boolean
|
||||||
historyPreviewLineLimit?: number
|
historyPreviewLineLimit?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||||
@ -117,8 +118,6 @@ export function ClipboardHistoryQuickPasteRowComponent({
|
|||||||
isPinnedTop = false,
|
isPinnedTop = false,
|
||||||
isPinnedTopFirst = false,
|
isPinnedTopFirst = false,
|
||||||
isWindows,
|
isWindows,
|
||||||
isDisabledPinnedMoveUp = false,
|
|
||||||
isDisabledPinnedMoveDown = false,
|
|
||||||
isExpanded = false,
|
isExpanded = false,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
isWrapText = false,
|
isWrapText = false,
|
||||||
@ -156,15 +155,13 @@ export function ClipboardHistoryQuickPasteRowComponent({
|
|||||||
isDragPreview = false,
|
isDragPreview = false,
|
||||||
setRowHeight = () => {},
|
setRowHeight = () => {},
|
||||||
isSingleClickToCopyPaste = false,
|
isSingleClickToCopyPaste = false,
|
||||||
historyPreviewLineLimit = 5,
|
historyPreviewLineLimit,
|
||||||
}: ClipboardHistoryQuickPasteRowProps) {
|
}: ClipboardHistoryQuickPasteRowProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const rowRef = useRef<HTMLDivElement>(null)
|
const rowRef = useRef<HTMLDivElement>(null)
|
||||||
const rowKeyboardRef = useRef<HTMLDivElement>(null)
|
const rowKeyboardRef = useRef<HTMLDivElement>(null)
|
||||||
const isCopiedOrPasted = isCopied || isPasted || isSaved
|
const isCopiedOrPasted = isCopied || isPasted || isSaved
|
||||||
|
|
||||||
console.log('isSingleClickToCopyPaste', isSingleClickToCopyPaste)
|
|
||||||
|
|
||||||
const contentElementRendered = useSignal<boolean>(false)
|
const contentElementRendered = useSignal<boolean>(false)
|
||||||
const contextMenuOpen = useSignal<boolean>(false)
|
const contextMenuOpen = useSignal<boolean>(false)
|
||||||
|
|
||||||
@ -261,6 +258,41 @@ export function ClipboardHistoryQuickPasteRowComponent({
|
|||||||
const isNowItem = index === 0 && clipboard.updatedAt > Date.now() - MINUTE_IN_MS
|
const isNowItem = index === 0 && clipboard.updatedAt > Date.now() - MINUTE_IN_MS
|
||||||
const isMp3 = clipboard?.isLink && clipboard?.value?.endsWith('.mp3')
|
const isMp3 = clipboard?.isLink && clipboard?.value?.endsWith('.mp3')
|
||||||
|
|
||||||
|
// Recalculate preview with custom line limit if provided
|
||||||
|
const { valuePreview, valueMorePreviewLines, valueMorePreviewChars } = useMemo(() => {
|
||||||
|
if (historyPreviewLineLimit && historyPreviewLineLimit > 0 && clipboard?.value) {
|
||||||
|
const result = getValuePreview(
|
||||||
|
clipboard.value,
|
||||||
|
clipboard.isImageData || false,
|
||||||
|
isExpanded,
|
||||||
|
historyPreviewLineLimit,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
// console.log('result', result)
|
||||||
|
return {
|
||||||
|
valuePreview: result.valuePreview,
|
||||||
|
valueMorePreviewLines: result.morePreviewLines,
|
||||||
|
valueMorePreviewChars: result.morePreviewChars,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Use default preview from backend
|
||||||
|
return {
|
||||||
|
valuePreview: clipboard?.valuePreview || '',
|
||||||
|
valueMorePreviewLines: clipboard?.valueMorePreviewLines || null,
|
||||||
|
valueMorePreviewChars: clipboard?.valueMorePreviewChars || null,
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
historyPreviewLineLimit,
|
||||||
|
clipboard?.value,
|
||||||
|
clipboard?.isImageData,
|
||||||
|
isExpanded,
|
||||||
|
clipboard?.valuePreview,
|
||||||
|
clipboard?.valueMorePreviewLines,
|
||||||
|
clipboard?.valueMorePreviewChars,
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('valuePreview', valuePreview)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!hasLinkCard &&
|
!hasLinkCard &&
|
||||||
@ -478,7 +510,7 @@ export function ClipboardHistoryQuickPasteRowComponent({
|
|||||||
{searchTerm ? (
|
{searchTerm ? (
|
||||||
highlightWithPreviewMatchedText(clipboard.value, searchTerm)
|
highlightWithPreviewMatchedText(clipboard.value, searchTerm)
|
||||||
) : (
|
) : (
|
||||||
<span>{clipboard.valuePreview}</span>
|
<span>{valuePreview}</span>
|
||||||
)}
|
)}
|
||||||
</code>
|
</code>
|
||||||
</Box>
|
</Box>
|
||||||
@ -528,7 +560,7 @@ export function ClipboardHistoryQuickPasteRowComponent({
|
|||||||
className="max-w-full max-h-56 min-h-10 rounded-md shadow-sm border border-slate-100 dark:border-slate-700"
|
className="max-w-full max-h-56 min-h-10 rounded-md shadow-sm border border-slate-100 dark:border-slate-700"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
) : clipboard.detectedLanguage && clipboard.valuePreview ? (
|
) : clipboard.detectedLanguage && valuePreview ? (
|
||||||
<Box
|
<Box
|
||||||
ref={ref => {
|
ref={ref => {
|
||||||
if (ref) {
|
if (ref) {
|
||||||
@ -539,25 +571,16 @@ export function ClipboardHistoryQuickPasteRowComponent({
|
|||||||
>
|
>
|
||||||
<Highlight
|
<Highlight
|
||||||
theme={isDark ? themes.vsDark : themes.github}
|
theme={isDark ? themes.vsDark : themes.github}
|
||||||
code={isExpanded ? textValue : clipboard.valuePreview.trim()}
|
code={isExpanded ? textValue : valuePreview.trim()}
|
||||||
language={clipboard.detectedLanguage}
|
language={clipboard.detectedLanguage}
|
||||||
>
|
>
|
||||||
{({ className, style, tokens, getLineProps, getTokenProps }) => {
|
{({ className, style, tokens, getLineProps, getTokenProps }) => {
|
||||||
const limitedTokens =
|
|
||||||
historyPreviewLineLimit && historyPreviewLineLimit > 0 && !isExpanded
|
|
||||||
? tokens.slice(0, historyPreviewLineLimit)
|
|
||||||
: tokens
|
|
||||||
const remainingLines = tokens.length - limitedTokens.length
|
|
||||||
return (
|
return (
|
||||||
<code className={`${className}`} style={style}>
|
<code className={`${className}`} style={style}>
|
||||||
{limitedTokens.map((line, i) => {
|
{tokens.map((line, i) => {
|
||||||
const isLastLineOfPreview = i === limitedTokens.length - 1
|
const isLastLine =
|
||||||
const isActuallyLastLineOfAllTokens = i === tokens.length - 1
|
i === tokens.length - 1 &&
|
||||||
|
valueMorePreviewLines &&
|
||||||
const showEllipsis =
|
|
||||||
isLastLineOfPreview &&
|
|
||||||
!isActuallyLastLineOfAllTokens &&
|
|
||||||
remainingLines > 0 &&
|
|
||||||
!isExpanded
|
!isExpanded
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -578,7 +601,7 @@ export function ClipboardHistoryQuickPasteRowComponent({
|
|||||||
: highlightMatchedText(token.content, searchTerm)}
|
: highlightMatchedText(token.content, searchTerm)}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{showEllipsis && <span className="select-none">...</span>}
|
{isLastLine && <span className="select-none">...</span>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@ -586,9 +609,6 @@ export function ClipboardHistoryQuickPasteRowComponent({
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</Highlight>
|
</Highlight>
|
||||||
{(remainingLines > 0 || clipboard.valueMorePreviewLines) && !isExpanded && (
|
|
||||||
<Box className="select-none"> {'\u00A0'} </Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Box
|
<Box
|
||||||
@ -621,38 +641,19 @@ export function ClipboardHistoryQuickPasteRowComponent({
|
|||||||
{searchTerm
|
{searchTerm
|
||||||
? highlightMatchedText(textValue, searchTerm)
|
? highlightMatchedText(textValue, searchTerm)
|
||||||
: hyperlinkText(textValue, clipboard.arrLinks)}
|
: hyperlinkText(textValue, clipboard.arrLinks)}
|
||||||
{clipboard.valueMorePreviewChars && !isExpanded && (
|
|
||||||
<Box className="select-none"> {'\u00A0'} </Box>
|
|
||||||
)}
|
|
||||||
</code>
|
</code>
|
||||||
) : (
|
) : (
|
||||||
<code className="justify-start cursor-pointer whitespace-pre">
|
<code className="justify-start cursor-pointer whitespace-pre">
|
||||||
{hyperlinkTextWithPreview({
|
{searchTerm
|
||||||
previewLinkCard: !hasLinkCard && isLinkCardPreviewEnabled,
|
? highlightWithPreviewMatchedText(textValue ?? '', searchTerm)
|
||||||
isPreviewError: hasClipboardHistoryURLErrors,
|
: hyperlinkTextWithPreview({
|
||||||
value:
|
previewLinkCard: !hasLinkCard && isLinkCardPreviewEnabled,
|
||||||
historyPreviewLineLimit && historyPreviewLineLimit > 0
|
isPreviewError: hasClipboardHistoryURLErrors,
|
||||||
? textValue
|
value: valuePreview ?? '',
|
||||||
.split('\n')
|
links: clipboard.arrLinks,
|
||||||
.slice(0, historyPreviewLineLimit)
|
itemId: null,
|
||||||
.join('\n')
|
historyId: clipboard.historyId,
|
||||||
: 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 && (
|
{isMp3 && (
|
||||||
<PlayButton
|
<PlayButton
|
||||||
src={textValue}
|
src={textValue}
|
||||||
@ -668,14 +669,7 @@ export function ClipboardHistoryQuickPasteRowComponent({
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{((historyPreviewLineLimit &&
|
{(valueMorePreviewLines || valueMorePreviewChars) && (
|
||||||
historyPreviewLineLimit > 0 &&
|
|
||||||
!isExpanded &&
|
|
||||||
(textValue.split('\n').length > historyPreviewLineLimit ||
|
|
||||||
(clipboard.detectedLanguage &&
|
|
||||||
clipboard.valuePreview &&
|
|
||||||
clipboard.value.split('\n').length > historyPreviewLineLimit))) ||
|
|
||||||
clipboard.valueMorePreviewChars) && (
|
|
||||||
<Box
|
<Box
|
||||||
className={`absolute left-1 bottom-1 flex flex-row items-center rounded mb-[2px] pl-0.5 ${bgToolsPanel}`}
|
className={`absolute left-1 bottom-1 flex flex-row items-center rounded mb-[2px] pl-0.5 ${bgToolsPanel}`}
|
||||||
>
|
>
|
||||||
@ -694,30 +688,15 @@ export function ClipboardHistoryQuickPasteRowComponent({
|
|||||||
sideOffset={10}
|
sideOffset={10}
|
||||||
>
|
>
|
||||||
{!isExpanded ? (
|
{!isExpanded ? (
|
||||||
historyPreviewLineLimit &&
|
valueMorePreviewChars ? (
|
||||||
historyPreviewLineLimit > 0 &&
|
|
||||||
(textValue.split('\n').length > historyPreviewLineLimit ||
|
|
||||||
(clipboard.detectedLanguage &&
|
|
||||||
clipboard.valuePreview &&
|
|
||||||
clipboard.value.split('\n').length > historyPreviewLineLimit)) ? (
|
|
||||||
<>
|
<>
|
||||||
+
|
+{valueMorePreviewChars} {t('chars', { ns: 'common' })}
|
||||||
{clipboard.detectedLanguage
|
|
||||||
? clipboard.value.split('\n').length - historyPreviewLineLimit
|
|
||||||
: textValue.split('\n').length - historyPreviewLineLimit}{' '}
|
|
||||||
{t('lines', { ns: 'common' })}
|
|
||||||
</>
|
</>
|
||||||
) : clipboard?.valueMorePreviewChars ? (
|
) : (
|
||||||
<>
|
<>
|
||||||
+{clipboard.valueMorePreviewChars}{' '}
|
+{valueMorePreviewLines} {t('lines', { ns: 'common' })}
|
||||||
{t('chars', { ns: 'common' })}
|
|
||||||
</>
|
</>
|
||||||
) : clipboard?.valueMorePreviewLines ? (
|
)
|
||||||
<>
|
|
||||||
+{clipboard.valueMorePreviewLines}{' '}
|
|
||||||
{t('lines', { ns: 'common' })}
|
|
||||||
</>
|
|
||||||
) : null
|
|
||||||
) : (
|
) : (
|
||||||
<>- {t('show less', { ns: 'common' })}</>
|
<>- {t('show less', { ns: 'common' })}</>
|
||||||
)}
|
)}
|
||||||
|
@ -1,306 +0,0 @@
|
|||||||
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()
|
|
||||||
})
|
|
@ -13,6 +13,7 @@ import WrapIcon from '~/assets/icons/wrap'
|
|||||||
import { MINUTE_IN_MS } from '~/constants'
|
import { MINUTE_IN_MS } from '~/constants'
|
||||||
import { isEmailNotUrl } from '~/libs/utils'
|
import { isEmailNotUrl } from '~/libs/utils'
|
||||||
import { formatLocale as format } from '~/locales/date-locales'
|
import { formatLocale as format } from '~/locales/date-locales'
|
||||||
|
import { getValuePreview } from '~/pages/components/Dashboard/components/utils'
|
||||||
import {
|
import {
|
||||||
hoveringHistoryRowId,
|
hoveringHistoryRowId,
|
||||||
isKeyAltPressed,
|
isKeyAltPressed,
|
||||||
@ -181,7 +182,7 @@ export function ClipboardHistoryRowComponent({
|
|||||||
setHistoryFilters = () => {},
|
setHistoryFilters = () => {},
|
||||||
setAppFilters = () => {},
|
setAppFilters = () => {},
|
||||||
isSingleClickToCopyPaste = false,
|
isSingleClickToCopyPaste = false,
|
||||||
historyPreviewLineLimit = 5,
|
historyPreviewLineLimit,
|
||||||
}: ClipboardHistoryRowProps) {
|
}: ClipboardHistoryRowProps) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const rowRef = useRef<HTMLDivElement>(null)
|
const rowRef = useRef<HTMLDivElement>(null)
|
||||||
@ -226,6 +227,7 @@ export function ClipboardHistoryRowComponent({
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, [
|
}, [
|
||||||
contentElementRendered.value,
|
contentElementRendered.value,
|
||||||
|
historyPreviewLineLimit,
|
||||||
rowRef.current?.clientHeight,
|
rowRef.current?.clientHeight,
|
||||||
setRowHeight,
|
setRowHeight,
|
||||||
timeAgo,
|
timeAgo,
|
||||||
@ -287,6 +289,38 @@ export function ClipboardHistoryRowComponent({
|
|||||||
const isNowItem = index === 0 && clipboard.updatedAt > Date.now() - MINUTE_IN_MS
|
const isNowItem = index === 0 && clipboard.updatedAt > Date.now() - MINUTE_IN_MS
|
||||||
const isMp3 = clipboard?.isLink && clipboard?.value?.endsWith('.mp3')
|
const isMp3 = clipboard?.isLink && clipboard?.value?.endsWith('.mp3')
|
||||||
|
|
||||||
|
// Recalculate preview with custom line limit if provided
|
||||||
|
const { valuePreview, valueMorePreviewLines, valueMorePreviewChars } = useMemo(() => {
|
||||||
|
if (historyPreviewLineLimit && historyPreviewLineLimit > 0 && clipboard?.value) {
|
||||||
|
const result = getValuePreview(
|
||||||
|
clipboard.value,
|
||||||
|
clipboard.isImageData || false,
|
||||||
|
isExpanded,
|
||||||
|
historyPreviewLineLimit,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
valuePreview: result.valuePreview,
|
||||||
|
valueMorePreviewLines: result.morePreviewLines,
|
||||||
|
valueMorePreviewChars: result.morePreviewChars,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Use default preview from backend
|
||||||
|
return {
|
||||||
|
valuePreview: clipboard?.valuePreview || '',
|
||||||
|
valueMorePreviewLines: clipboard?.valueMorePreviewLines || null,
|
||||||
|
valueMorePreviewChars: clipboard?.valueMorePreviewChars || null,
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
historyPreviewLineLimit,
|
||||||
|
clipboard?.value,
|
||||||
|
clipboard?.isImageData,
|
||||||
|
isExpanded,
|
||||||
|
clipboard?.valuePreview,
|
||||||
|
clipboard?.valueMorePreviewLines,
|
||||||
|
clipboard?.valueMorePreviewChars,
|
||||||
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!hasLinkCard &&
|
!hasLinkCard &&
|
||||||
@ -556,7 +590,7 @@ export function ClipboardHistoryRowComponent({
|
|||||||
{searchTerm ? (
|
{searchTerm ? (
|
||||||
highlightWithPreviewMatchedText(clipboard.value, searchTerm)
|
highlightWithPreviewMatchedText(clipboard.value, searchTerm)
|
||||||
) : (
|
) : (
|
||||||
<span>{clipboard.valuePreview}</span>
|
<span>{valuePreview}</span>
|
||||||
)}
|
)}
|
||||||
</code>
|
</code>
|
||||||
</Box>
|
</Box>
|
||||||
@ -606,7 +640,7 @@ export function ClipboardHistoryRowComponent({
|
|||||||
className="max-w-full max-h-56 min-h-10 rounded-md shadow-sm border border-slate-100 dark:border-slate-700"
|
className="max-w-full max-h-56 min-h-10 rounded-md shadow-sm border border-slate-100 dark:border-slate-700"
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
) : clipboard.detectedLanguage && clipboard.valuePreview ? (
|
) : clipboard.detectedLanguage && valuePreview ? (
|
||||||
<Box
|
<Box
|
||||||
ref={ref => {
|
ref={ref => {
|
||||||
if (ref) {
|
if (ref) {
|
||||||
@ -617,27 +651,13 @@ export function ClipboardHistoryRowComponent({
|
|||||||
>
|
>
|
||||||
<Highlight
|
<Highlight
|
||||||
theme={isDark ? themes.vsDark : themes.github}
|
theme={isDark ? themes.vsDark : themes.github}
|
||||||
code={isExpanded ? stringValue : clipboard.valuePreview}
|
code={isExpanded ? stringValue : valuePreview}
|
||||||
language={clipboard.detectedLanguage}
|
language={clipboard.detectedLanguage}
|
||||||
>
|
>
|
||||||
{({ className, style, tokens, getLineProps, getTokenProps }) => {
|
{({ className, style, tokens, getLineProps, getTokenProps }) => {
|
||||||
const limitedTokens =
|
|
||||||
historyPreviewLineLimit && historyPreviewLineLimit > 0 && !isExpanded
|
|
||||||
? tokens.slice(0, historyPreviewLineLimit)
|
|
||||||
: tokens
|
|
||||||
const remainingLines = tokens.length - limitedTokens.length
|
|
||||||
return (
|
return (
|
||||||
<code className={`${className}`} style={style}>
|
<code className={`${className}`} style={style}>
|
||||||
{limitedTokens.map((line, i) => {
|
{tokens.map((line, i) => {
|
||||||
const isLastLineOfPreview = i === limitedTokens.length - 1
|
|
||||||
const isActuallyLastLineOfAllTokens = i === tokens.length - 1
|
|
||||||
|
|
||||||
const showEllipsis =
|
|
||||||
isLastLineOfPreview &&
|
|
||||||
!isActuallyLastLineOfAllTokens &&
|
|
||||||
remainingLines > 0 &&
|
|
||||||
!isExpanded
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
@ -662,9 +682,6 @@ export function ClipboardHistoryRowComponent({
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{showEllipsis && (
|
|
||||||
<span className="select-none">...</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@ -672,9 +689,6 @@ export function ClipboardHistoryRowComponent({
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</Highlight>
|
</Highlight>
|
||||||
{(remainingLines > 0 || clipboard.valueMorePreviewLines) && !isExpanded && (
|
|
||||||
<Box className="select-none"> {'\u00A0'} </Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
) : (
|
) : (
|
||||||
<Box
|
<Box
|
||||||
@ -707,38 +721,22 @@ export function ClipboardHistoryRowComponent({
|
|||||||
{searchTerm
|
{searchTerm
|
||||||
? highlightMatchedText(stringValue, searchTerm)
|
? highlightMatchedText(stringValue, searchTerm)
|
||||||
: hyperlinkText(stringValue, clipboard.arrLinks)}
|
: hyperlinkText(stringValue, clipboard.arrLinks)}
|
||||||
{clipboard.valueMorePreviewChars && !isExpanded && (
|
|
||||||
<Box className="select-none"> {'\u00A0'} </Box>
|
|
||||||
)}
|
|
||||||
</code>
|
</code>
|
||||||
) : (
|
) : (
|
||||||
<code className="justify-start cursor-pointer whitespace-pre">
|
<code className="justify-start cursor-pointer whitespace-pre">
|
||||||
{hyperlinkTextWithPreview({
|
{searchTerm
|
||||||
previewLinkCard: !hasLinkCard && isLinkCardPreviewEnabled,
|
? highlightWithPreviewMatchedText(
|
||||||
isPreviewError: hasClipboardHistoryURLErrors,
|
stringValue ?? '',
|
||||||
value:
|
searchTerm
|
||||||
historyPreviewLineLimit && historyPreviewLineLimit > 0
|
)
|
||||||
? stringValue
|
: hyperlinkTextWithPreview({
|
||||||
.split('\n')
|
previewLinkCard: !hasLinkCard && isLinkCardPreviewEnabled,
|
||||||
.slice(0, historyPreviewLineLimit)
|
isPreviewError: hasClipboardHistoryURLErrors,
|
||||||
.join('\n')
|
value: valuePreview ?? '',
|
||||||
: clipboard.valuePreview ?? '',
|
links: clipboard.arrLinks,
|
||||||
links: clipboard.arrLinks,
|
itemId: null,
|
||||||
itemId: null,
|
historyId: clipboard.historyId,
|
||||||
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 && (
|
{isMp3 && (
|
||||||
<PlayButton
|
<PlayButton
|
||||||
src={stringValue}
|
src={stringValue}
|
||||||
@ -754,90 +752,66 @@ export function ClipboardHistoryRowComponent({
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{((historyPreviewLineLimit &&
|
{(valueMorePreviewLines || valueMorePreviewChars) &&
|
||||||
historyPreviewLineLimit > 0 &&
|
!isCopiedOrPasted && (
|
||||||
!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}`}
|
|
||||||
>
|
|
||||||
<Box
|
<Box
|
||||||
className={`text-xs text-muted-foreground px-1 cursor-pointer`}
|
className={`absolute left-1 bottom-1 flex flex-row items-center rounded mb-[2px] pl-0.5 ${bgToolsPanel}`}
|
||||||
onClick={() => {
|
|
||||||
setExpanded(clipboard.historyId, !isExpanded)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<ToolTip
|
|
||||||
text={!isExpanded ? t('Show all', { ns: 'common' }) : ''}
|
|
||||||
isCompact
|
|
||||||
isDisabled={isExpanded || isDragPreview}
|
|
||||||
delayDuration={2000}
|
|
||||||
side="bottom"
|
|
||||||
sideOffset={10}
|
|
||||||
>
|
|
||||||
{!isExpanded ? (
|
|
||||||
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' })}</>
|
|
||||||
)}
|
|
||||||
</ToolTip>
|
|
||||||
</Box>
|
|
||||||
{isExpanded && (
|
|
||||||
<Box
|
<Box
|
||||||
className={`text-xs text-muted-foreground px-1.5 cursor-pointer`}
|
className={`text-xs text-muted-foreground px-1 cursor-pointer`}
|
||||||
onClick={() => setWrapText(clipboard.historyId, !isWrapText)}
|
onClick={() => {
|
||||||
|
setExpanded(clipboard.historyId, !isExpanded)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ToolTip
|
<ToolTip
|
||||||
text={
|
text={!isExpanded ? t('Show all', { ns: 'common' }) : ''}
|
||||||
!isWrapText
|
|
||||||
? t('Lines Wrap', { ns: 'common' })
|
|
||||||
: t('No Wrap', { ns: 'common' })
|
|
||||||
}
|
|
||||||
delayDuration={2000}
|
|
||||||
isCompact
|
isCompact
|
||||||
|
isDisabled={isExpanded || isDragPreview}
|
||||||
|
delayDuration={2000}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
sideOffset={10}
|
sideOffset={10}
|
||||||
>
|
>
|
||||||
{!isWrapText ? (
|
{!isExpanded ? (
|
||||||
<WrapIcon width={20} height={20} />
|
valueMorePreviewChars ? (
|
||||||
|
<>
|
||||||
|
+{valueMorePreviewChars} {t('chars', { ns: 'common' })}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
+{valueMorePreviewLines} {t('lines', { ns: 'common' })}
|
||||||
|
</>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<NoWrapIcon width={20} height={20} />
|
<>- {t('show less', { ns: 'common' })}</>
|
||||||
)}
|
)}
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
{isExpanded && (
|
||||||
</Box>
|
<Box
|
||||||
)}
|
className={`text-xs text-muted-foreground px-1.5 cursor-pointer`}
|
||||||
|
onClick={() => setWrapText(clipboard.historyId, !isWrapText)}
|
||||||
|
>
|
||||||
|
<ToolTip
|
||||||
|
text={
|
||||||
|
!isWrapText
|
||||||
|
? t('Lines Wrap', { ns: 'common' })
|
||||||
|
: t('No Wrap', { ns: 'common' })
|
||||||
|
}
|
||||||
|
delayDuration={2000}
|
||||||
|
isCompact
|
||||||
|
side="bottom"
|
||||||
|
sideOffset={10}
|
||||||
|
>
|
||||||
|
{!isWrapText ? (
|
||||||
|
<WrapIcon width={20} height={20} />
|
||||||
|
) : (
|
||||||
|
<NoWrapIcon width={20} height={20} />
|
||||||
|
)}
|
||||||
|
</ToolTip>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
{clipboard.isImage && !clipboard.isLink && (
|
{clipboard.isImage && !clipboard.isLink && (
|
||||||
<Box className="absolute left-1 bottom-1 flex flex-row gap-1 rounded items-center pb-0.5 pl-0.5 z-100">
|
<Box className="absolute left-1 bottom-1 flex flex-row gap-1 rounded items-center pb-0.5 pl-0.5 z-100">
|
||||||
<Box
|
<Box
|
||||||
|
@ -68,7 +68,7 @@ interface ClipboardHistoryRowContextMenuProps {
|
|||||||
arrLinks: string[]
|
arrLinks: string[]
|
||||||
isImage: boolean
|
isImage: boolean
|
||||||
isText: boolean
|
isText: boolean
|
||||||
copiedFromApp: string | null
|
copiedFromApp?: string | null
|
||||||
isMasked: boolean
|
isMasked: boolean
|
||||||
isImageData: boolean
|
isImageData: boolean
|
||||||
isMp3: boolean | undefined
|
isMp3: boolean | undefined
|
||||||
|
@ -212,7 +212,9 @@ export function getValueMorePreviewLines(value: string) {
|
|||||||
export function getValuePreview(
|
export function getValuePreview(
|
||||||
value: string,
|
value: string,
|
||||||
isImageData: boolean = false,
|
isImageData: boolean = false,
|
||||||
isLargeView: boolean = false
|
isLargeView: boolean = false,
|
||||||
|
historyPreviewLineLimit?: number | null,
|
||||||
|
isHistoryItem?: boolean | undefined
|
||||||
): {
|
): {
|
||||||
valuePreview: string
|
valuePreview: string
|
||||||
morePreviewLines: number | null
|
morePreviewLines: number | null
|
||||||
@ -248,7 +250,10 @@ export function getValuePreview(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For non-image data, proceed with line-based truncation
|
// For non-image data, proceed with line-based truncation
|
||||||
const MAX_PREVIEW_LINES = 5
|
const MAX_PREVIEW_LINES =
|
||||||
|
historyPreviewLineLimit && isHistoryItem && historyPreviewLineLimit > 0
|
||||||
|
? historyPreviewLineLimit
|
||||||
|
: 5
|
||||||
const normalizedValue = value.replace(/\r\n/g, '\n')
|
const normalizedValue = value.replace(/\r\n/g, '\n')
|
||||||
const allLines = normalizedValue.split('\n')
|
const allLines = normalizedValue.split('\n')
|
||||||
|
|
||||||
@ -305,18 +310,6 @@ export function getValuePreview(
|
|||||||
finalPreviewText = bbCode.closeTags(finalPreviewText)
|
finalPreviewText = bbCode.closeTags(finalPreviewText)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (calculatedMorePreviewLines > 0) {
|
|
||||||
// Add ellipsis if lines were actually truncated.
|
|
||||||
// Avoid adding if bbCode.closeTags might have added its own form of ellipsis or if preview ends with one.
|
|
||||||
if (!finalPreviewText.trim().endsWith('...')) {
|
|
||||||
// Check if the last line of previewText is just "..." from a previous logic
|
|
||||||
const linesInPreview = finalPreviewText.split('\n')
|
|
||||||
if (linesInPreview[linesInPreview.length - 1] !== '...') {
|
|
||||||
finalPreviewText += '\n...'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
valuePreview: finalPreviewText,
|
valuePreview: finalPreviewText,
|
||||||
morePreviewLines: calculatedMorePreviewLines > 0 ? calculatedMorePreviewLines : null,
|
morePreviewLines: calculatedMorePreviewLines > 0 ? calculatedMorePreviewLines : null,
|
||||||
|
@ -90,7 +90,7 @@ export function MenuCardViewBody({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const isWrapText = useSignal(false)
|
const isWrapText = useSignal(false)
|
||||||
|
|
||||||
const { valuePreview, morePreviewLines, morePreviewChars } = getValuePreview(value)
|
const { valuePreview, morePreviewLines, morePreviewChars } = getValuePreview(value, false, false)
|
||||||
const textValue: string = value || ''
|
const textValue: string = value || ''
|
||||||
const isBrokenImage = useSignal(false)
|
const isBrokenImage = useSignal(false)
|
||||||
const pathTypeCheck = useSignal<string | null | undefined>('')
|
const pathTypeCheck = useSignal<string | null | undefined>('')
|
||||||
|
@ -360,59 +360,85 @@ export default function ClipboardHistorySettings() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box className="max-w-xl mt-4 animate-in fade-in">
|
<Box className="max-w-xl mt-4 animate-in fade-in">
|
||||||
<Card>
|
<Card
|
||||||
<CardHeader className="pb-1">
|
className={`${
|
||||||
|
historyPreviewLineLimit == null &&
|
||||||
|
'opacity-80 bg-gray-100 dark:bg-gray-900/80'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-1">
|
||||||
<CardTitle className="animate-in fade-in text-md font-medium w-full">
|
<CardTitle className="animate-in fade-in text-md font-medium w-full">
|
||||||
{t('History Item Preview Line Limit', {
|
{t('History Item Preview Max Lines', {
|
||||||
ns: 'settings',
|
ns: 'settings2',
|
||||||
})}
|
})}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
|
<Switch
|
||||||
|
checked={historyPreviewLineLimit !== null}
|
||||||
|
className="ml-auto"
|
||||||
|
onCheckedChange={() => {
|
||||||
|
if (historyPreviewLineLimit) {
|
||||||
|
setHistoryPreviewLineLimit(null)
|
||||||
|
} else {
|
||||||
|
setHistoryPreviewLineLimit(5)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Text className="text-sm text-muted-foreground">
|
<Text className="text-sm text-muted-foreground">
|
||||||
{t(
|
{t(
|
||||||
'Set the maximum number of lines to display in the preview of a history item. Setting it to 0 means unlimited.',
|
'Set the maximum number of lines to display in the preview of a history item',
|
||||||
{
|
{
|
||||||
ns: 'settings',
|
ns: 'settings2',
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
<Flex className="w-full gap-10 my-4 items-start justify-start">
|
{historyPreviewLineLimit !== null && (
|
||||||
<InputField
|
<>
|
||||||
className="text-md !w-36"
|
<Flex className="w-full gap-10 my-4 items-start justify-start">
|
||||||
type="number"
|
<InputField
|
||||||
step="1"
|
className="text-md !w-36"
|
||||||
min={0}
|
type="number"
|
||||||
small
|
step="1"
|
||||||
label={t('Line limit', { ns: 'common' })}
|
min={1}
|
||||||
value={historyPreviewLineLimit}
|
max={20}
|
||||||
onBlur={() => {
|
small
|
||||||
if (historyPreviewLineLimit < 0) {
|
label={t('Preview Max Lines', { ns: 'settings2' })}
|
||||||
setHistoryPreviewLineLimit(0)
|
value={
|
||||||
}
|
historyPreviewLineLimit ? historyPreviewLineLimit : ''
|
||||||
}}
|
}
|
||||||
onChange={e => {
|
onBlur={() => {
|
||||||
const value = e.target.value
|
if (
|
||||||
if (value === '') {
|
historyPreviewLineLimit &&
|
||||||
setHistoryPreviewLineLimit(0)
|
historyPreviewLineLimit < 0
|
||||||
} else {
|
) {
|
||||||
const number = parseInt(value)
|
setHistoryPreviewLineLimit(null)
|
||||||
setHistoryPreviewLineLimit(number)
|
}
|
||||||
}
|
}}
|
||||||
}}
|
onChange={e => {
|
||||||
/>
|
const value = e.target.value
|
||||||
</Flex>
|
if (value === '') {
|
||||||
<Button
|
setHistoryPreviewLineLimit(null)
|
||||||
variant="secondary"
|
} else {
|
||||||
size="sm"
|
const number = parseInt(value, 10)
|
||||||
disabled={historyPreviewLineLimit === 5}
|
setHistoryPreviewLineLimit(number)
|
||||||
onClick={() => {
|
}
|
||||||
setHistoryPreviewLineLimit(5)
|
}}
|
||||||
}}
|
/>
|
||||||
className="text-sm bg-slate-200 dark:bg-slate-700 dark:text-slate-200 mt-1"
|
</Flex>
|
||||||
>
|
<Button
|
||||||
{t('Reset', { ns: 'common' })}
|
variant="secondary"
|
||||||
</Button>
|
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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -89,7 +89,7 @@ type Settings = {
|
|||||||
isScreenLockPassCodeRequireOnStart: boolean
|
isScreenLockPassCodeRequireOnStart: boolean
|
||||||
clipTextMinLength: number
|
clipTextMinLength: number
|
||||||
clipTextMaxLength: number
|
clipTextMaxLength: number
|
||||||
historyPreviewLineLimit: number
|
historyPreviewLineLimit: number | null
|
||||||
isImageCaptureDisabled: boolean
|
isImageCaptureDisabled: boolean
|
||||||
isMenuItemCopyOnlyEnabled: boolean
|
isMenuItemCopyOnlyEnabled: boolean
|
||||||
isNoteIconsEnabled: boolean
|
isNoteIconsEnabled: boolean
|
||||||
@ -216,7 +216,7 @@ export interface SettingsStoreState {
|
|||||||
initSettings: (settings: Settings) => void
|
initSettings: (settings: Settings) => void
|
||||||
setClipTextMinLength: (width: number) => void
|
setClipTextMinLength: (width: number) => void
|
||||||
setClipTextMaxLength: (height: number) => void
|
setClipTextMaxLength: (height: number) => void
|
||||||
setHistoryPreviewLineLimit: (limit: number) => void
|
setHistoryPreviewLineLimit: (limit: number | null) => void
|
||||||
setProtectedCollections: (ids: string[]) => void
|
setProtectedCollections: (ids: string[]) => void
|
||||||
setHasPinProtectedCollections: (hasPinProtectedCollections: boolean) => Promise<void>
|
setHasPinProtectedCollections: (hasPinProtectedCollections: boolean) => Promise<void>
|
||||||
setGlobalTemplatesEnabled: (isEnabled: boolean) => void
|
setGlobalTemplatesEnabled: (isEnabled: boolean) => void
|
||||||
@ -716,7 +716,11 @@ export const settingsStore = createStore<SettingsStoreState & Settings>()((set,
|
|||||||
setClipTextMaxLength: async (length: number) => {
|
setClipTextMaxLength: async (length: number) => {
|
||||||
return get().updateSetting('clipTextMaxLength', length)
|
return get().updateSetting('clipTextMaxLength', length)
|
||||||
},
|
},
|
||||||
setHistoryPreviewLineLimit: async (limit: number) => {
|
setHistoryPreviewLineLimit: async (limit: number | null) => {
|
||||||
|
if (limit === 0) {
|
||||||
|
limit = null
|
||||||
|
}
|
||||||
|
get().syncStateUpdate('historyPreviewLineLimit', limit)
|
||||||
return get().updateSetting('historyPreviewLineLimit', limit)
|
return get().updateSetting('historyPreviewLineLimit', limit)
|
||||||
},
|
},
|
||||||
setIsImageCaptureDisabled: async (isEnabled: boolean) => {
|
setIsImageCaptureDisabled: async (isEnabled: boolean) => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user