All files / src/live ContactStoreElement.ts

89.41% Statements 169/189
92.5% Branches 37/40
83.33% Functions 10/12
89.41% Lines 169/189

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 88x 87x 87x 126x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 81x 126x 148x 148x 148x 148x 148x 267x 267x 148x 148x 126x 126x 101x 101x 126x 25x 25x 25x 25x 126x         148x 148x 148x 148x   126x                   126x 16x 16x 16x 16x 16x 16x 16x 16x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x             126x 81x 81x 81x 81x 126x 81x 81x 81x 81x 81x 126x 355x 355x 88x 86x 86x 86x 86x 88x 2x 126x 126x 126x 126x 126x 126x 250x 169x 65x 65x 250x 250x 250x 65x 96x 34x 34x 96x 34x 34x 34x 47x 126x 126x 126x 126x 126x 126x 126x 126x 47x 47x 47x 47x 47x 47x 47x 47x 47x 47x 47x 47x 47x 47x 47x 47x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x 126x  
import { PropertyValues } from 'lit';
import { property } from 'lit/decorators.js';
import { Contact, CustomEventType, Group, URN } from '../interfaces';
import { EndpointMonitorElement } from '../store/EndpointMonitorElement';
import {
  CONTACT_STATE_TYPES,
  refreshContact,
  updateContact,
  watchContact
} from './ContactWatch';
import { RealtimeSubscription } from './Realtime';
 
/**
 * Returns the URN that will be used to message the given contact — URNs are
 * ordered by priority and only ones with a channel are sendable.
 */
export const getDestinationURN = (contact: Contact | null): URN | null => {
  return (contact?.urns || []).find((urn) => !!urn.channel) || null;
};
 
export class ContactStoreElement extends EndpointMonitorElement {
  @property({ type: String })
  contact: string;
 
  @property({ type: Object, attribute: false })
  data: Contact;
 
  // the contact events this component registers interest in with the central
  // watcher - subclasses narrow this to what they actually render. An empty
  // list still receives eventless deliveries (initial values and refetches);
  // null opts out of watching entirely
  protected watchTypes: string[] = CONTACT_STATE_TYPES;
 
  private watch: RealtimeSubscription = null;
  private watchedContact: string = null;
 
  // Resolve each URN against a channel while retaining the user's priority
  // order. Consumers can select the first channel-backed URN for messaging.
  @property({ type: String })
  endpoint = '/api/v2/contacts.json?expand_urns=true&urn_order=priority&uuid=';
 
  // Writes go through the internal API, which responds with the same shape
  // as the read endpoint above.
  @property({ type: String })
  writeEndpoint = '/api/internal/contacts.json?uuid=';
 
  prepareData(data: any) {
    if (data) {
      data = Array.isArray(data) ? data[0] : data;
    }
    if (data) {
      data.groups.forEach((group: Group) => {
        group.is_dynamic = this.store.isDynamicGroup(group.uuid);
      });
 
      data.groups.sort((a: Group, b: Group) => {
        if (!a.is_dynamic || !b.is_dynamic) {
          if (a.is_dynamic) {
            return -1;
          }
 
          if (b.is_dynamic) {
            return 1;
          }
        }
 
        return a.name.localeCompare(b.name);
      });

      return data;
    }
    return null;
  }
 
  public postChanges(payload: any) {
    // clear our cache so we don't have any races
    this.store.removeFromCache(`${this.endpoint}${this.contact}`);
    return this.store
      .postJSON(`${this.writeEndpoint}${this.contact}`, payload)
      .then((response) => {
        this.setContact(response.json);
      });
  }

  public setContact(contact: any, contactId = this.contact) {
    // make sure contact data is properly prepped
    this.data = this.prepareData([contact]);
    this.store.updateCache(`${this.endpoint}${contactId}`, this.data);
 
    // sync every watcher on the page with the change immediately, without
    // waiting for it to echo back over the socket
    if (this.data) {
      updateContact(this.data.uuid, this.data);
    }
  }
 
  /**
   * Refetches the contact. When watched we also go through the central
   * watcher so every watcher on the page gets the fresh contact, not just us,
   * while the store fetch keeps the cache fresh for the components that read
   * it without watching. Two fetches for what is a rare manual operation.
   */
  public refresh(): void {
    if (this.watchedContact) {
      refreshContact(this.watchedContact);
    }
    super.refresh();
  }

  public connectedCallback(): void {
    super.connectedCallback();
    this.syncWatch();
  }
 
  public disconnectedCallback(): void {
    super.disconnectedCallback();
    this.syncWatch();
  }
 
  public willUpdate(changed: PropertyValues): void {
    // derive our url before the base class runs so it sees the url change
    // in this same pass (clearing stale data when the contact is unset)
    if (changed.has('contact') || changed.has('endpoint')) {
      if (this.contact) {
        this.url = `${this.endpoint}${this.contact}`;
      } else {
        this.url = null;
      }
      this.syncWatch();
    }
    super.willUpdate(changed);
  }
 
  // keeps our registration with the central watcher in sync with the
  // contact we're pointed at
  private syncWatch() {
    const target =
      (this.isConnected && this.watchTypes && this.contact) || null;
    if (target === this.watchedContact) {
      return;
    }
 
    if (this.watch) {
      this.watch.unsubscribe();
      this.watch = null;
    }
    this.watchedContact = target;
 
    if (target) {
      this.watch = watchContact(target, this.watchTypes, (event, contact) =>
        this.handleWatchedContact(event, contact)
      );
    }
  }
 
  /**
   * A delivery from the central watcher - an event we registered interest in
   * or an eventless delivery carrying initial or refetched values. Either
   * way the contact is current, so it simply becomes our data.
   */
  protected handleWatchedContact(event: any, contact: Contact) {
    if (contact && contact.uuid === this.watchedContact) {
      const previous = this.data;
      // fresh identity so change detection sees every delivery, copying the
      // parts prepareData mutates so we never write into the registry's
      // snapshot
      const copy = {
        ...contact,
        groups: (contact.groups || []).map((group) => ({ ...group })),
        fields: { ...contact.fields }
      };
      this.data = this.prepareData(this.mergeWatchedContact(copy, previous));
      this.fireCustomEvent(CustomEventType.Refreshed, {
        data: this.data,
        previous
      });
    }
  }
 
  /**
   * Lets subclasses keep values they have a write in flight for. A delivery
   * carries server state that predates our own unsaved edits, so anything
   * mid-save would otherwise flip back to its old value until the write
   * lands. Returns the contact to become our data.
   */
  protected mergeWatchedContact(contact: Contact, _previous: Contact): Contact {
    return contact;
  }
}