mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-30 12:55:02 +00:00
Fix collaborators-not-iterable crash; rename misspelled colloborators route
Crash root cause: UsersPermissionsCard and CapabilitiesCard both used the
TanStack Query key ["portfolioUsers", id], but a previous change made
UsersPermissionsCard's fetcher return { users, currentUser } while
CapabilitiesCard's still returned an array. TanStack Query dedupes by
key — whichever component mounted first won the cache; the other read an
incompatible shape and crashed on `for (const c of collaborators)`.
Fix: extracted a single shared fetcher (collaboratorsClient.ts) so both
components import the same fetchCollaborators function and consume the
same response shape. CapabilitiesCard projects data?.users ?? []; the
shared cache stays consistent regardless of mount order.
Also rename the route folder from "colloborators" to "collaborators"
(spelling fix). Used git mv so history follows. Fetch URLs, console
error labels, and the route doc comment updated.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
171c586db1
commit
66cc71d228
4 changed files with 58 additions and 56 deletions
|
|
@ -30,7 +30,7 @@ import {
|
|||
resolvePortfolioPrivilege,
|
||||
} from "@/app/lib/resolvePortfolioPrivilege";
|
||||
|
||||
// Get colloborators (users) that have access to the portfolio, plus the
|
||||
// Get collaborators (users) that have access to the portfolio, plus the
|
||||
// effective privilege of the requesting user (so the UI knows which actions
|
||||
// to expose).
|
||||
export async function GET(
|
||||
|
|
@ -140,7 +140,7 @@ export async function DELETE(
|
|||
{ status: 200 },
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("DELETE /colloborators error:", err);
|
||||
console.error("DELETE /collaborators error:", err);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to remove user" },
|
||||
{ status: 500 },
|
||||
|
|
@ -184,7 +184,7 @@ export async function PUT(
|
|||
{ status: 200 }
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("PUT /colloborators error:", err);
|
||||
console.error("PUT /collaborators error:", err);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to update role" },
|
||||
{ status: 500 }
|
||||
|
|
@ -372,7 +372,7 @@ export async function POST(
|
|||
{ status: 200 },
|
||||
);
|
||||
} catch (err) {
|
||||
console.error("POST /colloborators error:", err);
|
||||
console.error("POST /collaborators error:", err);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to invite user" },
|
||||
{ status: 500 }
|
||||
|
|
@ -11,6 +11,10 @@ import {
|
|||
import { Button } from "@/app/shadcn_components/ui/button";
|
||||
import { Badge } from "@/app/shadcn_components/ui/badge";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
COLLABORATORS_QUERY_KEY,
|
||||
fetchCollaborators,
|
||||
} from "./collaboratorsClient";
|
||||
|
||||
type Capability = "approver" | "contractor";
|
||||
|
||||
|
|
@ -30,20 +34,6 @@ async function getCapabilities(portfolioId: string): Promise<CapabilityEntry[]>
|
|||
return res.json();
|
||||
}
|
||||
|
||||
async function getCollaborators(
|
||||
portfolioId: string,
|
||||
): Promise<{ userId: string; name: string | null; email: string }[]> {
|
||||
const res = await fetch(`/api/portfolio/${portfolioId}/colloborators`);
|
||||
if (!res.ok) throw new Error("Failed to fetch collaborators");
|
||||
const json = await res.json();
|
||||
const users = Array.isArray(json) ? json : json.users ?? [];
|
||||
return users.map((u: any) => ({
|
||||
userId: String(u.userId),
|
||||
name: u.name ?? null,
|
||||
email: u.email ?? "",
|
||||
}));
|
||||
}
|
||||
|
||||
async function assignCapability(
|
||||
portfolioId: string,
|
||||
userId: string,
|
||||
|
|
@ -81,19 +71,20 @@ export function CapabilitiesCard({ portfolioId }: { portfolioId: string }) {
|
|||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const { data: collaborators = [], isLoading: loadingCollabs } = useQuery({
|
||||
queryKey: ["portfolioUsers", portfolioId],
|
||||
queryFn: () => getCollaborators(portfolioId),
|
||||
const { data: collaboratorsResponse, isLoading: loadingCollabs } = useQuery({
|
||||
queryKey: COLLABORATORS_QUERY_KEY(portfolioId),
|
||||
queryFn: () => fetchCollaborators(portfolioId),
|
||||
enabled: !!portfolioId,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
const collaborators = collaboratorsResponse?.users ?? [];
|
||||
|
||||
const isLoading = loadingCaps || loadingCollabs;
|
||||
|
||||
// Build a map: userId -> { capabilities: [] }
|
||||
const capMap: CapabilityMap = {};
|
||||
for (const c of collaborators) {
|
||||
capMap[c.userId] = { name: c.name, email: c.email, capabilities: [] };
|
||||
capMap[c.userId] = { name: c.name ?? null, email: c.email, capabilities: [] };
|
||||
}
|
||||
for (const e of entries) {
|
||||
if (capMap[e.userId]) {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,11 @@ import {
|
|||
canAdminister,
|
||||
type PortfolioPrivilege,
|
||||
} from "@/app/lib/portfolioAdmin";
|
||||
import {
|
||||
COLLABORATORS_QUERY_KEY,
|
||||
fetchCollaborators,
|
||||
type CollaboratorsResponse,
|
||||
} from "./collaboratorsClient";
|
||||
|
||||
type PendingInvitation = {
|
||||
invitationId: string;
|
||||
|
|
@ -30,35 +35,6 @@ type PendingInvitation = {
|
|||
createdAt: string;
|
||||
};
|
||||
|
||||
type CollaboratorsResponse = {
|
||||
users: Collaborator[];
|
||||
currentUser?: { privilege: PortfolioPrivilege };
|
||||
};
|
||||
|
||||
async function getPortfolioUsers(
|
||||
portfolioId: string,
|
||||
): Promise<CollaboratorsResponse> {
|
||||
const res = await fetch(`/api/portfolio/${portfolioId}/colloborators`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to fetch users");
|
||||
const json = await res.json();
|
||||
const rawUsers = Array.isArray(json) ? json : json.users;
|
||||
const users: Collaborator[] = Array.isArray(rawUsers)
|
||||
? rawUsers.map((u: any) => ({
|
||||
portfolioUserId: String(u.portfolioUserId),
|
||||
userId: String(u.userId),
|
||||
name: u.name ?? null,
|
||||
email: u.email ?? "",
|
||||
role: u.role,
|
||||
}))
|
||||
: [];
|
||||
const privilege: PortfolioPrivilege | undefined =
|
||||
json?.currentUser?.privilege;
|
||||
return privilege ? { users, currentUser: { privilege } } : { users };
|
||||
}
|
||||
|
||||
async function getPortfolioInvitations(
|
||||
portfolioId: string,
|
||||
): Promise<PendingInvitation[]> {
|
||||
|
|
@ -84,7 +60,7 @@ async function updatePortfolioUserRole(
|
|||
portfolioUserId: string,
|
||||
role: Role,
|
||||
): Promise<void> {
|
||||
const res = await fetch(`/api/portfolio/${portfolioId}/colloborators`, {
|
||||
const res = await fetch(`/api/portfolio/${portfolioId}/collaborators`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ portfolioUserId, role }),
|
||||
|
|
@ -101,7 +77,7 @@ async function invitePortfolioUser(
|
|||
role: Role,
|
||||
name: string,
|
||||
): Promise<void> {
|
||||
const res = await fetch(`/api/portfolio/${portfolioId}/colloborators`, {
|
||||
const res = await fetch(`/api/portfolio/${portfolioId}/collaborators`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, role, name }),
|
||||
|
|
@ -116,7 +92,7 @@ async function removePortfolioUser(
|
|||
portfolioId: string,
|
||||
portfolioUserId: string,
|
||||
): Promise<void> {
|
||||
const res = await fetch(`/api/portfolio/${portfolioId}/colloborators`, {
|
||||
const res = await fetch(`/api/portfolio/${portfolioId}/collaborators`, {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ portfolioUserId }),
|
||||
|
|
@ -149,7 +125,7 @@ export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) {
|
|||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const usersKey = ["portfolioUsers", portfolioId];
|
||||
const usersKey = COLLABORATORS_QUERY_KEY(portfolioId);
|
||||
const invitationsKey = ["portfolioInvitations", portfolioId];
|
||||
|
||||
const {
|
||||
|
|
@ -159,7 +135,7 @@ export function UsersPermissionsCard({ portfolioId }: { portfolioId: string }) {
|
|||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: usersKey,
|
||||
queryFn: () => getPortfolioUsers(portfolioId),
|
||||
queryFn: () => fetchCollaborators(portfolioId),
|
||||
enabled: !!portfolioId,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
import type { Collaborator } from "./roles";
|
||||
import type { PortfolioPrivilege } from "@/app/lib/portfolioAdmin";
|
||||
|
||||
export type CollaboratorsResponse = {
|
||||
users: Collaborator[];
|
||||
currentUser?: { privilege: PortfolioPrivilege };
|
||||
};
|
||||
|
||||
// Shared fetcher used by every component that queries the portfolio user
|
||||
// list. Keeping a single function (and a single response shape) means
|
||||
// useQuery deduping behaves correctly when the user-access page mounts
|
||||
// both UsersPermissionsCard and CapabilitiesCard against the same key.
|
||||
export async function fetchCollaborators(
|
||||
portfolioId: string,
|
||||
): Promise<CollaboratorsResponse> {
|
||||
const res = await fetch(`/api/portfolio/${portfolioId}/collaborators`);
|
||||
if (!res.ok) throw new Error("Failed to fetch collaborators");
|
||||
const json = await res.json();
|
||||
const rawUsers = Array.isArray(json) ? json : (json?.users ?? []);
|
||||
const users: Collaborator[] = Array.isArray(rawUsers)
|
||||
? rawUsers.map((u: any) => ({
|
||||
portfolioUserId: String(u.portfolioUserId),
|
||||
userId: String(u.userId),
|
||||
name: u.name ?? null,
|
||||
email: u.email ?? "",
|
||||
role: u.role,
|
||||
}))
|
||||
: [];
|
||||
const privilege: PortfolioPrivilege | undefined =
|
||||
json?.currentUser?.privilege;
|
||||
return privilege ? { users, currentUser: { privilege } } : { users };
|
||||
}
|
||||
|
||||
export const COLLABORATORS_QUERY_KEY = (portfolioId: string) =>
|
||||
["portfolioUsers", portfolioId] as const;
|
||||
Loading…
Add table
Reference in a new issue