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

FarmBot / Farmbot-Web-App / #4782

pending completion
#4782

push

gabrielburnworth
add curves feature (API)

11359 of 11425 branches covered (99.42%)

Branch coverage included in aggregate %.

16 of 16 new or added lines in 4 files covered. (100.0%)

23070 of 23073 relevant lines covered (99.99%)

1492.29 hits per line

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

98.63
/frontend/resources/reducer_support.ts
1
import {
520✔
2
  ResourceName, SpecialStatus, TaggedResource, TaggedSequence,
3
} from "farmbot";
4
import { combineReducers, ReducersMapObject } from "redux";
520✔
5
import { helpReducer as help } from "../help/reducer";
520✔
6
import { designer as farm_designer } from "../farm_designer/reducer";
520✔
7
import { photosReducer as photos } from "../photos/reducer";
520✔
8
import { farmwareReducer as farmware } from "../farmware/reducer";
520✔
9
import { regimensReducer as regimens } from "../regimens/reducer";
520✔
10
import { sequenceReducer as sequences } from "../sequences/reducer";
520✔
11
import { RestResources, ResourceIndex, TaggedPointGroup } from "./interfaces";
12
import { isTaggedResource } from "./tagged_resources";
520✔
13
import { arrayWrap, arrayUnwrap } from "./util";
520✔
14
import {
520✔
15
  sanitizeNodes,
16
} from "../sequences/locals_list/sanitize_nodes";
17
import {
520✔
18
  selectAllFarmEvents,
19
  selectAllPinBindings,
20
  selectAllLogs,
21
  selectAllRegimens,
22
  selectAllFolders,
23
  selectAllSequences,
24
  selectAllActivePoints,
25
  selectAllPointGroups,
26
} from "./selectors_by_kind";
27
import { findUuid, selectAllPlantPointers } from "./selectors";
520✔
28
import {
520✔
29
  ExecutableType, PinBindingType,
30
} from "farmbot/dist/resources/api_resources";
31
import { betterCompact, unpackUUID } from "../util";
520✔
32
import { createSequenceMeta } from "./sequence_meta";
520✔
33
import { alertsReducer as alerts } from "../messages/reducer";
520✔
34
import { warning } from "../toast/toast";
520✔
35
import { ReduxAction } from "../redux/interfaces";
36
import { ActionHandler } from "../redux/generate_reducer";
37
import { get } from "lodash";
520✔
38
import { Actions } from "../constants";
520✔
39
import { getFbosConfig } from "./getters";
520✔
40
import { ingest, PARENTLESS as NO_PARENT } from "../folders/data_transfer";
520✔
41
import { FolderNode, FolderMeta } from "../folders/interfaces";
42
import { pointsSelectedByGroup } from "../point_groups/criteria/apply";
520✔
43

44
export function findByUuid(index: ResourceIndex, uuid: string): TaggedResource {
520✔
45
  const x = index.references[uuid];
104✔
46
  if (x && isTaggedResource(x)) {
104✔
47
    return x;
103✔
48
  } else {
49
    throw new Error("BAD UUID- CANT FIND RESOURCE: " + uuid);
1✔
50
  }
51
}
52

53
type IndexerCallback = (self: TaggedResource, index: ResourceIndex) => void;
54
export interface Indexer {
55
  /** Resources entering index */
56
  up: IndexerCallback;
57
  /** Resources leaving index */
58
  down: IndexerCallback;
59
}
60

61
export const reindexFolders = (i: ResourceIndex) => {
520✔
62
  const folders = betterCompact(selectAllFolders(i)
713✔
63
    .map((x): FolderNode | undefined => {
64
      const { body } = x;
528✔
65
      if (typeof body.id === "number") {
528✔
66
        const fn: FolderNode = { id: body.id, ...body };
528✔
67
        return fn;
528✔
68
      }
69
    }));
70
  const allSequences = selectAllSequences(i);
713✔
71

72
  const oldMeta = i.sequenceFolders.localMetaAttributes;
713✔
73
  /** Open folder edit mode when adding a new folder (& not during init all). */
74
  const editing = !!oldMeta[-1];
713✔
75
  const localMetaAttributes: Record<number, FolderMeta> = {};
713✔
76
  folders.map(x => {
713✔
77
    localMetaAttributes[x.id] = {
528✔
78
      ...(oldMeta[x.id] || { editing }),
569✔
79
      sequences: [], // Clobber and re-init
80
    };
81
  });
82

83
  allSequences.map((s) => {
713✔
84
    const { folder_id } = s.body;
896✔
85
    const parentId = folder_id || NO_PARENT;
896✔
86

87
    if (!localMetaAttributes[parentId]) {
896✔
88
      localMetaAttributes[parentId] = {
670✔
89
        sequences: [],
90
        open: true,
91
        editing: false
92
      };
93
    }
94
    localMetaAttributes[parentId].sequences.push(s.uuid);
896✔
95
  });
96

97
  const { searchTerm } = i.sequenceFolders;
713✔
98

99
  i.sequenceFolders = {
713✔
100
    folders: ingest({ folders, localMetaAttributes }),
101
    localMetaAttributes,
102
    searchTerm: searchTerm,
103
    filteredFolders: searchTerm
713✔
104
      ? i.sequenceFolders.filteredFolders
105
      : undefined,
106
    stashedOpenState: i.sequenceFolders.stashedOpenState,
107
  };
108

109
  if (i.sequenceFolders.filteredFolders) {
713✔
110
    const existingFolders = i.sequenceFolders.folders.folders.map(f => f.id);
6✔
111
    i.sequenceFolders.filteredFolders.folders =
4✔
112
      i.sequenceFolders.filteredFolders.folders.filter(f =>
113
        existingFolders.includes(f.id));
4✔
114
    const folderResults = i.sequenceFolders.filteredFolders.folders.map(f => f.id);
4✔
115
    i.sequenceFolders.folders.folders.map(f =>
4✔
116
      !folderResults.includes(f.id) && searchTerm &&
6✔
117
      i.sequenceFolders.filteredFolders &&
118
      f.name.toLowerCase().includes(searchTerm.toLowerCase())
119
      && i.sequenceFolders.filteredFolders.folders.push({ ...f, editing: false }));
120
  }
121
};
122

123
export const folderIndexer: IndexerCallback = (r, i) => {
520✔
124
  if (r.kind === "Folder" || r.kind === "Sequence") {
10,769✔
125
    reindexFolders(i);
702✔
126
  }
127
};
128

129
const SEQUENCE_FOLDERS: Indexer = { up: folderIndexer, down: () => { } };
520✔
130

131
const REFERENCES: Indexer = {
520✔
132
  up: (r, i) => i.references[r.uuid] = r,
10,752✔
133
  down: (r, i) => delete i.references[r.uuid],
17✔
134
};
135

136
const ALL: Indexer = {
520✔
137
  up: (r, s) => s.all[r.uuid] = true,
10,752✔
138
  down: (r, i) => delete i.all[r.uuid],
17✔
139
};
140

141
const BY_KIND: Indexer = {
520✔
142
  up(r, i) {
143
    i.byKind[r.kind]
10,752✔
144
      ? i.byKind[r.kind][r.uuid] = r.uuid
145
      : console.error(`${r.kind} is not an indexed resource.`);
146
  },
147
  down(r, i) { delete i.byKind[r.kind][r.uuid]; },
17✔
148
};
149

150
const BY_KIND_AND_ID: Indexer = {
520✔
151
  up: (r, i) => {
152
    if (r.body.id) {
10,752✔
153
      i.byKindAndId[joinKindAndId(r.kind, r.body.id)] = r.uuid;
10,684✔
154
    }
155
  },
156
  down(r, i) {
157
    delete i.byKindAndId[joinKindAndId(r.kind, r.body.id)];
17✔
158
    delete i.byKindAndId[joinKindAndId(r.kind, 0)];
17✔
159
  },
160
};
161

162
function updateSequenceUsageIndex(
163
  myUuid: string, ids: number[], i: ResourceIndex) {
164
  ids.map(id => {
660✔
165
    const uuid = i.byKindAndId[joinKindAndId("Sequence", id)];
4✔
166
    if (uuid) { // `undefined` usually means "not ready".
4✔
167
      const inUse = i.inUse["Sequence.Sequence"][uuid] || {};
4✔
168
      i.inUse["Sequence.Sequence"][uuid] = { ...inUse, ...{ [myUuid]: true } };
4✔
169
    }
170
  });
171
}
172

173
const updateOtherSequenceIndexes =
174
  (tr: TaggedSequence, i: ResourceIndex) => {
520✔
175
    i.references[tr.uuid] = tr;
660✔
176
    i.sequenceMetas[tr.uuid] = createSequenceMeta(i, tr);
660✔
177
  };
178

179
const reindexSequences = (i: ResourceIndex) => (s: TaggedSequence) => {
608✔
180
  // STEP 1: Sanitize nodes, tag them with unique UUIDs (for React),
181
  //         collect up sequence_id's, etc. NOTE: This is CPU expensive,
182
  //         so if you need to do tree traversal, do it now.
183
  const { thisSequence, callsTheseSequences } = sanitizeNodes(s.body);
661✔
184
  // STEP 2: Add sequence to index.references, update variable reference
185
  //         indexes
186
  updateSequenceUsageIndex(s.uuid, callsTheseSequences, i);
660✔
187
  // Step 3: Update the in_use stats for Sequence-to-Sequence usage.
188
  updateOtherSequenceIndexes({ ...s, body: thisSequence }, i);
660✔
189
};
190

191
const reindexAllSequences = (i: ResourceIndex) => {
520✔
192
  i.inUse["Sequence.Sequence"] = {};
608✔
193
  const mapper = reindexSequences(i);
608✔
194
  betterCompact(Object.keys(i.byKind["Sequence"]).map(uuid => {
608✔
195
    const resource = i.references[uuid];
661✔
196
    return (resource?.kind == "Sequence") ? resource : undefined;
661!
197
  })).map(mapper);
198
};
199

200
function reindexAllFarmEventUsage(i: ResourceIndex) {
201
  i.inUse["Regimen.FarmEvent"] = {};
524✔
202
  i.inUse["Sequence.FarmEvent"] = {};
524✔
203
  const whichOne: Record<ExecutableType, typeof i.inUse["Regimen.FarmEvent"]> = {
524✔
204
    "Regimen": i.inUse["Regimen.FarmEvent"],
205
    "Sequence": i.inUse["Sequence.FarmEvent"],
206
  };
207

208
  // Which FarmEvents use which resource?
209
  betterCompact(selectAllFarmEvents(i)
524✔
210
    .map(fe => {
211
      const { executable_type, executable_id } = fe.body;
1,039✔
212
      const uuid = findUuid(i, executable_type, executable_id);
1,039✔
213
      return { exe_type: executable_type, exe_uuid: uuid, fe_uuid: fe.uuid };
1,037✔
214
    }))
215
    .map(({ exe_type, exe_uuid, fe_uuid }) => {
1,035✔
216
      whichOne[exe_type] = whichOne[exe_type] || {};
1,035!
217
      whichOne[exe_type][exe_uuid] = whichOne[exe_type][exe_uuid] || {};
1,035✔
218
      whichOne[exe_type][exe_uuid][fe_uuid] = true;
1,035✔
219
    });
220
}
221

222
const reindexAllPointGroups = (i: ResourceIndex) => {
520✔
223
  selectAllPointGroups(i).map((pg: TaggedPointGroup) => pg.body.member_count =
616✔
224
    pointsSelectedByGroup(pg, selectAllActivePoints(i)).length);
225
};
226

227
const reindexAllPoints = (i: ResourceIndex) => {
520✔
228
  reindexAllPointGroups(i);
611✔
229
  i.inUse["Curve.Point"] = {};
611✔
230
  const tracker = i.inUse["Curve.Point"];
611✔
231
  selectAllPlantPointers(i)
611✔
232
    .map(p => {
233
      if (p.body.water_curve_id) {
1,083✔
234
        const curveUuid = findUuid(i, "Curve", p.body.water_curve_id);
2✔
235
        tracker[curveUuid] = tracker[curveUuid] || {};
2✔
236
        tracker[curveUuid][p.uuid] = true;
2✔
237
      }
238
      if (p.body.spread_curve_id) {
1,083✔
239
        const curveUuid = findUuid(i, "Curve", p.body.spread_curve_id);
1✔
240
        tracker[curveUuid] = tracker[curveUuid] || {};
1✔
241
        tracker[curveUuid][p.uuid] = true;
1✔
242
      }
243
      if (p.body.height_curve_id) {
1,083✔
244
        const curveUuid = findUuid(i, "Curve", p.body.height_curve_id);
1✔
245
        tracker[curveUuid] = tracker[curveUuid] || {};
1✔
246
        tracker[curveUuid][p.uuid] = true;
1✔
247
      }
248
    });
249
};
250

251
const INDEXERS: Indexer[] = [
520✔
252
  REFERENCES,
253
  ALL,
254
  BY_KIND,
255
  BY_KIND_AND_ID,
256
  SEQUENCE_FOLDERS,
257
];
258

259
type IndexerHook = Partial<Record<TaggedResource["kind"], Reindexer>>;
260
type Reindexer = (i: ResourceIndex, strategy: "ongoing" | "initial") => void;
261

262
export function joinKindAndId(kind: ResourceName, id: number | undefined) {
520✔
263
  return `${kind}.${id || 0}`;
16,066✔
264
}
265

266
/** Any reducer that uses TaggedResources (or UUIDs) must live within the
267
 * resource reducer. Failure to do so can result in stale UUIDs, referential
268
 * integrity issues and other bad stuff. The variable below contains all
269
 * resource consuming reducers. */
270
const consumerReducer = combineReducers<RestResources["consumers"]>({
520✔
271
  regimens,
272
  sequences,
273
  farm_designer,
274
  photos,
275
  farmware,
276
  help,
277
  alerts
278
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
279
} as ReducersMapObject<RestResources["consumers"], any>);
280

281
/** The resource reducer must have the first say when a resource-related action
282
 * fires off. Afterwards, sub-reducers are allowed to make sense of data
283
 * changes. A common use case for sub-reducers is to listen for
284
 * `DESTROY_RESOURCE_OK` and clean up stale UUIDs. */
285
export const afterEach = (state: RestResources, a: ReduxAction<unknown>) => {
520✔
286
  state.consumers = consumerReducer({
7,613✔
287
    sequences: state.consumers.sequences,
288
    regimens: state.consumers.regimens,
289
    farm_designer: state.consumers.farm_designer,
290
    photos: state.consumers.photos,
291
    farmware: state.consumers.farmware,
292
    help: state.consumers.help,
293
    alerts: state.consumers.alerts
294
  }, a);
295
  return state;
7,613✔
296
};
297

298
/** Helper method to change the `specialStatus` of a resource in the index */
299
export const mutateSpecialStatus =
520✔
300
  (uuid: string, index: ResourceIndex, status = SpecialStatus.SAVED) => {
520✔
301
    findByUuid(index, uuid).specialStatus = status;
51✔
302
  };
303

304
export function initResourceReducer(s: RestResources,
520✔
305
  { payload }: ReduxAction<TaggedResource>): RestResources {
5✔
306
  indexUpsert(s.index, [payload], "ongoing");
5✔
307
  return s;
5✔
308
}
309

310
const BEFORE_HOOKS: IndexerHook = {
520✔
311
  Log(_index, strategy) {
312
    // IMPLEMENTATION DETAIL: When the app downloads a *list* of logs, we
313
    // replaces the entire logs collection.
314
    (strategy === "initial") &&
516✔
315
      selectAllLogs(_index).map(log => indexRemove(_index, log));
×
316
  },
317
};
318

319
const AFTER_HOOKS: IndexerHook = {
520✔
320
  FbosConfig: (i) => {
321
    const conf = getFbosConfig(i);
29✔
322

323
    if (conf?.body.boot_sequence_id) {
29✔
324
      const { boot_sequence_id } = conf.body;
1✔
325
      const tracker = i.inUse["Sequence.FbosConfig"];
1✔
326
      const uuid = i.byKindAndId[joinKindAndId("Sequence", boot_sequence_id)];
1✔
327
      if (uuid) {
1✔
328
        tracker[uuid] = tracker[uuid] || {};
1✔
329
        tracker[uuid][conf.uuid] = true;
1✔
330
      }
331
    } else {
332
      i.inUse["Sequence.FbosConfig"] = {};
28✔
333
    }
334
  },
335
  PinBinding: (i) => {
336
    i.inUse["Sequence.PinBinding"] = {};
16✔
337
    const tracker = i.inUse["Sequence.PinBinding"];
16✔
338
    selectAllPinBindings(i)
16✔
339
      .map(pinBinding => {
340
        if (pinBinding.body.binding_type === PinBindingType.standard) {
16✔
341
          const { sequence_id } = pinBinding.body;
14✔
342
          const uuid = i.byKindAndId[joinKindAndId("Sequence", sequence_id)];
14✔
343
          if (uuid) {
14✔
344
            tracker[uuid] = tracker[uuid] || {};
12✔
345
            tracker[uuid][pinBinding.uuid] = true;
12✔
346
          }
347
        }
348
      });
349
  },
350
  PointGroup: reindexAllPointGroups,
351
  Point: reindexAllPoints,
352
  FarmEvent: reindexAllFarmEventUsage,
353
  Sequence: reindexAllSequences,
354
  Regimen: (i) => {
355
    i.inUse["Sequence.Regimen"] = {};
535✔
356
    const tracker = i.inUse["Sequence.Regimen"];
535✔
357
    selectAllRegimens(i)
535✔
358
      .map(reg => {
359
        reg.body.regimen_items.map(ri => {
535✔
360
          const sequenceUuid = findUuid(i, "Sequence", ri.sequence_id);
529✔
361
          tracker[sequenceUuid] = tracker[sequenceUuid] || {};
529✔
362
          tracker[sequenceUuid][reg.uuid] = true;
529✔
363
        });
364
      });
365
  }
366
};
367

368
const ups = INDEXERS.map(x => x.up);
2,600✔
369
const downs = INDEXERS.map(x => x.down).reverse();
2,600✔
370

371
type UpsertStrategy =
372
  /** Do not throw away pre-existing resources. */
373
  | "ongoing"
374
  /** Replace everything in the index. */
375
  | "initial";
376

377
type IndexUpsert = (db: ResourceIndex,
378
  resources: TaggedResource[],
379
  strategy: UpsertStrategy) => void;
380
export const indexUpsert: IndexUpsert = (db, resources, strategy) => {
520✔
381
  if (resources.length == 0) {
6,243✔
382
    return;
1✔
383
  }
384
  const { kind } = arrayUnwrap(resources);
6,242✔
385
  // Clean up indexes (if needed)
386
  const before = BEFORE_HOOKS[kind];
6,242✔
387
  before?.(db, strategy);
6,242✔
388

389
  // Run indexers
390
  ups.map(callback => {
6,242✔
391
    resources.map(resource => callback(resource, db));
53,760✔
392
  });
393

394
  // Finalize indexing (if needed)
395
  const after = AFTER_HOOKS[kind];
6,242✔
396
  after?.(db, strategy);
6,242✔
397
};
398

399
export function indexRemove(db: ResourceIndex, resource: TaggedResource) {
520✔
400
  downs.map(callback => arrayWrap(resource).map(r => callback(r, db)));
85✔
401
  // Finalize indexing (if needed)
402
  const after = AFTER_HOOKS[resource.kind];
17✔
403
  after?.(db, "ongoing");
17✔
404
}
405

406
export const beforeEach = (state: RestResources,
520✔
407
  action: ReduxAction<unknown>,
408
  handler: ActionHandler<RestResources, unknown>) => {
409
  const { byKind, references } = state.index;
7,624✔
410
  const w = references[Object.keys(byKind.WebAppConfig)[0]];
7,624✔
411
  const readOnly = w &&
7,624✔
412
    w.kind == "WebAppConfig" &&
413
    w.body.user_interface_read_only_mode;
414
  if (!readOnly) {
7,624✔
415
    return handler(state, action);
7,617✔
416
  }
417
  const fail = (place: string) => {
7✔
418
    warning(`(${place}) Can't modify account data when in read-only mode.`);
5✔
419
  };
420
  const { kind } = unpackUUID(get(action, "payload.uuid", "x.y.z") as string);
7✔
421

422
  switch (action.type) {
7✔
423
    case Actions.EDIT_RESOURCE:
424
      if (kind === "WebAppConfig") {
2✔
425
        // User is trying to exit read-only mode.
426
        return handler(state, action);
1✔
427
      } else {
428
        fail("1");
1✔
429
        return state;
1✔
430
      }
431
    case Actions.SAVE_RESOURCE_START:
432
    case Actions.DESTROY_RESOURCE_START:
433
      if (kind !== "WebAppConfig") {
1✔
434
        // User is trying to make HTTP requests.
435
        fail("3");
1✔
436
      }
437
      // User is trying to exit read-only mode.
438
      return handler(state, action);
1✔
439
    case Actions.BATCH_INIT:
440
    case Actions.INIT_RESOURCE:
441
    case Actions.OVERWRITE_RESOURCE:
442
      fail("2");
3✔
443
      return state;
3✔
444
    default:
445
      return handler(state, action);
1✔
446
  }
447
};
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

© 2025 Coveralls, Inc