feat: streamline group dispatch reminders
This commit is contained in:
@@ -167,12 +167,18 @@ public class BossApiClient {
|
||||
return requestWithRestore("DELETE", "/api/v1/projects/" + encode(projectId) + "/memories/" + encode(memoryId), null);
|
||||
}
|
||||
|
||||
public ApiResponse confirmDispatchPlan(String projectId, String planId, JSONArray approvedTargetProjectIds) throws IOException, JSONException {
|
||||
public ApiResponse confirmDispatchPlan(
|
||||
String projectId,
|
||||
String planId,
|
||||
JSONArray approvedTargetProjectIds,
|
||||
boolean rememberLightReminder
|
||||
) throws IOException, JSONException {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put(
|
||||
"approvedTargetProjectIds",
|
||||
approvedTargetProjectIds == null ? new JSONArray() : approvedTargetProjectIds
|
||||
);
|
||||
payload.put("rememberLightReminder", rememberLightReminder);
|
||||
return requestWithRestoreRaw(
|
||||
"POST",
|
||||
"/api/v1/projects/" + encode(projectId) + "/dispatch-plans/" + encode(planId) + "/confirm",
|
||||
@@ -182,6 +188,10 @@ public class BossApiClient {
|
||||
);
|
||||
}
|
||||
|
||||
public ApiResponse confirmDispatchPlan(String projectId, String planId, JSONArray approvedTargetProjectIds) throws IOException, JSONException {
|
||||
return confirmDispatchPlan(projectId, planId, approvedTargetProjectIds, false);
|
||||
}
|
||||
|
||||
public ApiResponse rejectDispatchPlan(String projectId, String planId) throws IOException, JSONException {
|
||||
return requestWithRestoreRaw(
|
||||
"POST",
|
||||
@@ -227,6 +237,12 @@ public class BossApiClient {
|
||||
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/participants", payload);
|
||||
}
|
||||
|
||||
public ApiResponse updateProjectDispatchReminder(String projectId, boolean lightDispatchReminderEnabled) throws IOException, JSONException {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("lightDispatchReminderEnabled", lightDispatchReminderEnabled);
|
||||
return requestWithRestore("PATCH", "/api/v1/projects/" + encode(projectId) + "/dispatch-reminder", payload);
|
||||
}
|
||||
|
||||
public ApiResponse sendProjectMessage(String projectId, String body, String kind) throws IOException, JSONException {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("body", body);
|
||||
|
||||
@@ -106,6 +106,7 @@ public class GroupInfoActivity extends BossScreenActivity {
|
||||
if (orchestrationBackendPayload != null) {
|
||||
appendContent(buildOrchestrationBackendRow(orchestrationBackendPayload));
|
||||
}
|
||||
appendContent(buildDispatchReminderRow(project));
|
||||
|
||||
if (repairRequired) {
|
||||
String meta = invalidParticipantCount > 0
|
||||
@@ -150,6 +151,18 @@ public class GroupInfoActivity extends BossScreenActivity {
|
||||
setRefreshing(false);
|
||||
}
|
||||
|
||||
private LinearLayout buildDispatchReminderRow(JSONObject project) {
|
||||
boolean enabled = project.optBoolean("lightDispatchReminderEnabled", false);
|
||||
return BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"推荐下发默认轻提醒",
|
||||
enabled ? "已开启" : "已关闭",
|
||||
enabled ? "后续推荐会保留轻状态卡,不再弹重确认提醒。" : "当前仍会显式提醒你确认主 Agent 推荐。",
|
||||
enabled ? "开启" : "关闭",
|
||||
v -> openDispatchReminderDialog(enabled)
|
||||
);
|
||||
}
|
||||
|
||||
private LinearLayout buildMemberRow(JSONObject participant) {
|
||||
boolean sourceProject = participant.optBoolean("isSourceProject", false);
|
||||
boolean canOpenProject = participant.optBoolean("canOpenProject", true);
|
||||
@@ -426,6 +439,39 @@ public class GroupInfoActivity extends BossScreenActivity {
|
||||
});
|
||||
}
|
||||
|
||||
private void openDispatchReminderDialog(boolean enabled) {
|
||||
CharSequence[] items = enabled
|
||||
? new CharSequence[]{"关闭默认轻提醒"}
|
||||
: new CharSequence[]{"开启默认轻提醒"};
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("推荐下发默认轻提醒")
|
||||
.setMessage(enabled
|
||||
? "关闭后,这个群会恢复成每次都显式确认的提醒方式。"
|
||||
: "开启后,这个群后续仍会显示一张轻状态卡,但不再出现重提醒。")
|
||||
.setItems(items, (dialog, which) -> saveDispatchReminderPreference(!enabled))
|
||||
.setNegativeButton("取消", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void saveDispatchReminderPreference(boolean enabled) {
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
BossApiClient.ApiResponse response = apiClient.updateProjectDispatchReminder(projectId, enabled);
|
||||
if (!response.ok()) throw new IllegalStateException(response.message());
|
||||
runOnUiThread(() -> {
|
||||
showMessage(enabled ? "已开启默认轻提醒" : "已关闭默认轻提醒");
|
||||
reload();
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
showMessage("保存失败:" + error.getMessage());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showMoreMenu() {
|
||||
new AlertDialog.Builder(this)
|
||||
.setItems(new CharSequence[]{"改名", "刷新"}, (dialog, which) -> {
|
||||
|
||||
@@ -312,6 +312,50 @@ public final class ProjectChatUiState {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public static String summarizeDispatchPlanCompact(@Nullable JSONObject plan) {
|
||||
if (plan == null) {
|
||||
return "主 Agent 暂未生成推荐线程。";
|
||||
}
|
||||
List<String> targetTitles = dispatchPlanTargetTitles(plan);
|
||||
String summary = plan.optString("summary", "").trim();
|
||||
if (targetTitles.isEmpty()) {
|
||||
return isBlank(summary) ? "主 Agent 已生成推荐线程。" : truncate(summary, 32);
|
||||
}
|
||||
if (isBlank(summary)) {
|
||||
return "推荐给:" + String.join("、", targetTitles);
|
||||
}
|
||||
return "推荐给:" + String.join("、", targetTitles) + "\n" + truncate(summary, 32);
|
||||
}
|
||||
|
||||
public static String summarizeDispatchPlanLight(@Nullable JSONObject plan) {
|
||||
int targetCount = dispatchPlanTargetTitles(plan).size();
|
||||
if (targetCount <= 0) {
|
||||
return "主 Agent 已推荐线程";
|
||||
}
|
||||
return "主 Agent 已推荐 " + targetCount + " 个线程";
|
||||
}
|
||||
|
||||
private static List<String> dispatchPlanTargetTitles(@Nullable JSONObject plan) {
|
||||
List<String> targetTitles = new ArrayList<>();
|
||||
if (plan == null) {
|
||||
return targetTitles;
|
||||
}
|
||||
JSONArray targets = plan.optJSONArray("targets");
|
||||
if (targets != null) {
|
||||
for (int i = 0; i < targets.length(); i++) {
|
||||
JSONObject target = targets.optJSONObject(i);
|
||||
if (target == null) {
|
||||
continue;
|
||||
}
|
||||
String title = target.optString("threadDisplayName", "").trim();
|
||||
if (!title.isEmpty()) {
|
||||
targetTitles.add(title);
|
||||
}
|
||||
}
|
||||
}
|
||||
return targetTitles;
|
||||
}
|
||||
|
||||
public static String formatAttachmentSize(long fileSizeBytes) {
|
||||
if (fileSizeBytes >= 1024L * 1024L) {
|
||||
return String.format(java.util.Locale.US, "%.1f MB", fileSizeBytes / (1024f * 1024f));
|
||||
|
||||
@@ -70,6 +70,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
private String currentScreenSubtitle;
|
||||
private String projectCollaborationMode = "development";
|
||||
private String projectApprovalState = "not_required";
|
||||
private boolean lightDispatchReminderEnabled;
|
||||
private @Nullable JSONObject currentPendingDispatchPlan;
|
||||
private @Nullable JSONObject currentRejectedDispatchPlan;
|
||||
private ProjectChatUiState.SelectionState selectionState = ProjectChatUiState.emptySelection();
|
||||
@@ -310,6 +311,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
projectFolderName = threadMeta == null ? "" : threadMeta.optString("folderName", "");
|
||||
projectCollaborationMode = project == null ? "development" : project.optString("collaborationMode", "development");
|
||||
projectApprovalState = project == null ? "not_required" : project.optString("approvalState", "not_required");
|
||||
lightDispatchReminderEnabled = project != null && project.optBoolean("lightDispatchReminderEnabled", false);
|
||||
JSONObject agentControls = project == null ? null : project.optJSONObject("agentControls");
|
||||
currentAgentModelOverride = normalizeControlValue(agentControls == null ? null : agentControls.optString("modelOverride", null));
|
||||
currentReasoningEffortOverride = normalizeControlValue(agentControls == null ? null : agentControls.optString("reasoningEffortOverride", null));
|
||||
@@ -530,6 +532,8 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
if (collaborationGate != null) {
|
||||
projectCollaborationMode = collaborationGate.optString("collaborationMode", projectCollaborationMode);
|
||||
projectApprovalState = collaborationGate.optString("approvalState", projectApprovalState);
|
||||
lightDispatchReminderEnabled =
|
||||
collaborationGate.optBoolean("lightDispatchReminderEnabled", lightDispatchReminderEnabled);
|
||||
}
|
||||
currentPendingDispatchPlan = dispatchPlan;
|
||||
currentRejectedDispatchPlan = null;
|
||||
@@ -542,7 +546,6 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
: "消息已发送,主 Agent 已给出推荐线程。"
|
||||
);
|
||||
reload(true);
|
||||
showDispatchPlanConfirmation(dispatchPlan);
|
||||
return;
|
||||
}
|
||||
if (waitSpec.shouldWait) {
|
||||
@@ -852,12 +855,28 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
container.setOrientation(LinearLayout.VERTICAL);
|
||||
container.addView(BossUi.buildCard(
|
||||
this,
|
||||
"approval_required".equals(projectCollaborationMode) ? "等待你批准主 Agent 下发" : "主 Agent 推荐下发",
|
||||
ProjectChatUiState.summarizeDispatchPlan(dispatchPlan),
|
||||
"当前确认状态:" + describeDispatchPlanApprovalState(projectApprovalState)
|
||||
lightDispatchReminderEnabled
|
||||
? ProjectChatUiState.summarizeDispatchPlanLight(dispatchPlan)
|
||||
: ("approval_required".equals(projectCollaborationMode) ? "主 Agent 推荐下发" : "主 Agent 推荐"),
|
||||
lightDispatchReminderEnabled
|
||||
? ProjectChatUiState.summarizeDispatchPlanCompact(dispatchPlan)
|
||||
: ProjectChatUiState.summarizeDispatchPlanCompact(dispatchPlan),
|
||||
lightDispatchReminderEnabled ? "轻提醒已开启" : "当前状态:" + describeDispatchPlanApprovalState(projectApprovalState)
|
||||
));
|
||||
Button confirmButton = BossUi.buildMiniActionButton(this, "确认下发", true);
|
||||
confirmButton.setOnClickListener(v -> showDispatchPlanConfirmation(dispatchPlan));
|
||||
Button confirmButton = BossUi.buildMiniActionButton(
|
||||
this,
|
||||
lightDispatchReminderEnabled ? "继续下发" : "确认一下",
|
||||
true
|
||||
);
|
||||
confirmButton.setOnClickListener(v -> confirmDispatchPlan(dispatchPlan, false));
|
||||
if (!lightDispatchReminderEnabled) {
|
||||
Button rememberButton = BossUi.buildMiniActionButton(this, "确认并记住", false);
|
||||
rememberButton.setOnClickListener(v -> confirmDispatchPlan(dispatchPlan, true));
|
||||
Button rejectButton = BossUi.buildMiniActionButton(this, "拒绝", false);
|
||||
rejectButton.setOnClickListener(v -> rejectDispatchPlan(dispatchPlan));
|
||||
container.addView(BossUi.buildInlineActionRow(this, confirmButton, rememberButton, rejectButton));
|
||||
return container;
|
||||
}
|
||||
Button rejectButton = BossUi.buildMiniActionButton(this, "拒绝", false);
|
||||
rejectButton.setOnClickListener(v -> rejectDispatchPlan(dispatchPlan));
|
||||
container.addView(BossUi.buildInlineActionRow(this, confirmButton, rejectButton));
|
||||
@@ -899,22 +918,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
return container;
|
||||
}
|
||||
|
||||
private void showDispatchPlanConfirmation(JSONObject dispatchPlan) {
|
||||
String title = "approval_required".equals(projectCollaborationMode)
|
||||
? "批准主 Agent 下发"
|
||||
: "确认主 Agent 推荐";
|
||||
String message = ProjectChatUiState.summarizeDispatchPlan(dispatchPlan)
|
||||
+ "\n\n确认后会把任务下发到推荐线程,并把线程原始回复与主 Agent 汇总一起回到群聊。";
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setNegativeButton("拒绝", (dialog, which) -> rejectDispatchPlan(dispatchPlan))
|
||||
.setNeutralButton("稍后", null)
|
||||
.setPositiveButton("确认下发", (dialog, which) -> confirmDispatchPlan(dispatchPlan))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void confirmDispatchPlan(JSONObject dispatchPlan) {
|
||||
private void confirmDispatchPlan(JSONObject dispatchPlan, boolean rememberLightReminder) {
|
||||
String planId = dispatchPlan.optString("planId", "").trim();
|
||||
if (planId.isEmpty()) {
|
||||
showMessage("缺少调度方案 ID");
|
||||
@@ -932,7 +936,12 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
for (String approvedTargetProjectId : approvedTargetProjectIds) {
|
||||
approved.put(approvedTargetProjectId);
|
||||
}
|
||||
BossApiClient.ApiResponse response = apiClient.confirmDispatchPlan(projectId, planId, approved);
|
||||
BossApiClient.ApiResponse response = apiClient.confirmDispatchPlan(
|
||||
projectId,
|
||||
planId,
|
||||
approved,
|
||||
rememberLightReminder
|
||||
);
|
||||
if (!response.ok()) {
|
||||
throw new IllegalStateException(response.message());
|
||||
}
|
||||
@@ -942,6 +951,9 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
ProjectChatUiState.resolveReplyWaitAfterDispatchConfirm(response.json);
|
||||
runOnUiThread(() -> {
|
||||
applyDispatchPlanActionResponse(response.json);
|
||||
if (rememberLightReminder) {
|
||||
showMessage("已确认下发,并对这个群开启默认轻提醒");
|
||||
}
|
||||
if (waitSpec.shouldWait) {
|
||||
startReplyWait(
|
||||
waitSpec,
|
||||
@@ -1011,9 +1023,6 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
applyDispatchPlanActionResponse(response.json);
|
||||
showMessage("主 Agent 已重新生成推荐");
|
||||
reload(true);
|
||||
if (nextPlan != null) {
|
||||
showDispatchPlanConfirmation(nextPlan);
|
||||
}
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
@@ -2103,6 +2112,8 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
if (collaborationGate != null) {
|
||||
projectCollaborationMode = collaborationGate.optString("collaborationMode", projectCollaborationMode);
|
||||
projectApprovalState = collaborationGate.optString("approvalState", projectApprovalState);
|
||||
lightDispatchReminderEnabled =
|
||||
collaborationGate.optBoolean("lightDispatchReminderEnabled", lightDispatchReminderEnabled);
|
||||
}
|
||||
JSONObject plan = response.optJSONObject("plan");
|
||||
if (plan != null) {
|
||||
|
||||
@@ -161,6 +161,29 @@ public class GroupInfoActivityTest {
|
||||
assertTrue(viewTreeContainsText(content, "OMX Team Runtime 当前可用,当前可切换到该后端。"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renderGroupShowsLightReminderPreferenceRow() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(GroupInfoActivity.EXTRA_PROJECT_ID, "group-1")
|
||||
.putExtra(GroupInfoActivity.EXTRA_PROJECT_NAME, "巡检协作群");
|
||||
TestGroupInfoActivity activity = Robolectric
|
||||
.buildActivity(TestGroupInfoActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"renderGroup",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildDetailPayload(true)),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildParticipantsPayload()),
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildOrchestrationBackendPayload())
|
||||
);
|
||||
|
||||
LinearLayout content = activity.findViewById(R.id.screen_content);
|
||||
assertTrue(viewTreeContainsText(content, "推荐下发默认轻提醒"));
|
||||
assertTrue(viewTreeContainsText(content, "已开启"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renderGroupShowsOmxFallbackHintWhenOmxRuntimeIsUnavailable() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
@@ -236,7 +259,38 @@ public class GroupInfoActivityTest {
|
||||
assertEquals("{\"requestedBackendId\":\"omx-team\"}", connection.requestBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveDispatchReminderPreferenceUsesScopedEndpoint() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(GroupInfoActivity.EXTRA_PROJECT_ID, "group-1")
|
||||
.putExtra(GroupInfoActivity.EXTRA_PROJECT_NAME, "巡检协作群");
|
||||
TestGroupInfoActivity activity = Robolectric
|
||||
.buildActivity(TestGroupInfoActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
RecordingConnection connection = new RecordingConnection(
|
||||
new URL("https://boss.hyzq.net/api/v1/projects/group-1/dispatch-reminder")
|
||||
);
|
||||
ReflectionHelpers.setField(activity, "apiClient", new RecordingBossApiClient(connection));
|
||||
ReflectionHelpers.setField(activity, "executor", new DirectExecutorService());
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"saveDispatchReminderPreference",
|
||||
ReflectionHelpers.ClassParameter.from(boolean.class, true)
|
||||
);
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle();
|
||||
|
||||
assertEquals("/api/v1/projects/group-1/dispatch-reminder", connection.lastPath);
|
||||
assertEquals("PATCH", connection.requestMethodValue);
|
||||
assertEquals("{\"lightDispatchReminderEnabled\":true}", connection.requestBody());
|
||||
}
|
||||
|
||||
private static JSONObject buildDetailPayload() throws Exception {
|
||||
return buildDetailPayload(false);
|
||||
}
|
||||
|
||||
private static JSONObject buildDetailPayload(boolean lightReminderEnabled) throws Exception {
|
||||
JSONObject threadMeta = new JSONObject()
|
||||
.put("threadId", "group-thread-3")
|
||||
.put("folderName", "Boss");
|
||||
@@ -245,6 +299,7 @@ public class GroupInfoActivityTest {
|
||||
.put("name", "巡检协作群")
|
||||
.put("isGroup", true)
|
||||
.put("collaborationMode", "development")
|
||||
.put("lightDispatchReminderEnabled", lightReminderEnabled)
|
||||
.put("threadMeta", threadMeta);
|
||||
return new JSONObject().put("project", project);
|
||||
}
|
||||
|
||||
@@ -463,10 +463,45 @@ public class ProjectDetailActivityUiTest {
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, dispatchPlan)
|
||||
);
|
||||
|
||||
assertTrue(viewTreeContainsText(card, "确认下发"));
|
||||
assertTrue(viewTreeContainsText(card, "确认一下"));
|
||||
assertTrue(viewTreeContainsText(card, "确认并记住"));
|
||||
assertTrue(viewTreeContainsText(card, "拒绝"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pendingDispatchPlanViewUsesCompactCopyWhenLightReminderEnabled() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "project-group")
|
||||
.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "协作群");
|
||||
TestProjectDetailActivity activity = Robolectric
|
||||
.buildActivity(TestProjectDetailActivity.class, intent)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
ReflectionHelpers.setField(activity, "projectCollaborationMode", "approval_required");
|
||||
ReflectionHelpers.setField(activity, "projectApprovalState", "pending_user");
|
||||
ReflectionHelpers.setField(activity, "lightDispatchReminderEnabled", true);
|
||||
|
||||
JSONObject dispatchPlan = new JSONObject()
|
||||
.put("planId", "dispatch-plan-light")
|
||||
.put("summary", "建议先让 Boss 移动控制台同步树莓派二代状态。")
|
||||
.put("targets", new JSONArray().put(new JSONObject()
|
||||
.put("projectId", "thread-1")
|
||||
.put("threadDisplayName", "Boss 移动控制台")
|
||||
.put("reason", "最近活跃")));
|
||||
|
||||
View card = ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"buildPendingDispatchPlanView",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, dispatchPlan)
|
||||
);
|
||||
|
||||
assertTrue(viewTreeContainsText(card, "主 Agent 已推荐 1 个线程"));
|
||||
assertTrue(viewTreeContainsText(card, "继续下发"));
|
||||
assertFalse(viewTreeContainsText(card, "等待你批准主 Agent 下发"));
|
||||
assertFalse(viewTreeContainsText(card, "当前确认状态:"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renderProjectShowsRepairEntryForDirtyGroupAndOpensGroupInfo() throws Exception {
|
||||
Intent intent = new Intent()
|
||||
|
||||
@@ -24,6 +24,7 @@ export async function POST(
|
||||
|
||||
const body = (await request.json().catch(() => ({}))) as {
|
||||
approvedTargetProjectIds?: string[];
|
||||
rememberLightReminder?: boolean;
|
||||
};
|
||||
const { projectId, planId } = await context.params;
|
||||
|
||||
@@ -37,6 +38,7 @@ export async function POST(
|
||||
(item): item is string => typeof item === "string" && item.trim().length > 0,
|
||||
)
|
||||
: [],
|
||||
rememberLightReminder: body.rememberLightReminder === true,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import {
|
||||
buildCollaborationGate,
|
||||
getProject,
|
||||
updateProjectLightDispatchReminder,
|
||||
} from "@/lib/boss-data";
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ projectId: string }> },
|
||||
) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { projectId } = await context.params;
|
||||
const body = (await request.json().catch(() => ({}))) as {
|
||||
lightDispatchReminderEnabled?: unknown;
|
||||
};
|
||||
if (typeof body.lightDispatchReminderEnabled !== "boolean") {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: "INVALID_DISPATCH_REMINDER_PAYLOAD" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await updateProjectLightDispatchReminder({
|
||||
projectId,
|
||||
requestedBy: session.account,
|
||||
lightDispatchReminderEnabled: body.lightDispatchReminderEnabled,
|
||||
});
|
||||
const project = await getProject(projectId);
|
||||
if (!project) {
|
||||
return NextResponse.json({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 });
|
||||
}
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
project,
|
||||
collaborationGate: buildCollaborationGate(project),
|
||||
});
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : "UNKNOWN_ERROR";
|
||||
return NextResponse.json({ ok: false, message: reason }, { status: 400 });
|
||||
}
|
||||
}
|
||||
@@ -181,6 +181,7 @@ export default async function ProjectChatPage({
|
||||
</div>
|
||||
<ChatComposer
|
||||
projectId={detail.project.id}
|
||||
initialLightDispatchReminderEnabled={Boolean(detail.project.lightDispatchReminderEnabled)}
|
||||
dispatchPlanRecoveryHint={dispatchPlanState.pendingDispatchPlan ? dispatchPlanState.recoveryHint : null}
|
||||
initialPendingDispatchPlan={
|
||||
dispatchPlanState.pendingDispatchPlan
|
||||
|
||||
@@ -18,6 +18,8 @@ import { getMasterAgentChatMenuItems } from "@/lib/master-agent-chat-menu";
|
||||
import {
|
||||
extractApprovedTargetProjectIds,
|
||||
summarizeDispatchPlan,
|
||||
summarizeDispatchPlanCompact,
|
||||
summarizeDispatchPlanLightTitle,
|
||||
} from "@/lib/dispatch-plan-ui";
|
||||
import type {
|
||||
Device,
|
||||
@@ -1036,11 +1038,13 @@ export function ChatComposer({
|
||||
initialPendingDispatchPlan,
|
||||
initialRejectedDispatchPlan,
|
||||
dispatchPlanRecoveryHint,
|
||||
initialLightDispatchReminderEnabled = false,
|
||||
}: {
|
||||
projectId: string;
|
||||
initialPendingDispatchPlan?: PendingDispatchPlanState | null;
|
||||
initialRejectedDispatchPlan?: PendingDispatchPlanState | null;
|
||||
dispatchPlanRecoveryHint?: string | null;
|
||||
initialLightDispatchReminderEnabled?: boolean;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [value, setValue] = useState("");
|
||||
@@ -1052,6 +1056,9 @@ export function ChatComposer({
|
||||
const [localRejectedDispatchPlan, setLocalRejectedDispatchPlan] =
|
||||
useState<PendingDispatchPlanState | null>(null);
|
||||
const [dismissedPendingPlanId, setDismissedPendingPlanId] = useState<string | null>(null);
|
||||
const [lightDispatchReminderEnabled, setLightDispatchReminderEnabled] = useState(
|
||||
initialLightDispatchReminderEnabled,
|
||||
);
|
||||
const pendingDispatchPlan =
|
||||
localPendingDispatchPlan ??
|
||||
(initialPendingDispatchPlan && initialPendingDispatchPlan.planId !== dismissedPendingPlanId
|
||||
@@ -1060,7 +1067,7 @@ export function ChatComposer({
|
||||
const rejectedDispatchPlan =
|
||||
pendingDispatchPlan ? null : localRejectedDispatchPlan ?? initialRejectedDispatchPlan ?? null;
|
||||
|
||||
async function confirmDispatchPlan() {
|
||||
async function confirmDispatchPlan(rememberLightReminder = false) {
|
||||
if (!pendingDispatchPlan) return;
|
||||
setLoading(true);
|
||||
const response = await fetch(
|
||||
@@ -1070,12 +1077,16 @@ export function ChatComposer({
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
approvedTargetProjectIds: extractApprovedTargetProjectIds(pendingDispatchPlan),
|
||||
rememberLightReminder,
|
||||
}),
|
||||
},
|
||||
);
|
||||
const result = (await response.json()) as {
|
||||
ok: boolean;
|
||||
executions?: Array<unknown>;
|
||||
collaborationGate?: {
|
||||
lightDispatchReminderEnabled?: boolean;
|
||||
};
|
||||
message?: string;
|
||||
};
|
||||
setLoading(false);
|
||||
@@ -1085,11 +1096,18 @@ export function ChatComposer({
|
||||
return;
|
||||
}
|
||||
const executionCount = result.executions?.length ?? extractApprovedTargetProjectIds(pendingDispatchPlan).length;
|
||||
setLightDispatchReminderEnabled(
|
||||
result.collaborationGate?.lightDispatchReminderEnabled ?? lightDispatchReminderEnabled,
|
||||
);
|
||||
setLocalPendingDispatchPlan(null);
|
||||
setLocalRejectedDispatchPlan(null);
|
||||
setDismissedPendingPlanId(pendingDispatchPlan.planId);
|
||||
setMessageTone("success");
|
||||
setMessage(`已确认下发到 ${executionCount} 个线程。`);
|
||||
setMessage(
|
||||
rememberLightReminder
|
||||
? `已确认下发到 ${executionCount} 个线程,并记住这个群使用轻提醒。`
|
||||
: `已确认下发到 ${executionCount} 个线程。`,
|
||||
);
|
||||
router.refresh();
|
||||
}
|
||||
|
||||
@@ -1112,6 +1130,7 @@ export function ChatComposer({
|
||||
} | null;
|
||||
collaborationGate?: {
|
||||
requiresMasterAgentApproval?: boolean;
|
||||
lightDispatchReminderEnabled?: boolean;
|
||||
};
|
||||
message?: string;
|
||||
};
|
||||
@@ -1132,6 +1151,9 @@ export function ChatComposer({
|
||||
: null,
|
||||
);
|
||||
setDismissedPendingPlanId(null);
|
||||
setLightDispatchReminderEnabled(
|
||||
result.collaborationGate?.lightDispatchReminderEnabled ?? lightDispatchReminderEnabled,
|
||||
);
|
||||
setMessageTone("success");
|
||||
setMessage(
|
||||
result.collaborationGate?.requiresMasterAgentApproval
|
||||
@@ -1199,6 +1221,7 @@ export function ChatComposer({
|
||||
} | null;
|
||||
collaborationGate?: {
|
||||
requiresMasterAgentApproval?: boolean;
|
||||
lightDispatchReminderEnabled?: boolean;
|
||||
};
|
||||
messageText?: string;
|
||||
};
|
||||
@@ -1222,6 +1245,9 @@ export function ChatComposer({
|
||||
});
|
||||
setLocalRejectedDispatchPlan(null);
|
||||
setDismissedPendingPlanId(null);
|
||||
setLightDispatchReminderEnabled(
|
||||
result.collaborationGate?.lightDispatchReminderEnabled ?? lightDispatchReminderEnabled,
|
||||
);
|
||||
setMessage(
|
||||
result.collaborationGate?.requiresMasterAgentApproval
|
||||
? "消息已发送,等待你批准主 Agent 下发。"
|
||||
@@ -1305,17 +1331,32 @@ export function ChatComposer({
|
||||
) : null}
|
||||
{pendingDispatchPlan ? (
|
||||
<div className="mt-3 rounded-2xl border border-[#E5E5EA] bg-[#F7F8FA] px-4 py-4 text-[12px] leading-6 text-[#57606A]">
|
||||
<div className="text-[14px] font-semibold text-[#111111]">等待你确认主 Agent 推荐</div>
|
||||
<div className="mt-2 whitespace-pre-line">{summarizeDispatchPlan(pendingDispatchPlan)}</div>
|
||||
<div className="text-[14px] font-semibold text-[#111111]">
|
||||
{lightDispatchReminderEnabled ? summarizeDispatchPlanLightTitle(pendingDispatchPlan) : "主 Agent 推荐下发"}
|
||||
</div>
|
||||
<div className="mt-2 whitespace-pre-line">{summarizeDispatchPlanCompact(pendingDispatchPlan)}</div>
|
||||
<div className="mt-2 text-[12px] text-[#8C8C8C]">
|
||||
{lightDispatchReminderEnabled ? "轻提醒已开启" : "当前仍会显式提醒你确认"}
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={loading}
|
||||
onClick={() => void confirmDispatchPlan()}
|
||||
onClick={() => void confirmDispatchPlan(false)}
|
||||
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:bg-[#B7E6C9]"
|
||||
>
|
||||
确认下发
|
||||
{lightDispatchReminderEnabled ? "继续下发" : "确认一下"}
|
||||
</button>
|
||||
{!lightDispatchReminderEnabled ? (
|
||||
<button
|
||||
type="button"
|
||||
disabled={loading}
|
||||
onClick={() => void confirmDispatchPlan(true)}
|
||||
className="rounded-full border border-[#D9D9D9] px-4 py-2 text-[13px] font-semibold text-[#57606A]"
|
||||
>
|
||||
确认并记住
|
||||
</button>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
disabled={loading}
|
||||
|
||||
@@ -314,6 +314,7 @@ export interface Project {
|
||||
createdByAgent: boolean;
|
||||
collaborationMode: "development" | "approval_required";
|
||||
approvalState: "not_required" | "pending_agent" | "pending_user" | "approved" | "rejected";
|
||||
lightDispatchReminderEnabled?: boolean;
|
||||
orchestrationBackendOverride?: OrchestrationBackendOverride;
|
||||
agentControls?: ProjectAgentControls;
|
||||
unreadCount: number;
|
||||
@@ -371,7 +372,7 @@ export interface DispatchExecution {
|
||||
}
|
||||
|
||||
export function buildCollaborationGate(
|
||||
project?: Pick<Project, "isGroup" | "collaborationMode" | "approvalState">,
|
||||
project?: Pick<Project, "isGroup" | "collaborationMode" | "approvalState" | "lightDispatchReminderEnabled">,
|
||||
) {
|
||||
if (!project) {
|
||||
return {
|
||||
@@ -379,6 +380,7 @@ export function buildCollaborationGate(
|
||||
collaborationMode: "development" as const,
|
||||
requiresMasterAgentApproval: false,
|
||||
approvalState: "not_required" as const,
|
||||
lightDispatchReminderEnabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -387,6 +389,7 @@ export function buildCollaborationGate(
|
||||
collaborationMode: project.collaborationMode,
|
||||
requiresMasterAgentApproval: project.isGroup && project.collaborationMode === "approval_required",
|
||||
approvalState: project.approvalState,
|
||||
lightDispatchReminderEnabled: Boolean(project.lightDispatchReminderEnabled),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2783,6 +2786,7 @@ function normalizeProject(raw: Partial<Project>, fallback?: Project): Project {
|
||||
createdByAgent: raw.createdByAgent ?? false,
|
||||
collaborationMode: raw.collaborationMode ?? "development",
|
||||
approvalState: raw.approvalState ?? "not_required",
|
||||
lightDispatchReminderEnabled: raw.lightDispatchReminderEnabled ?? false,
|
||||
orchestrationBackendOverride: normalizeOrchestrationBackendOverride(raw.orchestrationBackendOverride),
|
||||
agentControls: normalizeProjectAgentControls(raw.agentControls),
|
||||
};
|
||||
@@ -3686,6 +3690,33 @@ export async function updateProjectOrchestrationBackendOverride(input: {
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateProjectLightDispatchReminder(input: {
|
||||
projectId: string;
|
||||
requestedBy: string;
|
||||
lightDispatchReminderEnabled: boolean;
|
||||
}) {
|
||||
return mutateStateIfChanged((state) => {
|
||||
const project = state.projects.find((item) => item.id === input.projectId);
|
||||
if (!project) {
|
||||
throw new Error("PROJECT_NOT_FOUND");
|
||||
}
|
||||
if (!project.isGroup) {
|
||||
throw new Error("PROJECT_NOT_GROUP_CHAT");
|
||||
}
|
||||
requireDispatchActorSession(state, input.requestedBy);
|
||||
|
||||
const nextValue = Boolean(input.lightDispatchReminderEnabled);
|
||||
if (Boolean(project.lightDispatchReminderEnabled) == nextValue) {
|
||||
return { result: project, changed: false };
|
||||
}
|
||||
|
||||
project.lightDispatchReminderEnabled = nextValue;
|
||||
project.updatedAt = nowIso();
|
||||
project.threadMeta.updatedAt = project.updatedAt;
|
||||
return { result: project, changed: true };
|
||||
});
|
||||
}
|
||||
|
||||
export async function hasPersistedProject(projectId: string) {
|
||||
const rawState = await loadPersistedStateRaw();
|
||||
return Array.isArray(rawState.projects) && rawState.projects.some((project) => project?.id === projectId);
|
||||
@@ -5559,6 +5590,7 @@ export async function confirmDispatchPlanAndCreateExecutions(input: {
|
||||
planId: string;
|
||||
confirmedBy: string;
|
||||
approvedTargetProjectIds: string[];
|
||||
rememberLightReminder?: boolean;
|
||||
}) {
|
||||
const result = await mutateState((state) => {
|
||||
const groupProjectId = input.groupProjectId.trim();
|
||||
@@ -5566,6 +5598,11 @@ export async function confirmDispatchPlanAndCreateExecutions(input: {
|
||||
const groupProject = state.projects.find((item) => item.id === groupProjectId);
|
||||
if (!groupProject) throw new Error("PROJECT_NOT_FOUND");
|
||||
if (!canOwnDispatchPlans(groupProject)) throw new Error("PROJECT_NOT_GROUP_CHAT");
|
||||
if (input.rememberLightReminder && !groupProject.lightDispatchReminderEnabled) {
|
||||
groupProject.lightDispatchReminderEnabled = true;
|
||||
groupProject.updatedAt = nowIso();
|
||||
groupProject.threadMeta.updatedAt = groupProject.updatedAt;
|
||||
}
|
||||
|
||||
const plan = applyDispatchPlanConfirmationInState(state, {
|
||||
planId: input.planId,
|
||||
|
||||
@@ -71,6 +71,29 @@ export function summarizeDispatchPlan(plan: DispatchPlanUiPayload | null | undef
|
||||
return `${summary}\n推荐线程:${titles.join("、")}`;
|
||||
}
|
||||
|
||||
export function summarizeDispatchPlanCompact(plan: DispatchPlanUiPayload | null | undefined) {
|
||||
if (!plan) {
|
||||
return "主 Agent 暂未生成推荐线程。";
|
||||
}
|
||||
const summary = plan.summary?.trim() || "主 Agent 已生成推荐线程。";
|
||||
const titles = (plan.targets ?? [])
|
||||
.map((target) => target.threadDisplayName?.trim() || "")
|
||||
.filter(Boolean);
|
||||
if (!titles.length) {
|
||||
return truncateDispatchSummary(summary);
|
||||
}
|
||||
return `推荐给:${titles.join("、")}\n${truncateDispatchSummary(summary)}`;
|
||||
}
|
||||
|
||||
export function summarizeDispatchPlanLightTitle(plan: DispatchPlanUiPayload | null | undefined) {
|
||||
const count = (plan?.targets ?? []).length;
|
||||
return count > 0 ? `主 Agent 已推荐 ${count} 个线程` : "主 Agent 已推荐线程";
|
||||
}
|
||||
|
||||
function truncateDispatchSummary(summary: string) {
|
||||
return summary.length > 32 ? `${summary.slice(0, 32)}…` : summary;
|
||||
}
|
||||
|
||||
export function extractApprovedTargetProjectIds(plan: DispatchPlanUiPayload | null | undefined) {
|
||||
return (plan?.targets ?? [])
|
||||
.map((target) => target.projectId?.trim() || "")
|
||||
|
||||
@@ -11,6 +11,7 @@ let getDispatchPlansRoute: (typeof import("../src/app/api/v1/projects/[projectId
|
||||
let confirmDispatchPlanRoute: (typeof import("../src/app/api/v1/projects/[projectId]/dispatch-plans/[planId]/confirm/route"))["POST"];
|
||||
let rejectDispatchPlanRoute: (typeof import("../src/app/api/v1/projects/[projectId]/dispatch-plans/[planId]/reject/route"))["POST"];
|
||||
let retryDispatchPlanRoute: (typeof import("../src/app/api/v1/projects/[projectId]/dispatch-plans/[planId]/retry/route"))["POST"];
|
||||
let updateDispatchReminderRoute: (typeof import("../src/app/api/v1/projects/[projectId]/dispatch-reminder/route"))["PATCH"];
|
||||
let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSession"];
|
||||
let createProjectGroupChat: (typeof import("../src/lib/boss-data"))["createProjectGroupChat"];
|
||||
let isDispatchableThreadProject: (typeof import("../src/lib/boss-data"))["isDispatchableThreadProject"];
|
||||
@@ -28,12 +29,13 @@ async function setup() {
|
||||
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
||||
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
||||
|
||||
const [messageModule, plansModule, confirmModule, rejectModule, retryModule, data, auth] = await Promise.all([
|
||||
const [messageModule, plansModule, confirmModule, rejectModule, retryModule, reminderModule, data, auth] = await Promise.all([
|
||||
import("../src/app/api/v1/projects/[projectId]/messages/route.ts"),
|
||||
import("../src/app/api/v1/projects/[projectId]/dispatch-plans/route.ts"),
|
||||
import("../src/app/api/v1/projects/[projectId]/dispatch-plans/[planId]/confirm/route.ts"),
|
||||
import("../src/app/api/v1/projects/[projectId]/dispatch-plans/[planId]/reject/route.ts"),
|
||||
import("../src/app/api/v1/projects/[projectId]/dispatch-plans/[planId]/retry/route.ts"),
|
||||
import("../src/app/api/v1/projects/[projectId]/dispatch-reminder/route.ts"),
|
||||
import("../src/lib/boss-data.ts"),
|
||||
import("../src/lib/boss-auth.ts"),
|
||||
]);
|
||||
@@ -43,6 +45,7 @@ async function setup() {
|
||||
confirmDispatchPlanRoute = confirmModule.POST;
|
||||
rejectDispatchPlanRoute = rejectModule.POST;
|
||||
retryDispatchPlanRoute = retryModule.POST;
|
||||
updateDispatchReminderRoute = reminderModule.PATCH;
|
||||
createAuthSession = data.createAuthSession;
|
||||
createProjectGroupChat = data.createProjectGroupChat;
|
||||
isDispatchableThreadProject = data.isDispatchableThreadProject;
|
||||
@@ -63,7 +66,7 @@ test.beforeEach(async () => {
|
||||
await writeState(structuredClone(baseState));
|
||||
});
|
||||
|
||||
async function createAuthedRequest(url: string, method: "GET" | "POST", body?: unknown) {
|
||||
async function createAuthedRequest(url: string, method: "GET" | "POST" | "PATCH", body?: unknown) {
|
||||
const session = await createAuthSession({
|
||||
account: "17600003315",
|
||||
role: "highest_admin",
|
||||
@@ -299,6 +302,39 @@ test("POST /api/v1/projects/[projectId]/dispatch-plans/[planId]/confirm confirms
|
||||
assert.equal(executionTask?.orchestrationBackendLabel, "Boss Native Orchestrator");
|
||||
});
|
||||
|
||||
test("confirming a dispatch plan with rememberLightReminder persists the group reminder preference", async () => {
|
||||
const { groupProject, dispatchPlan } = await createDispatchPlanForTest();
|
||||
const approvedTargetProjectId = dispatchPlan.targets[0]?.projectId;
|
||||
assert.ok(approvedTargetProjectId, "expected a recommended target project");
|
||||
|
||||
const response = await confirmDispatchPlanRoute(
|
||||
await createAuthedRequest(
|
||||
`http://127.0.0.1:3000/api/v1/projects/${groupProject.id}/dispatch-plans/${dispatchPlan.planId}/confirm`,
|
||||
"POST",
|
||||
{
|
||||
approvedTargetProjectIds: [approvedTargetProjectId],
|
||||
rememberLightReminder: true,
|
||||
},
|
||||
),
|
||||
{ params: Promise.resolve({ projectId: groupProject.id, planId: dispatchPlan.planId }) },
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
collaborationGate: {
|
||||
lightDispatchReminderEnabled?: boolean;
|
||||
};
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.collaborationGate.lightDispatchReminderEnabled, true);
|
||||
|
||||
const nextState = await readState();
|
||||
const nextGroupProject = nextState.projects.find((project) => project.id === groupProject.id);
|
||||
assert.ok(nextGroupProject, "expected group project to remain present");
|
||||
assert.equal(nextGroupProject?.lightDispatchReminderEnabled, true);
|
||||
});
|
||||
|
||||
test("master-agent dispatch plans can also be confirmed and create queued executions", async () => {
|
||||
const { dispatchPlan } = await createMasterAgentDispatchPlanForTest();
|
||||
const approvedTargetProjectId = dispatchPlan.targets[0]?.projectId;
|
||||
@@ -523,3 +559,56 @@ test("retrying a rejected dispatch plan creates a fresh pending recommendation a
|
||||
const nextGroupProject = nextState.projects.find((project) => project.id === groupProject.id);
|
||||
assert.equal(nextGroupProject?.approvalState, "pending_user");
|
||||
});
|
||||
|
||||
test("PATCH /api/v1/projects/[projectId]/dispatch-reminder updates the per-group light reminder preference", async () => {
|
||||
const { groupProject } = await createDispatchPlanForTest();
|
||||
|
||||
const enableResponse = await updateDispatchReminderRoute(
|
||||
await createAuthedRequest(
|
||||
`http://127.0.0.1:3000/api/v1/projects/${groupProject.id}/dispatch-reminder`,
|
||||
"PATCH",
|
||||
{ lightDispatchReminderEnabled: true },
|
||||
),
|
||||
{ params: Promise.resolve({ projectId: groupProject.id }) },
|
||||
);
|
||||
assert.equal(enableResponse.status, 200);
|
||||
const enablePayload = (await enableResponse.json()) as {
|
||||
ok: boolean;
|
||||
project: {
|
||||
id: string;
|
||||
lightDispatchReminderEnabled?: boolean;
|
||||
};
|
||||
};
|
||||
assert.equal(enablePayload.ok, true);
|
||||
assert.equal(enablePayload.project.id, groupProject.id);
|
||||
assert.equal(enablePayload.project.lightDispatchReminderEnabled, true);
|
||||
|
||||
const disableResponse = await updateDispatchReminderRoute(
|
||||
await createAuthedRequest(
|
||||
`http://127.0.0.1:3000/api/v1/projects/${groupProject.id}/dispatch-reminder`,
|
||||
"PATCH",
|
||||
{ lightDispatchReminderEnabled: false },
|
||||
),
|
||||
{ params: Promise.resolve({ projectId: groupProject.id }) },
|
||||
);
|
||||
assert.equal(disableResponse.status, 200);
|
||||
const disablePayload = (await disableResponse.json()) as {
|
||||
ok: boolean;
|
||||
project: {
|
||||
id: string;
|
||||
lightDispatchReminderEnabled?: boolean;
|
||||
};
|
||||
collaborationGate: {
|
||||
lightDispatchReminderEnabled?: boolean;
|
||||
};
|
||||
};
|
||||
assert.equal(disablePayload.ok, true);
|
||||
assert.equal(disablePayload.project.id, groupProject.id);
|
||||
assert.equal(disablePayload.project.lightDispatchReminderEnabled, false);
|
||||
assert.equal(disablePayload.collaborationGate.lightDispatchReminderEnabled, false);
|
||||
|
||||
const nextState = await readState();
|
||||
const nextGroupProject = nextState.projects.find((project) => project.id === groupProject.id);
|
||||
assert.ok(nextGroupProject, "expected group project to remain present");
|
||||
assert.equal(nextGroupProject?.lightDispatchReminderEnabled, false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user