feat: an assignee on the row, and the activity log made visible
Release / Build and publish image (push) Failing after 10m2s
Release / Build and publish image (push) Failing after 10m2s
Two more things the database has been recording and the interface has
never shown.
**Who a task is waiting on, on the row.** Only when it is not you: your own
list would otherwise carry your own face on every line, which is a column
of one initial telling you nothing. The point of showing an assignee is
that it is somebody else.
That needed names, and a task carries only an `assignee_id`. `GET /people`
is the primitive: everybody you share a project with, plus yourself,
fetched once per session. Not per row, and not per project either — a list
would have to know which project each task belongs to before it could ask
for that project's members. Deliberately *not* the instance's user list,
which is `/users` and administrators only: a task list has no business
enumerating everybody with an account here.
It sits first in the meta line, because who changes what the rest of the
line means — a date on somebody else's task is their deadline, not yours.
**And the activity log.** Nineteen kinds of entry have been written since
the beginning — every project, section, member, task and comment event —
and `GET /projects/{id}/activity` has answered honestly the whole time with
nothing on the other end. There is a Historik panel on the project now,
loaded when it is opened rather than with the page, and the event keys
become Danish in exactly one place. An unknown key falls through to itself
rather than being hidden: a build that records something this table has not
learned should still show that it happened.
One test assertion had to change, and it is worth saying why. It matched
"males inden fredag" against a row whose title is "males inden" — quick add
read "fredag" as the due date — so it was really matching the title
concatenated with the metadata beside it. Putting the assignee between them
broke it. The assertion now names the task's own text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b6ea87ac19
commit
35d9e3bdac
@@ -264,6 +264,7 @@ func New(cfg *config.Config, db *store.DB, log *slog.Logger, web fs.FS) *Server
|
||||
})
|
||||
r.Get("/upcoming", s.handleUpcoming)
|
||||
r.Get("/delegated", s.handleDelegated)
|
||||
r.Get("/people", s.handlePeople)
|
||||
r.Get("/search", s.handleSearch)
|
||||
|
||||
r.Route("/projects", func(r chi.Router) {
|
||||
|
||||
@@ -92,6 +92,31 @@ func (s *Server) handleUpcoming(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"days": out})
|
||||
}
|
||||
|
||||
// handlePeople lists everybody the caller shares a project with, so an interface
|
||||
// can put a name and a face on an assignee id.
|
||||
//
|
||||
// One request per session rather than a lookup per row, and not the instance's
|
||||
// user list — that is the administrator's page. A task list has no business
|
||||
// enumerating everybody with an account here.
|
||||
func (s *Server) handlePeople(w http.ResponseWriter, r *http.Request) {
|
||||
people, err := s.db.ListPeople(r.Context(), userFrom(r.Context()).ID)
|
||||
if err != nil {
|
||||
s.internal(w, "list people", err)
|
||||
return
|
||||
}
|
||||
out := make([]personJSON, 0, len(people))
|
||||
for _, p := range people {
|
||||
out = append(out, personJSON{ID: p.ID, Name: p.Name, AvatarColor: p.AvatarColor})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"people": out})
|
||||
}
|
||||
|
||||
type personJSON struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
AvatarColor string `json:"avatar_color"`
|
||||
}
|
||||
|
||||
type delegatedPerson struct {
|
||||
UserID string `json:"user_id"`
|
||||
Name string `json:"name"`
|
||||
|
||||
@@ -153,3 +153,50 @@ func TestDelegatedIsPerPersonAndRespectsAccess(t *testing.T) {
|
||||
t.Errorf("a task in a project they cannot see: %v", people)
|
||||
}
|
||||
}
|
||||
|
||||
// /people is what puts a name on an assignee id. It must be the people you share
|
||||
// work with — not the instance's user list, which is the administrator's page.
|
||||
func TestPeopleIsWhoYouShareWorkWith(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
ts.bootstrap(t)
|
||||
colleague := ts.newUser(t, "anders@example.dk", "Anders")
|
||||
ts.newUser(t, "fremmed@example.dk", "Fremmed")
|
||||
|
||||
_, project := ts.do(t, "POST", "/api/v1/projects", map[string]any{"name": "Delt"})
|
||||
ts.do(t, "POST", "/api/v1/projects/"+project["id"].(string)+"/invites", map[string]any{
|
||||
"email": "anders@example.dk", "role": "editor",
|
||||
})
|
||||
|
||||
_, body := ts.do(t, "GET", "/api/v1/people", nil)
|
||||
names := map[string]bool{}
|
||||
for _, raw := range body["people"].([]any) {
|
||||
p := raw.(map[string]any)
|
||||
names[p["name"].(string)] = true
|
||||
if p["avatar_color"] == "" {
|
||||
t.Errorf("no avatar colour for %v — the row cannot draw a face", p["name"])
|
||||
}
|
||||
}
|
||||
|
||||
if !names["Kristian"] {
|
||||
t.Error("yourself is missing, so a client cannot tell 'me' from 'somebody else' against one list")
|
||||
}
|
||||
if !names["Anders"] {
|
||||
t.Error("somebody sharing a project is missing")
|
||||
}
|
||||
if names["Fremmed"] {
|
||||
t.Error("an account with no project in common is listed — this is not the user directory")
|
||||
}
|
||||
|
||||
// And it is symmetric: the person invited sees the person who invited them.
|
||||
_, theirs := colleague.do(t, "GET", "/api/v1/people", nil)
|
||||
seen := map[string]bool{}
|
||||
for _, raw := range theirs["people"].([]any) {
|
||||
seen[raw.(map[string]any)["name"].(string)] = true
|
||||
}
|
||||
if !seen["Kristian"] || !seen["Anders"] {
|
||||
t.Errorf("the member cannot see who they share with: %v", seen)
|
||||
}
|
||||
if seen["Fremmed"] {
|
||||
t.Errorf("a stranger is listed: %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user