fix: AcceptSuggestionWith uses flat fields to avoid Wails marshalling issues; human-readable event labels

- AcceptSuggestionWith now accepts nodeID, summary, minutes, date, eventIDs
  as separate args instead of the entire Suggestion struct (Wails v2 skips
  nested struct fields during JS→Go marshalling)
- Error handling: event link failures now return an error instead of silent ignore
- Event type labels in suggestion detail and journal row detail now use
  eventLabel() which maps snake_case types to human-readable i18n labels
  (e.g. note_updated → 'Заметка изменена')
- Added missing event labels: note_deleted, node_deleted, folder_moved,
  action_created, action_done, worklog_added
This commit is contained in:
mirivlad 2026-06-03 12:35:13 +08:00
parent fd99dd4f5c
commit 7076980954
2 changed files with 24 additions and 15 deletions

View File

@ -111,26 +111,29 @@ func (a *App) GetSuggestions() ([]activity.Suggestion, error) {
return suggestions, nil return suggestions, nil
} }
// AcceptSuggestion creates a worklog entry from a suggestion (compatibility wrapper). // AcceptSuggestion creates a worklog entry from a suggestion (compatibility wrapper, uses flat fields).
func (a *App) AcceptSuggestion(s activity.Suggestion) (*WorklogDTO, error) { func (a *App) AcceptSuggestion(nodeID, summary string, minutes int, date string, eventIDs []string) (*WorklogDTO, error) {
return a.AcceptSuggestionWith(s, s.SuggestedMin, "") return a.AcceptSuggestionWith(nodeID, summary, minutes, date, eventIDs)
} }
// AcceptSuggestionWith creates a worklog entry with optional overrides. // AcceptSuggestionWith creates a worklog entry and links events. Uses flat fields to avoid Wails marshalling issues.
func (a *App) AcceptSuggestionWith(s activity.Suggestion, minutes int, date string) (*WorklogDTO, error) { func (a *App) AcceptSuggestionWith(nodeID, summary string, minutes int, date string, eventIDs []string) (*WorklogDTO, error) {
d := date d := date
if d == "" { if d == "" {
d = time.Now().Format("2006-01-02") d = time.Now().Format("2006-01-02")
} }
entry, err := a.worklog.AddWithSource(s.NodeID, s.Summary, "", d, minutes, true, false, worklog.SourceSuggestion) entry, err := a.worklog.AddWithSource(nodeID, summary, "", d, minutes, true, false, worklog.SourceSuggestion)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Link activity events to this worklog entry. // Link activity events to this worklog entry.
for _, eid := range s.EventIDs { for _, eid := range eventIDs {
_, _ = a.db.Exec( _, err := a.db.Exec(
`INSERT OR IGNORE INTO worklog_entry_events (entry_id, event_id) VALUES (?,?)`, `INSERT OR IGNORE INTO worklog_entry_events (entry_id, event_id) VALUES (?,?)`,
entry.ID, eid) entry.ID, eid)
if err != nil {
return nil, fmt.Errorf("link event %s: %w", eid, err)
}
} }
_ = a.sync.RecordOp(syncsvc.EntityWorklog, entry.ID, syncsvc.OpCreate, worklogPayload(entry)) _ = a.sync.RecordOp(syncsvc.EntityWorklog, entry.ID, syncsvc.OpCreate, worklogPayload(entry))
mins := 0 mins := 0

View File

@ -972,14 +972,14 @@
async function acceptTodaySuggestion(s) { async function acceptTodaySuggestion(s) {
try { try {
await wailsCall('AcceptSuggestionWith', s, s.suggestedMin, '') await wailsCall('AcceptSuggestionWith', s.nodeId, s.summary, s.suggestedMin, '', s.eventIds || [])
await refreshAfterSuggestion() await refreshAfterSuggestion()
} catch (e) { console.error(e) } } catch (e) { console.error(e) }
} }
async function acceptJournalSuggestion(s) { async function acceptJournalSuggestion(s) {
try { try {
await wailsCall('AcceptSuggestionWith', s, s.suggestedMin, '') await wailsCall('AcceptSuggestionWith', s.nodeId, s.summary, s.suggestedMin, '', s.eventIds || [])
await refreshAfterSuggestion() await refreshAfterSuggestion()
} catch (e) { console.error(e) } } catch (e) { console.error(e) }
} }
@ -1223,6 +1223,7 @@
const labels = { const labels = {
'note_created': t('event.noteCreated'), 'note_created': t('event.noteCreated'),
'note_updated': t('event.noteUpdated'), 'note_updated': t('event.noteUpdated'),
'note_deleted': 'Заметка удалена',
'file_added': t('event.fileAdded'), 'file_added': t('event.fileAdded'),
'file_deleted': t('event.fileDeleted'), 'file_deleted': t('event.fileDeleted'),
'file_renamed': t('event.fileRenamed'), 'file_renamed': t('event.fileRenamed'),
@ -1231,8 +1232,13 @@
'folder_added': t('event.folderAdded'), 'folder_added': t('event.folderAdded'),
'folder_deleted': t('event.folderDeleted'), 'folder_deleted': t('event.folderDeleted'),
'folder_renamed': t('event.folderRenamed'), 'folder_renamed': t('event.folderRenamed'),
'folder_moved': 'Папка перемещена',
'node_created': t('event.caseCreated'), 'node_created': t('event.caseCreated'),
'node_updated': t('event.caseUpdated'), 'node_updated': t('event.caseUpdated'),
'node_deleted': 'Узел удалён',
'action_created': 'Действие создано',
'action_done': 'Действие выполнено',
'worklog_added': 'Запись времени добавлена',
} }
return labels[type] || type return labels[type] || type
} }
@ -1769,7 +1775,7 @@
{#each s.events as ev} {#each s.events as ev}
<div class="suggestion-detail-event"> <div class="suggestion-detail-event">
<span class="suggestion-event-time">{formatTime(ev.createdAt)}</span> <span class="suggestion-event-time">{formatTime(ev.createdAt)}</span>
<span class="suggestion-event-type">{t('event.' + ev.eventType) || ev.eventType}</span> <span class="suggestion-event-type">{eventLabel(ev.eventType)}</span>
<span class="suggestion-event-title">{ev.title}</span> <span class="suggestion-event-title">{ev.title}</span>
<button class="link-btn" on:click={() => openNodeById(ev.nodeId)}>{t('common.open')}</button> <button class="link-btn" on:click={() => openNodeById(ev.nodeId)}>{t('common.open')}</button>
{#if ev.targetType === 'file' || ev.eventType.startsWith('file_')} {#if ev.targetType === 'file' || ev.eventType.startsWith('file_')}
@ -1824,7 +1830,7 @@
{#each e._events as ev} {#each e._events as ev}
<div class="journal-event-row"> <div class="journal-event-row">
<span class="journal-event-time">{formatTime(ev.createdAt)}</span> <span class="journal-event-time">{formatTime(ev.createdAt)}</span>
<span class="journal-event-type">{t('event.' + ev.eventType) || ev.eventType}</span> <span class="journal-event-type">{eventLabel(ev.eventType)}</span>
<span class="journal-event-title">{ev.title}</span> <span class="journal-event-title">{ev.title}</span>
<button class="link-btn" on:click={() => openNodeById(ev.nodeId)}>{t('common.open')}</button> <button class="link-btn" on:click={() => openNodeById(ev.nodeId)}>{t('common.open')}</button>
{#if ev.targetType === 'file' || ev.eventType.startsWith('file_')} {#if ev.targetType === 'file' || ev.eventType.startsWith('file_')}
@ -1960,7 +1966,7 @@
{#each s.events as ev} {#each s.events as ev}
<div class="suggestion-detail-event"> <div class="suggestion-detail-event">
<span class="suggestion-event-time">{formatTime(ev.createdAt)}</span> <span class="suggestion-event-time">{formatTime(ev.createdAt)}</span>
<span class="suggestion-event-type">{t('event.' + ev.eventType) || ev.eventType}</span> <span class="suggestion-event-type">{eventLabel(ev.eventType)}</span>
<span class="suggestion-event-title">{ev.title}</span> <span class="suggestion-event-title">{ev.title}</span>
<button class="link-btn" on:click={() => openNodeById(ev.nodeId)}>{t('common.open')}</button> <button class="link-btn" on:click={() => openNodeById(ev.nodeId)}>{t('common.open')}</button>
{#if ev.targetType === 'file' || ev.eventType.startsWith('file_')} {#if ev.targetType === 'file' || ev.eventType.startsWith('file_')}
@ -2058,7 +2064,7 @@
{#each r._events as ev} {#each r._events as ev}
<div class="journal-event-row"> <div class="journal-event-row">
<span class="journal-event-time">{formatTime(ev.createdAt)}</span> <span class="journal-event-time">{formatTime(ev.createdAt)}</span>
<span class="journal-event-type">{t('event.' + ev.eventType) || ev.eventType}</span> <span class="journal-event-type">{eventLabel(ev.eventType)}</span>
<span class="journal-event-title">{ev.title}</span> <span class="journal-event-title">{ev.title}</span>
<button class="link-btn" on:click={() => openNodeById(ev.nodeId)}>{t('common.open')}</button> <button class="link-btn" on:click={() => openNodeById(ev.nodeId)}>{t('common.open')}</button>
</div> </div>
@ -2114,7 +2120,7 @@
{#each s.events as ev} {#each s.events as ev}
<div class="suggestion-detail-event"> <div class="suggestion-detail-event">
<span class="suggestion-event-time">{formatTime(ev.createdAt)}</span> <span class="suggestion-event-time">{formatTime(ev.createdAt)}</span>
<span class="suggestion-event-type">{t('event.' + ev.eventType) || ev.eventType}</span> <span class="suggestion-event-type">{eventLabel(ev.eventType)}</span>
<span class="suggestion-event-title">{ev.title}</span> <span class="suggestion-event-title">{ev.title}</span>
<button class="link-btn" on:click={() => openNodeById(ev.nodeId)}>{t('common.open')}</button> <button class="link-btn" on:click={() => openNodeById(ev.nodeId)}>{t('common.open')}</button>
{#if ev.targetType === 'file' || ev.eventType.startsWith('file_')} {#if ev.targetType === 'file' || ev.eventType.startsWith('file_')}