mirror of
https://github.com/block/buzz.git
synced 2026-11-13 21:01:53 +00:00
## Summary - Makes Projects repository-aware across browsing, branch and tag selection, local and remote source management, work items, commits, and contextual repository actions. - Refines project details into consistent single-column reading surfaces with a resizable, section-aware context pod, while keeping project conversations available in the attached chat panel. - Adds persistent sidebar project navigation, direct entity links, repository discussion channels, contributor identity matching, and consistent loading and activity presentation. - Keeps branch-specific controls on code-oriented sections while Tasks, Reviews, and Channels remain repository-scoped, reducing misleading context and actions. Replacement for #5981 with an identical final tree flattened into one signed-off commit because the required DCO check suite remained stalled. ### Related issue N/A ### Testing - [x] Desktop pre-push checks, TypeScript typecheck, and unit tests - [x] E2E production build with `pnpm build:e2e` - [x] Focused Playwright smoke coverage for project workspace, task, review, commit, sidebar, and contextual-panel behavior - [ ] Attach final before/after screenshots after design review --------- Signed-off-by: Thomas Petersen <thomasp@squareup.com> Signed-off-by: Wintermute <165f0c871dd2586bb18b6aa109eeaf57bb2132ff4d27b10120f4368a0f627022@buzz.block.builderlab.xyz> Co-authored-by: Wintermute <165f0c871dd2586bb18b6aa109eeaf57bb2132ff4d27b10120f4368a0f627022@buzz.block.builderlab.xyz>
168 lines
4.5 KiB
TypeScript
168 lines
4.5 KiB
TypeScript
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import * as React from "react";
|
|
|
|
import {
|
|
signProjectIssueAssignment,
|
|
signProjectIssueUnassignment,
|
|
} from "@/shared/api/projectGit";
|
|
import { relayClient } from "@/shared/api/relayClient";
|
|
import { signRelayEvent } from "@/shared/api/tauri";
|
|
import { KIND_TEXT_NOTE } from "@/shared/constants/kinds";
|
|
import type { Repository as Project } from "./hooks";
|
|
import {
|
|
ISSUE_ASSIGNMENT_LABEL,
|
|
ISSUE_UNASSIGNMENT_LABEL,
|
|
nextProjectIssueCommentCreatedAt,
|
|
type ProjectIssue,
|
|
} from "./projectIssues.mjs";
|
|
|
|
function nextAssignmentOperationCreatedAt(
|
|
issue: ProjectIssue,
|
|
project: Project,
|
|
signAsManagedOwner: boolean,
|
|
signerPubkey: string,
|
|
) {
|
|
const signer = signAsManagedOwner ? project.owner : signerPubkey;
|
|
return nextProjectIssueCommentCreatedAt(
|
|
issue,
|
|
Math.floor(Date.now() / 1_000),
|
|
signer,
|
|
);
|
|
}
|
|
|
|
function normalizedAssigneeLabel(label: string) {
|
|
const normalized = label.trim();
|
|
if (normalized.length === 0 || Array.from(normalized).length > 128) {
|
|
throw new Error("Assignee label must be between 1 and 128 characters.");
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
type IssueAssignmentOperation = "assign" | "unassign";
|
|
type IssueAssignmentMutationInput = {
|
|
assignees: string[];
|
|
assigneeLabel: string;
|
|
issue: ProjectIssue;
|
|
signerPubkey: string;
|
|
signAsManagedOwner: boolean;
|
|
};
|
|
|
|
async function writeProjectIssueAssignment({
|
|
assignees,
|
|
assigneeLabel,
|
|
issue,
|
|
operation,
|
|
project,
|
|
signerPubkey,
|
|
signAsManagedOwner,
|
|
}: IssueAssignmentMutationInput & {
|
|
operation: IssueAssignmentOperation;
|
|
project: Project;
|
|
}): Promise<void> {
|
|
if (assignees.length === 0) {
|
|
throw new Error("Select at least one assignee.");
|
|
}
|
|
const normalizedLabel = normalizedAssigneeLabel(assigneeLabel);
|
|
const assigneePubkeys = [
|
|
...new Set(assignees.map((pubkey) => pubkey.toLowerCase())),
|
|
];
|
|
const createdAt = nextAssignmentOperationCreatedAt(
|
|
issue,
|
|
project,
|
|
signAsManagedOwner,
|
|
signerPubkey,
|
|
);
|
|
const isAssignment = operation === "assign";
|
|
const content = isAssignment
|
|
? `Assigned this task to ${normalizedLabel}`
|
|
: `Unassigned ${normalizedLabel} from this task`;
|
|
const label = isAssignment
|
|
? ISSUE_ASSIGNMENT_LABEL
|
|
: ISSUE_UNASSIGNMENT_LABEL;
|
|
if (signAsManagedOwner) {
|
|
const signManagedOperation = isAssignment
|
|
? signProjectIssueAssignment
|
|
: signProjectIssueUnassignment;
|
|
await signManagedOperation({
|
|
targetOwner: project.owner,
|
|
repoAddress: project.repoAddress,
|
|
issueId: issue.id,
|
|
assignees: assigneePubkeys,
|
|
assigneeLabel: normalizedLabel,
|
|
createdAt,
|
|
});
|
|
return;
|
|
}
|
|
const normalizedSigner = signerPubkey.toLowerCase();
|
|
const prior =
|
|
assigneePubkeys.length === 1 && assigneePubkeys[0] === normalizedSigner
|
|
? issue.assigneeOperationHeads[normalizedSigner]
|
|
: undefined;
|
|
const event = await signRelayEvent({
|
|
kind: KIND_TEXT_NOTE,
|
|
content,
|
|
createdAt,
|
|
tags: [
|
|
["e", issue.id, "", "root"],
|
|
["a", project.repoAddress],
|
|
...assigneePubkeys.map((pubkey) => ["p", pubkey]),
|
|
["t", label],
|
|
...(prior ? [["prior", prior]] : []),
|
|
],
|
|
});
|
|
|
|
await relayClient.publishEvent(
|
|
event,
|
|
`Timed out ${operation}ing task.`,
|
|
`Failed to ${operation} task.`,
|
|
);
|
|
}
|
|
|
|
export function useProjectIssueWriteInvalidation(
|
|
project: Project | null | undefined,
|
|
) {
|
|
const queryClient = useQueryClient();
|
|
return React.useCallback(() => {
|
|
void queryClient.invalidateQueries({
|
|
queryKey: ["project", project?.id ?? "none", "issues"],
|
|
});
|
|
void queryClient.invalidateQueries({
|
|
queryKey: ["projects", "work-items"],
|
|
});
|
|
void queryClient.invalidateQueries({
|
|
queryKey: ["projects", "activity-summaries"],
|
|
});
|
|
}, [project?.id, queryClient]);
|
|
}
|
|
|
|
function useProjectIssueAssignmentMutation(
|
|
project: Project | null | undefined,
|
|
operation: IssueAssignmentOperation,
|
|
) {
|
|
const invalidate = useProjectIssueWriteInvalidation(project);
|
|
|
|
return useMutation({
|
|
mutationFn: (input: IssueAssignmentMutationInput) => {
|
|
if (!project) throw new Error("No project selected.");
|
|
return writeProjectIssueAssignment({
|
|
...input,
|
|
operation,
|
|
project,
|
|
});
|
|
},
|
|
onSuccess: invalidate,
|
|
});
|
|
}
|
|
|
|
export function useAssignProjectIssueMutation(
|
|
project: Project | null | undefined,
|
|
) {
|
|
return useProjectIssueAssignmentMutation(project, "assign");
|
|
}
|
|
|
|
export function useUnassignProjectIssueMutation(
|
|
project: Project | null | undefined,
|
|
) {
|
|
return useProjectIssueAssignmentMutation(project, "unassign");
|
|
}
|