diff --git a/backend/modules.go b/backend/modules.go index cb2f8e6..a7190b0 100644 --- a/backend/modules.go +++ b/backend/modules.go @@ -679,6 +679,20 @@ type jiraConfig struct { StatusMap map[string]string `json:"status_map"` // kanban_column_name -> Jira status name LabelsMap map[string][]string `json:"labels_map,omitempty"` // kanban_column_name -> Jira labels (replaces every sync) AssigneeMap map[string]string `json:"assignee_map,omitempty"` // kanban_user_id -> Jira accountId + + // RequesterField is the Jira custom field id (e.g. "customfield_10158", + // "Área Solicitante") that some issue types (Epic, Mejora in project DATA) + // mark as required on the create screen. When set, create()/update() send a + // single-select option value resolved from the kanban card's requester. + RequesterField string `json:"requester_field,omitempty"` + // RequesterMap translates the free-text kanban requester to a Jira option + // value. Matched case-insensitively. Kanban requesters are usually person + // names, so most cards fall through to RequesterDefault. + RequesterMap map[string]string `json:"requester_map,omitempty"` + // RequesterDefault is the option value used when the card requester is + // empty or not present in RequesterMap. Required field never goes unfilled + // as long as this is set. + RequesterDefault string `json:"requester_default,omitempty"` } func parseJiraConfig(m Module) (jiraConfig, error) { @@ -823,6 +837,10 @@ func (h *jiraHandler) create(ctx context.Context, db *DB, c jiraConfig, ev Event if acct := resolveJiraAssignee(c, card); acct != "" { fields["assignee"] = map[string]string{"accountId": acct} } + // Epic / Mejora issue types require "Área Solicitante" on the create + // screen. Fill it from the card requester (mapped) or the default so the + // create does not 400 on a missing required field. + applyRequesterField(c, card, fields) body := map[string]interface{}{"fields": fields} status, resp, err := h.jiraRequest(ctx, c, http.MethodPost, "/rest/api/3/issue", body) if err != nil { @@ -876,6 +894,9 @@ func (h *jiraHandler) update(ctx context.Context, db *DB, c jiraConfig, ev Event if acct := resolveJiraAssignee(c, card); acct != "" { fields["assignee"] = map[string]string{"accountId": acct} } + // Keep "Área Solicitante" populated on edits too — the field is required + // and a PUT that omits it can be rejected on the edit screen. + applyRequesterField(c, card, fields) body := map[string]interface{}{"fields": fields} status, _, err := h.jiraRequest(ctx, c, http.MethodPut, "/rest/api/3/issue/"+card.JiraKey, body) return status, err @@ -892,6 +913,38 @@ func resolveJiraAssignee(c jiraConfig, card *cardForJira) string { return c.AssigneeMap[card.AssigneeID] } +// resolveRequesterOption maps the card's requester to a Jira single-select +// option value for RequesterField. Lookup order: exact map hit, case-insensitive +// map hit, then RequesterDefault. Returns "" only when the field is unconfigured +// or no default exists, signalling the caller to omit it. +func resolveRequesterOption(c jiraConfig, card *cardForJira) string { + if c.RequesterField == "" { + return "" + } + if card != nil { + r := strings.TrimSpace(card.Requester) + if r != "" && len(c.RequesterMap) > 0 { + if v, ok := c.RequesterMap[r]; ok { + return v + } + for k, v := range c.RequesterMap { + if strings.EqualFold(k, r) { + return v + } + } + } + } + return c.RequesterDefault +} + +// applyRequesterField injects RequesterField as a single-select option into a +// Jira fields map when configured and resolvable. No-op otherwise. +func applyRequesterField(c jiraConfig, card *cardForJira, fields map[string]interface{}) { + if opt := resolveRequesterOption(c, card); opt != "" { + fields[c.RequesterField] = map[string]string{"value": opt} + } +} + // transition uses the configured status_map to translate the kanban column // to a Jira transition name. Kanban remains the source of truth even if // Jira's current state differs. diff --git a/backend/seed_jira.go b/backend/seed_jira.go index 9f6d271..81edf0a 100644 --- a/backend/seed_jira.go +++ b/backend/seed_jira.go @@ -33,6 +33,10 @@ func runSeedJiraData(args []string) error { "Comma-separated event types this module subscribes to") enabled := fs.Bool("enabled", true, "Start with module enabled (true) or disabled (false)") passEntry := fs.String("pass-prefix", "jira/anjana", "pass entry prefix; reads ${prefix}/{email,api-token,domain}") + requesterField := fs.String("requester-field", "customfield_10158", + "Jira custom field id for the required 'Área Solicitante' select (empty to disable)") + requesterDefault := fs.String("requester-default", "Transformación", + "Default 'Área Solicitante' option value for auto-created cards whose requester is not mapped") if err := fs.Parse(args); err != nil { return err } @@ -91,6 +95,10 @@ func runSeedJiraData(args []string) error { "labels_map": labelsMap, "assignee_map": assigneeMap, } + if *requesterField != "" { + cfg["requester_field"] = *requesterField + cfg["requester_default"] = *requesterDefault + } // Upsert by name. Module name is the human-friendly identifier; we treat // it as unique for the purposes of seeding so re-running this command does @@ -111,7 +119,14 @@ func runSeedJiraData(args []string) error { existing.Kind = "jira" existing.Enabled = *enabled existing.EventFilter = splitCSV(*filter) - existing.Config = cfg + // Merge so keys the operator added via the UI (e.g. a custom + // requester_map) survive a re-seed. Seed-managed keys are refreshed. + if existing.Config == nil { + existing.Config = JSONValue{} + } + for k, v := range cfg { + existing.Config[k] = v + } if err := db.saveModule(existing); err != nil { return fmt.Errorf("update module: %w", err) }