feat: add organization-related models and update schemas for organization management

This commit is contained in:
Abhimanyu Saharan
2026-02-08 21:16:26 +05:30
parent 8422b0ca01
commit e03125a382
86 changed files with 8673 additions and 628 deletions

View File

@@ -27,9 +27,14 @@ import {
streamBoardGroupMemoryApiV1BoardGroupsGroupIdMemoryStreamGet,
useListBoardGroupMemoryApiV1BoardGroupsGroupIdMemoryGet,
} from "@/api/generated/board-group-memory/board-group-memory";
import {
type getMyMembershipApiV1OrganizationsMeMemberGetResponse,
useGetMyMembershipApiV1OrganizationsMeMemberGet,
} from "@/api/generated/organizations/organizations";
import type {
BoardGroupHeartbeatApplyResult,
BoardGroupMemoryRead,
OrganizationMemberRead,
} from "@/api/generated/model";
import type { BoardGroupBoardSnapshot } from "@/api/generated/model";
import { Markdown } from "@/components/atoms/Markdown";
@@ -96,6 +101,18 @@ const priorityTone = (value?: string | null) => {
const safeCount = (snapshot: BoardGroupBoardSnapshot, key: string) =>
snapshot.task_counts?.[key] ?? 0;
const canWriteGroupBoards = (
member: OrganizationMemberRead | null,
boardIds: Set<string>,
) => {
if (!member) return false;
if (member.all_boards_write) return true;
if (!member.board_access || boardIds.size === 0) return false;
return member.board_access.some(
(access) => access.can_write && boardIds.has(access.board_id),
);
};
function GroupChatMessageCard({ message }: { message: BoardGroupMemoryRead }) {
return (
<div className="rounded-2xl border border-slate-200 bg-slate-50/60 p-4">
@@ -215,6 +232,34 @@ export default function BoardGroupDetailPage() {
snapshotQuery.data?.status === 200 ? snapshotQuery.data.data : null;
const group = snapshot?.group ?? null;
const boards = useMemo(() => snapshot?.boards ?? [], [snapshot?.boards]);
const boardIdSet = useMemo(() => {
const ids = new Set<string>();
boards.forEach((item) => {
if (item.board?.id) {
ids.add(item.board.id);
}
});
return ids;
}, [boards]);
const membershipQuery = useGetMyMembershipApiV1OrganizationsMeMemberGet<
getMyMembershipApiV1OrganizationsMeMemberGetResponse,
ApiError
>({
query: {
enabled: Boolean(isSignedIn),
refetchOnMount: "always",
},
});
const member =
membershipQuery.data?.status === 200 ? membershipQuery.data.data : null;
const isAdmin = member?.role === "admin" || member?.role === "owner";
const canWriteGroup = useMemo(
() => canWriteGroupBoards(member, boardIdSet),
[boardIdSet, member],
);
const canManageHeartbeat = Boolean(isAdmin && canWriteGroup);
const chatHistoryQuery =
useListBoardGroupMemoryApiV1BoardGroupsGroupIdMemoryGet<
@@ -554,6 +599,10 @@ export default function BoardGroupDetailPage() {
setChatError("Sign in to send messages.");
return false;
}
if (!canWriteGroup) {
setChatError("Read-only access. You cannot post group messages.");
return false;
}
const trimmed = content.trim();
if (!trimmed) return false;
@@ -583,7 +632,7 @@ export default function BoardGroupDetailPage() {
setIsChatSending(false);
}
},
[chatBroadcast, groupId, isSignedIn, mergeChatMessages],
[canWriteGroup, chatBroadcast, groupId, isSignedIn, mergeChatMessages],
);
const sendGroupNote = useCallback(
@@ -592,6 +641,10 @@ export default function BoardGroupDetailPage() {
setNoteSendError("Sign in to post.");
return false;
}
if (!canWriteGroup) {
setNoteSendError("Read-only access. You cannot post notes.");
return false;
}
const trimmed = content.trim();
if (!trimmed) return false;
@@ -621,7 +674,7 @@ export default function BoardGroupDetailPage() {
setIsNoteSending(false);
}
},
[groupId, isSignedIn, mergeNotesMessages, notesBroadcast],
[canWriteGroup, groupId, isSignedIn, mergeNotesMessages, notesBroadcast],
);
const applyHeartbeat = useCallback(async () => {
@@ -629,6 +682,10 @@ export default function BoardGroupDetailPage() {
setHeartbeatApplyError("Sign in to apply.");
return;
}
if (!canManageHeartbeat) {
setHeartbeatApplyError("Read-only access. You cannot change agent pace.");
return;
}
const trimmed = heartbeatEvery.trim();
if (!trimmed) {
setHeartbeatApplyError("Heartbeat cadence is required.");
@@ -653,7 +710,7 @@ export default function BoardGroupDetailPage() {
} finally {
setIsHeartbeatApplying(false);
}
}, [groupId, heartbeatEvery, includeBoardLeads, isSignedIn]);
}, [canManageHeartbeat, groupId, heartbeatEvery, includeBoardLeads, isSignedIn]);
return (
<DashboardShell>
@@ -793,7 +850,9 @@ export default function BoardGroupDetailPage() {
heartbeatEvery === value
? "bg-slate-900 text-white"
: "text-slate-600 hover:bg-slate-100 hover:text-slate-900",
!canManageHeartbeat && "opacity-50 cursor-not-allowed",
)}
disabled={!canManageHeartbeat}
onClick={() => {
setHeartbeatAmount(String(preset.amount));
setHeartbeatUnit(preset.unit);
@@ -812,19 +871,25 @@ export default function BoardGroupDetailPage() {
heartbeatEvery
? "border-slate-200"
: "border-rose-300 focus:border-rose-400 focus:ring-2 focus:ring-rose-100",
!canManageHeartbeat && "opacity-60 cursor-not-allowed",
)}
placeholder="10"
inputMode="numeric"
type="number"
min={1}
step={1}
disabled={!canManageHeartbeat}
/>
<select
value={heartbeatUnit}
onChange={(event) =>
setHeartbeatUnit(event.target.value as HeartbeatUnit)
}
className="h-8 rounded-md border border-slate-200 bg-white px-2 text-xs text-slate-900 shadow-sm"
className={cn(
"h-8 rounded-md border border-slate-200 bg-white px-2 text-xs text-slate-900 shadow-sm",
!canManageHeartbeat && "opacity-60 cursor-not-allowed",
)}
disabled={!canManageHeartbeat}
>
<option value="s">sec</option>
<option value="m">min</option>
@@ -839,6 +904,7 @@ export default function BoardGroupDetailPage() {
onChange={(event) =>
setIncludeBoardLeads(event.target.checked)
}
disabled={!canManageHeartbeat}
/>
Include leads
</label>
@@ -846,11 +912,24 @@ export default function BoardGroupDetailPage() {
size="sm"
variant="outline"
onClick={() => void applyHeartbeat()}
disabled={isHeartbeatApplying || !heartbeatEvery}
disabled={
isHeartbeatApplying || !heartbeatEvery || !canManageHeartbeat
}
title={
canManageHeartbeat
? "Apply heartbeat"
: "Read-only access"
}
>
{isHeartbeatApplying ? "Applying…" : "Apply"}
</Button>
</div>
{!canManageHeartbeat ? (
<p className="text-xs text-slate-500">
Read-only access. You cannot change agent pace for this
group.
</p>
) : null}
</div>
</div>
</div>
@@ -1035,6 +1114,7 @@ export default function BoardGroupDetailPage() {
className="h-4 w-4 rounded border-slate-300 text-blue-600"
checked={chatBroadcast}
onChange={(event) => setChatBroadcast(event.target.checked)}
disabled={!canWriteGroup}
/>
Broadcast
</label>
@@ -1072,9 +1152,14 @@ export default function BoardGroupDetailPage() {
</div>
<BoardChatComposer
placeholder="Message the whole group. Tag @lead, @name, or @all."
placeholder={
canWriteGroup
? "Message the whole group. Tag @lead, @name, or @all."
: "Read-only access. Group chat is disabled."
}
isSending={isChatSending}
onSend={sendGroupChat}
disabled={!canWriteGroup}
/>
</div>
</div>
@@ -1115,6 +1200,7 @@ export default function BoardGroupDetailPage() {
className="h-4 w-4 rounded border-slate-300 text-blue-600"
checked={notesBroadcast}
onChange={(event) => setNotesBroadcast(event.target.checked)}
disabled={!canWriteGroup}
/>
Broadcast
</label>
@@ -1152,9 +1238,14 @@ export default function BoardGroupDetailPage() {
</div>
<BoardChatComposer
placeholder="Post a shared note for all linked boards. Tag @lead, @name, or @all."
placeholder={
canWriteGroup
? "Post a shared note for all linked boards. Tag @lead, @name, or @all."
: "Read-only access. Notes are disabled."
}
isSending={isNoteSending}
onSend={sendGroupNote}
disabled={!canWriteGroup}
/>
</div>
</div>