@sentry/junior-dashboard 0.233.0 → 0.235.0
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/dist/app.js +178 -29
- package/dist/client.js +53 -50
- package/dist/index.js +178 -29
- package/dist/tailwind.css +1 -1
- package/package.json +3 -3
- package/src/assets/junior-avatar.png +0 -0
- package/src/assets/junior-favicon.png +0 -0
- package/src/assets.ts +1 -0
- package/src/client/App.tsx +372 -345
- package/src/client/api.ts +60 -1
- package/src/client/components/ActorAvatar.tsx +37 -0
- package/src/client/components/Button.tsx +12 -10
- package/src/client/components/Field.tsx +14 -1
- package/src/client/components/FormNotice.tsx +40 -0
- package/src/client/components/ParticipantAvatarStack.tsx +6 -14
- package/src/client/components/README.md +19 -6
- package/src/client/components/SegmentedNav.tsx +167 -0
- package/src/client/components/SegmentedTabs.tsx +24 -5
- package/src/client/components/Select.tsx +18 -0
- package/src/client/components/StatusChip.tsx +15 -12
- package/src/client/components/TextInput.tsx +25 -5
- package/src/client/components/layout/SecondaryNavigation.tsx +40 -83
- package/src/client/http.ts +31 -3
- package/src/client/pages/automations/AutomationDetailsDrawer.tsx +78 -68
- package/src/client/pages/automations/AutomationEditPage.tsx +171 -0
- package/src/client/pages/automations/AutomationEditor.tsx +397 -0
- package/src/client/pages/automations/AutomationEditorLoading.tsx +18 -0
- package/src/client/pages/automations/AutomationEventFields.tsx +264 -0
- package/src/client/pages/automations/AutomationFilters.tsx +179 -0
- package/src/client/pages/automations/AutomationFormSection.tsx +22 -0
- package/src/client/pages/automations/AutomationLifecycle.tsx +146 -0
- package/src/client/pages/automations/AutomationOutcomeFields.tsx +177 -0
- package/src/client/pages/automations/AutomationRow.tsx +343 -0
- package/src/client/pages/automations/AutomationScheduleFields.tsx +328 -0
- package/src/client/pages/automations/AutomationsPage.tsx +186 -439
- package/src/client/pages/automations/AutomationsPageLayout.tsx +19 -7
- package/src/client/pages/automations/README.md +19 -0
- package/src/client/pages/automations/automationDraft.ts +110 -0
- package/src/client/pages/automations/automationFormat.ts +43 -0
- package/src/client/pages/automations/automationOutcomes.ts +23 -0
- package/src/client/pages/code/CodeChangeRow.tsx +41 -0
- package/src/client/pages/code/CodePage.tsx +18 -93
- package/src/client/pages/code/CodeSummaryCards.tsx +53 -0
- package/src/client/pages/code/RepositoryPage.tsx +227 -0
- package/src/client/pages/code/RepositorySections.tsx +108 -0
- package/src/client/pages/code/RepositoryTabs.tsx +146 -0
- package/src/client/pages/code/codeRepositoryRoutes.ts +18 -0
- package/src/client/pages/dev/ComponentsPage.tsx +55 -26
- package/src/client/pages/dev/SegmentedFixtures.tsx +58 -0
- package/src/client/pages/memory/MemoryPageLayout.tsx +12 -13
- package/src/client/pages/system/SystemNavigation.tsx +6 -2
- package/src/client/pages/system/SystemPageLayout.tsx +4 -6
- package/src/client/searchParams.ts +4 -2
- package/src/client.tsx +7 -4
- package/src/mock-reporting/automation-collection.ts +101 -0
- package/src/mock-reporting/code-repository.ts +77 -0
- package/src/mock-reporting/fixtures.ts +9 -3
- package/src/mock-reporting/routes.ts +39 -17
- package/src/shell.ts +21 -5
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { useId, useState } from "react";
|
|
2
|
+
import { SlidersHorizontal } from "lucide-react";
|
|
3
|
+
import { cn } from "../../styles";
|
|
4
|
+
import type {
|
|
5
|
+
AutomationList,
|
|
6
|
+
AutomationListQuery,
|
|
7
|
+
} from "@sentry/junior/api/schema";
|
|
8
|
+
import { Button, ToggleButton } from "../../components/Button";
|
|
9
|
+
import { DirectorySortSelect } from "../../components/controls/DirectorySortSelect";
|
|
10
|
+
import { FilterGroup } from "../../components/FilterBar";
|
|
11
|
+
import { SearchInput } from "../../components/SearchInput";
|
|
12
|
+
|
|
13
|
+
/** Keep collection controls visible while a filtered page loads. */
|
|
14
|
+
export function AutomationFilters(props: {
|
|
15
|
+
filters: AutomationListQuery;
|
|
16
|
+
data: AutomationList | undefined;
|
|
17
|
+
onChange(key: string, value: string): void;
|
|
18
|
+
searchText: string;
|
|
19
|
+
onSearch(value: string): void;
|
|
20
|
+
}) {
|
|
21
|
+
const { filters, data, onChange } = props;
|
|
22
|
+
const [expanded, setExpanded] = useState(false);
|
|
23
|
+
const controlsId = useId();
|
|
24
|
+
const activeCount = [
|
|
25
|
+
filters.type !== "all",
|
|
26
|
+
Boolean(filters.creator),
|
|
27
|
+
Boolean(filters.destination),
|
|
28
|
+
filters.state !== "all",
|
|
29
|
+
].filter(Boolean).length;
|
|
30
|
+
return (
|
|
31
|
+
<div className="grid gap-3">
|
|
32
|
+
<div className="grid gap-2 lg:grid-cols-[auto_minmax(16rem,1fr)] lg:items-center lg:gap-4">
|
|
33
|
+
<div aria-label="Scope" role="group" className="flex flex-wrap gap-2">
|
|
34
|
+
{(
|
|
35
|
+
[
|
|
36
|
+
["all", "All"],
|
|
37
|
+
["mine", "Mine"],
|
|
38
|
+
["public", "Public"],
|
|
39
|
+
["attention", "Needs attention"],
|
|
40
|
+
] as const
|
|
41
|
+
).map(([value, label]) => (
|
|
42
|
+
<ToggleButton
|
|
43
|
+
className="min-h-11 font-sans! normal-case! lg:min-h-9"
|
|
44
|
+
key={value}
|
|
45
|
+
onClick={() => onChange("scope", value)}
|
|
46
|
+
pressed={filters.scope === value}
|
|
47
|
+
variant="pill"
|
|
48
|
+
>
|
|
49
|
+
{label}
|
|
50
|
+
{data && value !== "attention" ? (
|
|
51
|
+
<span className="ml-1.5 opacity-65">{data.counts[value]}</span>
|
|
52
|
+
) : null}
|
|
53
|
+
</ToggleButton>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
<div className="flex items-center gap-2">
|
|
57
|
+
<SearchInput
|
|
58
|
+
className="flex-1"
|
|
59
|
+
size="default"
|
|
60
|
+
label="Search automations"
|
|
61
|
+
placeholder="Search automations"
|
|
62
|
+
value={props.searchText}
|
|
63
|
+
onChange={props.onSearch}
|
|
64
|
+
/>
|
|
65
|
+
<Button
|
|
66
|
+
className="h-11 shrink-0"
|
|
67
|
+
aria-expanded={expanded}
|
|
68
|
+
aria-controls={controlsId}
|
|
69
|
+
onClick={() => setExpanded(!expanded)}
|
|
70
|
+
>
|
|
71
|
+
<SlidersHorizontal aria-hidden="true" size={14} /> Filters
|
|
72
|
+
{activeCount ? ` (${activeCount})` : ""}
|
|
73
|
+
</Button>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<div
|
|
77
|
+
id={controlsId}
|
|
78
|
+
className={cn(
|
|
79
|
+
"grid-cols-2 items-end gap-3 lg:grid-cols-3 xl:grid-cols-[auto_repeat(3,minmax(0,1fr))]",
|
|
80
|
+
expanded ? "grid" : "hidden",
|
|
81
|
+
)}
|
|
82
|
+
>
|
|
83
|
+
<FilterGroup
|
|
84
|
+
className="col-span-2 lg:col-span-3 xl:col-span-1"
|
|
85
|
+
label="Type"
|
|
86
|
+
>
|
|
87
|
+
{(["all", "scheduled", "event"] as const).map((value) => (
|
|
88
|
+
<ToggleButton
|
|
89
|
+
className="min-h-9"
|
|
90
|
+
key={value}
|
|
91
|
+
onClick={() => onChange("type", value)}
|
|
92
|
+
pressed={filters.type === value}
|
|
93
|
+
variant="pill"
|
|
94
|
+
>
|
|
95
|
+
{value}
|
|
96
|
+
</ToggleButton>
|
|
97
|
+
))}
|
|
98
|
+
</FilterGroup>
|
|
99
|
+
<AutomationFilterSelect
|
|
100
|
+
label="Creator"
|
|
101
|
+
emptyLabel="All"
|
|
102
|
+
value={filters.creator ?? ""}
|
|
103
|
+
options={data?.creators ?? []}
|
|
104
|
+
onChange={(value) => onChange("creator", value)}
|
|
105
|
+
/>
|
|
106
|
+
<AutomationFilterSelect
|
|
107
|
+
label="Destination"
|
|
108
|
+
emptyLabel="All"
|
|
109
|
+
value={filters.destination ?? ""}
|
|
110
|
+
options={data?.destinations ?? []}
|
|
111
|
+
onChange={(value) => onChange("destination", value)}
|
|
112
|
+
/>
|
|
113
|
+
<AutomationFilterSelect
|
|
114
|
+
label="State"
|
|
115
|
+
emptyLabel="All"
|
|
116
|
+
value={filters.state === "all" ? "" : filters.state}
|
|
117
|
+
options={[
|
|
118
|
+
{ value: "active", label: "Active" },
|
|
119
|
+
{ value: "blocked", label: "Blocked" },
|
|
120
|
+
{ value: "paused", label: "Paused" },
|
|
121
|
+
{ value: "unavailable", label: "Trigger unavailable" },
|
|
122
|
+
{ value: "completed", label: "Completed" },
|
|
123
|
+
]}
|
|
124
|
+
onChange={(value) => onChange("state", value)}
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Keep list order visible beside the result count instead of inside Filters. */
|
|
132
|
+
export function AutomationSort(props: {
|
|
133
|
+
value: AutomationListQuery["sort"];
|
|
134
|
+
onChange(value: string): void;
|
|
135
|
+
}) {
|
|
136
|
+
return (
|
|
137
|
+
<DirectorySortSelect
|
|
138
|
+
ariaLabel="Sort automations"
|
|
139
|
+
onChange={props.onChange}
|
|
140
|
+
options={[
|
|
141
|
+
{ value: "newest", label: "Recently created" },
|
|
142
|
+
{ value: "oldest", label: "Oldest created" },
|
|
143
|
+
{ value: "title", label: "Title A–Z" },
|
|
144
|
+
]}
|
|
145
|
+
value={props.value}
|
|
146
|
+
/>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function AutomationFilterSelect(props: {
|
|
151
|
+
label: string;
|
|
152
|
+
ariaLabel?: string;
|
|
153
|
+
emptyLabel?: string;
|
|
154
|
+
value: string;
|
|
155
|
+
options: Array<{ value: string; label: string }>;
|
|
156
|
+
onChange(value: string): void;
|
|
157
|
+
}) {
|
|
158
|
+
return (
|
|
159
|
+
<FilterGroup label={props.label}>
|
|
160
|
+
<select
|
|
161
|
+
aria-label={props.ariaLabel ?? `Filter by ${props.label.toLowerCase()}`}
|
|
162
|
+
className="h-9 w-full min-w-0 rounded-lg border border-dashboard-border-emphasis bg-dashboard-control px-2 text-sm text-dashboard-text transition-colors hover:border-dashboard-border-interactive focus:border-dashboard-focus focus:outline-none focus:ring-1 focus:ring-dashboard-focus/25 [color-scheme:var(--dashboard-color-scheme,dark)]"
|
|
163
|
+
value={props.value}
|
|
164
|
+
onChange={(event) => props.onChange(event.currentTarget.value)}
|
|
165
|
+
>
|
|
166
|
+
{props.emptyLabel ? <option value="">{props.emptyLabel}</option> : null}
|
|
167
|
+
{props.value &&
|
|
168
|
+
!props.options.some((option) => option.value === props.value) ? (
|
|
169
|
+
<option value={props.value}>{props.value}</option>
|
|
170
|
+
) : null}
|
|
171
|
+
{props.options.map((option) => (
|
|
172
|
+
<option key={option.value} value={option.value}>
|
|
173
|
+
{option.label}
|
|
174
|
+
</option>
|
|
175
|
+
))}
|
|
176
|
+
</select>
|
|
177
|
+
</FilterGroup>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/** Keep section headings and fields aligned without nested form cards. */
|
|
4
|
+
export function AutomationFormSection(props: {
|
|
5
|
+
title: string;
|
|
6
|
+
detail?: string;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}) {
|
|
9
|
+
return (
|
|
10
|
+
<section className="grid min-w-0 gap-5 border-t border-dashboard-border-emphasis py-8 md:grid-cols-[12rem_minmax(0,1fr)] md:gap-10">
|
|
11
|
+
<div>
|
|
12
|
+
<h2 className="m-0 font-display text-lg font-medium">{props.title}</h2>
|
|
13
|
+
{props.detail ? (
|
|
14
|
+
<p className="mt-2 mb-0 text-sm leading-relaxed text-dashboard-text-muted">
|
|
15
|
+
{props.detail}
|
|
16
|
+
</p>
|
|
17
|
+
) : null}
|
|
18
|
+
</div>
|
|
19
|
+
<div className="grid min-w-0 content-start gap-5">{props.children}</div>
|
|
20
|
+
</section>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import {
|
|
3
|
+
automationEditSchema,
|
|
4
|
+
type AutomationSummary,
|
|
5
|
+
} from "@sentry/junior/api/schema";
|
|
6
|
+
import { Link } from "react-router";
|
|
7
|
+
import { Button, ButtonLink } from "../../components/Button";
|
|
8
|
+
import { StatusChip } from "../../components/StatusChip";
|
|
9
|
+
import { FormNotice } from "../../components/FormNotice";
|
|
10
|
+
import { DashboardApiError, fetchDashboardJson, post } from "../../http";
|
|
11
|
+
|
|
12
|
+
/** Explain lifecycle separately from the last execution and offer creator-only actions. */
|
|
13
|
+
export function AutomationLifecycle({
|
|
14
|
+
automation,
|
|
15
|
+
editPath,
|
|
16
|
+
}: {
|
|
17
|
+
automation: AutomationSummary;
|
|
18
|
+
editPath: string;
|
|
19
|
+
}) {
|
|
20
|
+
const queryClient = useQueryClient();
|
|
21
|
+
const completed = automation.status === "completed";
|
|
22
|
+
const paused = automation.status === "paused";
|
|
23
|
+
const blocked = automation.status === "blocked";
|
|
24
|
+
const unavailable =
|
|
25
|
+
automation.kind === "event" && !automation.triggerAvailable;
|
|
26
|
+
const canEdit = automation.ownedByViewer && !completed;
|
|
27
|
+
const blockReason =
|
|
28
|
+
automation.kind === "scheduled" ? automation.statusReason : undefined;
|
|
29
|
+
const action = paused || blocked ? "resume" : "pause";
|
|
30
|
+
const mutation = useMutation({
|
|
31
|
+
mutationFn: async () => {
|
|
32
|
+
const path = `/api/automations/${automation.kind}/${encodeURIComponent(automation.id)}`;
|
|
33
|
+
const current = await fetchDashboardJson(
|
|
34
|
+
automationEditSchema,
|
|
35
|
+
`${path}/edit`,
|
|
36
|
+
);
|
|
37
|
+
// Do not apply the opposite action if another surface changed lifecycle.
|
|
38
|
+
if (current.status !== automation.status) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"The Automation changed. Close and reopen its details before trying again.",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return post(automationEditSchema, `${path}/lifecycle`, {
|
|
44
|
+
action,
|
|
45
|
+
revision: current.revision,
|
|
46
|
+
});
|
|
47
|
+
},
|
|
48
|
+
onSuccess: async () => {
|
|
49
|
+
await queryClient.invalidateQueries({
|
|
50
|
+
queryKey: ["dashboard", "automations"],
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
const status =
|
|
55
|
+
automation.status === "active" && unavailable
|
|
56
|
+
? "Trigger unavailable"
|
|
57
|
+
: automation.status;
|
|
58
|
+
const failed =
|
|
59
|
+
automation.lastRunStatus === "failed" ||
|
|
60
|
+
automation.lastRunStatus === "blocked";
|
|
61
|
+
return (
|
|
62
|
+
<div className="grid gap-3 text-sm">
|
|
63
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
64
|
+
<div role="status" aria-live="polite">
|
|
65
|
+
<StatusChip
|
|
66
|
+
tone={
|
|
67
|
+
paused || completed
|
|
68
|
+
? "neutral"
|
|
69
|
+
: blocked || unavailable
|
|
70
|
+
? "warning"
|
|
71
|
+
: "success"
|
|
72
|
+
}
|
|
73
|
+
>
|
|
74
|
+
{status}
|
|
75
|
+
</StatusChip>
|
|
76
|
+
</div>
|
|
77
|
+
<div className="flex items-center gap-2">
|
|
78
|
+
<ButtonLink className="min-h-11" to={editPath}>
|
|
79
|
+
{canEdit ? "Edit automation" : "View settings"}
|
|
80
|
+
</ButtonLink>
|
|
81
|
+
{canEdit ? (
|
|
82
|
+
<Button
|
|
83
|
+
className="min-h-11"
|
|
84
|
+
disabled={mutation.isPending}
|
|
85
|
+
onClick={() => mutation.mutate()}
|
|
86
|
+
>
|
|
87
|
+
{mutation.isPending
|
|
88
|
+
? "Updating…"
|
|
89
|
+
: action === "pause"
|
|
90
|
+
? "Pause"
|
|
91
|
+
: "Resume"}
|
|
92
|
+
</Button>
|
|
93
|
+
) : null}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
{blocked || blockReason ? (
|
|
97
|
+
<p className="m-0 break-words text-dashboard-text-muted">
|
|
98
|
+
{blockReason ?? "Check the last run to see what needs fixing."}{" "}
|
|
99
|
+
{paused ? "Fix this before resuming." : "Fix this, then resume."}
|
|
100
|
+
</p>
|
|
101
|
+
) : null}
|
|
102
|
+
{unavailable ? (
|
|
103
|
+
<p className="m-0 text-dashboard-text-muted">
|
|
104
|
+
This event source is unavailable. Enable its plugin or choose another
|
|
105
|
+
event.
|
|
106
|
+
</p>
|
|
107
|
+
) : null}
|
|
108
|
+
{failed ? (
|
|
109
|
+
<p className="m-0 text-dashboard-text-muted">
|
|
110
|
+
Last run {automation.lastRunStatus}.
|
|
111
|
+
{automation.status === "active" && !unavailable
|
|
112
|
+
? " Future runs are still on."
|
|
113
|
+
: ""}{" "}
|
|
114
|
+
<Link
|
|
115
|
+
className="text-dashboard-text underline underline-offset-2"
|
|
116
|
+
to={`/automations/${automation.kind}/${encodeURIComponent(automation.id)}/executions`}
|
|
117
|
+
>
|
|
118
|
+
View run history
|
|
119
|
+
</Link>
|
|
120
|
+
</p>
|
|
121
|
+
) : null}
|
|
122
|
+
{canEdit ? (
|
|
123
|
+
<details className="text-dashboard-text-muted">
|
|
124
|
+
<summary className="w-fit cursor-pointer py-2 text-xs hover:text-dashboard-text focus-visible:outline focus-visible:outline-dashboard-focus">
|
|
125
|
+
About pausing
|
|
126
|
+
</summary>
|
|
127
|
+
<p className="mt-1 mb-0 leading-relaxed">
|
|
128
|
+
Pausing stops new runs. Work already queued or running may finish.
|
|
129
|
+
Resuming skips missed runs and waits for the next scheduled time or
|
|
130
|
+
new event.
|
|
131
|
+
{paused && blockReason
|
|
132
|
+
? " Removing the pause keeps the block until you fix it and resume again."
|
|
133
|
+
: ""}
|
|
134
|
+
</p>
|
|
135
|
+
</details>
|
|
136
|
+
) : null}
|
|
137
|
+
{mutation.error ? (
|
|
138
|
+
<FormNotice tone="error" title="Could not update this automation.">
|
|
139
|
+
{mutation.error instanceof DashboardApiError
|
|
140
|
+
? (mutation.error.apiError ?? "Try again.")
|
|
141
|
+
: mutation.error.message}
|
|
142
|
+
</FormNotice>
|
|
143
|
+
) : null}
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { ArrowUp, Trash2, Plus } from "lucide-react";
|
|
2
|
+
import type {
|
|
3
|
+
AutomationEdit,
|
|
4
|
+
AutomationSummary,
|
|
5
|
+
} from "@sentry/junior/api/schema";
|
|
6
|
+
import { Button } from "../../components/Button";
|
|
7
|
+
import { Select } from "../../components/Select";
|
|
8
|
+
import { cn } from "../../styles";
|
|
9
|
+
import type { AutomationDraft } from "./automationDraft";
|
|
10
|
+
import { automationOutcomeLabel } from "./automationOutcomes";
|
|
11
|
+
|
|
12
|
+
/** Keep outcome order and offer only supported message Destinations. */
|
|
13
|
+
export function AutomationOutcomeFields(props: {
|
|
14
|
+
value: AutomationDraft["outcomes"];
|
|
15
|
+
original: AutomationEdit["outcomes"];
|
|
16
|
+
destination: AutomationSummary["destination"];
|
|
17
|
+
onChange(value: AutomationDraft["outcomes"]): void;
|
|
18
|
+
}) {
|
|
19
|
+
const defaultOutcomes: AutomationDraft["outcomes"] = props.original.length
|
|
20
|
+
? props.original
|
|
21
|
+
: [{ action: "send_message", destination: "current_conversation" }];
|
|
22
|
+
const options = [
|
|
23
|
+
...props.original.flatMap((outcome, index) =>
|
|
24
|
+
typeof outcome.destination === "string"
|
|
25
|
+
? []
|
|
26
|
+
: [
|
|
27
|
+
{
|
|
28
|
+
value: `saved:${index}`,
|
|
29
|
+
label: automationOutcomeLabel(outcome, props.destination),
|
|
30
|
+
outcome,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
),
|
|
34
|
+
{
|
|
35
|
+
value: "current_conversation",
|
|
36
|
+
label: `${props.destination.label} · ${props.destination.visibility}`,
|
|
37
|
+
outcome: {
|
|
38
|
+
action: "send_message",
|
|
39
|
+
destination: "current_conversation",
|
|
40
|
+
} as const,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
value: "task_creator",
|
|
44
|
+
label: "Creator · Direct message",
|
|
45
|
+
outcome: { action: "send_message", destination: "task_creator" } as const,
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
return (
|
|
49
|
+
<>
|
|
50
|
+
<fieldset>
|
|
51
|
+
<legend className="sr-only">Success messages</legend>
|
|
52
|
+
<div className="flex flex-wrap gap-x-6 gap-y-3">
|
|
53
|
+
{[true, false].map((send) => (
|
|
54
|
+
<label
|
|
55
|
+
key={String(send)}
|
|
56
|
+
className="inline-flex min-h-8 cursor-pointer items-center gap-2 text-sm"
|
|
57
|
+
>
|
|
58
|
+
<input
|
|
59
|
+
type="radio"
|
|
60
|
+
name="messages"
|
|
61
|
+
className="size-4 accent-dashboard-focus"
|
|
62
|
+
checked={Boolean(props.value.length) === send}
|
|
63
|
+
onChange={() => props.onChange(send ? defaultOutcomes : [])}
|
|
64
|
+
/>
|
|
65
|
+
{send ? "Send a message" : "No success message"}
|
|
66
|
+
</label>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
</fieldset>
|
|
70
|
+
{props.value.length ? (
|
|
71
|
+
<div className="grid gap-4 sm:gap-2">
|
|
72
|
+
{props.value.map((outcome, index) => {
|
|
73
|
+
const retainedIndex = props.original.findIndex(
|
|
74
|
+
(item) => JSON.stringify(item) === JSON.stringify(outcome),
|
|
75
|
+
);
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
key={index}
|
|
79
|
+
className="flex min-w-0 flex-wrap items-center gap-2"
|
|
80
|
+
>
|
|
81
|
+
<span className="w-4 text-xs text-dashboard-text-muted">
|
|
82
|
+
{index + 1}.
|
|
83
|
+
</span>
|
|
84
|
+
<div
|
|
85
|
+
className={cn(
|
|
86
|
+
"min-w-0 flex-1",
|
|
87
|
+
props.value.length > 1 &&
|
|
88
|
+
"order-last basis-full sm:order-none sm:basis-auto",
|
|
89
|
+
)}
|
|
90
|
+
>
|
|
91
|
+
<Select
|
|
92
|
+
aria-label={`Message ${index + 1} destination`}
|
|
93
|
+
value={
|
|
94
|
+
typeof outcome.destination === "string"
|
|
95
|
+
? outcome.destination
|
|
96
|
+
: `saved:${retainedIndex}`
|
|
97
|
+
}
|
|
98
|
+
onChange={(e) => {
|
|
99
|
+
const selected = options.find(
|
|
100
|
+
(option) => option.value === e.target.value,
|
|
101
|
+
);
|
|
102
|
+
if (selected)
|
|
103
|
+
props.onChange(
|
|
104
|
+
props.value.map((item, i) =>
|
|
105
|
+
i === index ? selected.outcome : item,
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
{options.map((option) => (
|
|
111
|
+
<option key={option.value} value={option.value}>
|
|
112
|
+
{option.label}
|
|
113
|
+
</option>
|
|
114
|
+
))}
|
|
115
|
+
</Select>
|
|
116
|
+
</div>
|
|
117
|
+
{props.value.length > 1 ? (
|
|
118
|
+
<>
|
|
119
|
+
<Button
|
|
120
|
+
size="icon"
|
|
121
|
+
className="ml-auto sm:ml-0"
|
|
122
|
+
aria-label={`Move message ${index + 1} up`}
|
|
123
|
+
disabled={index === 0}
|
|
124
|
+
onClick={() => {
|
|
125
|
+
const next = [...props.value];
|
|
126
|
+
[next[index - 1], next[index]] = [
|
|
127
|
+
next[index],
|
|
128
|
+
next[index - 1],
|
|
129
|
+
];
|
|
130
|
+
props.onChange(next);
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
<ArrowUp size={16} />
|
|
134
|
+
</Button>
|
|
135
|
+
<Button
|
|
136
|
+
size="icon"
|
|
137
|
+
aria-label={`Remove message ${index + 1}`}
|
|
138
|
+
onClick={() =>
|
|
139
|
+
props.onChange(
|
|
140
|
+
props.value.filter((_, i) => i !== index),
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
>
|
|
144
|
+
<Trash2 size={16} />
|
|
145
|
+
</Button>
|
|
146
|
+
</>
|
|
147
|
+
) : null}
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
})}
|
|
151
|
+
{props.value.length < 5 ? (
|
|
152
|
+
<Button
|
|
153
|
+
className="w-fit"
|
|
154
|
+
onClick={() =>
|
|
155
|
+
props.onChange([
|
|
156
|
+
...props.value,
|
|
157
|
+
{ action: "send_message", destination: "task_creator" },
|
|
158
|
+
])
|
|
159
|
+
}
|
|
160
|
+
>
|
|
161
|
+
<Plus size={15} />
|
|
162
|
+
Add message
|
|
163
|
+
</Button>
|
|
164
|
+
) : null}
|
|
165
|
+
</div>
|
|
166
|
+
) : (
|
|
167
|
+
<p className="m-0 text-sm leading-relaxed text-dashboard-text-muted">
|
|
168
|
+
Junior still does the work. No message is sent after success.
|
|
169
|
+
</p>
|
|
170
|
+
)}
|
|
171
|
+
<p className="m-0 text-xs leading-relaxed text-dashboard-text-muted">
|
|
172
|
+
{props.value.length ? "Messages are sent in this order. " : ""}This does
|
|
173
|
+
not limit actions in the instruction.
|
|
174
|
+
</p>
|
|
175
|
+
</>
|
|
176
|
+
);
|
|
177
|
+
}
|