From de7a668b32b0635a97b63191b5728a3450e4463a Mon Sep 17 00:00:00 2001 From: ji ruo <79234113+RuoJi6@users.noreply.github.com> Date: Mon, 19 Oct 2026 09:58:32 +0800 Subject: [PATCH] feat: align list editors with name hover actions --- db/schedules.go | 15 +- server/remarks.go | 30 ++- server/remarks_test.go | 24 +++ server/server.go | 3 +- .../app/(main)/function/schedules/page.tsx | 17 +- web/src/components/remark-editor.tsx | 175 +++++++++++------- web/src/lib/api.ts | 1 + 7 files changed, 188 insertions(+), 77 deletions(-) diff --git a/db/schedules.go b/db/schedules.go index 43e01cf..be0871e 100644 --- a/db/schedules.go +++ b/db/schedules.go @@ -405,9 +405,22 @@ func (d *DB) ListScheduleHistory(id int64) ([]ScheduleHistory, error) { return items, rows.Err() } +// TaskSchedulePatch changes list metadata without resetting execution windows. +type TaskSchedulePatch struct { + Name *string + Remark *string +} + // UpdateTaskScheduleRemark leaves calendar state and task bindings untouched. func (d *DB) UpdateTaskScheduleRemark(id int64, remark string) (*TaskSchedule, error) { - item, err := scanSchedule(d.QueryRow(`UPDATE task_schedules SET remark=$2 WHERE id=$1 RETURNING `+scheduleCols, id, remark)) + return d.UpdateTaskScheduleMetadata(id, TaskSchedulePatch{Remark: &remark}) +} + +func (d *DB) UpdateTaskScheduleMetadata(id int64, patch TaskSchedulePatch) (*TaskSchedule, error) { + item, err := scanSchedule(d.QueryRow(`UPDATE task_schedules SET + name=CASE WHEN $2::boolean THEN $3 ELSE name END, + remark=CASE WHEN $4::boolean THEN $5 ELSE remark END + WHERE id=$1 RETURNING `+scheduleCols, id, patch.Name != nil, patch.Name, patch.Remark != nil, patch.Remark)) if err == sql.ErrNoRows { return nil, nil } diff --git a/server/remarks.go b/server/remarks.go index fe4619a..c33ecc6 100644 --- a/server/remarks.go +++ b/server/remarks.go @@ -6,6 +6,8 @@ import ( "net/http" "strings" "unicode/utf8" + + "github.com/Autumn-27/artex/db" ) const maxRemarkRunes = 2000 @@ -18,7 +20,7 @@ func normalizeRemark(value string) (string, error) { return value, nil } -func (s *Server) updateScheduleRemark(w http.ResponseWriter, r *http.Request) { +func (s *Server) updateScheduleMetadata(w http.ResponseWriter, r *http.Request) { id, err := parseScheduleID(r) if err != nil || id <= 0 { writeErr(w, 400, "计划编号无效") @@ -26,6 +28,7 @@ func (s *Server) updateScheduleRemark(w http.ResponseWriter, r *http.Request) { } r.Body = http.MaxBytesReader(w, r.Body, maxTaskMetadataRequestSize) var request struct { + Name *string `json:"name"` Remark *string `json:"remark"` } if err := decode(r, &request); err != nil { @@ -37,16 +40,27 @@ func (s *Server) updateScheduleRemark(w http.ResponseWriter, r *http.Request) { } return } - if request.Remark == nil { - writeErr(w, 400, "需要提供 remark") + if request.Name == nil && request.Remark == nil { + writeErr(w, 400, "需要提供 name 或 remark") return } - remark, err := normalizeRemark(*request.Remark) - if err != nil { - writeErr(w, 400, err.Error()) - return + if request.Name != nil { + name := strings.TrimSpace(*request.Name) + if name == "" || utf8.RuneCountInString(name) > maxTaskNameRunes { + writeErr(w, 400, fmt.Sprintf("计划名称须为 1 至 %d 个字符", maxTaskNameRunes)) + return + } + request.Name = &name } - item, err := s.m.pg.UpdateTaskScheduleRemark(id, remark) + if request.Remark != nil { + remark, err := normalizeRemark(*request.Remark) + if err != nil { + writeErr(w, 400, err.Error()) + return + } + request.Remark = &remark + } + item, err := s.m.pg.UpdateTaskScheduleMetadata(id, db.TaskSchedulePatch{Name: request.Name, Remark: request.Remark}) if err != nil { writeErr(w, 500, err.Error()) return diff --git a/server/remarks_test.go b/server/remarks_test.go index ea8b82f..8d6362e 100644 --- a/server/remarks_test.go +++ b/server/remarks_test.go @@ -157,6 +157,30 @@ func TestRemarksRoundTripPreservesExecutionState(t *testing.T) { if _, err := m.pg.UpdateTaskScheduleRemark(schedule.ID, note); err != nil { t.Fatal(err) } + metadataPath := fmt.Sprintf("/api/schedules/%d/metadata", schedule.ID) + rec = request(metadataPath, `{"name":" 行内改名 "}`, true) + if rec.Code != 200 { + t.Fatalf("schedule rename: %d %s", rec.Code, rec.Body.String()) + } + renamed, err := m.pg.GetTaskSchedule(schedule.ID) + if err != nil { + t.Fatal(err) + } + if renamed.Name != "行内改名" || renamed.Remark != note || renamed.Status != original.Status || renamed.Enabled != original.Enabled || renamed.LastWindowKey != original.LastWindowKey || renamed.LastError != original.LastError || !renamed.LastTransitionAt.Equal(*original.LastTransitionAt) || len(renamed.TaskIDs) != 1 || renamed.TaskIDs[0] != taskID { + t.Fatalf("rename changed calendar state or metadata: %+v", renamed) + } + for _, body := range []string{`{}`, `{"name":null}`, `{"name":" "}`, `{"name":123}`, `{"name":"` + strings.Repeat("文", maxTaskNameRunes+1) + `"}`, `{"name":"must not apply","remark":"` + strings.Repeat("文", maxRemarkRunes+1) + `"}`} { + if rec := request(metadataPath, body, true); rec.Code != 400 { + t.Fatalf("invalid rename: %d %s", rec.Code, rec.Body.String()) + } + } + unchanged, err := m.pg.GetTaskSchedule(schedule.ID) + if err != nil || unchanged.Name != "行内改名" || unchanged.Remark != note { + t.Fatalf("invalid patch partially changed metadata: %+v %v", unchanged, err) + } + if rec := request(metadataPath, `{"name":"denied"}`, false); rec.Code != 401 { + t.Fatalf("rename auth: %d", rec.Code) + } scheduleInput.Name = "新计划名称" updated, err := m.pg.UpdateTaskSchedule(schedule.ID, scheduleInput) if err != nil || updated.Remark != note { diff --git a/server/server.go b/server/server.go index b6aef93..01a7d12 100644 --- a/server/server.go +++ b/server/server.go @@ -717,7 +717,8 @@ func (s *Server) Handler() http.Handler { mux.HandleFunc("POST /api/schedules", s.createSchedule) mux.HandleFunc("GET /api/schedules/{id}", s.getSchedule) mux.HandleFunc("PATCH /api/schedules/{id}", s.updateSchedule) - mux.HandleFunc("PATCH /api/schedules/{id}/remark", s.updateScheduleRemark) + mux.HandleFunc("PATCH /api/schedules/{id}/remark", s.updateScheduleMetadata) + mux.HandleFunc("PATCH /api/schedules/{id}/metadata", s.updateScheduleMetadata) mux.HandleFunc("DELETE /api/schedules/{id}", s.deleteSchedule) mux.HandleFunc("POST /api/schedules/{id}/pause", s.pauseSchedule) mux.HandleFunc("POST /api/schedules/{id}/resume", s.resumeSchedule) diff --git a/web/src/app/(main)/function/schedules/page.tsx b/web/src/app/(main)/function/schedules/page.tsx index d450c61..1365695 100644 --- a/web/src/app/(main)/function/schedules/page.tsx +++ b/web/src/app/(main)/function/schedules/page.tsx @@ -17,7 +17,7 @@ import { import { toast } from "sonner"; import { DateSelect } from "@/components/date-select"; -import { RemarkEditor } from "@/components/remark-editor"; +import { RemarkEditor, ScheduleNameEditor } from "@/components/remark-editor"; import { TimeSelect } from "@/components/time-select"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -703,7 +703,7 @@ export default function SchedulesPage() { ) : (