feat: internal verstak:// link navigation (case/note/file)
1. handleVerstakLink now performs real navigation:
- case/<id>: GetNodeDetail → selectNode (selects node in tree)
- note/<id>: GetNodeDetail → find parent → selectNode(parent) → setActiveTab('notes') → openNote
- file/<id>: GetNodeDetail → find parent → selectNode(parent) → setActiveTab('files') → loadFolder → openPreview
- secret/<id>: toast 'Seaf access not implemented'
- missing ids: toast 'Not found'
2. Changed href from about:blank to # (safe, DOMPurify won't strip)
e.preventDefault() in click handler prevents scroll-to-top
3. Added i18n keys: caseNotFound, noteNotFound, fileNotFound, fileFound
4. All existing tests pass, build OK
This commit is contained in:
parent
c8c5531c0c
commit
db961ff0c3
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -19,7 +19,7 @@
|
||||||
background: #13131f;
|
background: #13131f;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/main-BpXZraKT.js"></script>
|
<script type="module" crossorigin src="/assets/main-BuG9iGkd.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-bQpH1es2.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-bQpH1es2.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -1413,26 +1413,97 @@
|
||||||
// Replaced by simple openInternalLinkModal above.
|
// Replaced by simple openInternalLinkModal above.
|
||||||
|
|
||||||
// ===== Verstak link handler =====
|
// ===== Verstak link handler =====
|
||||||
function handleVerstakLink(e) {
|
async function handleVerstakLink(e) {
|
||||||
const { type, id } = e.detail
|
const { type, id } = e.detail
|
||||||
let msg = ''
|
if (!id) return
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'secret':
|
|
||||||
msg = t('note.internal.secretNotImplemented')
|
|
||||||
break
|
|
||||||
case 'case':
|
case 'case':
|
||||||
msg = t('note.internal.caseNotImplemented')
|
await navigateToCase(id)
|
||||||
break
|
break
|
||||||
case 'note':
|
case 'note':
|
||||||
msg = t('note.internal.noteNotImplemented')
|
await navigateToNote(id)
|
||||||
break
|
break
|
||||||
case 'file':
|
case 'file':
|
||||||
msg = t('note.internal.fileNotImplemented')
|
await navigateToFile(id)
|
||||||
|
break
|
||||||
|
case 'secret':
|
||||||
|
showVerstakToastMessage(t('note.internal.secretNotImplemented'))
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
msg = `verstak://${type}/${id}`
|
showVerstakToastMessage(`verstak://${type}/${id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function navigateToCase(id) {
|
||||||
|
try {
|
||||||
|
const node = await wailsCall('GetNodeDetail', id)
|
||||||
|
if (!node) {
|
||||||
|
showVerstakToastMessage(t('note.internal.caseNotFound'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await selectNode(node)
|
||||||
|
} catch (e) {
|
||||||
|
showVerstakToastMessage(t('note.internal.caseNotFound'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function navigateToNote(id) {
|
||||||
|
try {
|
||||||
|
const node = await wailsCall('GetNodeDetail', id)
|
||||||
|
if (!node) {
|
||||||
|
showVerstakToastMessage(t('note.internal.noteNotFound'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const parentId = node.parent_id || node.parentId || ''
|
||||||
|
if (parentId) {
|
||||||
|
const parent = await wailsCall('GetNodeDetail', parentId)
|
||||||
|
if (parent) {
|
||||||
|
await selectNode(parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setActiveTab('notes')
|
||||||
|
// Find the note in the loaded notes list and open it
|
||||||
|
const note = notes.find(n => n.id === id)
|
||||||
|
if (note) {
|
||||||
|
await openNote(note)
|
||||||
|
} else {
|
||||||
|
// Note might not be loaded yet, try to open by id directly
|
||||||
|
await openNote({ id: node.id, title: node.title })
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showVerstakToastMessage(t('note.internal.noteNotFound'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function navigateToFile(id) {
|
||||||
|
try {
|
||||||
|
const node = await wailsCall('GetNodeDetail', id)
|
||||||
|
if (!node) {
|
||||||
|
showVerstakToastMessage(t('note.internal.fileNotFound'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const parentId = node.parent_id || node.parentId || ''
|
||||||
|
if (parentId) {
|
||||||
|
const parent = await wailsCall('GetNodeDetail', parentId)
|
||||||
|
if (parent) {
|
||||||
|
await selectNode(parent)
|
||||||
|
}
|
||||||
|
setActiveTab('files')
|
||||||
|
await loadFolder(parentId)
|
||||||
|
// Find the file in the loaded fileItems and open preview
|
||||||
|
const fileItem = fileItems.find(f => f.id === id)
|
||||||
|
if (fileItem) {
|
||||||
|
await openPreview(fileItem)
|
||||||
|
} else {
|
||||||
|
showVerstakToastMessage(t('note.internal.fileFound', { title: node.title }))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showVerstakToastMessage(t('note.internal.fileFound', { title: node.title }))
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showVerstakToastMessage(t('note.internal.fileNotFound'))
|
||||||
}
|
}
|
||||||
showVerstakToastMessage(msg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showVerstakToastMessage(msg) {
|
function showVerstakToastMessage(msg) {
|
||||||
|
|
|
||||||
|
|
@ -207,9 +207,10 @@ export default {
|
||||||
'note.toolbar.internalLink': 'Внутренняя ссылка Verstak',
|
'note.toolbar.internalLink': 'Внутренняя ссылка Verstak',
|
||||||
|
|
||||||
'note.internal.secretNotImplemented': 'Сейф доступов ещё не реализован',
|
'note.internal.secretNotImplemented': 'Сейф доступов ещё не реализован',
|
||||||
'note.internal.caseNotImplemented': 'Переход к делу ещё не реализован',
|
'note.internal.caseNotFound': 'Дело не найдено',
|
||||||
'note.internal.noteNotImplemented': 'Переход к заметке ещё не реализован',
|
'note.internal.noteNotFound': 'Заметка не найдена',
|
||||||
'note.internal.fileNotImplemented': 'Переход к файлу ещё не реализован',
|
'note.internal.fileNotFound': 'Файл не найден',
|
||||||
|
'note.internal.fileFound': 'Файл найден: {title}',
|
||||||
|
|
||||||
'note.rename': 'Переименовать заметку',
|
'note.rename': 'Переименовать заметку',
|
||||||
'note.deleteConfirm': 'Удалить заметку «{title}»?',
|
'note.deleteConfirm': 'Удалить заметку «{title}»?',
|
||||||
|
|
|
||||||
|
|
@ -73,9 +73,10 @@ renderer.link = function ({ href, title, text }) {
|
||||||
if (parsed && ALLOWED_VERSTAK_TYPES.has(parsed.type)) {
|
if (parsed && ALLOWED_VERSTAK_TYPES.has(parsed.type)) {
|
||||||
const escapedHref = escapeAttr(trimmedHref);
|
const escapedHref = escapeAttr(trimmedHref);
|
||||||
const escapedText = escapeHtml(text);
|
const escapedText = escapeHtml(text);
|
||||||
// Use about:blank as a safe href that DOMPurify won't strip.
|
// Use href="#" as a safe placeholder — DOMPurify won't strip it.
|
||||||
// The actual navigation is handled by data-verstak-href + click handler.
|
// The actual navigation is handled by data-verstak-href + click handler.
|
||||||
return `<a href="about:blank" class="md-link md-link--internal" data-verstak-href="${escapedHref}" data-verstak-type="${escapeAttr(parsed.type)}" data-verstak-id="${escapeAttr(parsed.id)}">${escapedText}</a>`;
|
// e.preventDefault() in the click handler prevents scrolling to top.
|
||||||
|
return `<a href="#" class="md-link md-link--internal" data-verstak-href="${escapedHref}" data-verstak-type="${escapeAttr(parsed.type)}" data-verstak-id="${escapeAttr(parsed.id)}">${escapedText}</a>`;
|
||||||
}
|
}
|
||||||
// Unknown verstak type — render as blocked
|
// Unknown verstak type — render as blocked
|
||||||
const escapedText = escapeHtml(text);
|
const escapedText = escapeHtml(text);
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ function renderLink(href, text) {
|
||||||
if (parsed && ALLOWED_VERSTAK_TYPES.has(parsed.type)) {
|
if (parsed && ALLOWED_VERSTAK_TYPES.has(parsed.type)) {
|
||||||
const escapedHref = escapeAttr(trimmedHref);
|
const escapedHref = escapeAttr(trimmedHref);
|
||||||
const escapedText = escapeHtml(text);
|
const escapedText = escapeHtml(text);
|
||||||
return `<a href="about:blank" class="md-link md-link--internal" data-verstak-href="${escapedHref}" data-verstak-type="${escapeAttr(parsed.type)}" data-verstak-id="${escapeAttr(parsed.id)}">${escapedText}</a>`;
|
return `<a href="#" class="md-link md-link--internal" data-verstak-href="${escapedHref}" data-verstak-type="${escapeAttr(parsed.type)}" data-verstak-id="${escapeAttr(parsed.id)}">${escapedText}</a>`;
|
||||||
}
|
}
|
||||||
return `<span class="md-link--blocked" data-blocked-href="${escapeAttr(trimmedHref)}">${escapeHtml(text)}</span>`;
|
return `<span class="md-link--blocked" data-blocked-href="${escapeAttr(trimmedHref)}">${escapeHtml(text)}</span>`;
|
||||||
}
|
}
|
||||||
|
|
@ -69,7 +69,7 @@ const fileLink = renderLink('verstak://file/019e971db8967a53991d3ee22a64ccc7', '
|
||||||
assert('file link is <a>', fileLink.startsWith('<a '), true);
|
assert('file link is <a>', fileLink.startsWith('<a '), true);
|
||||||
assert('file link has data-verstak-href', fileLink.includes('data-verstak-href="verstak://file/019e971db8967a53991d3ee22a64ccc7"'), true);
|
assert('file link has data-verstak-href', fileLink.includes('data-verstak-href="verstak://file/019e971db8967a53991d3ee22a64ccc7"'), true);
|
||||||
assert('file link has md-link--internal', fileLink.includes('md-link--internal'), true);
|
assert('file link has md-link--internal', fileLink.includes('md-link--internal'), true);
|
||||||
assert('file link has about:blank href', fileLink.includes('href="about:blank"'), true);
|
assert('file link has # href', fileLink.includes('href="#"'), true);
|
||||||
assert('file link is NOT blocked', fileLink.includes('md-link--blocked'), false);
|
assert('file link is NOT blocked', fileLink.includes('md-link--blocked'), false);
|
||||||
assert('file link text', fileLink.includes('>screen_4.png</a>'), true);
|
assert('file link text', fileLink.includes('>screen_4.png</a>'), true);
|
||||||
assert('file link has data-verstak-type', fileLink.includes('data-verstak-type="file"'), true);
|
assert('file link has data-verstak-type', fileLink.includes('data-verstak-type="file"'), true);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue