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

RoundingWell / care-ops-frontend / 692f9f2e-3aac-4156-9197-4812d5314b95

20 Aug 2025 07:09AM UTC coverage: 92.095% (-7.9%) from 100.0%
692f9f2e-3aac-4156-9197-4812d5314b95

Pull #1488

circleci

paulfalgout
Test patient overline on worklist
Pull Request #1488: Add subgrid to lists

1639 of 1844 branches covered (88.88%)

Branch coverage included in aggregate %.

5771 of 6202 relevant lines covered (93.05%)

195.02 hits per line

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

88.51
/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 { versions } from '@roundingwell/care-ops-config';
12

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

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

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

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

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

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

41
    this.currentUser = Radio.request('bootstrap', 'currentUser');
25✔
42
  },
43
  onBeforeDestroy() {
44
    this.updateDraft.cancel();
4✔
45
    this.refreshForm.cancel();
4✔
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
    'update:field': 'updateField',
61
    'fetch:icd': 'fetchIcd',
62
    'version': 'checkVersion',
63
  },
64
  readyForm() {
65
    this.trigger('ready');
23✔
66

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

77
    return this.form.isReadOnly() || isLocked || isSubmitRestricted;
83✔
78
  },
79
  getStoreId() {
80
    const actionId = get(this.action, 'id');
64✔
81
    const ids = [this.currentUser.id, this.patient.id, this.form.id];
64✔
82
    if (actionId) ids.push(actionId);
64✔
83
    return `form-subm-${ ids.join('-') }`;
64✔
84
  },
85
  getLatestDraft() {
86
    if (this.responses) {
43✔
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 {};
5!
90
    }
91

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

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

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

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

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

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

117
    try {
18✔
118
      store.set(this.getStoreId(), { submission, updated });
18✔
119
      this.trigger('update:submission', updated);
18✔
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();
18✔
128
    this.refreshForm();
18✔
129
  },
130
  clearStoredSubmission() {
131
    this.latestResponse = null;
3✔
132
    store.remove(this.getStoreId());
3✔
133
    this.trigger('update:submission');
3✔
134
  },
135
  fetchField({ fieldName }, requestId) {
136
    const field = Radio.request('entities', 'patientFields:model', {
2✔
137
      name: fieldName,
138
      _patient: this.patient.getResource(),
139
    });
140

141
    const message = 'fetch:field';
2✔
142

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

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

174
    const message = 'update:field';
3✔
175

176
    return field.saveAll()
3✔
177
      .then(() => {
178
        this.send(message, { value: field.get('value') }, requestId);
2✔
179
      })
180
      .catch(({ responseData }) => {
181
        this.send(message, { error: responseData }, requestId);
1✔
182
      });
183
  },
184
  fetchClinicians({ teamId }, requestId) {
185
    const clinicians = getClinicians(teamId);
4✔
186

187
    this.send('fetch:directory', { value: clinicians.toJSON() }, requestId);
4✔
188
  },
189
  fetchDirectory({ directoryName, query }, requestId) {
190
    const message = 'fetch:directory';
4✔
191
    return Promise.resolve(Radio.request('entities', 'fetch:directories:model', directoryName, query))
4✔
192
      .then(directory => {
193
        this.send(message, { value: directory.get('value') }, requestId);
2✔
194
      })
195
      .catch(({ responseData }) => {
196
        this.send(message, { error: responseData }, requestId);
2✔
197
      });
198
  },
199
  fetchIcd({ by }, requestId) {
200
    const message = 'fetch:icd';
2✔
201
    return Promise.resolve(Radio.request('entities', 'fetch:icd', by))
2✔
202
      .then(icd => {
203
        this.send(message, { value: get(icd, ['data', 'icdCodes']) }, requestId);
1✔
204
      })
205
      .catch(({ responseData }) => {
206
        this.send(message, { error: responseData }, requestId);
1✔
207
      });
208
  },
209
  fetchFormDefinition(args, requestId) {
210
    const fetchFormDefinition = Radio.request('entities', 'fetch:forms:definition', this.form.id);
23✔
211
    const message = 'fetch:form:definition';
23✔
212
    fetchFormDefinition
23✔
213
      .then(definition => {
214
        this.send(message, { value: definition }, requestId);
23✔
215
      })
216
      .catch(
217
        /* istanbul ignore next: Don't test BE errors */
218
        ({ responseData }) => {
219
          this.send(message, { error: responseData }, requestId);
220
        },
221
      );
222
  },
223
  fetchOtherFormResponse(flow) {
224
    const flowId = flow && flow.id;
×
225
    const patientId = this.action.getPatient().id;
×
226
    const actionTags = this.form.getPrefillActionTag();
×
227
    const formId = !actionTags && this.form.getPrefillFormId();
×
228
    const submittedAt = this.form.isReport() && `<=${ this.action.get('created_at') }`;
×
229

230
    return Radio.request('entities', 'fetch:formResponses:byPatient', { patientId, flowId, formId, actionTags, submittedAt });
×
231
  },
232
  fetchLatestFormResponse() {
233
    const firstResponse = this.responses && this.responses.getFirstSubmission();
20✔
234

235
    if (!firstResponse && this.action) {
20✔
236
      if (this.action.hasTag('prefill-latest-response')) return this.fetchOtherFormResponse();
2!
237
      if (this.action.hasTag('prefill-flow-response')) return this.fetchOtherFormResponse(this.action.getFlow());
2!
238
    }
239

240
    return Radio.request('entities', 'fetch:formResponses:model', get(firstResponse, 'id'));
20✔
241
  },
242
  fetchFormData(args, requestId) {
243
    const message = 'fetch:form:data';
22✔
244
    const storedSubmission = this.getStoredSubmission();
22✔
245

246
    if (storedSubmission.updated) {
22✔
247
      this.send(message, { value: {
2✔
248
        storedSubmission: storedSubmission.submission,
249
        options: this.form.get('options'),
250
      } }, requestId);
251
      return;
2✔
252
    }
253

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

296
    if (!this.latestResponse || this.latestResponse.get('status') !== FORM_RESPONSE_STATUS.DRAFT) return responseData;
5!
297

298
    return {
×
299
      ...responseData,
300
      id: this.latestResponse.id,
301
    };
302
  },
303
  updateDraft() {
304
    const data = this.useLatestDraft({
1✔
305
      response: { data: this._draft },
306
      status: FORM_RESPONSE_STATUS.DRAFT,
307
    });
308

309
    const formResponse = Radio.request('entities', 'formResponses:model', data);
1✔
310

311
    this.latestResponse = formResponse;
1✔
312

313
    return formResponse.saveAll()
1✔
314
      .catch(({ responseData }) => {
315
        /* istanbul ignore next: Don't handle non-API errors */
316
        if (!responseData) return;
317

318
        this.trigger('error', responseData.errors);
×
319
      });
320
  },
321
  refreshForm() {
322
    this.trigger('refresh');
×
323
  },
324
  submitForm({ response }) {
325
    // Cancel any pending draft updates or stale form refreshes
326
    this.updateDraft.cancel();
4✔
327
    this.refreshForm.cancel();
4✔
328

329
    const data = this.useLatestDraft({
4✔
330
      response,
331
      status: FORM_RESPONSE_STATUS.SUBMITTED,
332
    });
333

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

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

347
        this.trigger('error', responseData.errors);
2✔
348

349
        const error = map(responseData.errors, 'detail');
2✔
350
        this.send('form:errors', { error });
2✔
351
      });
352
  },
353
});
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