@saadnvd1/agent-os 0.3.839 → 0.3.848
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/api/approvals/route.ts +5 -1
- package/app/api/health/config/route.ts +2 -0
- package/app/api/health/route.ts +4 -2
- package/app/globals.css +6 -0
- package/components/Audits/AuditsPanel.tsx +42 -35
- package/components/Chat/quote-selection.test.ts +6 -0
- package/components/Chat/quote-selection.ts +13 -10
- package/components/Devices/ConnectCard.tsx +52 -56
- package/components/Devices/DeviceGroupRow.tsx +65 -0
- package/components/Devices/DeviceRow.tsx +2 -2
- package/components/Devices/DevicesPanel.tsx +63 -49
- package/components/Devices/NetworkSection.tsx +29 -26
- package/components/Devices/TailscaleCard.tsx +9 -10
- package/components/Devices/group.test.ts +66 -0
- package/components/Devices/group.ts +67 -0
- package/components/Health/HealthPanel.tsx +5 -81
- package/components/Health/MachineChecks.tsx +20 -0
- package/components/Health/SweepRuns.tsx +46 -51
- package/components/Health/Thresholds.tsx +50 -0
- package/components/Merge/GlobalMergeSection.tsx +5 -6
- package/components/Merge/MergeApprovalToggle.tsx +20 -14
- package/components/Merge/MergeSettingsFields.tsx +0 -2
- package/components/Merge/WorktreeSweepSection.tsx +15 -34
- package/components/Models/AgentRoleCard.tsx +7 -7
- package/components/Models/ModelSelect.tsx +14 -3
- package/components/Models/ModelsSection.tsx +42 -37
- package/components/Models/ProviderCard.tsx +1 -1
- package/components/Models/RoleModelSelect.tsx +15 -3
- package/components/Models/SubagentModels.tsx +60 -45
- package/components/PhoneNotify/PhoneNotifyForm.tsx +6 -13
- package/components/Recipes/RecipesPanel.tsx +20 -9
- package/components/Recipes/RunHistory.tsx +1 -1
- package/components/Schedules/SchedulesPanel.tsx +12 -10
- package/components/SessionList/GroupShelves.tsx +8 -3
- package/components/SessionList/OrchestratorPinRow.tsx +2 -2
- package/components/SessionList/RowParts.tsx +39 -35
- package/components/SessionList/RowRename.test.ts +86 -17
- package/components/SessionList/RowShell.tsx +2 -5
- package/components/SessionList/SessionRow.tsx +0 -1
- package/components/Settings/AppWideLimits.tsx +2 -2
- package/components/Settings/{NoApprovalsSection.test.ts → ApprovalsSection.test.ts} +61 -22
- package/components/Settings/ApprovalsSection.tsx +93 -0
- package/components/Settings/ExperimentsSection.test.ts +20 -4
- package/components/Settings/ExperimentsSection.tsx +54 -34
- package/components/Settings/GeneralSection.tsx +39 -0
- package/components/Settings/ListSection.test.ts +85 -0
- package/components/Settings/ListSection.tsx +132 -0
- package/components/Settings/NotificationsSection.tsx +31 -29
- package/components/Settings/PickerSetting.tsx +5 -1
- package/components/Settings/SettingRow.tsx +91 -49
- package/components/Settings/SettingsDialog.test.ts +31 -4
- package/components/Settings/SettingsDialog.tsx +22 -17
- package/components/Settings/ValueLabel.tsx +31 -0
- package/components/Settings/settings-fields.test.ts +81 -2
- package/components/Settings/settings-fields.tsx +28 -27
- package/components/Shares/SharedLinksPanel.tsx +11 -14
- package/components/Updates/AutoUpdates.tsx +12 -8
- package/components/Updates/UpdatesSection.tsx +18 -17
- package/components/Workspaces/HqFolderForm.tsx +21 -19
- package/components/Workspaces/McpServersForm.tsx +20 -28
- package/components/Workspaces/ProjectTiersForm.tsx +25 -12
- package/components/Workspaces/RuleEditor.tsx +86 -0
- package/components/Workspaces/RulesForm.test.ts +58 -31
- package/components/Workspaces/RulesForm.tsx +129 -58
- package/components/Workspaces/WorkspaceModeForm.tsx +3 -3
- package/components/Workspaces/WorkspaceModelsForm.tsx +40 -41
- package/components/Workspaces/WorkspacePanel.tsx +73 -0
- package/components/Workspaces/WorkspacePicker.tsx +44 -0
- package/components/Workspaces/WorkspaceSettings.test.ts +153 -0
- package/components/Workspaces/WorkspaceSettings.tsx +51 -79
- package/components/Workspaces/useSettingsWorkspace.ts +47 -0
- package/components/ui/switch.tsx +1 -1
- package/content/whats-new.json +40 -0
- package/data/approvals/index.ts +2 -2
- package/data/health/index.ts +3 -0
- package/hooks/useAutoSave.ts +18 -5
- package/hooks/useSettingsUrl.ts +6 -3
- package/lib/approvals/describe.ts +64 -0
- package/lib/away/deploy.ts +34 -7
- package/lib/health/config.ts +70 -17
- package/lib/models/plan-fresh.ts +61 -0
- package/lib/models/plan-step.ts +40 -24
- package/lib/models/providers.ts +1 -1
- package/lib/orchestrator/scope.ts +10 -0
- package/lib/recipes/settle.ts +22 -0
- package/lib/settings-url.ts +14 -2
- package/lib/updates/banner.ts +2 -0
- package/lib/updates/run.ts +9 -0
- package/lib/updates/view.ts +28 -6
- package/lib/worktree-deps/paths.ts +9 -4
- package/lib/worktrees.ts +3 -0
- package/package.json +1 -1
- package/scripts/autodeploy +11 -0
- package/scripts/autodeploy.test.ts +30 -0
- package/scripts/lib/update-verify.mjs +11 -0
- package/scripts/update-run.mjs +29 -0
- package/scripts/update-run.test.ts +62 -4
- package/components/Settings/NoApprovalsSection.tsx +0 -103
- package/components/Settings/SettingToggle.tsx +0 -36
- package/components/Settings/SetupSection.tsx +0 -46
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { toast } from "sonner";
|
|
4
4
|
import { CheckCircle2, CircleAlert, RotateCcw } from "lucide-react";
|
|
5
|
-
import {
|
|
5
|
+
import { SwitchSetting } from "@/components/Settings/settings-fields";
|
|
6
|
+
import { SettingSection } from "@/components/Settings/SettingRow";
|
|
6
7
|
import {
|
|
7
8
|
Select,
|
|
8
9
|
SelectContent,
|
|
@@ -80,13 +81,16 @@ export function AutoUpdates({ auto }: { auto: AutoUpdateView }) {
|
|
|
80
81
|
const onError = (e: Error) => toast.error(e.message);
|
|
81
82
|
return (
|
|
82
83
|
<div className="space-y-3">
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
<SettingSection>
|
|
85
|
+
<SwitchSetting
|
|
86
|
+
id="auto-update"
|
|
87
|
+
label="Install updates automatically"
|
|
88
|
+
help="Once everything has been idle for a while. Terminals and chats keep running through the restart, and a version that doesn't start cleanly is rolled back."
|
|
89
|
+
checked={auto.enabled}
|
|
90
|
+
locked={auto.locked}
|
|
91
|
+
onSave={(on) => save.mutateAsync({ auto: on })}
|
|
92
|
+
/>
|
|
93
|
+
</SettingSection>
|
|
90
94
|
{auto.enabled && (
|
|
91
95
|
<div className="flex flex-wrap items-center justify-between gap-2 px-3">
|
|
92
96
|
<label htmlFor="auto-idle" className="text-sm">
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import { toast } from "sonner";
|
|
4
4
|
import { Loader2 } from "lucide-react";
|
|
5
5
|
import { Button } from "@/components/ui/button";
|
|
6
|
-
import {
|
|
6
|
+
import { SwitchSetting } from "@/components/Settings/settings-fields";
|
|
7
|
+
import { SettingSection } from "@/components/Settings/SettingRow";
|
|
7
8
|
import { useSetTipsOff, useTips } from "@/data/tips";
|
|
8
9
|
import {
|
|
9
10
|
useCheckForUpdates,
|
|
@@ -141,16 +142,17 @@ export function UpdatesSection() {
|
|
|
141
142
|
|
|
142
143
|
{view.auto && <AutoUpdates auto={view.auto} />}
|
|
143
144
|
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
145
|
+
<SettingSection>
|
|
146
|
+
<SwitchSetting
|
|
147
|
+
id="update-banner"
|
|
148
|
+
label="Show update banner"
|
|
149
|
+
help="Tell everyone using this AgentOS when a newer version is out, and when it updated itself."
|
|
150
|
+
checked={view.bannerEnabled}
|
|
151
|
+
locked={view.bannerLocked}
|
|
152
|
+
onSave={(on) => banner.mutateAsync(on)}
|
|
153
|
+
/>
|
|
154
|
+
<TipsToggle />
|
|
155
|
+
</SettingSection>
|
|
154
156
|
</div>
|
|
155
157
|
);
|
|
156
158
|
}
|
|
@@ -159,13 +161,12 @@ function TipsToggle() {
|
|
|
159
161
|
const { data } = useTips();
|
|
160
162
|
const setOff = useSetTipsOff();
|
|
161
163
|
return (
|
|
162
|
-
<
|
|
163
|
-
|
|
164
|
-
|
|
164
|
+
<SwitchSetting
|
|
165
|
+
id="tips"
|
|
166
|
+
label="Show Tip of the Day"
|
|
167
|
+
help="A short tip about something AgentOS can do, behind the lightbulb in the sidebar. It never opens on its own."
|
|
165
168
|
checked={!!data && !data.off}
|
|
166
|
-
|
|
167
|
-
setOff.mutate(!on, { onError: (e) => toast.error(e.message) })
|
|
168
|
-
}
|
|
169
|
+
onSave={(on) => setOff.mutateAsync(!on)}
|
|
169
170
|
/>
|
|
170
171
|
);
|
|
171
172
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { SettingBlock } from "@/components/Settings/SettingRow";
|
|
4
4
|
import { TextSetting } from "@/components/Settings/settings-fields";
|
|
5
5
|
import { useSetWorkspaceHq, useWorkspaceHqQuery } from "@/data/session-logs";
|
|
6
6
|
|
|
@@ -13,26 +13,28 @@ export function HqFolderForm({ workspaceId }: { workspaceId: string }) {
|
|
|
13
13
|
const save = useSetWorkspaceHq(workspaceId);
|
|
14
14
|
|
|
15
15
|
if (isPending)
|
|
16
|
-
return <div className="bg-muted/60 h-11 animate-pulse rounded-md" />;
|
|
16
|
+
return <div className="bg-muted/60 m-3 h-11 animate-pulse rounded-md" />;
|
|
17
17
|
if (isError)
|
|
18
|
-
return
|
|
18
|
+
return (
|
|
19
|
+
<SettingBlock id={`hq-folder-${workspaceId}`} label="HQ folder">
|
|
20
|
+
<p className="text-destructive text-xs">{error.message}</p>
|
|
21
|
+
</SettingBlock>
|
|
22
|
+
);
|
|
19
23
|
|
|
20
24
|
return (
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
/>
|
|
36
|
-
</SettingSection>
|
|
25
|
+
<TextSetting
|
|
26
|
+
id={`hq-folder-${workspaceId}`}
|
|
27
|
+
label="HQ folder"
|
|
28
|
+
help={`Session logs go to docs/sessions/<project>/ in this repo, one commit per file. ${
|
|
29
|
+
data.hq?.visibility === "private"
|
|
30
|
+
? "The repo is private, so each commit is pushed."
|
|
31
|
+
: data.hq
|
|
32
|
+
? "Commits stay local: the repo isn't a private GitHub repository."
|
|
33
|
+
: "It must be a private git repo outside the workspace's projects."
|
|
34
|
+
}`}
|
|
35
|
+
placeholder="~/path/to/a/private/git/repo"
|
|
36
|
+
value={data.hq?.folder ?? ""}
|
|
37
|
+
onSave={(folder) => save.mutateAsync(folder)}
|
|
38
|
+
/>
|
|
37
39
|
);
|
|
38
40
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { SettingSection } from "@/components/Settings/SettingRow";
|
|
3
|
+
import { SettingBlock, SettingSection } from "@/components/Settings/SettingRow";
|
|
4
4
|
import { SwitchSetting } from "@/components/Settings/settings-fields";
|
|
5
5
|
import { useMcpServersQuery, useUpdateWorkspace } from "@/data/workspaces";
|
|
6
6
|
import type { Workspace } from "@/lib/db";
|
|
@@ -21,35 +21,27 @@ export function McpServersForm({ workspace }: { workspace: Workspace }) {
|
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
return (
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
</p>
|
|
29
|
-
<p className="text-muted-foreground text-xs">
|
|
30
|
-
All of a server's tools are allowed once it's on. Sending a
|
|
31
|
-
message, email or post through one still goes to you as an ask. Takes
|
|
32
|
-
effect at its next turn.
|
|
33
|
-
</p>
|
|
34
|
-
</div>
|
|
24
|
+
<SettingSection
|
|
25
|
+
title="MCP servers"
|
|
26
|
+
help="One switch per server; all its tools are allowed once it's on. Sending a message, email or post through one still asks you. Takes effect at the orchestrator's next turn."
|
|
27
|
+
>
|
|
35
28
|
{names.length === 0 ? (
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
<SettingBlock id={`mcp-none-${workspace.id}`} label="MCP servers">
|
|
30
|
+
<p className="text-muted-foreground text-xs">
|
|
31
|
+
No MCP servers are configured on this machine.
|
|
32
|
+
</p>
|
|
33
|
+
</SettingBlock>
|
|
39
34
|
) : (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
/>
|
|
50
|
-
))}
|
|
51
|
-
</SettingSection>
|
|
35
|
+
names.map((name) => (
|
|
36
|
+
<SwitchSetting
|
|
37
|
+
key={name}
|
|
38
|
+
id={`mcp-${workspace.id}-${name}`}
|
|
39
|
+
label={name}
|
|
40
|
+
checked={allowed.includes(name)}
|
|
41
|
+
onSave={(on) => toggle(name, on)}
|
|
42
|
+
/>
|
|
43
|
+
))
|
|
52
44
|
)}
|
|
53
|
-
</
|
|
45
|
+
</SettingSection>
|
|
54
46
|
);
|
|
55
47
|
}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { PickerSetting } from "@/components/Settings/PickerSetting";
|
|
3
4
|
import { SettingSection } from "@/components/Settings/SettingRow";
|
|
4
|
-
import {
|
|
5
|
+
import { Segmented } from "@/components/ui/segmented";
|
|
5
6
|
import { useProjectsQuery, useUpdateProject } from "@/data/projects";
|
|
6
7
|
|
|
8
|
+
type Tier = "production" | "fast";
|
|
9
|
+
|
|
10
|
+
const TIERS: { value: Tier; label: string }[] = [
|
|
11
|
+
{ value: "production", label: "Production" },
|
|
12
|
+
{ value: "fast", label: "Fast" },
|
|
13
|
+
];
|
|
14
|
+
|
|
7
15
|
// A workspace's projects, each switched between production (every merge gate)
|
|
8
16
|
// and fast (a task's PR merges as soon as it opens).
|
|
9
17
|
export function ProjectTiersForm({ workspaceId }: { workspaceId: string }) {
|
|
@@ -14,20 +22,25 @@ export function ProjectTiersForm({ workspaceId }: { workspaceId: string }) {
|
|
|
14
22
|
);
|
|
15
23
|
if (!mine.length) return null;
|
|
16
24
|
return (
|
|
17
|
-
<SettingSection
|
|
25
|
+
<SettingSection
|
|
26
|
+
title="Projects"
|
|
27
|
+
help="Production: CI, the independent review and the scope gate every merge. Fast: a task's PR merges as soon as it opens."
|
|
28
|
+
>
|
|
18
29
|
{mine.map((p) => (
|
|
19
|
-
<
|
|
30
|
+
<PickerSetting<Tier>
|
|
20
31
|
key={p.id}
|
|
21
32
|
id={`tier-${p.id}`}
|
|
22
|
-
label={
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
label={p.name}
|
|
34
|
+
value={p.tier === "production" ? "production" : "fast"}
|
|
35
|
+
onSave={(tier) => update.mutateAsync({ projectId: p.id, tier })}
|
|
36
|
+
render={({ value, commit }) => (
|
|
37
|
+
<Segmented
|
|
38
|
+
label={`${p.name} merge tier`}
|
|
39
|
+
value={value}
|
|
40
|
+
options={TIERS}
|
|
41
|
+
onChange={commit}
|
|
42
|
+
/>
|
|
43
|
+
)}
|
|
31
44
|
/>
|
|
32
45
|
))}
|
|
33
46
|
</SettingSection>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
6
|
+
|
|
7
|
+
export const MAX_RULE_CHARS = 300;
|
|
8
|
+
|
|
9
|
+
// The one editor for a rule, new or existing. Save and Cancel are here only
|
|
10
|
+
// while it's open, and Save waits for a real change.
|
|
11
|
+
export function RuleEditor({
|
|
12
|
+
id,
|
|
13
|
+
label,
|
|
14
|
+
initial,
|
|
15
|
+
saveLabel = "Save",
|
|
16
|
+
placeholder,
|
|
17
|
+
onSave,
|
|
18
|
+
onCancel,
|
|
19
|
+
}: {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
initial: string;
|
|
23
|
+
saveLabel?: string;
|
|
24
|
+
placeholder?: string;
|
|
25
|
+
// Throws to refuse; the editor stays open with what was typed.
|
|
26
|
+
onSave: (text: string) => Promise<unknown>;
|
|
27
|
+
onCancel: () => void;
|
|
28
|
+
}) {
|
|
29
|
+
const [text, setText] = useState(initial);
|
|
30
|
+
const [saving, setSaving] = useState(false);
|
|
31
|
+
const next = text.trim();
|
|
32
|
+
const canSave = !!next && next !== initial.trim() && !saving;
|
|
33
|
+
|
|
34
|
+
const save = async () => {
|
|
35
|
+
if (!canSave) return;
|
|
36
|
+
setSaving(true);
|
|
37
|
+
try {
|
|
38
|
+
await onSave(next);
|
|
39
|
+
} catch {
|
|
40
|
+
setSaving(false);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="space-y-2">
|
|
46
|
+
<Textarea
|
|
47
|
+
id={id}
|
|
48
|
+
autoFocus
|
|
49
|
+
rows={3}
|
|
50
|
+
value={text}
|
|
51
|
+
maxLength={MAX_RULE_CHARS}
|
|
52
|
+
placeholder={placeholder}
|
|
53
|
+
aria-label={label}
|
|
54
|
+
onChange={(e) => setText(e.target.value)}
|
|
55
|
+
onKeyDown={(e) => {
|
|
56
|
+
if (e.key === "Escape") onCancel();
|
|
57
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save();
|
|
58
|
+
}}
|
|
59
|
+
className="text-base md:text-sm"
|
|
60
|
+
/>
|
|
61
|
+
<div className="flex items-center justify-end gap-2">
|
|
62
|
+
<span className="text-muted-foreground mr-auto text-xs tabular-nums">
|
|
63
|
+
{text.length}/{MAX_RULE_CHARS}
|
|
64
|
+
</span>
|
|
65
|
+
<Button
|
|
66
|
+
type="button"
|
|
67
|
+
variant="ghost"
|
|
68
|
+
size="sm"
|
|
69
|
+
className="h-11 sm:h-9"
|
|
70
|
+
onClick={onCancel}
|
|
71
|
+
>
|
|
72
|
+
Cancel
|
|
73
|
+
</Button>
|
|
74
|
+
<Button
|
|
75
|
+
type="button"
|
|
76
|
+
size="sm"
|
|
77
|
+
className="h-11 sm:h-9"
|
|
78
|
+
disabled={!canSave}
|
|
79
|
+
onClick={() => void save()}
|
|
80
|
+
>
|
|
81
|
+
{saveLabel}
|
|
82
|
+
</Button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -49,9 +49,9 @@ afterEach(async () => {
|
|
|
49
49
|
host.remove();
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
const type = async (input:
|
|
52
|
+
const type = async (input: HTMLTextAreaElement, text: string) => {
|
|
53
53
|
const set = Object.getOwnPropertyDescriptor(
|
|
54
|
-
|
|
54
|
+
window.HTMLTextAreaElement.prototype,
|
|
55
55
|
"value"
|
|
56
56
|
)!.set!;
|
|
57
57
|
await act(async () => {
|
|
@@ -59,13 +59,14 @@ const type = async (input: HTMLInputElement, text: string) => {
|
|
|
59
59
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
60
60
|
});
|
|
61
61
|
};
|
|
62
|
-
const
|
|
62
|
+
const editor = (id: string) => host.querySelector<HTMLTextAreaElement>(id)!;
|
|
63
63
|
const click = (el: Element | null | undefined) =>
|
|
64
64
|
act(async () => {
|
|
65
65
|
el?.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
|
|
66
66
|
});
|
|
67
67
|
const button = (name: string) =>
|
|
68
68
|
[...host.querySelectorAll("button")].find((b) => b.textContent === name);
|
|
69
|
+
const edit = () => click(host.querySelector('button[aria-label^="Edit rule"]'));
|
|
69
70
|
|
|
70
71
|
describe("RulesForm", () => {
|
|
71
72
|
beforeEach(async () => {
|
|
@@ -74,54 +75,80 @@ describe("RulesForm", () => {
|
|
|
74
75
|
);
|
|
75
76
|
});
|
|
76
77
|
|
|
78
|
+
it("lists the rule as text with a You badge, and no editor or Save", () => {
|
|
79
|
+
expect(host.textContent).toContain("Never merge #12");
|
|
80
|
+
expect(host.textContent).toContain("You");
|
|
81
|
+
expect(host.querySelector("textarea")).toBeNull();
|
|
82
|
+
expect(button("Save")).toBeUndefined();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("opens a multi-line editor on a click of the rule or its edit icon", async () => {
|
|
86
|
+
await edit();
|
|
87
|
+
expect(editor("#rule-4").value).toBe("Never merge #12");
|
|
88
|
+
await click(button("Cancel"));
|
|
89
|
+
await click(host.querySelector("li, [data-setting='rule-4'] button"));
|
|
90
|
+
expect(editor("#rule-4")).not.toBeNull();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("only enables Save once the text has changed", async () => {
|
|
94
|
+
await edit();
|
|
95
|
+
expect(button("Save")!.disabled).toBe(true);
|
|
96
|
+
await type(editor("#rule-4"), "Never merge #13");
|
|
97
|
+
expect(button("Save")!.disabled).toBe(false);
|
|
98
|
+
await type(editor("#rule-4"), "Never merge #12");
|
|
99
|
+
expect(button("Save")!.disabled).toBe(true);
|
|
100
|
+
await type(editor("#rule-4"), " ");
|
|
101
|
+
expect(button("Save")!.disabled).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
77
104
|
it("never asks for a passkey on blur", async () => {
|
|
78
|
-
await
|
|
105
|
+
await edit();
|
|
106
|
+
await type(editor("#rule-4"), "Never merge #13");
|
|
79
107
|
await act(async () => {
|
|
80
|
-
rule
|
|
108
|
+
editor("#rule-4").dispatchEvent(
|
|
109
|
+
new FocusEvent("focusout", { bubbles: true })
|
|
110
|
+
);
|
|
81
111
|
});
|
|
82
112
|
expect(state.update).not.toHaveBeenCalled();
|
|
83
113
|
});
|
|
84
114
|
|
|
85
|
-
it("saves through the passkey step on
|
|
86
|
-
await
|
|
115
|
+
it("saves through the passkey step on Save and closes the editor", async () => {
|
|
116
|
+
await edit();
|
|
117
|
+
await type(editor("#rule-4"), "Never merge #13");
|
|
87
118
|
await click(button("Save"));
|
|
88
119
|
expect(state.update).toHaveBeenCalledWith({
|
|
89
120
|
id: 4,
|
|
90
121
|
text: "Never merge #13",
|
|
91
122
|
});
|
|
92
|
-
expect(host.
|
|
123
|
+
expect(host.querySelector("textarea")).toBeNull();
|
|
93
124
|
});
|
|
94
125
|
|
|
95
|
-
it("
|
|
96
|
-
await
|
|
97
|
-
await
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
126
|
+
it("cancel closes the editor and drops what was typed", async () => {
|
|
127
|
+
await edit();
|
|
128
|
+
await type(editor("#rule-4"), "Never merge #13");
|
|
129
|
+
await click(button("Cancel"));
|
|
130
|
+
expect(state.update).not.toHaveBeenCalled();
|
|
131
|
+
expect(host.querySelector("textarea")).toBeNull();
|
|
132
|
+
expect(host.textContent).toContain("Never merge #12");
|
|
133
|
+
await edit();
|
|
134
|
+
expect(editor("#rule-4").value).toBe("Never merge #12");
|
|
103
135
|
});
|
|
104
136
|
|
|
105
|
-
it("
|
|
137
|
+
it("keeps the editor open and toasts when the passkey is cancelled", async () => {
|
|
106
138
|
state.update.mockRejectedValue(new Error("Passkey cancelled"));
|
|
107
|
-
await
|
|
139
|
+
await edit();
|
|
140
|
+
await type(editor("#rule-4"), "Never merge #13");
|
|
108
141
|
await click(button("Save"));
|
|
109
|
-
expect(rule
|
|
142
|
+
expect(editor("#rule-4").value).toBe("Never merge #13");
|
|
110
143
|
expect(state.toast).toHaveBeenCalledWith("Passkey cancelled");
|
|
111
|
-
expect(host.textContent).not.toContain("Saved");
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("won't save an empty rule", async () => {
|
|
115
|
-
await type(rule(), " ");
|
|
116
|
-
await click(button("Save"));
|
|
117
|
-
expect(state.update).not.toHaveBeenCalled();
|
|
118
|
-
expect(host.textContent).toContain("A rule can't be empty");
|
|
119
144
|
});
|
|
120
145
|
|
|
121
|
-
it("adds a rule through the same step", async () => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
await
|
|
146
|
+
it("adds a rule from the same editor, through the same step", async () => {
|
|
147
|
+
await click(button("Add rule"));
|
|
148
|
+
expect(button("Add rule")!.disabled).toBe(true);
|
|
149
|
+
await type(editor("#rule-new-w1"), "House rule");
|
|
150
|
+
await click(button("Add rule"));
|
|
125
151
|
expect(state.add).toHaveBeenCalledWith("House rule");
|
|
152
|
+
expect(host.querySelector("textarea")).toBeNull();
|
|
126
153
|
});
|
|
127
154
|
});
|