Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/aevatar-console-web/src/locales/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,7 @@ const enUSMessages = {
'teams.automations.actions.retryRevocation': 'Retry revocation',
'teams.automations.actions.resume': 'Resume',
'teams.automations.actions.runNow': 'Run now',
'teams.automations.actions.viewRuns': 'View runs',
'teams.automations.actions.runHistory': 'View member run history',
'teams.automations.actions.tryAgain': 'Try again',
'teams.automations.authorization.back': 'Back',
Expand Down Expand Up @@ -993,6 +994,8 @@ const enUSMessages = {
'teams.automations.preview.weekly.title': 'Weekly release handoff',
'teams.automations.previewOnly': 'Automation API wiring is coming next.',
'teams.automations.row.nextRun': 'Next {time}',
'teams.automations.row.lastRun': 'Last {time}',
'teams.automations.row.noLastRun': 'No previous run',
'teams.automations.row.awaitingReadModel': 'Waiting for schedule sync',
'teams.automations.row.manualRunRequested': 'Run requested {time}',
'teams.automations.row.noNextRun': 'No next run',
Expand Down
3 changes: 3 additions & 0 deletions apps/aevatar-console-web/src/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,7 @@ const zhCNMessages = {
'teams.automations.actions.retryRevocation': '重试撤销',
'teams.automations.actions.resume': '恢复',
'teams.automations.actions.runNow': '立即运行',
'teams.automations.actions.viewRuns': '查看运行记录',
'teams.automations.actions.runHistory': '查看成员运行记录',
'teams.automations.actions.tryAgain': '重试',
'teams.automations.authorization.back': '返回',
Expand Down Expand Up @@ -936,6 +937,8 @@ const zhCNMessages = {
'teams.automations.preview.weekly.title': '每周发布交接',
'teams.automations.previewOnly': '自动化 API 接入会在下一步完成。',
'teams.automations.row.nextRun': '下次 {time}',
'teams.automations.row.lastRun': '上次 {time}',
'teams.automations.row.noLastRun': '暂无历史运行',
'teams.automations.row.awaitingReadModel': '正在等待计划同步',
'teams.automations.row.manualRunRequested': '已在 {time} 请求运行',
'teams.automations.row.noNextRun': '暂无下次运行',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,95 @@ describe("TeamAutomationsTab canonical member authority", () => {
);
});

it("opens schedule-filtered runs from the row's canonical member authority", async () => {
const otherMember = {
...member,
key: "m-beta",
memberId: "m-beta",
name: "Reviewer",
serviceId: "svc-beta",
};
(teamAutomationApi.listAll as jest.Mock).mockResolvedValue({
items: [
automationView({
displayName: "Reviewer review",
memberId: "m-beta",
publishedServiceId: "svc-beta",
scheduleId: "sch-beta",
}),
],
nextCursor: null,
totalCount: 1,
});

renderTab("", [member, otherMember]);

const reviewerRow = await screen.findByRole("article", {
name: "Reviewer review",
});
fireEvent.click(
within(reviewerRow).getByRole("button", { name: "View runs" }),
);

expect(history.push).toHaveBeenCalledWith(
"/scopes/scope-alpha/teams/team-alpha/members/m-beta/runs?scheduleId=sch-beta",
);
});

it("shows the schedule timezone with next and last fire state", async () => {
(teamAutomationApi.listAll as jest.Mock).mockResolvedValue({
items: [
automationView({
lastFireAt: "2026-07-28T01:00:00Z",
timezone: "Asia/Singapore",
}),
],
nextCursor: null,
totalCount: 1,
});

renderTab("m-alpha");

const row = await screen.findByRole("article", { name: "Daily review" });
expect(within(row).getByText("Asia/Singapore")).toBeInTheDocument();
expect(within(row).getByText(/^Next /)).toBeInTheDocument();
expect(within(row).getByText(/^Last /)).toBeInTheDocument();
});

it("keeps Run now single-flight while the accepted request is pending", async () => {
let resolveRunNow: ((value: unknown) => void) | undefined;
(teamAutomationApi.listAll as jest.Mock).mockResolvedValue({
items: [automationView()],
nextCursor: null,
totalCount: 1,
});
(teamAutomationApi.runNow as jest.Mock).mockImplementation(
() =>
new Promise((resolve) => {
resolveRunNow = resolve;
}),
);
renderTab("m-alpha");

const runNow = await screen.findByRole("button", { name: "Run now" });
fireEvent.click(runNow);
fireEvent.click(runNow);

expect(teamAutomationApi.runNow).toHaveBeenCalledTimes(1);
expect(runNow).toBeDisabled();

await act(async () => {
resolveRunNow?.({
accepted: true,
commandId: "cmd-run",
operationId: "op-run",
scheduleId: "sch-alpha",
status: "accepted",
});
await Promise.resolve();
});
});

it("opens the complete dev form directly from the Team shell", async () => {
renderTab();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
DeleteOutlined,
EditOutlined,
ExclamationCircleOutlined,
HistoryOutlined,
PauseCircleOutlined,
PlayCircleOutlined,
PlusOutlined,
Expand Down Expand Up @@ -47,8 +48,10 @@ import { previewScheduledDispatch } from "@/shared/api/scheduledDispatchApi";
import { NyxIDAuthClient } from "@/shared/auth/client";
import { getNyxIDRuntimeConfig } from "@/shared/auth/config";
import { formatCompactDateTime } from "@/shared/datetime/dateTime";
import { history } from "@/shared/navigation/history";
import {
buildTeamMemberAutomationsHref,
buildTeamMemberPublishedRunsHref,
} from "@/shared/navigation/teamRoutes";
import {
AevatarInspectorEmpty,
Expand Down Expand Up @@ -269,7 +272,7 @@ const responsiveStyle = `
@media (max-width: 520px) {
.team-automation-actions {
display: grid !important;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-columns: repeat(5, minmax(0, 1fr));
}

.team-automation-actions .ant-btn {
Expand Down Expand Up @@ -1371,6 +1374,19 @@ const TeamAutomationsTab: React.FC<Props> = ({
border: `1px solid ${token.colorBorderSecondary}`,
}}
>
{renderAutomationActionButton({
icon: <HistoryOutlined />,
label: copy("teams.automations.actions.viewRuns", "View runs"),
onClick: () =>
history.push(
buildTeamMemberPublishedRunsHref({
scopeId: view.scopeId,
teamId: view.teamId,
memberId: view.memberId,
scheduleId: view.scheduleId,
}),
),
})}
{active
? renderAutomationActionButton({
icon: <EditOutlined />,
Expand Down Expand Up @@ -1502,13 +1518,23 @@ const TeamAutomationsTab: React.FC<Props> = ({
</div>
<div className="team-automation-row__schedule" style={{ display: "grid", gap: 5, minWidth: 0 }}>
<FactLine monospace={false} text={cadence.summary} tooltipText={`${cadence.detail} · ${view.cronExpression}`} />
<Typography.Text style={{ fontSize: 12 }} type="secondary">
{view.timezone}
</Typography.Text>
<Typography.Text style={{ fontSize: 12 }} type="secondary">
{view.nextFireAt
? copy("teams.automations.row.nextRun", "Next {time}", {
time: formatCompactDateTime(view.nextFireAt, "--"),
})
: copy("teams.automations.row.noNextRun", "No next run")}
</Typography.Text>
<Typography.Text style={{ fontSize: 12 }} type="secondary">
{view.lastFireAt
? copy("teams.automations.row.lastRun", "Last {time}", {
time: formatCompactDateTime(view.lastFireAt, "--"),
})
: copy("teams.automations.row.noLastRun", "No previous run")}
</Typography.Text>
</div>
{renderActions(view)}
</article>
Expand Down
Loading