mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 18:27:19 +08:00
fix: improve note node (#35461)
This commit is contained in:
parent
1c5d62d98a
commit
38e831c1b3
@ -17,6 +17,15 @@ import DatasetSidebarDropdown from './dataset-sidebar-dropdown'
|
||||
import NavLink from './nav-link'
|
||||
import ToggleButton from './toggle-button'
|
||||
|
||||
const isShortcutFromInputArea = (target: EventTarget | null) => {
|
||||
if (!(target instanceof HTMLElement))
|
||||
return false
|
||||
|
||||
return target.tagName === 'INPUT'
|
||||
|| target.tagName === 'TEXTAREA'
|
||||
|| target.isContentEditable
|
||||
}
|
||||
|
||||
type IAppDetailNavProps = {
|
||||
iconType?: 'app' | 'dataset'
|
||||
navigation: Array<{
|
||||
@ -70,6 +79,9 @@ const AppDetailNav = ({
|
||||
}, [appSidebarExpand, setAppSidebarExpand])
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.b`, (e) => {
|
||||
if (isShortcutFromInputArea(e.target))
|
||||
return
|
||||
|
||||
e.preventDefault()
|
||||
handleToggle()
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
@ -4,6 +4,7 @@ import { createEdge, createNode } from '../../__tests__/fixtures'
|
||||
import { resetReactFlowMockState, rfState } from '../../__tests__/reactflow-mock-state'
|
||||
import { renderWorkflowHook } from '../../__tests__/workflow-test-env'
|
||||
import { collaborationManager } from '../../collaboration/core/collaboration-manager'
|
||||
import { CUSTOM_NOTE_NODE } from '../../note-node/constants'
|
||||
import { BlockEnum, ControlMode } from '../../types'
|
||||
import { useNodesInteractions } from '../use-nodes-interactions'
|
||||
|
||||
@ -317,6 +318,41 @@ describe('useNodesInteractions', () => {
|
||||
expect(rfState.setEdges).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('ignores note node selection when clicking a linked text target', () => {
|
||||
currentNodes = [
|
||||
createNode({
|
||||
id: 'note-1',
|
||||
type: CUSTOM_NOTE_NODE,
|
||||
data: {
|
||||
type: '' as unknown as BlockEnum,
|
||||
title: 'Note',
|
||||
desc: '',
|
||||
selected: false,
|
||||
},
|
||||
}),
|
||||
]
|
||||
currentEdges = []
|
||||
rfState.nodes = currentNodes as unknown as typeof rfState.nodes
|
||||
rfState.edges = currentEdges as unknown as typeof rfState.edges
|
||||
|
||||
const { result } = renderWorkflowHook(() => useNodesInteractions(), {
|
||||
historyStore: {
|
||||
nodes: currentNodes,
|
||||
edges: currentEdges,
|
||||
},
|
||||
})
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.className = 'note-editor-theme_link'
|
||||
|
||||
act(() => {
|
||||
result.current.handleNodeClick({ target: link } as never, currentNodes[0] as Node)
|
||||
})
|
||||
|
||||
expect(rfState.setNodes).not.toHaveBeenCalled()
|
||||
expect(rfState.setEdges).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('updates entering states on node enter and clears them on leave using collaborative workflow state', () => {
|
||||
currentNodes = [
|
||||
createNode({
|
||||
|
||||
@ -137,6 +137,12 @@ const getUniquePastedNodeTitle = (
|
||||
return titleCandidate
|
||||
}
|
||||
|
||||
const isNoteLinkClickTarget = (target: EventTarget | null, node: Node) => {
|
||||
return node.type === CUSTOM_NOTE_NODE
|
||||
&& target instanceof HTMLElement
|
||||
&& !!target.closest('.note-editor-theme_link')
|
||||
}
|
||||
|
||||
export const useNodesInteractions = () => {
|
||||
const { t } = useTranslation()
|
||||
const { data: appDslVersion } = useSuspenseQuery({
|
||||
@ -474,10 +480,12 @@ export const useNodesInteractions = () => {
|
||||
)
|
||||
|
||||
const handleNodeClick = useCallback<NodeMouseHandler>(
|
||||
(_, node) => {
|
||||
(event, node) => {
|
||||
const { controlMode } = workflowStore.getState()
|
||||
if (controlMode === ControlMode.Comment)
|
||||
return
|
||||
if (isNoteLinkClickTarget(event.target, node))
|
||||
return
|
||||
if (node.type === CUSTOM_ITERATION_START_NODE)
|
||||
return
|
||||
if (node.type === CUSTOM_LOOP_START_NODE)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { NOTE_SHOW_AUTHOR_STORAGE_KEY } from '../constants'
|
||||
import { useNote } from '../hooks'
|
||||
|
||||
const mockHandleNodeDataUpdateWithSyncDraft = vi.hoisted(() => vi.fn())
|
||||
@ -19,6 +20,7 @@ vi.mock('../../hooks', () => ({
|
||||
describe('useNote', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('updates theme and author visibility while saving note history entries', () => {
|
||||
@ -39,6 +41,7 @@ describe('useNote', () => {
|
||||
})
|
||||
expect(mockSaveStateToHistory).toHaveBeenNthCalledWith(1, 'note-change', { nodeId: 'note-1' })
|
||||
expect(mockSaveStateToHistory).toHaveBeenNthCalledWith(2, 'note-change', { nodeId: 'note-1' })
|
||||
expect(localStorage.getItem(NOTE_SHOW_AUTHOR_STORAGE_KEY)).toBe('true')
|
||||
})
|
||||
|
||||
it('serializes non-empty editor state and clears empty editor state', () => {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { NoteTheme } from './types'
|
||||
|
||||
export const CUSTOM_NOTE_NODE = 'custom-note'
|
||||
export const NOTE_SHOW_AUTHOR_STORAGE_KEY = 'workflow-note-show-author'
|
||||
|
||||
export const THEME_MAP: Record<string, { outer: string, title: string, bg: string, border: string }> = {
|
||||
[NoteTheme.blue]: {
|
||||
|
||||
@ -2,6 +2,7 @@ import type { EditorState } from 'lexical'
|
||||
import type { NoteTheme } from './types'
|
||||
import { useCallback } from 'react'
|
||||
import { useNodeDataUpdate, useWorkflowHistory, WorkflowHistoryEvent } from '../hooks'
|
||||
import { NOTE_SHOW_AUTHOR_STORAGE_KEY } from './constants'
|
||||
|
||||
export const useNote = (id: string) => {
|
||||
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate()
|
||||
@ -20,6 +21,7 @@ export const useNote = (id: string) => {
|
||||
}, [handleNodeDataUpdateWithSyncDraft, id])
|
||||
|
||||
const handleShowAuthorChange = useCallback((showAuthor: boolean) => {
|
||||
localStorage.setItem(NOTE_SHOW_AUTHOR_STORAGE_KEY, String(showAuthor))
|
||||
handleNodeDataUpdateWithSyncDraft({ id, data: { showAuthor } })
|
||||
saveStateToHistory(WorkflowHistoryEvent.NoteChange, { nodeId: id })
|
||||
}, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory])
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import type { EditorState, LexicalEditor } from 'lexical'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { $createLinkNode } from '@lexical/link'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { $createParagraphNode, $createTextNode, $getRoot } from 'lexical'
|
||||
@ -7,6 +10,10 @@ import { NoteEditorContextProvider } from '../context'
|
||||
import Editor from '../editor'
|
||||
|
||||
const emptyValue = JSON.stringify({ root: { children: [] } })
|
||||
const themeCss = readFileSync(
|
||||
resolve(process.cwd(), 'app/components/workflow/note-node/note-editor/theme/theme.css'),
|
||||
'utf8',
|
||||
)
|
||||
|
||||
const EditorProbe = ({
|
||||
onReady,
|
||||
@ -52,6 +59,35 @@ describe('Editor', () => {
|
||||
expect(screen.getByText('Type note')).toBeInTheDocument()
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render linked text with distinct link styling', async () => {
|
||||
let editor: LexicalEditor | null = null
|
||||
|
||||
renderEditor({}, instance => (editor = instance))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(editor).not.toBeNull()
|
||||
})
|
||||
|
||||
act(() => {
|
||||
editor!.update(() => {
|
||||
const root = $getRoot()
|
||||
root.clear()
|
||||
const paragraph = $createParagraphNode()
|
||||
const link = $createLinkNode('https://example.com/docs')
|
||||
link.append($createTextNode('Linked docs'))
|
||||
paragraph.append(link)
|
||||
root.append(paragraph)
|
||||
}, { discrete: true })
|
||||
})
|
||||
|
||||
const link = await screen.findByRole('link', { name: 'Linked docs' })
|
||||
|
||||
expect(link).toHaveClass('note-editor-theme_link')
|
||||
expect(themeCss).toContain('.note-editor-theme_link')
|
||||
expect(themeCss).toContain('font-weight: 500;')
|
||||
expect(themeCss).toContain('text-decoration: underline;')
|
||||
})
|
||||
})
|
||||
|
||||
// Focus and blur should toggle workflow shortcuts while editing content.
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import NoteEditorContext from '../../../context'
|
||||
import { createNoteEditorStore } from '../../../store'
|
||||
import LinkEditorComponent from '../component'
|
||||
@ -18,6 +18,59 @@ describe('link editor component', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('cancels a newly created empty link when pressing Escape', () => {
|
||||
const store = createNoteEditorStore()
|
||||
const anchor = document.createElement('button')
|
||||
const portalRoot = document.createElement('div')
|
||||
document.body.appendChild(anchor)
|
||||
document.body.appendChild(portalRoot)
|
||||
store.setState({
|
||||
linkAnchorElement: anchor,
|
||||
linkOperatorShow: false,
|
||||
selectedLinkUrl: '',
|
||||
})
|
||||
|
||||
render(
|
||||
<NoteEditorContext.Provider value={store}>
|
||||
<LinkEditorComponent containerElement={portalRoot} />
|
||||
</NoteEditorContext.Provider>,
|
||||
)
|
||||
|
||||
fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Escape' })
|
||||
|
||||
expect(mockHandleUnlink).toHaveBeenCalledTimes(1)
|
||||
expect(mockHandleSaveLink).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('cancels a newly created empty link when clicking outside the editor', async () => {
|
||||
const store = createNoteEditorStore()
|
||||
const anchor = document.createElement('button')
|
||||
const portalRoot = document.createElement('div')
|
||||
document.body.appendChild(anchor)
|
||||
document.body.appendChild(portalRoot)
|
||||
store.setState({
|
||||
linkAnchorElement: anchor,
|
||||
linkOperatorShow: false,
|
||||
selectedLinkUrl: '',
|
||||
})
|
||||
|
||||
render(
|
||||
<NoteEditorContext.Provider value={store}>
|
||||
<LinkEditorComponent containerElement={portalRoot} />
|
||||
</NoteEditorContext.Provider>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument()
|
||||
fireEvent.mouseDown(document.body)
|
||||
fireEvent.mouseUp(document.body)
|
||||
fireEvent.click(document.body)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHandleUnlink).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
expect(mockHandleSaveLink).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders the inline link editor and saves the edited url', () => {
|
||||
const store = createNoteEditorStore()
|
||||
const anchor = document.createElement('button')
|
||||
@ -42,4 +95,27 @@ describe('link editor component', () => {
|
||||
|
||||
expect(mockHandleSaveLink).toHaveBeenCalledWith('https://example.com')
|
||||
})
|
||||
|
||||
it('saves the edited url when pressing Enter', () => {
|
||||
const store = createNoteEditorStore()
|
||||
const anchor = document.createElement('button')
|
||||
const portalRoot = document.createElement('div')
|
||||
document.body.appendChild(anchor)
|
||||
document.body.appendChild(portalRoot)
|
||||
store.setState({
|
||||
linkAnchorElement: anchor,
|
||||
linkOperatorShow: false,
|
||||
selectedLinkUrl: 'https://example.com',
|
||||
})
|
||||
|
||||
render(
|
||||
<NoteEditorContext.Provider value={store}>
|
||||
<LinkEditorComponent containerElement={portalRoot} />
|
||||
</NoteEditorContext.Provider>,
|
||||
)
|
||||
|
||||
fireEvent.keyDown(screen.getByRole('textbox'), { key: 'Enter' })
|
||||
|
||||
expect(mockHandleSaveLink).toHaveBeenCalledWith('https://example.com')
|
||||
})
|
||||
})
|
||||
|
||||
@ -14,7 +14,7 @@ const {
|
||||
} = vi.hoisted(() => {
|
||||
const listeners: {
|
||||
update?: () => void
|
||||
click?: (payload: { metaKey?: boolean, ctrlKey?: boolean }) => boolean
|
||||
click?: (payload: { metaKey?: boolean, ctrlKey?: boolean, target?: EventTarget | null }) => boolean
|
||||
} = {}
|
||||
|
||||
const editor = {
|
||||
@ -36,6 +36,8 @@ const {
|
||||
selectedLinkUrl: '',
|
||||
setLinkAnchorElement: vi.fn(),
|
||||
setLinkOperatorShow: vi.fn(),
|
||||
setSelectedLinkUrl: vi.fn(),
|
||||
setSelectedIsLink: vi.fn(),
|
||||
},
|
||||
mockListeners: listeners,
|
||||
}
|
||||
@ -78,6 +80,8 @@ describe('link editor hooks', () => {
|
||||
mockStoreState.selectedLinkUrl = ''
|
||||
mockStoreState.setLinkAnchorElement = mockSetLinkAnchorElement
|
||||
mockStoreState.setLinkOperatorShow = mockSetLinkOperatorShow
|
||||
mockStoreState.setSelectedLinkUrl = vi.fn()
|
||||
mockStoreState.setSelectedIsLink = vi.fn()
|
||||
mockListeners.update = undefined
|
||||
mockListeners.click = undefined
|
||||
|
||||
@ -124,6 +128,26 @@ describe('link editor hooks', () => {
|
||||
expect(mockSetLinkOperatorShow).toHaveBeenCalledWith(false)
|
||||
})
|
||||
|
||||
it('should show the link operator immediately when clicking a link target', () => {
|
||||
const target = document.createElement('a')
|
||||
target.className = 'note-editor-theme_link'
|
||||
target.href = 'https://dify.ai/docs'
|
||||
|
||||
renderHook(() => useOpenLink())
|
||||
|
||||
let handled = false
|
||||
act(() => {
|
||||
handled = mockListeners.click?.({ target }) ?? false
|
||||
vi.runAllTimers()
|
||||
})
|
||||
|
||||
expect(handled).toBe(true)
|
||||
expect(mockStoreState.setSelectedLinkUrl).toHaveBeenCalledWith('https://dify.ai/docs')
|
||||
expect(mockStoreState.setSelectedIsLink).toHaveBeenCalledWith(true)
|
||||
expect(mockSetLinkAnchorElement).toHaveBeenCalledWith(target)
|
||||
expect(mockSetLinkOperatorShow).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should open the selected link in a new tab on meta or ctrl click', () => {
|
||||
mockStoreState.selectedIsLink = true
|
||||
mockStoreState.selectedLinkUrl = 'https://dify.ai'
|
||||
|
||||
@ -16,7 +16,9 @@ import { useClickAway } from 'ahooks'
|
||||
import { escape } from 'es-toolkit/string'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -40,6 +42,7 @@ const LinkEditorComponent = ({
|
||||
const setLinkAnchorElement = useStore(s => s.setLinkAnchorElement)
|
||||
const setLinkOperatorShow = useStore(s => s.setLinkOperatorShow)
|
||||
const [url, setUrl] = useState(selectedLinkUrl)
|
||||
const floatingRef = useRef<HTMLDivElement | null>(null)
|
||||
const { refs, floatingStyles, elements } = useFloating({
|
||||
placement: 'top',
|
||||
middleware: [
|
||||
@ -49,9 +52,19 @@ const LinkEditorComponent = ({
|
||||
],
|
||||
})
|
||||
|
||||
useClickAway(() => {
|
||||
const handleCancelLinkEdit = useCallback(() => {
|
||||
if (!linkOperatorShow && !selectedLinkUrl) {
|
||||
handleUnlink()
|
||||
return
|
||||
}
|
||||
|
||||
setLinkAnchorElement()
|
||||
}, linkAnchorElement)
|
||||
setLinkOperatorShow(false)
|
||||
}, [handleUnlink, linkOperatorShow, selectedLinkUrl, setLinkAnchorElement, setLinkOperatorShow])
|
||||
|
||||
useClickAway(() => {
|
||||
handleCancelLinkEdit()
|
||||
}, [floatingRef, linkAnchorElement])
|
||||
|
||||
useEffect(() => {
|
||||
setUrl(selectedLinkUrl)
|
||||
@ -74,7 +87,10 @@ const LinkEditorComponent = ({
|
||||
linkOperatorShow && 'p-0.5 system-xs-medium text-text-tertiary shadow-sm',
|
||||
)}
|
||||
style={floatingStyles}
|
||||
ref={refs.setFloating}
|
||||
ref={(node) => {
|
||||
refs.setFloating(node)
|
||||
floatingRef.current = node
|
||||
}}
|
||||
>
|
||||
{
|
||||
!linkOperatorShow && (
|
||||
@ -83,6 +99,21 @@ const LinkEditorComponent = ({
|
||||
className="mr-0.5 h-6 w-[196px] appearance-none rounded-xs bg-transparent p-1 text-[13px] text-components-input-text-filled outline-hidden"
|
||||
value={url}
|
||||
onChange={e => setUrl(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (url)
|
||||
handleSaveLink(url)
|
||||
return
|
||||
}
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
handleCancelLinkEdit()
|
||||
}
|
||||
}}
|
||||
placeholder={t('nodes.note.editor.enterUrl', { ns: 'workflow' }) || ''}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
@ -9,6 +9,12 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useNoteEditorStore } from '../../store'
|
||||
import { urlRegExp } from '../../utils'
|
||||
|
||||
const getClickedLinkElement = (target: EventTarget | null) => {
|
||||
return target instanceof HTMLElement
|
||||
? target.closest('.note-editor-theme_link') as HTMLElement | null
|
||||
: null
|
||||
}
|
||||
|
||||
export const useOpenLink = () => {
|
||||
const [editor] = useLexicalComposerContext()
|
||||
const noteEditorStore = useNoteEditorStore()
|
||||
@ -30,11 +36,34 @@ export const useOpenLink = () => {
|
||||
})
|
||||
}), editor.registerCommand(CLICK_COMMAND, (payload) => {
|
||||
setTimeout(() => {
|
||||
const { selectedLinkUrl, selectedIsLink, setLinkAnchorElement, setLinkOperatorShow } = noteEditorStore.getState()
|
||||
const {
|
||||
selectedLinkUrl,
|
||||
selectedIsLink,
|
||||
setLinkAnchorElement,
|
||||
setLinkOperatorShow,
|
||||
setSelectedLinkUrl,
|
||||
setSelectedIsLink,
|
||||
} = noteEditorStore.getState()
|
||||
const clickedLinkElement = getClickedLinkElement(payload.target)
|
||||
const clickedLinkUrl = clickedLinkElement?.getAttribute('href') || selectedLinkUrl
|
||||
|
||||
if (clickedLinkElement && clickedLinkUrl) {
|
||||
if (payload.metaKey || payload.ctrlKey) {
|
||||
window.open(clickedLinkUrl, '_blank')
|
||||
return
|
||||
}
|
||||
|
||||
setSelectedLinkUrl(clickedLinkUrl)
|
||||
setSelectedIsLink(true)
|
||||
setLinkAnchorElement(clickedLinkElement)
|
||||
setLinkOperatorShow(true)
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedIsLink) {
|
||||
if ((payload.metaKey || payload.ctrlKey) && selectedLinkUrl) {
|
||||
window.open(selectedLinkUrl, '_blank')
|
||||
return true
|
||||
return
|
||||
}
|
||||
setLinkAnchorElement(true)
|
||||
if (selectedLinkUrl)
|
||||
@ -47,7 +76,7 @@ export const useOpenLink = () => {
|
||||
setLinkOperatorShow(false)
|
||||
}
|
||||
})
|
||||
return false
|
||||
return !!getClickedLinkElement(payload.target)
|
||||
}, COMMAND_PRIORITY_LOW))
|
||||
}, [editor, noteEditorStore])
|
||||
}
|
||||
|
||||
@ -7,7 +7,7 @@ import NoteEditorContext from './context'
|
||||
|
||||
type Shape = {
|
||||
linkAnchorElement: HTMLElement | null
|
||||
setLinkAnchorElement: (open?: boolean) => void
|
||||
setLinkAnchorElement: (open?: boolean | HTMLElement | null) => void
|
||||
linkOperatorShow: boolean
|
||||
setLinkOperatorShow: (linkOperatorShow: boolean) => void
|
||||
selectedIsBold: boolean
|
||||
@ -28,6 +28,11 @@ export const createNoteEditorStore = () => {
|
||||
return createStore<Shape>(set => ({
|
||||
linkAnchorElement: null,
|
||||
setLinkAnchorElement: (open) => {
|
||||
if (open instanceof HTMLElement) {
|
||||
set(() => ({ linkAnchorElement: open }))
|
||||
return
|
||||
}
|
||||
|
||||
if (open) {
|
||||
setTimeout(() => {
|
||||
const nativeSelection = window.getSelection()
|
||||
|
||||
@ -8,7 +8,7 @@ const theme: EditorThemeClasses = {
|
||||
ul: 'note-editor-theme_list-ul',
|
||||
listitem: 'note-editor-theme_list-li',
|
||||
},
|
||||
link: 'note-editor-theme_link',
|
||||
link: 'note-editor-theme_link nodrag nopan nowheel',
|
||||
text: {
|
||||
italic: 'note-editor-theme_text-italic',
|
||||
strikethrough: 'note-editor-theme_text-strikethrough',
|
||||
|
||||
@ -16,11 +16,18 @@
|
||||
|
||||
.note-editor-theme_link {
|
||||
cursor: pointer;
|
||||
color: var(--text-text-selected);
|
||||
color: var(--color-text-accent);
|
||||
font-weight: 500;
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 2px;
|
||||
text-decoration-color: color-mix(in srgb, var(--color-text-accent) 60%, transparent);
|
||||
transition: color 0.15s ease, text-decoration-color 0.15s ease;
|
||||
}
|
||||
|
||||
.note-editor-theme_link:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--color-text-accent-secondary);
|
||||
text-decoration-color: currentColor;
|
||||
}
|
||||
|
||||
.note-editor-theme_text-strikethrough {
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import type { NoteNodeType } from '../note-node/types'
|
||||
import { useCallback } from 'react'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { CUSTOM_NOTE_NODE } from '../note-node/constants'
|
||||
import {
|
||||
CUSTOM_NOTE_NODE,
|
||||
NOTE_SHOW_AUTHOR_STORAGE_KEY,
|
||||
} from '../note-node/constants'
|
||||
import { NoteTheme } from '../note-node/types'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import { generateNewNode } from '../utils'
|
||||
@ -20,7 +23,7 @@ export const useOperator = () => {
|
||||
text: '',
|
||||
theme: NoteTheme.blue,
|
||||
author: userProfile?.name || '',
|
||||
showAuthor: true,
|
||||
showAuthor: localStorage.getItem(NOTE_SHOW_AUTHOR_STORAGE_KEY) !== 'false',
|
||||
width: 240,
|
||||
height: 88,
|
||||
_isCandidate: true,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user