mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-13 08:05:05 +00:00
feat: fix the segment dialog UI and make a contact created inside a segment join it (issue #285): the add-to-campaign picker now shows a humanised, colour-toned campaign status instead of the raw PAUSED_NO_ACCOUNTS enum, its footer wraps instead of clipping the hint mid-sentence and the Add leads button no longer breaks across two lines, campaign status labels move to a shared components/app/campaigns/status module, the Segments, Categories and Segment pages plus every unmapped settings and onboarding route get a document title so they stop reading Page not found, and POST /contacts takes a segments array that pins the new contacts in as include overrides (validated before the write, best-effort after it) which the New contact dialog sends when opened from a segment page
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/warmbly/warmbly/internal/config"
|
||||
"github.com/warmbly/warmbly/internal/errx"
|
||||
"github.com/warmbly/warmbly/internal/models"
|
||||
@@ -56,10 +57,18 @@ func (s *contactService) Add(ctx context.Context, userID string, orgID uuid.UUID
|
||||
return nil, xerr
|
||||
}
|
||||
|
||||
// Resolved before the write so an unknown segment is a 400 rather than a
|
||||
// contact that exists but never reached the segment it was created in.
|
||||
pins, xerr := s.segmentPins(ctx, orgID, contacts)
|
||||
if xerr != nil {
|
||||
return nil, xerr
|
||||
}
|
||||
|
||||
created, xerr := s.contactRepository.Add(ctx, userID, orgID, contacts)
|
||||
if xerr != nil {
|
||||
return nil, xerr
|
||||
}
|
||||
s.applySegmentPins(ctx, orgID, pins, created)
|
||||
|
||||
s.publishContactsReload(ctx, userID, "contacts:add")
|
||||
var attached []string
|
||||
@@ -71,6 +80,72 @@ func (s *contactService) Add(ctx context.Context, userID string, orgID uuid.UUID
|
||||
return created, nil
|
||||
}
|
||||
|
||||
// segmentPins maps each target segment to the positions in `contacts` that
|
||||
// asked for it. Existence is checked once for the whole batch, so N contacts
|
||||
// naming the same segment cost one lookup.
|
||||
func (s *contactService) segmentPins(ctx context.Context, orgID uuid.UUID, contacts []models.AddContact) (map[uuid.UUID][]int, *errx.Error) {
|
||||
var raw []string
|
||||
for i := range contacts {
|
||||
raw = append(raw, contacts[i].Segments...)
|
||||
}
|
||||
if len(raw) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
valid, xerr := s.parseSegmentIDs(ctx, orgID, raw)
|
||||
if xerr != nil {
|
||||
return nil, xerr
|
||||
}
|
||||
known := make(map[uuid.UUID]bool, len(valid))
|
||||
for _, id := range valid {
|
||||
known[id] = true
|
||||
}
|
||||
|
||||
pins := make(map[uuid.UUID][]int, len(valid))
|
||||
for i := range contacts {
|
||||
seen := make(map[uuid.UUID]bool, len(contacts[i].Segments))
|
||||
for _, r := range contacts[i].Segments {
|
||||
id, err := uuid.Parse(strings.TrimSpace(r))
|
||||
if err != nil || !known[id] {
|
||||
return nil, errx.New(errx.BadRequest, "invalid segment id")
|
||||
}
|
||||
if seen[id] {
|
||||
continue
|
||||
}
|
||||
seen[id] = true
|
||||
pins[id] = append(pins[id], i)
|
||||
}
|
||||
}
|
||||
return pins, nil
|
||||
}
|
||||
|
||||
// applySegmentPins writes the include overrides for a batch that has just been
|
||||
// created. The repository answers one row per input contact, in order, so
|
||||
// position i of `created` is the contact that position i of the request asked
|
||||
// to pin. Best effort like wakeCampaigns: the contacts are already stored.
|
||||
func (s *contactService) applySegmentPins(ctx context.Context, orgID uuid.UUID, pins map[uuid.UUID][]int, created []models.Contact) {
|
||||
if len(pins) == 0 || s.segmentLinker == nil {
|
||||
return
|
||||
}
|
||||
for segmentID, positions := range pins {
|
||||
ids := make([]uuid.UUID, 0, len(positions))
|
||||
for _, i := range positions {
|
||||
if i < len(created) {
|
||||
ids = append(ids, created[i].ID)
|
||||
}
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
continue
|
||||
}
|
||||
if _, xerr := s.segmentLinker.SetMembers(ctx, orgID, segmentID, ids, models.SegmentMemberInclude); xerr != nil {
|
||||
log.Warn().
|
||||
Str("organization_id", orgID.String()).
|
||||
Str("segment_id", segmentID.String()).
|
||||
Int("contacts", len(ids)).
|
||||
Msg("could not pin the new contacts into their segment")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *contactService) Search(ctx context.Context, orgID, cursor, category, limit string, filters models.SearchContacts) (*models.ContactsResult, *errx.Error) {
|
||||
cursorId, err := paging.DecodeCursor(cursor)
|
||||
if err != nil {
|
||||
|
||||
@@ -486,6 +486,11 @@ type AddContact struct {
|
||||
Campaigns []string `json:"campaigns"`
|
||||
Categories []string `json:"categories"`
|
||||
|
||||
// Segments the new contact joins as an include override, so a contact
|
||||
// created from inside a segment belongs to it whatever the conditions
|
||||
// say. Unknown ids are a 400 before anything is written.
|
||||
Segments []string `json:"segments,omitempty"`
|
||||
|
||||
CustomFields map[string]string `json:"custom_fields"`
|
||||
|
||||
// VerificationStatus is a verdict the caller already holds for this
|
||||
|
||||
Reference in New Issue
Block a user