Today view and Inbox section: dynamic query, not stored sections

- ListTodayView() on backend: queries nodes created or updated today
  using local timezone day boundaries
- todayBoundaries() helper returns start/end of current day in RFC3339
- Section validation: Create() rejects today and inbox as node sections
- validSections moved from repository.go to types.go with IsValidSection
  and IsServiceSection helpers
- Frontend selectSection('today') calls ListTodayView instead of
  ListNodesBySection('today')
- FAB (create node) hidden in today and inbox sections
- CreateNode in app.go guarded against today/inbox sections
- types.go: today/inbox defined as service sections (sidebar only)
This commit is contained in:
mirivlad 2026-06-01 01:39:02 +08:00
parent a4ae22c445
commit 69891e395c
8 changed files with 107 additions and 11 deletions

View File

@ -143,6 +143,15 @@ func (a *App) ListNodesBySection(section string) ([]NodeDTO, error) {
return toNodeDTOs(list), nil
}
// ListTodayView returns nodes created or updated today — a dynamic day view.
func (a *App) ListTodayView() ([]NodeDTO, error) {
list, err := a.nodes.ListTodayNodes()
if err != nil {
return nil, err
}
return toNodeDTOs(list), nil
}
func (a *App) ListChildren(parentID string) ([]NodeDTO, error) {
list, err := a.nodes.ListChildren(parentID, false)
if err != nil {
@ -161,6 +170,9 @@ func (a *App) GetNodeDetail(nodeID string) (*NodeDTO, error) {
}
func (a *App) CreateNode(parentID, nodeType, title, section string) (*NodeDTO, error) {
if section == "today" || section == "inbox" {
return nil, fmt.Errorf("cannot create node with section %q", section)
}
n, err := a.nodes.Create(parentID, nodeType, title, section)
if err != nil {
return nil, err

File diff suppressed because one or more lines are too long

View File

@ -16,7 +16,7 @@
background: #13131f;
}
</style>
<script type="module" crossorigin src="/assets/main-B8EIu1OK.js"></script>
<script type="module" crossorigin src="/assets/main-BY9JF_6I.js"></script>
<link rel="stylesheet" crossorigin href="/assets/main-Bo58X7Pc.css">
</head>
<body>

View File

@ -135,7 +135,11 @@
showCreateNode = false
error = ''
try {
nodes = await wailsCall('ListNodesBySection', id) || []
if (id === 'today') {
nodes = await wailsCall('ListTodayView') || []
} else {
nodes = await wailsCall('ListNodesBySection', id) || []
}
} catch (e) {
error = String(e)
nodes = []
@ -1055,7 +1059,7 @@
</div>
{/if}
{#if !noteEditor && !selectedNode}
{#if !noteEditor && !selectedNode && selectedSection !== 'today' && selectedSection !== 'inbox'}
<div class="fab" on:click={openCreateNode} title="Добавить дело">+</div>
{/if}

View File

@ -3,6 +3,7 @@ import * as App from '../wailsjs/go/main/App.js'
// Re-export all methods
export const listSections = () => App.ListSections()
export const listTodayView = () => App.ListTodayView()
export const listNodesBySection = (section) => App.ListNodesBySection(section)
export const listChildren = (parentID) => App.ListChildren(parentID)
export const getNodeDetail = (id) => App.GetNodeDetail(id)
@ -31,6 +32,7 @@ export const duplicateNode = (nodeID) => App.DuplicateNode(nodeID)
export const renameNode = (nodeID, newTitle) => App.RenameNode(nodeID, newTitle)
export const moveNode = (nodeID, newParentID) => App.MoveNode(nodeID, newParentID)
export const openFolder = (nodeID) => App.OpenFolder(nodeID)
export const validateName = (name) => App.ValidateName(name)
export const listActions = (nodeID) => App.ListActions(nodeID)
export const runAction = (id) => App.RunAction(id)

View File

@ -6,6 +6,10 @@ export function ListSections() {
return window['go']['main']['App']['ListSections']();
}
export function ListTodayView() {
return window['go']['main']['App']['ListTodayView']();
}
export function ListNodesBySection(arg1) {
return window['go']['main']['App']['ListNodesBySection'](arg1);
}

View File

@ -50,16 +50,10 @@ func now() string {
return time.Now().UTC().Format(time.RFC3339)
}
// --- CRUD ---
// Valid sections for root-level nodes.
var validSections = map[string]struct{}{
"clients": {}, "projects": {}, "recipes": {}, "documents": {}, "archive": {},
}
// Create inserts a root or child node.
// parentID may be empty for root-level nodes.
// For root nodes, section determines sidebar placement (may be empty → inbox).
// For root nodes, section determines sidebar placement (may be empty = inbox).
// section must be a valid section (clients, projects, etc.) or empty for inbox.
func (r *Repository) Create(parentID, typ, title, section string) (*Node, error) {
if !IsValidType(typ) {
return nil, fmt.Errorf("invalid node type: %s", typ)
@ -67,6 +61,9 @@ func (r *Repository) Create(parentID, typ, title, section string) (*Node, error)
if title == "" {
return nil, errors.New("title is required")
}
if section != "" && !IsValidSection(section) {
return nil, fmt.Errorf("invalid section: %s", section)
}
n := &Node{
ID: util.UUID7(),
@ -182,6 +179,39 @@ func (r *Repository) ListRoots(includeDeleted bool, section string) ([]Node, err
return scanNodes(rows)
}
// todayBoundaries returns RFC3339 start and end strings for the current day
// in the local timezone (the server's timezone, which should match the user's).
// TODO: accept a user timezone offset when multi-user support is added.
func todayBoundaries() (string, string) {
now := time.Now()
y, m, d := now.Date()
start := time.Date(y, m, d, 0, 0, 0, 0, now.Location())
end := start.Add(24 * time.Hour)
return start.Format(time.RFC3339), end.Format(time.RFC3339)
}
// ListTodayNodes returns active root-level nodes created or updated today.
// This is a dynamic view, not a section — it shows the day's activity.
func (r *Repository) ListTodayNodes() ([]Node, error) {
start, end := todayBoundaries()
q := `SELECT id,parent_id,type,title,slug,path,section,sort_order,
created_at,updated_at,deleted_at,revision,device_id
FROM nodes
WHERE deleted_at IS NULL
AND (
(created_at >= ? AND created_at < ?)
OR
(updated_at >= ? AND updated_at < ?)
)
ORDER BY updated_at DESC, created_at DESC`
rows, err := r.db.Query(q, start, end, start, end)
if err != nil {
return nil, err
}
defer rows.Close()
return scanNodes(rows)
}
// UpdateTitle changes title (slug is recomputed).
func (r *Repository) UpdateTitle(id, title string) error {
if title == "" {

View File

@ -35,12 +35,40 @@ var TypeSet = map[string]struct{}{
TypeLink: {},
}
// Valid sections for root-level nodes.
// today and inbox are service sections, not stored in nodes.section.
var validSections = map[string]struct{}{
"clients": {},
"projects": {},
"recipes": {},
"documents": {},
"archive": {},
}
// serviceSections are sidebar entries that are not stored as node sections.
var serviceSections = map[string]struct{}{
"today": {},
"inbox": {},
}
// IsValidType checks whether a type string is recognized.
func IsValidType(t string) bool {
_, ok := TypeSet[t]
return ok
}
// IsValidSection returns true for sections that can be stored on a node.
func IsValidSection(s string) bool {
_, ok := validSections[s]
return ok
}
// IsServiceSection returns true for sidebar-only sections (today, inbox).
func IsServiceSection(s string) bool {
_, ok := serviceSections[s]
return ok
}
// Slugify converts a title into a filesystem-safe slug.
// Examples:
//