• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

RoundingWell / care-ops-frontend / 8b61514d-90b5-45fb-b81d-e60b440f5d22

09 Apr 2026 07:39PM UTC coverage: 92.417% (-7.6%) from 99.976%
8b61514d-90b5-45fb-b81d-e60b440f5d22

push

circleci

nmajor25
On incoming call ringing, show 'Incoming Call' in UI panel header

1735 of 1923 branches covered (90.22%)

Branch coverage included in aggregate %.

5919 of 6359 relevant lines covered (93.08%)

195.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.46
/src/js/services/forms.js
1
import { map, get, debounce } from 'underscore';
2
import dayjs from 'dayjs';
3
import store from 'store';
4

5
import Radio from 'backbone.radio';
6

7
import App from 'js/base/app';
8

9
import { FORM_RESPONSE_STATUS } from 'js/static';
10

11
import { getAppVersion } from '@roundingwell/care-ops-config';
12

13
function getClinicians(teamId) {
14
  if (teamId) {
×
15
    const team = Radio.request('entities', 'teams:model', teamId);
×
16
    return team.getAssignableClinicians();
×
17
  }
18

19
  const currentWorkspace = Radio.request('workspace', 'current');
×
20
  return currentWorkspace.getAssignableClinicians();
×
21
}
22

23
export default App.extend({
24
  startAfterInitialized: true,
25
  channelName() {
26
    return `form${ this.getOption('form').id }`;
37✔
27
  },
28
  send(message, ...args) {
29
    if (this.isDestroyed()) return;
71!
30

31
    const channel = this.getChannel();
71✔
32

33
    return channel.request('send', message, ...args);
71✔
34
  },
35
  initialize(options) {
36
    this.updateDraft = debounce(this.updateDraft, 15000);
37✔
37
    this.refreshForm = debounce(this.refreshForm, 1800000);
37✔
38

39
    this.mergeOptions(options, ['action', 'form', 'patient', 'responses', 'latestResponse']);
37✔
40

41
    this.currentUser = Radio.request('bootstrap', 'currentUser');
37✔
42
  },
43
  onBeforeDestroy() {
44
    this.updateDraft.cancel();
12✔
45
    this.refreshForm.cancel();
12✔
46
  },
47
  radioRequests: {
48
    'ready:form': 'readyForm',
49
    'submit:form': 'submitForm',
50
    'fetch:clinicians': 'fetchClinicians',
51
    'fetch:directory': 'fetchDirectory',
52
    'fetch:form:definition': 'fetchFormDefinition',
53
    'fetch:form:data': 'fetchFormData',
54
    'fetch:form:response': 'fetchFormResponse',
55
    'update:storedSubmission': 'updateStoredSubmission',
56
    'get:storedSubmission': 'getStoredSubmission',
57
    'clear:storedSubmission': 'clearStoredSubmission',
58
    'fetch:field': 'fetchField',
59
    'fetch:fieldHistory': 'fetchFieldHistory',
60
    'fetch:patientsBy': 'fetchPatientsBy',
61
    'update:field': 'updateField',
62
    'version': 'checkVersion',
63
  },
64
  readyForm() {
65
    this.trigger('ready');
30✔
66

67
    this.refreshForm();
30✔
68
  },
69
  checkVersion(feVersion) {
70
    /* istanbul ignore if: can't test reload */
71
    if (feVersion !== getAppVersion()) window.location.reload();
34✔
72
  },
73
  isReadOnly() {
74
    const isLocked = this.action && this.action.isLocked();
92✔
75
    const isSubmitRestricted = this.action && !this.action.canSubmit();
92✔
76

77
    return this.form.isReadOnly() || isLocked || isSubmitRestricted;
92✔
78
  },
79
  getStoreId() {
80
    const actionId = get(this.action, 'id');
67✔
81
    const ids = [this.currentUser.id, this.patient.id, this.form.id];
67✔
82
    if (actionId) ids.push(actionId);
67!
83
    return `form-subm-${ ids.join('-') }`;
67✔
84
  },
85
  getLatestDraft() {
86
    if (this.responses) {
56!
87
      // NOTE: latestResponse is for the currentUser
88
      // If the first response is not the latestResponse, the draft is invalidated
89
      if (this.responses.first() !== this.latestResponse) return {};
56✔
90
    }
91

92
    return (this.latestResponse && this.latestResponse.getDraft()) || {};
14✔
93
  },
94
  getStoredSubmission() {
95
    if (this.isReadOnly()) return {};
60✔
96

97
    const draft = this.getLatestDraft();
56✔
98
    const localDraft = store.get(this.getStoreId()) || {};
56✔
99

100
    if (draft.updated && (!localDraft.updated || dayjs(draft.updated).isAfter(localDraft.updated))) {
56✔
101
      this.trigger('update:submission', draft.updated);
4✔
102
      return draft;
4✔
103
    }
104

105
    if (localDraft.updated) this.trigger('update:submission', localDraft.updated);
52✔
106
    return localDraft;
52✔
107
  },
108
  updateStoredSubmission(submission) {
109
    /* istanbul ignore if: difficult to test read only submission change */
110
    if (this.isReadOnly()) return;
7✔
111

112
    const updated = dayjs().format();
7✔
113

114
    // Cache the draft for debounced updateDraft
115
    this._draft = submission;
7✔
116

117
    try {
7✔
118
      store.set(this.getStoreId(), { submission, updated });
7✔
119
      this.trigger('update:submission', updated);
7✔
120
    } catch /* istanbul ignore next: Tested locally, test runner borks on CI */ {
121
      store.each((value, key) => {
122
        if (String(key).startsWith('form-subm-')) store.remove(key);
123
      });
124
      store.set(this.getStoreId(), { submission, updated });
125
    }
126

127
    this.updateDraft();
7✔
128
    this.refreshForm();
7✔
129
  },
130
  clearStoredSubmission() {
131
    this.latestResponse = null;
4✔
132
    store.remove(this.getStoreId());
4✔
133
    this.trigger('update:submission');
4✔
134
  },
135
  fetchField({ fieldName }, requestId) {
136
    const field = Radio.request('entities', 'patientFields:model', {
×
137
      name: fieldName,
138
      _patient: this.patient.getResource(),
139
    });
140

141
    const message = 'fetch:field';
×
142

143
    return field.fetch()
×
144
      .then(() => {
145
        this.send(message, { value: field.get('value') }, requestId);
×
146
      })
147
      .catch(({ responseData }) => {
148
        this.send(message, { error: responseData }, requestId);
×
149
      });
150
  },
151
  fetchFieldHistory({ fieldName, limit, sort }, requestId) {
152
    const message = 'fetch:fieldHistory';
×
153

154
    return Radio.request('entities', 'fetch:patientFields:model:history', {
×
155
      name: fieldName,
156
      _patient: this.patient.getResource(),
157
    }, { limit, sort })
158
      .then(field => {
159
        this.send(message, { value: field.get('values') }, requestId);
×
160
      })
161
      .catch(
162
        /* istanbul ignore next: Don't test BE errors */
163
        ({ responseData }) => {
164
          this.send(message, { error: responseData }, requestId);
165
        });
166
  },
167
  fetchPatientsBy(filter, requestId) {
168
    const message = 'fetch:patientsBy';
×
169

170
    const patients = Radio.request('entities', 'searchPatients:collection');
×
171

172
    return patients.fetch({ data: { filter } })
×
173
      .then(() => {
174
        this.send(message, { value: patients.toJSON() }, requestId);
×
175
      })
176
      .catch(
177
        /* istanbul ignore next: Don't test BE errors */
178
        ({ responseData }) => {
179
          this.send(message, { error: responseData }, requestId);
180
        });
181
  },
182
  updateField({ fieldName, value }, requestId) {
183
    const field = Radio.request('entities', 'patientFields:model', {
×
184
      name: fieldName,
185
      value,
186
      _patient: this.patient.getResource(),
187
    });
188

189
    const message = 'update:field';
×
190

191
    return field.saveAll()
×
192
      .then(() => {
193
        this.send(message, { value: field.get('value') }, requestId);
×
194
      })
195
      .catch(({ responseData }) => {
196
        this.send(message, { error: responseData }, requestId);
×
197
      });
198
  },
199
  fetchClinicians({ teamId }, requestId) {
200
    const clinicians = getClinicians(teamId);
×
201

202
    this.send('fetch:directory', { value: clinicians.toJSON() }, requestId);
×
203
  },
204
  fetchDirectory({ directoryName, query }, requestId) {
205
    const message = 'fetch:directory';
×
206
    return Promise.resolve(Radio.request('entities', 'fetch:directories:model', directoryName, query))
×
207
      .then(directory => {
208
        this.send(message, { value: directory.get('value') }, requestId);
×
209
      })
210
      .catch(({ responseData }) => {
211
        this.send(message, { error: responseData }, requestId);
×
212
      });
213
  },
214
  fetchFormDefinition(args, requestId) {
215
    const fetchFormDefinition = Radio.request('entities', 'fetch:forms:definition', this.form.id);
34✔
216
    const message = 'fetch:form:definition';
34✔
217
    fetchFormDefinition
34✔
218
      .then(definition => {
219
        this.send(message, { value: definition }, requestId);
34✔
220
      })
221
      .catch(
222
        /* istanbul ignore next: Don't test BE errors */
223
        ({ responseData }) => {
224
          this.send(message, { error: responseData }, requestId);
225
        },
226
      );
227
  },
228
  fetchOtherFormResponse(flow) {
229
    const flowId = flow && flow.id;
4✔
230
    const patientId = this.action.getPatient().id;
4✔
231
    const actionTags = this.form.getPrefillActionTag();
4✔
232
    const formId = !actionTags && this.form.getPrefillFormId();
4✔
233
    const submittedAt = this.form.isReport() && `<=${ this.action.get('created_at') }`;
4✔
234

235
    return Radio.request('entities', 'fetch:formResponses:byPatient', { patientId, flowId, formId, actionTags, submittedAt });
4✔
236
  },
237
  fetchLatestFormResponse() {
238
    const firstResponse = this.responses && this.responses.getFirstSubmission();
25✔
239

240
    if (!firstResponse && this.action) {
25✔
241
      if (this.action.hasTag('prefill-latest-response')) return this.fetchOtherFormResponse();
20✔
242
      if (this.action.hasTag('prefill-flow-response')) return this.fetchOtherFormResponse(this.action.getFlow());
17✔
243
    }
244

245
    return Radio.request('entities', 'fetch:formResponses:model', get(firstResponse, 'id'));
21✔
246
  },
247
  fetchFormData(args, requestId) {
248
    const message = 'fetch:form:data';
28✔
249
    const storedSubmission = this.getStoredSubmission();
28✔
250

251
    if (storedSubmission.updated) {
28✔
252
      this.send(message, { value: {
3✔
253
        storedSubmission: storedSubmission.submission,
254
        options: this.form.get('options'),
255
      } }, requestId);
256
      return;
3✔
257
    }
258

259
    Promise.all([
25✔
260
      Radio.request('entities', 'fetch:forms:data', get(this.action, 'id'), this.patient.id, this.form.id),
261
      this.fetchLatestFormResponse(),
262
    ])
263
      .then(([data, response]) => {
264
        this.send(message, { value: {
25✔
265
          isReadOnly: this.isReadOnly(),
266
          formData: data.attributes,
267
          responseData: response.getFormData(),
268
          formSubmission: response.getResponse(),
269
          options: this.form.get('options'),
270
        } }, requestId);
271
      })
272
      .catch(
273
        /* istanbul ignore next: Don't test BE errors */
274
        ({ responseData }) => {
275
          this.send(message, { error: responseData }, requestId);
276
        },
277
      );
278
  },
279
  fetchFormResponse({ responseId }, requestId) {
280
    const message = 'fetch:form:response';
6✔
281
    return Promise.all([
6✔
282
      Radio.request('entities', 'fetch:formResponses:model', responseId),
283
    ]).then(([response]) => {
284
      this.send(message, { value: {
6✔
285
        responseData: response.getFormData(),
286
        formSubmission: response.getResponse(),
287
        options: this.form.get('options'),
288
      } }, requestId);
289
    }).catch(
290
      /* istanbul ignore next: Don't test BE errors */
291
      ({ responseData }) => {
292
        this.send(message, { error: responseData }, requestId);
293
      },
294
    );
295
  },
296
  useLatestDraft(responseData) {
297
    responseData._form = this.form.getResource();
10✔
298
    responseData._patient = this.patient.getResource();
10✔
299
    if (this.action) responseData._action = this.action.getResource();
10!
300

301
    if (!this.latestResponse || this.latestResponse.get('status') !== FORM_RESPONSE_STATUS.DRAFT) return responseData;
10✔
302

303
    return {
3✔
304
      ...responseData,
305
      id: this.latestResponse.id,
306
    };
307
  },
308
  updateDraft() {
309
    const data = this.useLatestDraft({
4✔
310
      response: { data: this._draft },
311
      status: FORM_RESPONSE_STATUS.DRAFT,
312
    });
313

314
    const formResponse = Radio.request('entities', 'formResponses:model', data);
4✔
315

316
    this.latestResponse = formResponse;
4✔
317

318
    return formResponse.saveAll()
4✔
319
      .catch(({ responseData }) => {
320
        /* istanbul ignore next: Don't handle non-API errors */
321
        if (!responseData) return;
322

323
        this.trigger('error', responseData.errors);
1✔
324
      });
325
  },
326
  refreshForm() {
327
    this.trigger('refresh');
2✔
328
  },
329
  submitForm({ response }) {
330
    // Cancel any pending draft updates or stale form refreshes
331
    this.updateDraft.cancel();
6✔
332
    this.refreshForm.cancel();
6✔
333

334
    const data = this.useLatestDraft({
6✔
335
      response,
336
      status: FORM_RESPONSE_STATUS.SUBMITTED,
337
    });
338

339
    const formResponse = Radio.request('entities', 'formResponses:model', data);
6✔
340

341
    return formResponse.saveAll()
6✔
342
      .then(() => {
343
        // Cancel any draft updates or stale form refreshes that may have been queued while the form was submitting
344
        this.updateDraft.cancel();
3✔
345
        this.refreshForm.cancel();
3✔
346
        this.clearStoredSubmission();
3✔
347
        this.trigger('success', formResponse);
3✔
348
      }).catch(({ responseData }) => {
349
        /* istanbul ignore next: Don't handle non-API errors */
350
        if (!responseData) return;
351

352
        this.trigger('error', responseData.errors);
3✔
353

354
        const error = map(responseData.errors, 'detail');
3✔
355
        this.send('form:errors', { error });
3✔
356
      });
357
  },
358
});
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc