feat(office): users admin UI, table rendering, and user save fixes

Add Users list/detail with ACL toggles and library people update/status APIs.
Fix project table layout and ANSI-safe cell rendering, and decode array
responses from people/status so saves no longer fail on JSON unmarshal.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-25 11:07:22 +01:00
co-authored by Cursor
parent 72844dfe24
commit 40b74eb8ab
45 changed files with 3012 additions and 438 deletions
+92 -4
View File
@@ -11,7 +11,18 @@ type FormFields struct {
ReadOnly bool
HasStatus bool
Status ProjectLifecycle
HasTaskStatus bool
TaskStatus TaskLifecycle
HasResponsible bool
ResponsibleID string
UserChoices []UserOption
ProjectTitle string
TimingSummary string
HasUserEdit bool
UserEnabled bool
UserACL UserACLState
GroupsText string
UserPassword string
}
// KindHeading returns a short label for the detail pane header.
@@ -62,10 +73,13 @@ func FormFieldsFromRaw(kind Kind, raw map[string]any) FormFields {
ReadOnly: true,
}
case KindUser:
acl := UserACLFromRaw(raw)
return FormFields{
PrimaryLabel: "Name", SecondaryLabel: "Email",
Primary: strRaw(raw, "displayName"), Secondary: strRaw(raw, "email"),
ReadOnly: true,
HasUserEdit: true,
ReadOnly: false,
UserEnabled: UserIsEnabled(raw),
UserACL: acl,
GroupsText: UserGroupsText(raw),
}
case KindContact:
name := strRaw(raw, "displayName")
@@ -98,6 +112,20 @@ func FormFieldsFromRaw(kind Kind, raw map[string]any) FormFields {
Status: ProjectStatusFromAny(raw["status"]),
ResponsibleID: ResponsibleIDFromRaw(raw),
}
case KindTask:
return FormFields{
PrimaryLabel: "Title",
SecondaryLabel: "Description",
Primary: strRaw(raw, "title"),
Secondary: strRaw(raw, "description"),
ReadOnly: false,
HasTaskStatus: true,
TaskStatus: TaskStatusFromAny(raw["status"]),
HasResponsible: true,
ResponsibleID: TaskResponsibleIDFromRaw(raw),
ProjectTitle: TaskProjectTitle(raw),
TimingSummary: TaskTimingSummary(raw),
}
default:
title := strRaw(raw, "title")
if title == "" {
@@ -109,11 +137,71 @@ func FormFieldsFromRaw(kind Kind, raw map[string]any) FormFields {
return FormFields{
PrimaryLabel: "Title", SecondaryLabel: "Description",
Primary: title, Secondary: strRaw(raw, "description"),
ReadOnly: kind != KindTask && kind != KindProject,
ReadOnly: true,
}
}
}
// TaskResponsibleIDFromRaw returns the first assignee user id from task detail.
func TaskResponsibleIDFromRaw(raw map[string]any) string {
if raw == nil {
return ""
}
if ids, ok := raw["responsibleIds"].([]any); ok && len(ids) > 0 {
return strRaw(map[string]any{"id": ids[0]}, "id")
}
if list, ok := raw["responsibles"].([]any); ok && len(list) > 0 {
if m, ok := list[0].(map[string]any); ok {
return strRaw(m, "id")
}
}
return ""
}
// TaskProjectTitle returns the owning project title when present.
func TaskProjectTitle(raw map[string]any) string {
if raw == nil {
return ""
}
if po, ok := raw["projectOwner"].(map[string]any); ok {
if t := strRaw(po, "title"); t != "" {
return t
}
}
return strRaw(raw, "projectTitle")
}
// TaskTimingSummary formats start and deadline for the detail form.
func TaskTimingSummary(raw map[string]any) string {
if raw == nil {
return ""
}
start := formatTaskDateLabel(raw["startDate"])
deadline := formatTaskDateLabel(raw["deadline"])
switch {
case start != "" && deadline != "":
return start + " → " + deadline
case deadline != "":
return "Due " + deadline
case start != "":
return "From " + start
default:
return ""
}
}
func formatTaskDateLabel(v any) string {
t, ok := parseDeadlineTime(v)
if !ok {
s := strRaw(map[string]any{"v": v}, "v")
if s == "" || s == "<nil>" {
return ""
}
return s
}
return t.Format("2006-01-02 15:04")
}
func strRaw(m map[string]any, key string) string {
if m == nil {
return ""