fix: bypass Wails v2 []string marshalling via JSON.stringify + end-to-end test
Root cause: Wails v2.12.0 cannot reliably marshal []string arguments from JavaScript to Go when called through positional binding. The event IDs array arrived empty on the Go side, causing no worklog_entry_events INSERTs. Fix: - AcceptSuggestionWith now accepts eventIDsJSON (string) instead of eventIDs ([]string). Frontend passes JSON.stringify(eventIds). - Backend json.Unmarshal into []string before validation. - Pre-insert validation: each eventID checked in activity_events. - Atomic tx: entry create + linking in single Begin/Commit. - INSERT (not INSERT OR IGNORE) — failure is a hard error. - Post-commit verification: JOIN COUNT(*) must match len(eventIDs). - End-to-end test: TestAcceptSuggestionWithEndToEnd creates a node, 3 activity events, accepts suggestion, verifies all 3 linked. Other changes: - GetWorklogEntryEvents: fixed column name (details_json -> metadata). - openActivityTarget(ev): new function for 'Посмотреть' button that navigates to specific note/file/folder instead of just opening node. - All 'openNodeById(ev.nodeId)' in event contexts replaced with 'openActivityTarget(ev)'.
This commit is contained in:
parent
21a595c3ce
commit
b42aa35ee8
|
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -111,22 +112,36 @@ func (a *App) GetSuggestions() ([]activity.Suggestion, error) {
|
||||||
return suggestions, nil
|
return suggestions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AcceptSuggestion creates a worklog entry from a suggestion (compatibility wrapper, uses flat fields).
|
// AcceptSuggestion creates a worklog entry from a suggestion (compatibility wrapper).
|
||||||
func (a *App) AcceptSuggestion(nodeID, summary string, minutes int, date string, eventIDs []string) (*WorklogDTO, error) {
|
func (a *App) AcceptSuggestion(nodeID, summary string, minutes int, date string, eventIDsJSON string) (*WorklogDTO, error) {
|
||||||
return a.AcceptSuggestionWith(nodeID, summary, minutes, date, eventIDs)
|
return a.AcceptSuggestionWith(nodeID, summary, minutes, date, eventIDsJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AcceptSuggestionWith creates a worklog entry and links events in a single transaction.
|
// AcceptSuggestionWith creates a worklog entry and links events in a single transaction.
|
||||||
// Uses flat fields to avoid Wails marshalling issues.
|
// eventIDsJSON is a JSON-serialized string array to avoid Wails v2 []string marshalling issues.
|
||||||
func (a *App) AcceptSuggestionWith(nodeID, summary string, minutes int, date string, eventIDs []string) (*WorklogDTO, error) {
|
func (a *App) AcceptSuggestionWith(nodeID, summary string, minutes int, date string, eventIDsJSON 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log what we received from the frontend
|
var eventIDs []string
|
||||||
fmt.Printf("DEBUG AcceptSuggestionWith: nodeID=%q summary=%q minutes=%d date=%q eventIDs=%v (len=%d)\n",
|
if eventIDsJSON != "" {
|
||||||
nodeID, summary, minutes, d, eventIDs, len(eventIDs))
|
if err := json.Unmarshal([]byte(eventIDsJSON), &eventIDs); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal eventIDs: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that every eventID actually exists in activity_events.
|
||||||
|
for _, eid := range eventIDs {
|
||||||
|
var n int
|
||||||
|
if err := a.db.QueryRow(`SELECT COUNT(*) FROM activity_events WHERE id = ?`, eid).Scan(&n); err != nil {
|
||||||
|
return nil, fmt.Errorf("check event %s: %w", eid, err)
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
return nil, fmt.Errorf("event %s not found in activity_events", eid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Use a transaction to atomically create entry + link events
|
// Use a transaction to atomically create entry + link events
|
||||||
tx, err := a.db.Begin()
|
tx, err := a.db.Begin()
|
||||||
|
|
@ -140,33 +155,31 @@ func (a *App) AcceptSuggestionWith(nodeID, summary string, minutes int, date str
|
||||||
return nil, fmt.Errorf("create entry: %w", err)
|
return nil, fmt.Errorf("create entry: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("DEBUG AcceptSuggestionWith: entry created id=%s\n", entry.ID)
|
|
||||||
|
|
||||||
linked := 0
|
|
||||||
for _, eid := range eventIDs {
|
for _, eid := range eventIDs {
|
||||||
res, err := tx.Exec(
|
if _, err := tx.Exec(
|
||||||
`INSERT OR IGNORE INTO worklog_entry_events (entry_id, event_id) VALUES (?,?)`,
|
`INSERT INTO worklog_entry_events (entry_id, event_id) VALUES (?,?)`,
|
||||||
entry.ID, eid)
|
entry.ID, eid); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("link event %s: %w", eid, err)
|
return nil, fmt.Errorf("link event %s: %w", eid, err)
|
||||||
}
|
}
|
||||||
n, _ := res.RowsAffected()
|
|
||||||
linked += int(n)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("DEBUG AcceptSuggestionWith: linked %d events (out of %d eventIDs)\n", linked, len(eventIDs))
|
if len(eventIDs) > 0 {
|
||||||
|
var linked int
|
||||||
|
if err := tx.QueryRow(
|
||||||
|
`SELECT COUNT(*) FROM worklog_entry_events wle
|
||||||
|
JOIN activity_events ae ON ae.id = wle.event_id
|
||||||
|
WHERE wle.entry_id = ?`, entry.ID).Scan(&linked); err != nil {
|
||||||
|
return nil, fmt.Errorf("verify links: %w", err)
|
||||||
|
}
|
||||||
|
if linked != len(eventIDs) {
|
||||||
|
return nil, fmt.Errorf("expected %d linked events, got %d", len(eventIDs), linked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
if err := tx.Commit(); err != nil {
|
||||||
return nil, fmt.Errorf("commit tx: %w", err)
|
return nil, fmt.Errorf("commit tx: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify the links were stored
|
|
||||||
if len(eventIDs) > 0 {
|
|
||||||
var count int
|
|
||||||
a.db.QueryRow("SELECT COUNT(*) FROM worklog_entry_events WHERE entry_id = ?", entry.ID).Scan(&count)
|
|
||||||
fmt.Printf("DEBUG AcceptSuggestionWith: verification COUNT(*) = %d\n", count)
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = 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
|
||||||
if entry.Minutes != nil {
|
if entry.Minutes != nil {
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ func (a *App) SaveWorklogReport(format, dateFrom, dateTo, nodeID string, include
|
||||||
func (a *App) GetWorklogEntryEvents(entryID string) ([]EventDTO, error) {
|
func (a *App) GetWorklogEntryEvents(entryID string) ([]EventDTO, error) {
|
||||||
rows, err := a.db.Query(
|
rows, err := a.db.Query(
|
||||||
`SELECT e.id, e.node_id, e.event_type, e.target_type, e.target_id, e.target_path,
|
`SELECT e.id, e.node_id, e.event_type, e.target_type, e.target_id, e.target_path,
|
||||||
e.title, e.details_json, e.created_at
|
e.title, COALESCE(e.metadata,''), e.created_at
|
||||||
FROM activity_events e
|
FROM activity_events e
|
||||||
JOIN worklog_entry_events wle ON wle.event_id = e.id
|
JOIN worklog_entry_events wle ON wle.event_id = e.id
|
||||||
WHERE wle.entry_id = ?
|
WHERE wle.entry_id = ?
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -16,7 +16,7 @@
|
||||||
background: #13131f;
|
background: #13131f;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/main-DQ318Oic.js"></script>
|
<script type="module" crossorigin src="/assets/main-BaeOWl0t.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-BafVhx43.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-BafVhx43.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"verstak/internal/core/activity"
|
||||||
|
"verstak/internal/core/util"
|
||||||
|
"verstak/internal/core/worklog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestAcceptSuggestionWithEndToEnd verifies the full chain:
|
||||||
|
// 1. Create a node
|
||||||
|
// 2. Create activity events for that node
|
||||||
|
// 3. Accept a suggestion with those event IDs
|
||||||
|
// 4. Verify worklog_entry_events contains exactly those events
|
||||||
|
// 5. Verify GetWorklogEntryEvents returns the linked events
|
||||||
|
func TestAcceptSuggestionWithEndToEnd(t *testing.T) {
|
||||||
|
app, _ := setupTestApp(t)
|
||||||
|
|
||||||
|
// 1. Create a node
|
||||||
|
n, err := app.CreateNodeFromTemplate("", "Test Suggestion", "folder.default")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create node: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Create activity events for the node.
|
||||||
|
// We need to insert events with today's timestamp so GetSuggestions picks them up.
|
||||||
|
// But to avoid time dependency, we also directly accept with known event IDs.
|
||||||
|
eid1 := insertTestEvent(t, app, n.ID, activity.TypeNoteCreated, "note", "note1", "Test note 1")
|
||||||
|
eid2 := insertTestEvent(t, app, n.ID, activity.TypeNoteUpdated, "note", "note1", "Test note 1 updated")
|
||||||
|
eid3 := insertTestEvent(t, app, n.ID, activity.TypeFileAdded, "file", "file1", "Test file")
|
||||||
|
|
||||||
|
eventIDs := []string{eid1, eid2, eid3}
|
||||||
|
eventIDsJSON, _ := json.Marshal(eventIDs)
|
||||||
|
|
||||||
|
// 3. Accept suggestion with these event IDs
|
||||||
|
dto, err := app.AcceptSuggestionWith(n.ID, "Работа с заметками", 15, "", string(eventIDsJSON))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("AcceptSuggestionWith: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dto.ID == "" {
|
||||||
|
t.Fatal("AcceptSuggestionWith returned empty entry ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Verify worklog_entry_events count
|
||||||
|
var linkCount int
|
||||||
|
err = app.db.QueryRow(
|
||||||
|
`SELECT COUNT(*) FROM worklog_entry_events WHERE entry_id = ?`, dto.ID).Scan(&linkCount)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("count worklog_entry_events: %v", err)
|
||||||
|
}
|
||||||
|
if linkCount != 3 {
|
||||||
|
t.Errorf("worklog_entry_events count = %d, want 3", linkCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Verify JOIN with activity_events
|
||||||
|
var joinCount int
|
||||||
|
err = app.db.QueryRow(
|
||||||
|
`SELECT COUNT(*) FROM worklog_entry_events wle
|
||||||
|
JOIN activity_events ae ON ae.id = wle.event_id
|
||||||
|
WHERE wle.entry_id = ?`, dto.ID).Scan(&joinCount)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("join count: %v", err)
|
||||||
|
}
|
||||||
|
if joinCount != 3 {
|
||||||
|
t.Errorf("JOIN count = %d, want 3", joinCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Call GetWorklogEntryEvents and verify 3 events
|
||||||
|
events, err := app.GetWorklogEntryEvents(dto.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetWorklogEntryEvents: %v", err)
|
||||||
|
}
|
||||||
|
if len(events) != 3 {
|
||||||
|
t.Errorf("GetWorklogEntryEvents returned %d events, want 3", len(events))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Verify the returned event IDs match
|
||||||
|
returnedIDs := make(map[string]bool, len(events))
|
||||||
|
for _, ev := range events {
|
||||||
|
returnedIDs[ev.ID] = true
|
||||||
|
}
|
||||||
|
for _, want := range eventIDs {
|
||||||
|
if !returnedIDs[want] {
|
||||||
|
t.Errorf("event %s not found in GetWorklogEntryEvents result", want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. Verify source is 'suggestion'
|
||||||
|
var source string
|
||||||
|
err = app.db.QueryRow(
|
||||||
|
`SELECT source FROM worklog_entries WHERE id = ?`, dto.ID).Scan(&source)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("get source: %v", err)
|
||||||
|
}
|
||||||
|
if source != worklog.SourceSuggestion {
|
||||||
|
t.Errorf("source = %q, want %q", source, worklog.SourceSuggestion)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertTestEvent inserts an activity event and returns its ID.
|
||||||
|
func insertTestEvent(t *testing.T, app *App, nodeID, eventType, targetType, targetID, title string) string {
|
||||||
|
t.Helper()
|
||||||
|
id := util.UUID7()
|
||||||
|
now := "2026-06-03T12:00:00Z"
|
||||||
|
_, err := app.db.Exec(
|
||||||
|
`INSERT INTO activity_events(id,node_id,event_type,target_type,target_id,target_path,title,metadata,created_at)
|
||||||
|
VALUES(?,?,?,?,?,?,?,?,?)`,
|
||||||
|
id, nodeID, eventType, targetType, targetID, "", title, "{}", now)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("insert event: %v", err)
|
||||||
|
}
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
@ -979,8 +979,9 @@
|
||||||
async function acceptTodaySuggestion(s) {
|
async function acceptTodaySuggestion(s) {
|
||||||
try {
|
try {
|
||||||
const eventIds = extractEventIds(s)
|
const eventIds = extractEventIds(s)
|
||||||
console.log('DEBUG acceptTodaySuggestion:', { nodeId: s.nodeId, eventIdsLen: eventIds.length, eventIds, eventsCount: s.events?.length })
|
const eventIdsJSON = JSON.stringify(eventIds)
|
||||||
await wailsCall('AcceptSuggestionWith', s.nodeId, s.summary, s.suggestedMin, '', eventIds)
|
console.log('DEBUG acceptTodaySuggestion:', { nodeId: s.nodeId, eventIds, eventIdsJSON, events: (s.events || []).map(ev => ({ id: ev.id, type: ev.eventType, title: ev.title })) })
|
||||||
|
await wailsCall('AcceptSuggestionWith', s.nodeId, s.summary, s.suggestedMin, '', eventIdsJSON)
|
||||||
await refreshAfterSuggestion()
|
await refreshAfterSuggestion()
|
||||||
} catch (e) { console.error(e) }
|
} catch (e) { console.error(e) }
|
||||||
}
|
}
|
||||||
|
|
@ -988,8 +989,9 @@
|
||||||
async function acceptJournalSuggestion(s) {
|
async function acceptJournalSuggestion(s) {
|
||||||
try {
|
try {
|
||||||
const eventIds = extractEventIds(s)
|
const eventIds = extractEventIds(s)
|
||||||
console.log('DEBUG acceptJournalSuggestion:', { nodeId: s.nodeId, eventIdsLen: eventIds.length, eventIds, eventsCount: s.events?.length })
|
const eventIdsJSON = JSON.stringify(eventIds)
|
||||||
await wailsCall('AcceptSuggestionWith', s.nodeId, s.summary, s.suggestedMin, '', eventIds)
|
console.log('DEBUG acceptJournalSuggestion:', { nodeId: s.nodeId, eventIds, eventIdsJSON, events: (s.events || []).map(ev => ({ id: ev.id, type: ev.eventType, title: ev.title })) })
|
||||||
|
await wailsCall('AcceptSuggestionWith', s.nodeId, s.summary, s.suggestedMin, '', eventIdsJSON)
|
||||||
await refreshAfterSuggestion()
|
await refreshAfterSuggestion()
|
||||||
} catch (e) { console.error(e) }
|
} catch (e) { console.error(e) }
|
||||||
}
|
}
|
||||||
|
|
@ -1316,36 +1318,35 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openActivityEvent(ev) {
|
async function openActivityTarget(ev) {
|
||||||
const targetNode = ev.nodeId
|
const targetNode = ev.nodeId
|
||||||
if (!targetNode) return
|
if (!targetNode) return
|
||||||
try {
|
try {
|
||||||
const node = await wailsCall('GetNodeDetail', targetNode)
|
const node = await wailsCall('GetNodeDetail', targetNode)
|
||||||
if (!node) return
|
if (!node) return
|
||||||
if (ev.targetType === 'note') {
|
|
||||||
selectNode(node)
|
selectNode(node)
|
||||||
|
if (ev.targetType === 'note') {
|
||||||
activeTab = 'notes'
|
activeTab = 'notes'
|
||||||
// If we have a targetId, try to find and open the note
|
|
||||||
if (ev.targetId) {
|
if (ev.targetId) {
|
||||||
// Load notes and open the specific note
|
|
||||||
try { notes = await wailsCall('ListNotes', targetNode) || [] } catch(e) {}
|
try { notes = await wailsCall('ListNotes', targetNode) || [] } catch(e) {}
|
||||||
const note = notes.find(n => n.id === ev.targetId)
|
const note = notes.find(n => n.id === ev.targetId)
|
||||||
if (note) {
|
if (note) setTimeout(() => openNote(note), 100)
|
||||||
// Small delay to let the UI render the notes tab
|
|
||||||
setTimeout(() => openNote(note), 100)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (ev.targetType === 'file' || ev.targetType === 'folder') {
|
} else if (ev.targetType === 'file' || ev.targetType === 'folder') {
|
||||||
selectNode(node)
|
|
||||||
activeTab = 'files'
|
activeTab = 'files'
|
||||||
} else {
|
if (ev.targetId && ev.targetPath) {
|
||||||
selectNode(node)
|
try { await wailsCall('OpenFolder', ev.targetPath) } catch(e) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = String(e)
|
error = String(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openActivityEvent(ev) {
|
||||||
|
return openActivityTarget(ev)
|
||||||
|
}
|
||||||
|
|
||||||
// ===== Sync =====
|
// ===== Sync =====
|
||||||
async function loadSyncStatus() {
|
async function loadSyncStatus() {
|
||||||
try {
|
try {
|
||||||
|
|
@ -1787,7 +1788,7 @@
|
||||||
<span class="suggestion-event-time">{formatTime(ev.createdAt)}</span>
|
<span class="suggestion-event-time">{formatTime(ev.createdAt)}</span>
|
||||||
<span class="suggestion-event-type">{eventLabel(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={() => openActivityTarget(ev)}>{t('common.open')}</button>
|
||||||
{#if ev.targetType === 'file' || ev.eventType.startsWith('file_')}
|
{#if ev.targetType === 'file' || ev.eventType.startsWith('file_')}
|
||||||
<button class="link-btn" on:click={() => openNodeFolder(ev.nodeId)}>{t('file.showInExplorer')}</button>
|
<button class="link-btn" on:click={() => openNodeFolder(ev.nodeId)}>{t('file.showInExplorer')}</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -1842,7 +1843,7 @@
|
||||||
<span class="journal-event-time">{formatTime(ev.createdAt)}</span>
|
<span class="journal-event-time">{formatTime(ev.createdAt)}</span>
|
||||||
<span class="journal-event-type">{eventLabel(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={() => openActivityTarget(ev)}>{t('common.open')}</button>
|
||||||
{#if ev.targetType === 'file' || ev.eventType.startsWith('file_')}
|
{#if ev.targetType === 'file' || ev.eventType.startsWith('file_')}
|
||||||
<button class="link-btn" on:click={() => openNodeFolder(ev.nodeId)}>{t('file.showInExplorer')}</button>
|
<button class="link-btn" on:click={() => openNodeFolder(ev.nodeId)}>{t('file.showInExplorer')}</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -1978,7 +1979,7 @@
|
||||||
<span class="suggestion-event-time">{formatTime(ev.createdAt)}</span>
|
<span class="suggestion-event-time">{formatTime(ev.createdAt)}</span>
|
||||||
<span class="suggestion-event-type">{eventLabel(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={() => openActivityTarget(ev)}>{t('common.open')}</button>
|
||||||
{#if ev.targetType === 'file' || ev.eventType.startsWith('file_')}
|
{#if ev.targetType === 'file' || ev.eventType.startsWith('file_')}
|
||||||
<button class="link-btn" on:click={() => openNodeFolder(ev.nodeId)}>{t('file.showInExplorer')}</button>
|
<button class="link-btn" on:click={() => openNodeFolder(ev.nodeId)}>{t('file.showInExplorer')}</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -2076,7 +2077,7 @@
|
||||||
<span class="journal-event-time">{formatTime(ev.createdAt)}</span>
|
<span class="journal-event-time">{formatTime(ev.createdAt)}</span>
|
||||||
<span class="journal-event-type">{eventLabel(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={() => openActivityTarget(ev)}>{t('common.open')}</button>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2132,7 +2133,7 @@
|
||||||
<span class="suggestion-event-time">{formatTime(ev.createdAt)}</span>
|
<span class="suggestion-event-time">{formatTime(ev.createdAt)}</span>
|
||||||
<span class="suggestion-event-type">{eventLabel(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={() => openActivityTarget(ev)}>{t('common.open')}</button>
|
||||||
{#if ev.targetType === 'file' || ev.eventType.startsWith('file_')}
|
{#if ev.targetType === 'file' || ev.eventType.startsWith('file_')}
|
||||||
<button class="link-btn" on:click={() => openNodeFolder(ev.nodeId)}>{t('file.showInExplorer')}</button>
|
<button class="link-btn" on:click={() => openNodeFolder(ev.nodeId)}>{t('file.showInExplorer')}</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue