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;
|
background: #13131f;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/main-Da3BSkUM.js"></script>
|
<script type="module" crossorigin src="/assets/main-nSPvOkeD.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-bQpH1es2.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-CUROQg6p.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
import { t } from './lib/i18n'
|
import { t } from './lib/i18n'
|
||||||
import NoteEditorPanel from './lib/components/notes/NoteEditorPanel.svelte'
|
import NoteEditorPanel from './lib/components/notes/NoteEditorPanel.svelte'
|
||||||
import InternalLinkPicker from './lib/components/notes/InternalLinkPicker.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'
|
import FilesTab from './lib/components/files/FilesTab.svelte'
|
||||||
|
|
||||||
// ===== Wails v2 API call helper =====
|
// ===== Wails v2 API call helper =====
|
||||||
|
|
@ -144,8 +145,6 @@
|
||||||
let createInNode = null
|
let createInNode = null
|
||||||
let createWithTemplate = null
|
let createWithTemplate = null
|
||||||
let contextMenu = { visible: false, x: 0, y: 0, node: null }
|
let contextMenu = { visible: false, x: 0, y: 0, node: null }
|
||||||
let showCreateNote = false
|
|
||||||
let newNoteTitle = ''
|
|
||||||
let showCreateAction = false
|
let showCreateAction = false
|
||||||
let newActionTitle = ''
|
let newActionTitle = ''
|
||||||
let newActionKind = 'open_url'
|
let newActionKind = 'open_url'
|
||||||
|
|
@ -298,7 +297,6 @@
|
||||||
if (showRename) { showRename = false; return true }
|
if (showRename) { showRename = false; return true }
|
||||||
if (showWorklogModal) { closeWorklogModal(); return true }
|
if (showWorklogModal) { closeWorklogModal(); return true }
|
||||||
if (showCreateAction) { cancelCreateAction(); return true }
|
if (showCreateAction) { cancelCreateAction(); return true }
|
||||||
if (showCreateNote) { cancelCreateNote(); return true }
|
|
||||||
if (showCreateNode) { cancelCreateNode(); return true }
|
if (showCreateNode) { cancelCreateNode(); return true }
|
||||||
if (contextMenu.visible) { closeContextMenu(); return true }
|
if (contextMenu.visible) { closeContextMenu(); return true }
|
||||||
if (noteEditor) { closeNoteEditor(); return true }
|
if (noteEditor) { closeNoteEditor(); return true }
|
||||||
|
|
@ -513,7 +511,6 @@
|
||||||
resetTrashBrowser()
|
resetTrashBrowser()
|
||||||
noteEditor = null
|
noteEditor = null
|
||||||
showCreateNode = false
|
showCreateNode = false
|
||||||
showCreateNote = false
|
|
||||||
error = ''
|
error = ''
|
||||||
caseActivity = []
|
caseActivity = []
|
||||||
await loadTabData(node.id)
|
await loadTabData(node.id)
|
||||||
|
|
@ -915,21 +912,17 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Notes =====
|
// ===== Notes =====
|
||||||
function openCreateNote() { showCreateNote = true; newNoteTitle = '' }
|
// Notes create/delete/rename are handled by NotesTab component via events.
|
||||||
function cancelCreateNote() { showCreateNote = false; newNoteTitle = '' }
|
// App.svelte keeps note editor lifecycle (doOpenNote, saveCurrentNote, etc.)
|
||||||
async function submitCreateNote() {
|
|
||||||
if (!newNoteTitle.trim() || !selectedNode) return
|
async function _handleSubmitCreateNote(title) {
|
||||||
|
if (!title.trim() || !selectedNode) return
|
||||||
try {
|
try {
|
||||||
const note = await wailsCall('CreateNote', selectedNode.id, newNoteTitle.trim())
|
const note = await wailsCall('CreateNote', selectedNode.id, title.trim())
|
||||||
notes = [...notes, (note && note.id) ? note : { id: Date.now().toString(), title: newNoteTitle.trim(), createdAt: new Date().toISOString() }]
|
notes = [...notes, (note && note.id) ? note : { id: Date.now().toString(), title: title.trim(), createdAt: new Date().toISOString() }]
|
||||||
showCreateNote = false
|
|
||||||
newNoteTitle = ''
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Fallback: create note locally
|
const newNote = { id: Date.now().toString(), title: title.trim(), createdAt: new Date().toISOString() }
|
||||||
const newNote = { id: Date.now().toString(), title: newNoteTitle.trim(), createdAt: new Date().toISOString() }
|
|
||||||
notes = [...notes, newNote]
|
notes = [...notes, newNote]
|
||||||
showCreateNote = false
|
|
||||||
newNoteTitle = ''
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2613,43 +2606,14 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{:else if activeTab === 'notes'}
|
{:else if activeTab === 'notes'}
|
||||||
<div class="notes-tab">
|
<NotesTab
|
||||||
<div class="tab-toolbar">
|
{notes}
|
||||||
<button class="btn btn-primary" on:click={openCreateNote}>{t('note.add')}</button>
|
{formatDate}
|
||||||
</div>
|
on:submitCreateNote={(e) => _handleSubmitCreateNote(e.detail.title)}
|
||||||
{#if showCreateNote}
|
on:openNote={(e) => openNote(e.detail.note)}
|
||||||
<div class="create-form">
|
on:startRename={(e) => startRenameNote(e.detail.noteId, e.detail.currentTitle)}
|
||||||
<input type="text" placeholder={t('note.title')} bind:value={newNoteTitle}
|
on:deleteNote={(e) => deleteNote(e.detail.note)}
|
||||||
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>
|
|
||||||
|
|
||||||
{:else if activeTab === 'files'}
|
{:else if activeTab === 'files'}
|
||||||
<FilesTab
|
<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 { 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-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 groups in modals */
|
||||||
.form-group { margin-bottom: 14px; }
|
.form-group { margin-bottom: 14px; }
|
||||||
.form-group label { display: block; font-size: 12px; color: #888; margin-bottom: 4px; }
|
.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-date { font-size: 11px; color: #555; }
|
||||||
.recent-entry { padding: 6px 0; font-size: 13px; color: #888; border-bottom: 1px solid #1a1a28; }
|
.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 */
|
||||||
.worklog-tab { padding: 24px; }
|
.worklog-tab { padding: 24px; }
|
||||||
.worklog-toolbar { margin-bottom: 16px; }
|
.worklog-toolbar { margin-bottom: 16px; }
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -2,10 +2,10 @@
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import FileBreadcrumbs from '../../FileBreadcrumbs.svelte'
|
import FileBreadcrumbs from '../../FileBreadcrumbs.svelte'
|
||||||
import FileTreeRow from '../../../FileTreeRow.svelte'
|
import FileTreeRow from '../../../FileTreeRow.svelte'
|
||||||
import FilePreviewModal from '../FilePreviewModal.svelte'
|
import FilePreviewModal from '../../FilePreviewModal.svelte'
|
||||||
import ConfirmModal from '../ConfirmModal.svelte'
|
import ConfirmModal from '../../ConfirmModal.svelte'
|
||||||
import { canPreviewFile, needsBase64Preview, needsTextPreview, isMarkdownFile } from '../fileUtils.js'
|
import { canPreviewFile, needsBase64Preview, needsTextPreview, isMarkdownFile } from '../../fileUtils.js'
|
||||||
import { t } from '../i18n'
|
import { t } from '../../i18n'
|
||||||
|
|
||||||
// ===== Props =====
|
// ===== Props =====
|
||||||
export let selectedNode = null
|
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