taskfolder

package
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 9, 2026 License: AGPL-3.0 Imports: 17 Imported by: 0

Documentation

Overview

Package taskfolder serves the Tasks app's folders: the saved filters a person builds for themselves out of listboxes, so a recurring question ("what is open on customer enquiries?") becomes a place in the sidebar instead of something retyped into the search box every morning (ADR-0268).

A folder stores a *rule*, not an expression. The rule is a small structured document — a match mode and a list of field/operator/value conditions — and the FEEL expression that decides which tasks belong is generated from it. That direction is the whole design: a generated expression can always be rendered back into the listboxes that produced it, and the person who built the folder never has to write, or read, a language to own their own worklist.

Everything here is design-time state, like forms and projects: a folder never enters the event log, the processor or recovery. The single-writer boundary is the runloop.Loop the Service holds — every store access goes through it and there is no other route from here to shared state (I3, ADR-0002/0147).

Index

Constants

View Source
const (
	FieldProcess     = "process"
	FieldTaskName    = "taskName"
	FieldAssignee    = "assignee"
	FieldGroup       = "group"
	FieldLane        = "lane"
	FieldPriority    = "priority"
	FieldDue         = "due"
	FieldInstanceAge = "instanceAge"
	FieldForm        = "form"
)

The fields a folder rule can ask about, and the FEEL name each binds to. The left-hand side is the id the API and the editor speak; the right-hand side is the evaluation context below. They are separate on purpose: the id is a stable part of the stored rule, the FEEL name is the expression contract, and neither should be forced to change because the other did.

View Source
const (
	OpIs         = "is"
	OpIsNot      = "isNot"
	OpIsOneOf    = "isOneOf"
	OpContains   = "contains"
	OpStartsWith = "startsWith"
	OpIsMe       = "isMe"
	OpIsEmpty    = "isEmpty"
	OpUnder      = "under"
	OpAtLeast    = "atLeast"
	OpAtMost     = "atMost"
	OpOverdue    = "overdue"
	OpWithin     = "within"
	OpNone       = "none"
	OpAny        = "any"
	OpOlderThan  = "olderThan"
	OpNewerThan  = "newerThan"
	OpHas        = "has"
	OpHasNot     = "hasNot"
)

The operators. Not every operator applies to every field — Catalog says which pairs exist, and Rule.Validate is what enforces it.

View Source
const (
	ValueNone     = "none"     // the operator is complete on its own ("is overdue")
	ValueText     = "text"     // free text the person types (a name fragment)
	ValueChoice   = "choice"   // one entry from a server-supplied list
	ValueChoices  = "choices"  // several entries from that list
	ValueNumber   = "number"   // a plain number (priority)
	ValueDuration = "duration" // an ISO-8601 duration from a fixed set of offers
	ValueCount    = "count"    // a number plus a unit (hours or days)
)

The value shapes an operator asks for. This is what the editor reads to decide which control to draw — a listbox, a text field, a number, nothing at all — so the server describes the form and the client never hard-codes it.

View Source
const (
	UnitHours = "h"
	UnitDays  = "d"
)

The units a ValueCount value may carry.

View Source
const (
	OptionsProcesses = "processes"
	OptionsTaskNames = "taskNames"
	OptionsUsers     = "users"
	OptionsGroups    = "groups"
	OptionsLanes     = "lanes"
)

The value lists the API fills in for a field. The names are part of the /task-folders/fields response, so the editor can pair a field with its list without knowing what either contains.

View Source
const (
	// VisibilityPrivate is the default: only the owner sees the folder.
	VisibilityPrivate = "private"
	// VisibilityGroup shares the folder with one identity group (ADR-0180), which
	// is how a team lead builds the queues their team works from.
	VisibilityGroup = "group"
	// VisibilityOrg shares the folder with every signed-in identity.
	VisibilityOrg = "org"
)

Visibility says who else may see a folder. It is deliberately not an access control on the *tasks*: a folder is a saved question, and answering it still runs under the asker's own role. Sharing a folder shares the question.

View Source
const (
	MatchAll = "all" // every condition must hold
	MatchAny = "any" // at least one must hold
)

Match says how a rule's conditions combine.

Variables

This section is empty.

Functions

func NewID

func NewID() (string, error)

NewID mints a folder id. Sixteen bytes of crypto randomness, hex-encoded, is filename-safe — so the id is its own store key — and collision-free in practice.

Types

type Condition

type Condition struct {
	Field  string   `json:"field"`
	Op     string   `json:"op"`
	Value  string   `json:"value,omitempty"`
	Values []string `json:"values,omitempty"`
	// Unit qualifies a numeric Value where the number alone is ambiguous: the
	// instance-age conditions count hours or days.
	Unit string `json:"unit,omitempty"`
}

Condition is one row of the folder editor: a field, an operator over it, and the value the operator compares against. Which of Value and Values carries the comparand depends on the operator — a single-choice operator uses Value, a multi-choice one Values, and an operator like "is overdue" needs neither.

type CountFunc

type CountFunc func(matchers []*Matcher, u User) (perMatcher []int, total int, truncated bool, err error)

CountFunc answers "how many open tasks does this rule match, out of how many". It scans the engine, which this package deliberately cannot reach: the server owns that scan and hands it in, the same way the documentation service is handed its deployment lookup.

type Counts

type Counts struct {
	Folders   map[string]int `json:"folders"`
	Total     int            `json:"total"`
	Truncated bool           `json:"truncated"`
}

Counts is what the sidebar needs in one call: how many open tasks each visible folder holds, and how many the scan looked at. Truncated says the scan hit its budget, so the numbers are a floor rather than a total — the sidebar says so instead of showing a confident wrong number.

type FieldSpec

type FieldSpec struct {
	ID      string   `json:"id"`
	Options string   `json:"options,omitempty"`
	Ops     []OpSpec `json:"ops"`
}

FieldSpec is one field of the editor's first listbox, with the operators its second listbox offers and the name of the value list that fills its third. Options names a list the API supplies (processes, users, groups, …); it is empty for a field whose values are typed or numeric.

func Catalog

func Catalog() []FieldSpec

Catalog is every field/operator pair a folder rule may use — the one place the editor, the validator and the FEEL generator agree on what exists.

It is a function rather than a package variable so a caller cannot reach in and mutate the catalogue the validator is about to consult.

type Folder

type Folder struct {
	ID   string `json:"id"`
	Name string `json:"name"`
	// Owner is the user id that created the folder, empty when the server runs
	// without authentication — where there is no identity, there is one shared set
	// of folders, which is the same bargain the rest of the app makes (ADR-0045).
	Owner      string `json:"owner,omitempty"`
	Visibility string `json:"visibility"`
	// GroupID names the identity group a group-visible folder is shared with.
	GroupID string `json:"groupId,omitempty"`
	// Position orders the sidebar. Ties fall back to creation order, so a listing
	// is deterministic even before anyone has reordered anything.
	Position  int   `json:"position"`
	Rule      Rule  `json:"rule"`
	CreatedAt int64 `json:"createdAt"`
	UpdatedAt int64 `json:"updatedAt"`
}

Folder is one saved filter in a person's sidebar.

func (Folder) EditableBy

func (f Folder) EditableBy(userID string) bool

EditableBy reports whether a viewer may change or delete this folder. Only the owner may: a shared folder is shared to be *used*, and a queue rewritten under the team by whoever opened it last is not a queue anybody can rely on. An admin override lives at the route layer, not here.

func (Folder) VisibleTo

func (f Folder) VisibleTo(userID string, groupIDs []string) bool

VisibleTo reports whether a viewer may see this folder: their own, one shared with a group they are in, or one shared with the whole organisation.

type Matcher

type Matcher struct {
	// contains filtered or unexported fields
}

Matcher is a rule compiled once and evaluated per task. Compiling at save time rather than per request is invariant 5 (ADR-0008): the parse, type-check and lowering happen when a person presses Save, and a listing only evaluates.

func Compile

func Compile(r Rule) (*Matcher, error)

Compile validates a rule and compiles the expression it generates. The returned matcher is immutable and safe to evaluate concurrently, like every other compiled FEEL in Atlas.

func (*Matcher) Match

func (m *Matcher) Match(t Task, u User, now time.Time) bool

Match reports whether one task belongs in the folder.

Anything that is not FEEL true excludes the task: a null from comparing an absent field, a rule that somehow evaluates to a string, an evaluation error. A filter that fails open would quietly put foreign work in somebody's queue, which is the worse of the two failures.

func (*Matcher) NeedsInstance

func (m *Matcher) NeedsInstance() bool

NeedsInstance reports whether the rule asks about the process instance behind the task. A scan reads the instance only when it does — that read is one more store lookup per task, and most rules never mention it. The answer comes from the compiled expression's own input list, so it cannot disagree with the expression actually being evaluated.

func (*Matcher) Source

func (m *Matcher) Source() string

Source returns the FEEL the matcher was compiled from.

type OpSpec

type OpSpec struct {
	ID    string `json:"id"`
	Value string `json:"value"`
	// contains filtered or unexported fields
}

OpSpec is one operator as the editor meets it: its id and the value shape it needs. The FEEL it produces stays here — the client never builds an expression.

type Option

type Option struct {
	Value string `json:"value"`
	Label string `json:"label,omitempty"`
}

Option is one entry of a value listbox: the value a condition stores, and the label a person picks it by. The label is model or directory data — a process's name, a person's display name — never interface text, so it needs no translation and the client owns every word it adds around it.

type Options

type Options struct {
	Processes []Option `json:"processes"`
	TaskNames []Option `json:"taskNames"`
	Users     []Option `json:"users"`
	Groups    []Option `json:"groups"`
	Lanes     []Option `json:"lanes"`
	// MyGroups are the identity groups the caller belongs to (ADR-0180), which is
	// what the "shared with" control offers. It is deliberately not the same list as
	// Groups above: those are the candidate groups a *model* names, which is what a
	// rule filters on, and the two being different lists that both read "group" is
	// exactly the confusion worth spelling out here.
	MyGroups []Option `json:"myGroups"`
}

Options are the value lists the editor's third control is filled from. They come from the deployments and the design-time stores, which the service cannot reach — the server supplies them through the collaborator given to New.

type Rule

type Rule struct {
	Match      string      `json:"match"`
	Conditions []Condition `json:"conditions"`
}

Rule is what a folder actually stores. It is the source of truth; the FEEL expression is derived from it (see Rule.FEEL) and never stored, so the two cannot drift apart and a saved folder can always be reopened in the editor.

func (Rule) FEEL

func (r Rule) FEEL() string

FEEL renders the rule as the expression that decides membership — the text the editor shows beneath the conditions, and the text the server compiles.

A rule with no conditions is `true`: a folder someone has started but not narrowed shows everything, which is what the empty editor looks like it means.

func (Rule) Validate

func (r Rule) Validate() error

Validate reports whether a rule is one the server will store and compile. It is the only gate: Compile runs it first, so nothing unvalidated reaches FEEL.

type Service

type Service struct {

	// Limits are the installation's resource budgets. New sets them to
	// [limits.Default]; the server overwrites them with its own once it has read the
	// environment, so every ceiling in this service is the one operators configured
	// (ADR-0291).
	Limits limits.Limits
	// contains filtered or unexported fields
}

Service serves the task-folder area (ADR-0268). Build it with New.

func New

func New(loop *runloop.Loop, store *Store, options func(User) Options, count CountFunc, newID func() (string, error)) *Service

New builds the task-folder service. options and count are the collaborators the server supplies; options is called on the loop, count off it.

func (*Service) HandleCounts

func (s *Service) HandleCounts(w http.ResponseWriter, r *http.Request)

HandleCounts returns the badge number for every folder this viewer sees, from one scan. The alternative — a request per folder — multiplies the work by the number of folders somebody happens to have made.

func (*Service) HandleCreate

func (s *Service) HandleCreate(w http.ResponseWriter, r *http.Request)

HandleCreate stores a new folder for the calling identity.

func (*Service) HandleDelete

func (s *Service) HandleDelete(w http.ResponseWriter, r *http.Request)

HandleDelete removes a folder the caller owns.

func (*Service) HandleFields

func (s *Service) HandleFields(w http.ResponseWriter, r *http.Request)

HandleFields describes what the editor can build: the field/operator catalogue and the value lists that fill each field's third control.

It deliberately carries no interface text. Every string here is an id or a name that came out of a model or the directory, so the console renders the labels in whatever language it is showing and the server never has to be translated.

func (*Service) HandleList

func (s *Service) HandleList(w http.ResponseWriter, r *http.Request)

HandleList returns the folders this viewer may see, in sidebar order.

func (*Service) HandlePreview

func (s *Service) HandlePreview(w http.ResponseWriter, r *http.Request)

HandlePreview answers what a rule would select, without saving it: the expression it generates and how many open tasks it matches. It is what makes the editor's live counter honest — the same scan the folder itself will use, rather than a client-side guess over whatever page happened to be loaded.

func (*Service) HandleUpdate

func (s *Service) HandleUpdate(w http.ResponseWriter, r *http.Request)

HandleUpdate rewrites a folder the caller owns.

func (*Service) MatcherFor

func (s *Service) MatcherFor(id string, v User) (Folder, *Matcher, bool, error)

MatcherFor returns one visible folder's compiled rule. found=false covers both "no such folder" and "not yours to see", which are the same answer to a caller.

func (*Service) Visible

func (s *Service) Visible(v User) ([]Folder, []*Matcher, error)

Visible returns the folders a viewer may see together with their compiled rules, dropping any folder whose rule no longer compiles rather than failing the whole listing — one broken folder must not empty somebody's sidebar.

type Store

type Store struct {
	*sidecar.Store[Folder]
}

Store is a durable store for task folders: one JSON file per folder id under a single directory, with the atomic-write discipline every design-time store shares. It is owned by the run loop, like the stores it sits beside, and does no locking of its own.

func NewStore

func NewStore(dir string) (*Store, error)

NewStore opens (creating if needed) the task-folders directory. Folders list in sidebar order: the position a person dragged them into, then creation order, then id — so a listing is deterministic before anyone has reordered anything.

func (*Store) VisibleTo

func (s *Store) VisibleTo(userID string, groupIDs []string) ([]Folder, error)

VisibleTo returns the folders a viewer may see, in sidebar order: their own, those shared with a group they belong to, and those shared organisation-wide.

type Task

type Task struct {
	ProcessID       string
	ProcessName     string
	TaskName        string
	ElementID       string
	Assignee        string
	CandidateGroups string
	Lane            string
	LanePath        []string
	Priority        int32
	// DueDate is the task's due instant in Unix milliseconds, 0 when it has none.
	DueDate int64
	// InstanceCreatedAt is when the process instance carrying the task started, in
	// Unix milliseconds, 0 when it was not read (see [Matcher.NeedsInstance]).
	InstanceCreatedAt int64
	HasForm           bool
}

Task is one open user task as a rule sees it: the evaluation context, and the public contract of the expression a folder generates.

type User

type User struct {
	ID     string
	Name   string
	Groups []string
}

User is who is asking. It is what the `user` context in a rule resolves against, so one shared folder ("assigned to me") answers differently for each viewer.

ID and Name are deliberately two fields. A task's assignee is a *username* — that is what claiming writes onto the job (ADR-0042) — while a folder is owned by an account id, which survives a rename. Binding one to the other would make "assigned to me" compare a username against an account id and quietly match nothing, which is why `user.name` and not `user.id` is what an assignee condition generates.

func Viewer

func Viewer(r *http.Request) User

Viewer is who a request is acting as. With authentication on it is the signed-in principal, including the groups a shared folder is matched against. With authentication off there is no identity to own anything, so the folder set is shared and `?me=` carries only the display-only name the Tasks app already uses for claiming (ADR-0045) — enough for "assigned to me" to mean something, not enough to own a folder.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL