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

FarmBot / Farmbot-Web-App / 40808ec3-d713-4b05-a9d6-8dc6ebc4baa8

12 Dec 2024 08:07PM UTC coverage: 99.826% (+0.5%) from 99.332%
40808ec3-d713-4b05-a9d6-8dc6ebc4baa8

push

circleci

gabrielburnworth
use text for dark mode toggle

11689 of 11750 branches covered (99.48%)

Branch coverage included in aggregate %.

25015 of 25018 relevant lines covered (99.99%)

1436.31 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 {
543✔
2
  ResourceName, SpecialStatus, TaggedResource, TaggedSequence,
3
} from "farmbot";
4
import { combineReducers, ReducersMapObject, UnknownAction } from "redux";
543✔
5
import { helpReducer as help } from "../help/reducer";
543✔
6
import { designer as farm_designer } from "../farm_designer/reducer";
543✔
7
import { photosReducer as photos } from "../photos/reducer";
543✔
8
import { farmwareReducer as farmware } from "../farmware/reducer";
543✔
9
import { regimensReducer as regimens } from "../regimens/reducer";
543✔
10
import { sequenceReducer as sequences } from "../sequences/reducer";
543✔
11
import { RestResources, ResourceIndex, TaggedPointGroup } from "./interfaces";
12
import { isTaggedResource } from "./tagged_resources";
543✔
13
import { arrayWrap, arrayUnwrap } from "./util";
543✔
14
import {
543✔
15
  sanitizeNodes,
16
} from "../sequences/locals_list/sanitize_nodes";
17
import {
543✔
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";
543✔
28
import {
543✔
29
  ExecutableType, PinBindingType,
30
} from "farmbot/dist/resources/api_resources";
31
import { betterCompact, unpackUUID } from "../util";
543✔
32
import { createSequenceMeta } from "./sequence_meta";
543✔
33
import { alertsReducer as alerts } from "../messages/reducer";
543✔
34
import { warning } from "../toast/toast";
543✔
35
import { ReduxAction } from "../redux/interfaces";
36
import { ActionHandler } from "../redux/generate_reducer";
37
import { get } from "lodash";
543✔
38
import { Actions } from "../constants";
543✔
39
import { getFbosConfig } from "./getters";
543✔
40
import { ingest, PARENTLESS as NO_PARENT } from "../folders/data_transfer";
543✔
41
import { FolderNode, FolderMeta } from "../folders/interfaces";
42
import { pointsSelectedByGroup } from "../point_groups/criteria/apply";
543✔
43
import { Everything } from "../interfaces";
44

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

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

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

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

84
  allSequences.map((s) => {
747✔
85
    const { folder_id } = s.body;
930✔
86
    const parentId = folder_id || NO_PARENT;
930✔
87

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

98
  const { searchTerm } = i.sequenceFolders;
747✔
99

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

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

124
export const folderIndexer: IndexerCallback = (r, i) => {
543✔
125
  if (r.kind === "Folder" || r.kind === "Sequence") {
10,857✔
126
    reindexFolders(i);
736✔
127
  }
128
};
129

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

320
const AFTER_HOOKS: IndexerHook = {
543✔
321
  FbosConfig: (i) => {
322
    const conf = getFbosConfig(i);
38✔
323

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

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

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

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

390
  // Run indexers
391
  ups.map(callback => {
6,492✔
392
    resources.map(resource => callback(resource, db));
54,200✔
393
  });
394

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

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

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

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