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:
parent
a4ae22c445
commit
69891e395c
|
|
@ -143,6 +143,15 @@ func (a *App) ListNodesBySection(section string) ([]NodeDTO, error) {
|
||||||
return toNodeDTOs(list), nil
|
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) {
|
func (a *App) ListChildren(parentID string) ([]NodeDTO, error) {
|
||||||
list, err := a.nodes.ListChildren(parentID, false)
|
list, err := a.nodes.ListChildren(parentID, false)
|
||||||
if err != nil {
|
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) {
|
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)
|
n, err := a.nodes.Create(parentID, nodeType, title, section)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
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-B8EIu1OK.js"></script>
|
<script type="module" crossorigin src="/assets/main-BY9JF_6I.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/main-Bo58X7Pc.css">
|
<link rel="stylesheet" crossorigin href="/assets/main-Bo58X7Pc.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,11 @@
|
||||||
showCreateNode = false
|
showCreateNode = false
|
||||||
error = ''
|
error = ''
|
||||||
try {
|
try {
|
||||||
|
if (id === 'today') {
|
||||||
|
nodes = await wailsCall('ListTodayView') || []
|
||||||
|
} else {
|
||||||
nodes = await wailsCall('ListNodesBySection', id) || []
|
nodes = await wailsCall('ListNodesBySection', id) || []
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = String(e)
|
error = String(e)
|
||||||
nodes = []
|
nodes = []
|
||||||
|
|
@ -1055,7 +1059,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !noteEditor && !selectedNode}
|
{#if !noteEditor && !selectedNode && selectedSection !== 'today' && selectedSection !== 'inbox'}
|
||||||
<div class="fab" on:click={openCreateNode} title="Добавить дело">+</div>
|
<div class="fab" on:click={openCreateNode} title="Добавить дело">+</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import * as App from '../wailsjs/go/main/App.js'
|
||||||
|
|
||||||
// Re-export all methods
|
// Re-export all methods
|
||||||
export const listSections = () => App.ListSections()
|
export const listSections = () => App.ListSections()
|
||||||
|
export const listTodayView = () => App.ListTodayView()
|
||||||
export const listNodesBySection = (section) => App.ListNodesBySection(section)
|
export const listNodesBySection = (section) => App.ListNodesBySection(section)
|
||||||
export const listChildren = (parentID) => App.ListChildren(parentID)
|
export const listChildren = (parentID) => App.ListChildren(parentID)
|
||||||
export const getNodeDetail = (id) => App.GetNodeDetail(id)
|
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 renameNode = (nodeID, newTitle) => App.RenameNode(nodeID, newTitle)
|
||||||
export const moveNode = (nodeID, newParentID) => App.MoveNode(nodeID, newParentID)
|
export const moveNode = (nodeID, newParentID) => App.MoveNode(nodeID, newParentID)
|
||||||
export const openFolder = (nodeID) => App.OpenFolder(nodeID)
|
export const openFolder = (nodeID) => App.OpenFolder(nodeID)
|
||||||
|
export const validateName = (name) => App.ValidateName(name)
|
||||||
|
|
||||||
export const listActions = (nodeID) => App.ListActions(nodeID)
|
export const listActions = (nodeID) => App.ListActions(nodeID)
|
||||||
export const runAction = (id) => App.RunAction(id)
|
export const runAction = (id) => App.RunAction(id)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,10 @@ export function ListSections() {
|
||||||
return window['go']['main']['App']['ListSections']();
|
return window['go']['main']['App']['ListSections']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ListTodayView() {
|
||||||
|
return window['go']['main']['App']['ListTodayView']();
|
||||||
|
}
|
||||||
|
|
||||||
export function ListNodesBySection(arg1) {
|
export function ListNodesBySection(arg1) {
|
||||||
return window['go']['main']['App']['ListNodesBySection'](arg1);
|
return window['go']['main']['App']['ListNodesBySection'](arg1);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,16 +50,10 @@ func now() string {
|
||||||
return time.Now().UTC().Format(time.RFC3339)
|
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.
|
// Create inserts a root or child node.
|
||||||
// parentID may be empty for root-level nodes.
|
// 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) {
|
func (r *Repository) Create(parentID, typ, title, section string) (*Node, error) {
|
||||||
if !IsValidType(typ) {
|
if !IsValidType(typ) {
|
||||||
return nil, fmt.Errorf("invalid node type: %s", 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 == "" {
|
if title == "" {
|
||||||
return nil, errors.New("title is required")
|
return nil, errors.New("title is required")
|
||||||
}
|
}
|
||||||
|
if section != "" && !IsValidSection(section) {
|
||||||
|
return nil, fmt.Errorf("invalid section: %s", section)
|
||||||
|
}
|
||||||
|
|
||||||
n := &Node{
|
n := &Node{
|
||||||
ID: util.UUID7(),
|
ID: util.UUID7(),
|
||||||
|
|
@ -182,6 +179,39 @@ func (r *Repository) ListRoots(includeDeleted bool, section string) ([]Node, err
|
||||||
return scanNodes(rows)
|
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).
|
// UpdateTitle changes title (slug is recomputed).
|
||||||
func (r *Repository) UpdateTitle(id, title string) error {
|
func (r *Repository) UpdateTitle(id, title string) error {
|
||||||
if title == "" {
|
if title == "" {
|
||||||
|
|
|
||||||
|
|
@ -35,12 +35,40 @@ var TypeSet = map[string]struct{}{
|
||||||
TypeLink: {},
|
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.
|
// IsValidType checks whether a type string is recognized.
|
||||||
func IsValidType(t string) bool {
|
func IsValidType(t string) bool {
|
||||||
_, ok := TypeSet[t]
|
_, ok := TypeSet[t]
|
||||||
return ok
|
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.
|
// Slugify converts a title into a filesystem-safe slug.
|
||||||
// Examples:
|
// Examples:
|
||||||
//
|
//
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue