refactor(frontend): extract NotesTab safely
- New component: frontend/src/lib/components/notes/NotesTab.svelte - Owns notes list UI, create-note form, note cards with rename/delete - Props: notes array, formatDate - Events: submitCreateNote, openNote, startRename, deleteNote - App.svelte changes: - Removed inline notes tab markup (replaced with <NotesTab>) - Removed showCreateNote, newNoteTitle state variables - Removed openCreateNote, cancelCreateNote, submitCreateNote functions - Added _handleSubmitCreateNote() event handler from NotesTab - Removed notes-related CSS (notes-tab, note-card, note-action-*) - Fixed import paths in FilesTab (../../ instead of ../ for lib/ files) - Build: npm run build ✅, go test ./... ✅, build.sh gui ✅
This commit is contained in:
parent
acdbbdfa55
commit
58cdd61d27
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -19,8 +19,8 @@
|
|||
background: #13131f;
|
||||
}
|
||||
</style>
|
||||
<script type="module" crossorigin src="/assets/main-Da3BSkUM.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-bQpH1es2.css">
|
||||
<script type="module" crossorigin src="/assets/main-nSPvOkeD.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/main-CUROQg6p.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
import { t } from './lib/i18n'
|
||||
import NoteEditorPanel from './lib/components/notes/NoteEditorPanel.svelte'
|
||||
import InternalLinkPicker from './lib/components/notes/InternalLinkPicker.svelte'
|
||||
import NotesTab from './lib/components/notes/NotesTab.svelte'
|
||||
import FilesTab from './lib/components/files/FilesTab.svelte'
|
||||
|
||||
// ===== Wails v2 API call helper =====
|
||||
|
|
@ -144,8 +145,6 @@
|
|||
let createInNode = null
|
||||
let createWithTemplate = null
|
||||
let contextMenu = { visible: false, x: 0, y: 0, node: null }
|
||||
let showCreateNote = false
|
||||
let newNoteTitle = ''
|
||||
let showCreateAction = false
|
||||
let newActionTitle = ''
|
||||
let newActionKind = 'open_url'
|
||||
|
|
@ -298,7 +297,6 @@
|
|||
if (showRename) { showRename = false; return true }
|
||||
if (showWorklogModal) { closeWorklogModal(); return true }
|
||||
if (showCreateAction) { cancelCreateAction(); return true }
|
||||
if (showCreateNote) { cancelCreateNote(); return true }
|
||||
if (showCreateNode) { cancelCreateNode(); return true }
|
||||
if (contextMenu.visible) { closeContextMenu(); return true }
|
||||
if (noteEditor) { closeNoteEditor(); return true }
|
||||
|
|
@ -513,7 +511,6 @@
|
|||
resetTrashBrowser()
|
||||
noteEditor = null
|
||||
showCreateNode = false
|
||||
showCreateNote = false
|
||||
error = ''
|
||||
caseActivity = []
|
||||
await loadTabData(node.id)
|
||||
|
|
@ -915,21 +912,17 @@
|
|||
}
|
||||
|
||||
// ===== Notes =====
|
||||
function openCreateNote() { showCreateNote = true; newNoteTitle = '' }
|
||||
function cancelCreateNote() { showCreateNote = false; newNoteTitle = '' }
|
||||
async function submitCreateNote() {
|
||||
if (!newNoteTitle.trim() || !selectedNode) return
|
||||
// Notes create/delete/rename are handled by NotesTab component via events.
|
||||
// App.svelte keeps note editor lifecycle (doOpenNote, saveCurrentNote, etc.)
|
||||
|
||||
async function _handleSubmitCreateNote(title) {
|
||||
if (!title.trim() || !selectedNode) return
|
||||
try {
|
||||
const note = await wailsCall('CreateNote', selectedNode.id, newNoteTitle.trim())
|
||||
notes = [...notes, (note && note.id) ? note : { id: Date.now().toString(), title: newNoteTitle.trim(), createdAt: new Date().toISOString() }]
|
||||
showCreateNote = false
|
||||
newNoteTitle = ''
|
||||
const note = await wailsCall('CreateNote', selectedNode.id, title.trim())
|
||||
notes = [...notes, (note && note.id) ? note : { id: Date.now().toString(), title: title.trim(), createdAt: new Date().toISOString() }]
|
||||
} catch (e) {
|
||||
// Fallback: create note locally
|
||||
const newNote = { id: Date.now().toString(), title: newNoteTitle.trim(), createdAt: new Date().toISOString() }
|
||||
const newNote = { id: Date.now().toString(), title: title.trim(), createdAt: new Date().toISOString() }
|
||||
notes = [...notes, newNote]
|
||||
showCreateNote = false
|
||||
newNoteTitle = ''
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2613,43 +2606,14 @@
|
|||
</div>
|
||||
|
||||
{:else if activeTab === 'notes'}
|
||||
<div class="notes-tab">
|
||||
<div class="tab-toolbar">
|
||||
<button class="btn btn-primary" on:click={openCreateNote}>{t('note.add')}</button>
|
||||
</div>
|
||||
{#if showCreateNote}
|
||||
<div class="create-form">
|
||||
<input type="text" placeholder={t('note.title')} bind:value={newNoteTitle}
|
||||
on:keydown={(e) => e.key === 'Enter' && submitCreateNote()} />
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" on:click={submitCreateNote}>{t('common.create')}</button>
|
||||
<button class="btn" on:click={cancelCreateNote}>{t('common.cancel')}</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if notes.length === 0 && !showCreateNote}
|
||||
<div class="empty-state"><p>{t('note.noNotes')}</p><p class="hint">{t('note.createFirst')}</p></div>
|
||||
{:else}
|
||||
<div class="notes-list">
|
||||
{#each notes as note}
|
||||
<div class="note-card" role="button" tabindex="0" on:click={() => openNote(note)} on:keydown={onKeyActivate(() => openNote(note))}>
|
||||
<div class="note-card-info">
|
||||
<div class="note-card-title">{note.title}</div>
|
||||
<div class="note-card-date">{formatDate(note.createdAt)}</div>
|
||||
</div>
|
||||
<div class="note-card-actions" on:click|stopPropagation>
|
||||
<button class="note-action-btn" on:click={() => startRenameNote(note.id, note.title)} title={t('common.rename')}>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>
|
||||
</button>
|
||||
<button class="note-action-btn note-action-danger" on:click={() => deleteNote(note)} title={t('common.delete')}>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<NotesTab
|
||||
{notes}
|
||||
{formatDate}
|
||||
on:submitCreateNote={(e) => _handleSubmitCreateNote(e.detail.title)}
|
||||
on:openNote={(e) => openNote(e.detail.note)}
|
||||
on:startRename={(e) => startRenameNote(e.detail.noteId, e.detail.currentTitle)}
|
||||
on:deleteNote={(e) => deleteNote(e.detail.note)}
|
||||
/>
|
||||
|
||||
{:else if activeTab === 'files'}
|
||||
<FilesTab
|
||||
|
|
@ -3714,15 +3678,6 @@
|
|||
.note-rename-input { flex: 1; padding: 6px 10px; border: 1px solid #2a2a3c; border-radius: 4px; background: #13131f; color: #e4e4ef; font-size: 14px; font-family: inherit; outline: none; }
|
||||
.note-rename-input:focus { border-color: #818cf8; }
|
||||
|
||||
/* Note card actions */
|
||||
.note-card { position: relative; }
|
||||
.note-card-info { flex: 1; min-width: 0; }
|
||||
.note-card-actions { display: flex; gap: 4px; opacity: 0; transition: opacity 0.12s; }
|
||||
.note-card:hover .note-card-actions { opacity: 1; }
|
||||
.note-action-btn { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border: none; border-radius: 4px; background: transparent; color: #666; cursor: pointer; transition: background 0.12s, color 0.12s; }
|
||||
.note-action-btn:hover { background: #2a2a3c; color: #ccc; }
|
||||
.note-action-danger:hover { background: rgba(239, 68, 68, 0.15); color: #f87171; }
|
||||
|
||||
/* Form groups in modals */
|
||||
.form-group { margin-bottom: 14px; }
|
||||
.form-group label { display: block; font-size: 12px; color: #888; margin-bottom: 4px; }
|
||||
|
|
@ -3747,19 +3702,6 @@
|
|||
.recent-date { font-size: 11px; color: #555; }
|
||||
.recent-entry { padding: 6px 0; font-size: 13px; color: #888; border-bottom: 1px solid #1a1a28; }
|
||||
|
||||
/* Notes tab */
|
||||
.notes-tab { padding: 24px; }
|
||||
.tab-toolbar { margin-bottom: 16px; }
|
||||
.create-form { background: #1a1a28; padding: 16px; border-radius: 8px; margin-bottom: 16px; }
|
||||
.create-form input { width: 100%; padding: 8px 12px; border: 1px solid #2a2a3c; background: #13131f; color: #e4e4ef; border-radius: 4px; font-size: 14px; font-family: inherit; margin-bottom: 8px; }
|
||||
.create-form input:focus { outline: none; border-color: #6366f1; }
|
||||
.form-actions { display: flex; gap: 8px; }
|
||||
.notes-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; }
|
||||
.note-card { background: #1a1a28; border: 1px solid #2a2a3c; border-radius: 8px; padding: 16px; cursor: pointer; }
|
||||
.note-card:hover { border-color: #3a3a5c; }
|
||||
.note-card-title { font-size: 14px; font-weight: 500; margin-bottom: 4px; }
|
||||
.note-card-date { font-size: 11px; color: #555; }
|
||||
|
||||
/* Worklog tab */
|
||||
.worklog-tab { padding: 24px; }
|
||||
.worklog-toolbar { margin-bottom: 16px; }
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -2,10 +2,10 @@
|
|||
import { createEventDispatcher } from 'svelte'
|
||||
import FileBreadcrumbs from '../../FileBreadcrumbs.svelte'
|
||||
import FileTreeRow from '../../../FileTreeRow.svelte'
|
||||
import FilePreviewModal from '../FilePreviewModal.svelte'
|
||||
import ConfirmModal from '../ConfirmModal.svelte'
|
||||
import { canPreviewFile, needsBase64Preview, needsTextPreview, isMarkdownFile } from '../fileUtils.js'
|
||||
import { t } from '../i18n'
|
||||
import FilePreviewModal from '../../FilePreviewModal.svelte'
|
||||
import ConfirmModal from '../../ConfirmModal.svelte'
|
||||
import { canPreviewFile, needsBase64Preview, needsTextPreview, isMarkdownFile } from '../../fileUtils.js'
|
||||
import { t } from '../../i18n'
|
||||
|
||||
// ===== Props =====
|
||||
export let selectedNode = null
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
// ===== Props =====
|
||||
export let notes = []
|
||||
export let formatDate = (str) => ''
|
||||
|
||||
// ===== Events =====
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
// ===== Internal state =====
|
||||
let showCreateNote = false
|
||||
let newNoteTitle = ''
|
||||
|
||||
function openCreateNote() {
|
||||
showCreateNote = true
|
||||
newNoteTitle = ''
|
||||
}
|
||||
|
||||
function cancelCreateNote() {
|
||||
showCreateNote = false
|
||||
newNoteTitle = ''
|
||||
}
|
||||
|
||||
function submitCreateNote() {
|
||||
if (!newNoteTitle.trim()) return
|
||||
dispatch('submitCreateNote', { title: newNoteTitle.trim() })
|
||||
showCreateNote = false
|
||||
newNoteTitle = ''
|
||||
}
|
||||
|
||||
function handleCreateKeydown(e) {
|
||||
if (e.key === 'Enter') submitCreateNote()
|
||||
}
|
||||
|
||||
function openNote(note) {
|
||||
dispatch('openNote', { note })
|
||||
}
|
||||
|
||||
function startRename(noteId, currentTitle) {
|
||||
dispatch('startRename', { noteId, currentTitle })
|
||||
}
|
||||
|
||||
function deleteNoteHandler(note) {
|
||||
dispatch('deleteNote', { note })
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="notes-tab">
|
||||
<div class="tab-toolbar">
|
||||
<button class="btn btn-primary" on:click={openCreateNote}>Добавить заметку</button>
|
||||
</div>
|
||||
|
||||
{#if showCreateNote}
|
||||
<div class="create-form">
|
||||
<input type="text" placeholder="Название заметки" bind:value={newNoteTitle}
|
||||
on:keydown={handleCreateKeydown} />
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" on:click={submitCreateNote}>Создать</button>
|
||||
<button class="btn" on:click={cancelCreateNote}>Отмена</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if notes.length === 0 && !showCreateNote}
|
||||
<div class="empty-state"><p>Нет заметок</p><p class="hint">Создайте первую заметку</p></div>
|
||||
{:else if notes.length > 0}
|
||||
<div class="notes-list">
|
||||
{#each notes as note}
|
||||
<div class="note-card" role="button" tabindex="0" on:click={() => openNote(note)} on:keydown={(e) => e.key === 'Enter' && openNote(note)}>
|
||||
<div class="note-card-info">
|
||||
<div class="note-card-title">{note.title}</div>
|
||||
<div class="note-card-date">{formatDate(note.createdAt)}</div>
|
||||
</div>
|
||||
<div class="note-card-actions" on:click|stopPropagation>
|
||||
<button class="note-action-btn" on:click={() => startRename(note.id, note.title)} title="Переименовать">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>
|
||||
</button>
|
||||
<button class="note-action-btn note-action-danger" on:click={() => deleteNoteHandler(note)} title="Удалить">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.notes-tab { padding: 24px; }
|
||||
.tab-toolbar { margin-bottom: 16px; }
|
||||
.create-form { background: #1a1a28; padding: 16px; border-radius: 8px; margin-bottom: 16px; }
|
||||
.create-form input { width: 100%; padding: 8px 12px; border: 1px solid #2a2a3c; background: #13131f; color: #e4e4ef; border-radius: 4px; font-size: 14px; font-family: inherit; margin-bottom: 8px; }
|
||||
.create-form input:focus { outline: none; border-color: #6366f1; }
|
||||
.form-actions { display: flex; gap: 8px; }
|
||||
.notes-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; }
|
||||
.note-card { background: #1a1a28; border: 1px solid #2a2a3c; border-radius: 8px; padding: 16px; cursor: pointer; position: relative; }
|
||||
.note-card:hover { border-color: #3a3a5c; }
|
||||
.note-card-info { flex: 1; min-width: 0; }
|
||||
.note-card-title { font-size: 14px; font-weight: 500; margin-bottom: 4px; }
|
||||
.note-card-date { font-size: 11px; color: #555; }
|
||||
.note-card-actions { display: flex; gap: 4px; opacity: 0; transition: opacity 0.12s; }
|
||||
.note-card:hover .note-card-actions { opacity: 1; }
|
||||
.note-action-btn { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; border: none; border-radius: 4px; background: transparent; color: #666; cursor: pointer; transition: background 0.12s, color 0.12s; }
|
||||
.note-action-btn:hover { background: #2a2a3c; color: #ccc; }
|
||||
.note-action-danger:hover { background: rgba(239, 68, 68, 0.15); color: #f87171; }
|
||||
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 48px 24px; text-align: center; }
|
||||
.empty-state p { margin: 0; font-size: 14px; color: #666; }
|
||||
.empty-state .hint { font-size: 12px; color: #555; margin-top: 6px; }
|
||||
</style>
|
||||
Loading…
Reference in New Issue