Skip to content

Commit 03c9c4d

Browse files
create and use account_seeded_at field
1 parent 25ff4fb commit 03c9c4d

File tree

12 files changed

+143
-109
lines changed

12 files changed

+143
-109
lines changed

app/mutations/devices/create_seed_data.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ class CreateSeedData < Mutations::Command
3434
end
3535

3636
def execute
37+
if device.account_seeded_at
38+
return { done: "Device already has seed data." }
39+
end
40+
device.update(account_seeded_at: Time.now)
3741
self.delay.run_seeds!
3842
{ done: "Loading resources now." }
3943
end

app/mutations/devices/reset.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ def execute
2020

2121
def run_it
2222
ActiveRecord::Base.transaction do
23-
device.update!(name: "FarmBot", mounted_tool_id: nil)
23+
device.update!(name: "FarmBot",
24+
mounted_tool_id: nil,
25+
setup_completed_at: nil,
26+
account_seeded_at: nil)
2427
device.folders.update_all(parent_id: nil)
2528
Device::SINGULAR_RESOURCES.keys.map do |resource|
2629
device.send(resource).destroy!

app/mutations/devices/seeders/demo_account_seeder.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ class DemoAccountSeeder < AbstractSeeder
88
"Genesis XL" => "Genesis_XL_Demo_Webcam.jpg",
99
"Genesis" => "Genesis_Demo_Webcam.jpg",
1010
}
11-
UNUSED_ALERTS = ["api.seed_data.missing", "api.user.not_welcomed"]
11+
UNUSED_ALERTS = [
12+
Alert::USER[:problem_tag],
13+
]
1214

1315
def feed(product_line)
1416
feed_name = ""

app/mutations/devices/update.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class Update < Mutations::Command
1111
string :timezone
1212
string :fb_order_number, nils: true
1313
string :setup_completed_at, nils: true
14+
string :account_seeded_at, nils: true
1415
integer :mounted_tool_id, nils: true
1516
integer :ota_hour, nils: true
1617
float :lat, nils: true

app/serializers/device_serializer.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class DeviceSerializer < ApplicationSerializer
1212
:rpi,
1313
:serial_number,
1414
:setup_completed_at,
15+
:account_seeded_at,
1516
:throttled_at,
1617
:throttled_until,
1718
:timezone,
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class AddAccountSeededAtToDevice < ActiveRecord::Migration[6.1]
2+
def up
3+
add_column :devices, :account_seeded_at, :datetime
4+
end
5+
6+
def down
7+
remove_column :devices, :account_seeded_at
8+
end
9+
end

db/structure.sql

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,8 @@ CREATE TABLE public.devices (
389389
rpi character varying(3),
390390
max_log_age_in_days integer DEFAULT 0,
391391
max_sequence_count integer DEFAULT 0,
392-
max_sequence_length integer DEFAULT 0
392+
max_sequence_length integer DEFAULT 0,
393+
account_seeded_at timestamp without time zone
393394
);
394395

395396

@@ -3988,6 +3989,7 @@ INSERT INTO "schema_migrations" (version) VALUES
39883989
('20250514203443'),
39893990
('20250722234106'),
39903991
('20250802174543'),
3991-
('20250925195004');
3992+
('20250925195004'),
3993+
('20250930204600');
39923994

39933995

frontend/messages/cards.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ const UserNotWelcomed = (props: CommonAlertCardProps) =>
359359
<AlertCardTemplate
360360
alert={props.alert}
361361
className={"user-not-welcomed-alert"}
362-
title={t("Welcome to the FarmBot Web App")}
362+
title={t("Welcome")}
363363
message={t(Content.WELCOME)}
364364
timeSettings={props.timeSettings}
365365
dispatch={props.dispatch}

frontend/wizard/__tests__/checks_test.tsx

Lines changed: 54 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jest.mock("../../messages/actions", () => ({
3333
}));
3434

3535
import React from "react";
36-
import { render, screen } from "@testing-library/react";
36+
import { fireEvent, render, screen } from "@testing-library/react";
3737
import { mount, shallow } from "enzyme";
3838
import { bot } from "../../__test_support__/fake_state/bot";
3939
import {
@@ -103,7 +103,7 @@ import { PLACEHOLDER_FARMBOT } from "../../photos/images/image_flipper";
103103
import {
104104
changeBlurableInput, changeBlurableInputRTL, clickButton,
105105
} from "../../__test_support__/helpers";
106-
import { Actions } from "../../constants";
106+
import { Actions, SetupWizardContent } from "../../constants";
107107
import { tourPath } from "../../help/tours";
108108
import { FBSelect } from "../../ui";
109109

@@ -395,12 +395,14 @@ describe("<FirmwareHardwareSelection />", () => {
395395

396396
it("selects model", () => {
397397
const p = fakeProps();
398-
p.resources = buildResourceIndex([fakeFbosConfig(), fakeDevice()]).index;
398+
const device = fakeDevice();
399+
p.resources = buildResourceIndex([fakeFbosConfig(), device]).index;
399400
p.dispatch = mockDispatch(jest.fn(), () => state);
400-
const wrapper = shallow(<FirmwareHardwareSelection {...p} />);
401-
wrapper.find("FBSelect").simulate("change", {
402-
label: "", value: "genesis_1.2"
403-
});
401+
render(<FirmwareHardwareSelection {...p} />);
402+
const dropdown = screen.getByRole("button");
403+
fireEvent.click(dropdown);
404+
const item = screen.getByRole("menuitem", { name: "Genesis v1.2" });
405+
fireEvent.click(item);
404406
expect(edit).toHaveBeenCalledWith(expect.any(Object), {
405407
firmware_hardware: "arduino"
406408
});
@@ -411,61 +413,66 @@ describe("<FirmwareHardwareSelection />", () => {
411413
const alert = fakeAlert();
412414
alert.body.id = 1;
413415
alert.body.problem_tag = "api.seed_data.missing";
414-
p.resources = buildResourceIndex([alert, fakeDevice()]).index;
415-
mockState.resources = buildResourceIndex([alert]);
416-
p.dispatch = mockDispatch(jest.fn(), () => state);
417-
const wrapper = mount<FirmwareHardwareSelection>(
418-
<FirmwareHardwareSelection {...p} />);
419-
wrapper.instance().onChange({ label: "", value: "genesis_1.2" });
420-
expect(destroy).toHaveBeenCalled();
421-
});
422-
423-
it("doesn't seed account twice", () => {
424-
const p = fakeProps();
425-
const alert = fakeAlert();
426-
alert.body.id = 1;
427-
alert.body.problem_tag = "api.seed_data.missing";
428-
p.resources = buildResourceIndex([alert, fakeDevice()]).index;
416+
const device = fakeDevice();
417+
p.resources = buildResourceIndex([alert, device]).index;
429418
mockState.resources = buildResourceIndex([alert]);
430419
p.dispatch = mockDispatch(jest.fn(), () => state);
431-
const wrapper = mount<FirmwareHardwareSelection>(
432-
<FirmwareHardwareSelection {...p} />);
433-
wrapper.setState({ seeded: true });
434-
wrapper.instance().onChange({ label: "", value: "genesis_1.2" });
435-
expect(destroy).not.toHaveBeenCalled();
420+
render(<FirmwareHardwareSelection {...p} />);
421+
expect(screen.getByText(SetupWizardContent.SEED_DATA)).toBeInTheDocument();
422+
// once
423+
const dropdown = screen.getByRole("button");
424+
fireEvent.click(dropdown);
425+
const item = screen.getByRole("menuitem", { name: "Genesis v1.2" });
426+
fireEvent.click(item);
427+
expect(edit).toHaveBeenCalledWith(expect.any(Object), {
428+
firmware_hardware: "arduino"
429+
});
430+
expect(destroy).toHaveBeenCalledTimes(1);
431+
expect(screen.getByText("Resources added!")).toBeInTheDocument();
432+
// not twice
433+
const newDropdown = screen.getByRole("button");
434+
fireEvent.click(newDropdown);
435+
const newItem = screen.getByRole("menuitem", { name: "Genesis v1.3" });
436+
fireEvent.click(newItem);
437+
expect(edit).toHaveBeenCalledWith(expect.any(Object), {
438+
firmware_hardware: "farmduino"
439+
});
440+
expect(destroy).toHaveBeenCalledTimes(1);
436441
});
437442

438443
it("doesn't seed account", () => {
439444
const p = fakeProps();
440-
p.resources = buildResourceIndex([fakeDevice()]).index;
445+
const device = fakeDevice();
446+
device.body.account_seeded_at = "2023-01-01T11:22:33.000Z";
447+
p.resources = buildResourceIndex([device]).index;
441448
p.dispatch = mockDispatch(jest.fn(), () => state);
442-
const wrapper = mount<FirmwareHardwareSelection>(
443-
<FirmwareHardwareSelection {...p} />);
444-
wrapper.instance().onChange({ label: "", value: "genesis_1.2" });
449+
render(<FirmwareHardwareSelection {...p} />);
450+
expect(screen.queryByText(SetupWizardContent.SEED_DATA))
451+
.not.toBeInTheDocument();
452+
const dropdown = screen.getByRole("button");
453+
fireEvent.click(dropdown);
454+
const item = screen.getByRole("menuitem", { name: "Genesis v1.2" });
455+
fireEvent.click(item);
456+
expect(edit).toHaveBeenCalledWith(expect.any(Object), {
457+
firmware_hardware: "arduino"
458+
});
445459
expect(destroy).not.toHaveBeenCalled();
446-
expect(wrapper.text().toLowerCase()).not.toContain("resources");
447-
});
448-
449-
it("renders after account seeding", () => {
450-
const p = fakeProps();
451-
p.resources = buildResourceIndex([fakeDevice()]).index;
452-
const wrapper = mount<FirmwareHardwareSelection>(
453-
<FirmwareHardwareSelection {...p} />);
454-
wrapper.setState({ autoSeed: true });
455-
expect(wrapper.text().toLowerCase()).toContain("resources added");
460+
expect(screen.queryByText("Resources added!")).not.toBeInTheDocument();
456461
});
457462

458463
it("toggles auto-seed", () => {
459464
const p = fakeProps();
460465
const alert = fakeAlert();
461466
alert.body.id = 1;
462467
alert.body.problem_tag = "api.seed_data.missing";
463-
p.resources = buildResourceIndex([alert, fakeDevice()]).index;
464-
const wrapper = shallow<FirmwareHardwareSelection>(
465-
<FirmwareHardwareSelection {...p} />);
466-
expect(wrapper.state().autoSeed).toEqual(true);
467-
wrapper.instance().toggleAutoSeed();
468-
expect(wrapper.state().autoSeed).toEqual(false);
468+
const device = fakeDevice();
469+
p.resources = buildResourceIndex([alert, device]).index;
470+
render(<FirmwareHardwareSelection {...p} />);
471+
expect(screen.getByText(SetupWizardContent.SEED_DATA)).toBeInTheDocument();
472+
const checkbox = screen.getByRole("checkbox");
473+
fireEvent.click(checkbox);
474+
expect(screen.queryByText(SetupWizardContent.SEED_DATA))
475+
.not.toBeInTheDocument();
469476
});
470477
});
471478

frontend/wizard/checks.tsx

Lines changed: 35 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -373,74 +373,54 @@ const FW_HARDWARE_TO_RPI: Record<FirmwareHardware, string | undefined> = {
373373
"none": "3",
374374
};
375375

376-
interface FirmwareHardwareSelectionState {
377-
selection: string;
378-
autoSeed: boolean;
379-
seeded: boolean;
380-
}
381-
382-
export class FirmwareHardwareSelection
383-
extends React.Component<WizardStepComponentProps,
384-
FirmwareHardwareSelectionState> {
385-
state: FirmwareHardwareSelectionState = {
386-
selection: "",
387-
autoSeed: this.seedAlerts.length > 0,
388-
seeded: false,
389-
};
390-
391-
get seedAlerts() {
392-
return selectAllAlerts(this.props.resources)
393-
.filter(alert => alert.body.problem_tag == "api.seed_data.missing");
394-
}
395-
396-
onChange = (ddi: DropDownItem) => {
397-
const { dispatch, resources } = this.props;
398-
399-
this.setState({ selection: "" + ddi.value });
376+
export const FirmwareHardwareSelection = (props: WizardStepComponentProps) => {
377+
const device = getDeviceAccountSettings(props.resources);
378+
const notSeeded = !device.body.account_seeded_at;
379+
const [selection, setSelection] = React.useState("");
380+
const [autoSeed, setAutoSeed] = React.useState(notSeeded);
381+
const [seeded, setSeeded] = React.useState(!notSeeded);
382+
const seedAlerts = selectAllAlerts(props.resources)
383+
.filter(alert => alert.body.problem_tag == "api.seed_data.missing");
384+
385+
const onChange = (ddi: DropDownItem) => {
386+
const { dispatch, resources } = props;
387+
setSelection("" + ddi.value);
400388
const firmwareHardware = SEED_DATA_OPTION_TO_FW_HARDWARE["" + ddi.value];
401389
changeFirmwareHardware(dispatch)({ label: "", value: firmwareHardware });
402390
const rpi = FW_HARDWARE_TO_RPI[firmwareHardware];
403391
if (rpi) {
404-
const device = getDeviceAccountSettings(this.props.resources);
405392
dispatch(edit(device, { rpi }));
406393
dispatch(save(device.uuid));
407394
}
408-
409-
const seedAlertId = this.seedAlerts[0]?.body.id;
395+
const seedAlertId = seedAlerts[0]?.body.id;
410396
const dismiss = () => seedAlertId && dispatch(destroy(
411397
findUuid(resources, "Alert", seedAlertId)));
412-
if (this.state.autoSeed && !this.state.seeded) {
413-
this.setState({ seeded: true });
398+
if (autoSeed && !seeded) {
399+
setSeeded(true);
414400
seedAccount(dismiss)({ label: "", value: ddi.value });
415401
}
416402
};
417403

418-
toggleAutoSeed = () => this.setState({ autoSeed: !this.state.autoSeed });
419-
420-
render() {
421-
const { selection, autoSeed } = this.state;
422-
const notSeeded = this.seedAlerts.length > 0;
423-
return <div className={"farmbot-model-selection"}>
424-
<FBSelect
425-
key={selection}
426-
list={SEED_DATA_OPTIONS()}
427-
selectedItem={SEED_DATA_OPTIONS_DDI()[selection]}
428-
onChange={this.onChange} />
429-
{notSeeded &&
430-
<div className={"seed-checkbox"}>
431-
<Checkbox
432-
onChange={this.toggleAutoSeed}
433-
checked={autoSeed}
434-
title={t("Add pre-made resources upon selection")} />
435-
<p>{t("Add pre-made resources upon selection")}</p>
436-
</div>}
437-
{autoSeed && notSeeded &&
438-
<p>{t(SetupWizardContent.SEED_DATA)}</p>}
439-
{autoSeed && !notSeeded &&
440-
<p>{t("Resources added!")}</p>}
441-
</div>;
442-
}
443-
}
404+
return <div className={"farmbot-model-selection"}>
405+
<FBSelect
406+
key={selection + autoSeed + seeded}
407+
list={SEED_DATA_OPTIONS()}
408+
selectedItem={SEED_DATA_OPTIONS_DDI()[selection]}
409+
onChange={onChange} />
410+
{!seeded &&
411+
<div className={"seed-checkbox"}>
412+
<Checkbox
413+
onChange={() => setAutoSeed(!autoSeed)}
414+
checked={autoSeed}
415+
title={t("Add pre-made resources upon selection")} />
416+
<p>{t("Add pre-made resources upon selection")}</p>
417+
</div>}
418+
{autoSeed &&
419+
(!seeded
420+
? <p>{t(SetupWizardContent.SEED_DATA)}</p>
421+
: <p>{t("Resources added!")}</p>)}
422+
</div>;
423+
};
444424

445425
export const RpiSelection = (props: WizardStepComponentProps) => {
446426
const device = getDeviceAccountSettings(props.resources);

0 commit comments

Comments
 (0)