Skip to content

Commit 4c714e5

Browse files
author
raineer
committed
fix merge conflicts
1 parent 1a4f555 commit 4c714e5

2 files changed

Lines changed: 142 additions & 67 deletions

File tree

‎apps/codebility/app/home/projects/_components/ProjectEditModal.tsx‎

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { ChangeEvent, useEffect, useMemo, useState } from "react";
3+
import { ChangeEvent, useEffect, useMemo, useRef, useState } from "react";
44
import dynamic from "next/dynamic";
55
import Image from "next/image";
66
import {
@@ -104,13 +104,13 @@ const ProjectEditModal = () => {
104104
} = useForm<ProjectFormData>({ mode: "onChange" });
105105

106106
const { data: users = [], isLoading: isUsersLoading } = useQuery({
107-
queryKey: ["projectCodevs"],
107+
queryKey: ["projectCodevs", "v3"],
108108
queryFn: async () => {
109109
const result = await getProjectCodevs();
110110
return result || [];
111111
},
112-
staleTime: 10 * 60 * 1000,
113-
refetchOnWindowFocus: false,
112+
staleTime: 2 * 60 * 1000,
113+
refetchOnWindowFocus: true,
114114
});
115115

116116
const { data: clients = [], isLoading: isClientsLoading } = useQuery({
@@ -179,14 +179,11 @@ const ProjectEditModal = () => {
179179
// getProjectCodevs() may exclude users due to RLS or internal_status filtering.
180180
// We use fullProjectData.project_members (from getProjectByID join) as the
181181
// source of truth — it always returns codev data regardless of RLS.
182-
// This covers the team leader (CBP-95) and any member used as sublead (CBP-116).
183182
const enhancedUserOptions = useMemo(() => {
184183
if (!fullProjectData?.project_members) return userOptions;
185184

186-
// Build a set of codev_ids already present in userOptions for O(1) lookup
187185
const existingIds = new Set(userOptions.map((opt) => opt.value));
188186

189-
// Collect all project members missing from userOptions
190187
const missingMembers = fullProjectData.project_members
191188
.filter((pm: any) => pm.codev && !existingIds.has(pm.codev_id))
192189
.map((pm: any) => ({
@@ -199,7 +196,6 @@ const ProjectEditModal = () => {
199196

200197
if (missingMembers.length === 0) return userOptions;
201198

202-
// Inject missing members at the top so they are always findable
203199
return [...missingMembers, ...userOptions];
204200
}, [userOptions, fullProjectData]);
205201

@@ -246,8 +242,19 @@ const ProjectEditModal = () => {
246242
// Pre-populate team leader, sublead, and members from fullProjectData.
247243
// fullProjectData comes from getProjectByID which includes project_members.
248244
// The list page query does not join project_members so we cannot use data directly.
245+
//
246+
// IMPORTANT: Only run when modal opens (isModalOpen changes to true) to avoid
247+
// resetting selectedMembers when users list refetches during editing.
248+
const hasInitialized = useRef(false);
249+
249250
useEffect(() => {
250-
if (!fullProjectData || !isModalOpen) return;
251+
if (!fullProjectData || !isModalOpen) {
252+
hasInitialized.current = false;
253+
return;
254+
}
255+
256+
// Skip if already initialized and users are available
257+
if (hasInitialized.current && users.length > 0) return;
251258

252259
const projectMembers = fullProjectData.project_members;
253260
if (!projectMembers?.length) return;
@@ -314,18 +321,14 @@ const ProjectEditModal = () => {
314321
// users.filter(u => memberIds.includes(u.id))
315322
// which silently excluded RLS-filtered members from selectedMembers.
316323
// On submit, those missing members were deleted from project_members DB.
317-
// Fix: use pm.codev fallback (same pattern as team leader / sublead above)
318-
// so every member in DB is represented in the form payload on save.
324+
// Fix: use pm.codev fallback so every member in DB is in the form payload.
319325
const memberPMs = projectMembers.filter((pm: any) => pm.role === "member");
320326

321327
const resolvedMembers = memberPMs
322328
.map((pm: any) => {
323-
// Try full Codev object from users first
324329
const fromUsers = users.find((u) => u.id === pm.codev_id);
325330
if (fromUsers) return fromUsers;
326331

327-
// Fallback: construct minimal Codev from embedded codev data.
328-
// Handles users absent from getProjectCodevs() due to RLS filtering.
329332
if (pm.codev) {
330333
return {
331334
id: pm.codev_id,
@@ -339,14 +342,15 @@ const ProjectEditModal = () => {
339342
} as unknown as Codev;
340343
}
341344

342-
// pm.codev is null — RLS dropped the codev row even from the two-query
343-
// approach. Log a warning; skip this member to avoid a null in the array.
344345
console.warn(`⚠️ Could not resolve member codev data for: ${pm.codev_id}`);
345346
return null;
346347
})
347348
.filter(Boolean) as Codev[];
348349

349350
setSelectedMembers(resolvedMembers);
351+
352+
// Mark as initialized to prevent resets when users list refetches
353+
hasInitialized.current = true;
350354
// ─────────────────────────────────────────────────────────────────────────
351355
}, [fullProjectData, users, isModalOpen]);
352356

@@ -715,9 +719,8 @@ const ProjectEditModal = () => {
715719
</div>
716720

717721
{/* ── CBP-116: Sublead selector ─────────────────────────────────────
718-
Optional — no asterisk, no toast validation required.
719-
Excluded from members list and excluded from team leader list.
720-
Clearing the select (empty string value) sets sublead to null.
722+
Optional. Sourced from fullProjectData.project_members to bypass
723+
RLS filtering that drops some users from getProjectCodevs().
721724
──────────────────────────────────────────────────────────────────── */}
722725
<div className="space-y-1">
723726
<label className="text-xs font-semibold text-gray-700 dark:text-gray-300">
@@ -728,11 +731,7 @@ const ProjectEditModal = () => {
728731
</label>
729732
<CustomSelect
730733
options={[
731-
// "None" option — value must not be empty string (Radix UI Select.Item constraint)
732734
{ id: "none", value: "none", label: "No sublead assigned", subLabel: "" },
733-
// Only show members of this project, excluding the team leader.
734-
// Source: fullProjectData.project_members (bypasses RLS filtering
735-
// that drops some users from getProjectCodevs results).
736735
...(fullProjectData?.project_members ?? [])
737736
.filter(
738737
(pm: any) =>
@@ -759,7 +758,6 @@ const ProjectEditModal = () => {
759758
setCurrentSubLead(subLeadFromUsers);
760759
return;
761760
}
762-
// Fallback: construct from fullProjectData (handles RLS-filtered users)
763761
const subLeadMember = fullProjectData?.project_members?.find(
764762
(pm: any) => pm.codev_id === value,
765763
);

‎apps/codebility/app/home/projects/actions.ts‎

Lines changed: 120 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,11 @@ export const updateProjectMembers = async (
740740
members: Codev[],
741741
teamLeaderId: string,
742742
): Promise<{ success: boolean; error?: string }> => {
743+
console.log('🔧 [updateProjectMembers] Server-side update starting');
744+
console.log(' Project ID:', projectId);
745+
console.log(' Members to add:', members.length);
746+
console.log(' Team Leader ID:', teamLeaderId);
747+
743748
const supabase = await createClientServerComponent();
744749

745750
try {
@@ -751,6 +756,8 @@ export const updateProjectMembers = async (
751756

752757
if (fetchError) throw fetchError;
753758

759+
console.log(' Existing members in DB:', existingMembers?.length ?? 0);
760+
754761
// Preserve joined_at timestamps
755762
const joinedAtMap = new Map(
756763
existingMembers?.map(m => [m.codev_id, m.joined_at]) ?? []
@@ -767,6 +774,8 @@ export const updateProjectMembers = async (
767774

768775
if (deleteError) throw deleteError;
769776

777+
console.log(' Old members deleted, preparing inserts...');
778+
770779
const memberInserts = [
771780
// Team lead + regular members
772781
...members.map((member) => ({
@@ -786,27 +795,50 @@ export const updateProjectMembers = async (
786795
: []),
787796
];
788797

798+
console.log(' Inserting members:', memberInserts.length);
799+
console.log(' Member IDs:', memberInserts.map(m => m.codev_id));
800+
console.log(' Roles:', memberInserts.map(m => m.role));
801+
789802
const { error: insertError } = await supabase
790803
.from("project_members")
791804
.insert(memberInserts);
792805

793806
if (insertError) throw insertError;
794807

808+
console.log('✅ [updateProjectMembers] Successfully inserted', memberInserts.length, 'members');
809+
795810
revalidatePath("/projects");
796811
return { success: true };
797812
} catch (error) {
798-
console.error("Error updating project members:", error);
813+
console.error("❌ [updateProjectMembers] Error updating project members:", error);
799814
return {
800815
success: false,
801816
error: error instanceof Error ? error.message : "Unknown error occurred",
802817
};
803818
}
804819
};
805820

821+
/**
822+
* ─── PATCH: Fix RLS filtering issue ──────────────────────────────────────────
823+
* Use two-query approach to avoid PostgREST join RLS filtering that silently
824+
* drops users. Same pattern as getProjectByID() and getMembers().
825+
*
826+
* IMPORTANT: Uses anon client (no auth) to bypass RLS policies that may filter
827+
* role_id field. This matches the landing page behavior where all mentors are visible.
828+
*
829+
* Updated to match Add Members Modal filtering logic - includes users based on
830+
* both role_id AND internal_status to ensure GRADUATED users are included.
831+
* ─────────────────────────────────────────────────────────────────────────────
832+
*/
806833
export const getProjectCodevs = async (filters = {}): Promise<Codev[]> => {
807-
const supabase = await createClientServerComponent();
834+
// Use anon client to avoid RLS filtering of role_id field (same as landing page)
835+
const { createClient } = await import("@supabase/supabase-js");
836+
const supabase = createClient(
837+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
838+
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
839+
);
808840

809-
let query = supabase.from("codev").select(`
841+
const selectFields = `
810842
id,
811843
first_name,
812844
last_name,
@@ -816,54 +848,99 @@ export const getProjectCodevs = async (filters = {}): Promise<Codev[]> => {
816848
tech_stacks,
817849
display_position,
818850
internal_status,
819-
project_members!codev_id (
820-
project:project_id (
821-
id,
822-
name
823-
),
824-
role,
825-
joined_at
826-
)
827-
`);
851+
role_id
852+
`;
853+
854+
// Step 1: Fetch users using same logic as Add Members Modal "smart filter"
855+
// Smart filter: role_id = 5 (Mentor) OR role_id = 1 (Admin) OR internal_status = GRADUATED
856+
const queries = [
857+
supabase.from("codev").select(selectFields).eq("role_id", 5),
858+
supabase.from("codev").select(selectFields).eq("role_id", 1),
859+
supabase.from("codev").select(selectFields).eq("internal_status", "GRADUATED"),
860+
supabase.from("codev").select(selectFields).in("internal_status", ["TRAINING", "INTERN", "ONBOARDING"]),
861+
];
862+
863+
const results = await Promise.all(queries);
828864

829-
Object.entries(filters).forEach(([key, value]) => {
830-
if (value !== undefined) {
831-
query = query.eq(key, value);
865+
// Combine all results and deduplicate by id
866+
const codevMap = new Map<string, any>();
867+
868+
results.forEach(({ data, error }, index) => {
869+
if (error) {
870+
console.error(`Error fetching codevs (query ${index}):`, error);
871+
} else if (data) {
872+
data.forEach(codev => {
873+
if (!codevMap.has(codev.id)) {
874+
codevMap.set(codev.id, codev);
875+
}
876+
});
832877
}
833878
});
834879

835-
const { data, error } = await query;
880+
let codevs = Array.from(codevMap.values());
836881

837-
if (error) {
838-
console.error("Error fetching Codevs:", error);
839-
throw new Error("Failed to fetch Codevs");
882+
// Apply additional filters if provided
883+
if (Object.keys(filters).length > 0) {
884+
codevs = codevs.filter(codev => {
885+
return Object.entries(filters).every(([key, value]) => {
886+
if (value === undefined) return true;
887+
return codev[key] === value;
888+
});
889+
});
840890
}
841891

842-
return (
843-
data?.map((codev: any) => {
844-
const projects = (codev.project_members || []).map(
845-
(pm: DbProjectMember) => {
846-
const project: Project & { role: string; joined_at: string } = {
847-
id: pm.project.id,
848-
name: pm.project.name,
849-
role: pm.role,
850-
joined_at: pm.joined_at,
851-
status: pm.project.status,
852-
kanban_display: pm.project.kanban_display,
853-
public_display: pm.project.public_display,
854-
};
855-
return project;
856-
},
857-
);
892+
if (codevs.length === 0) {
893+
return [];
894+
}
858895

859-
return {
860-
...codev,
861-
positions: codev.positions || [],
862-
tech_stacks: codev.tech_stacks || [],
863-
projects,
864-
} as Codev;
865-
}) || []
866-
);
896+
// Step 2: Fetch project_members separately for all codevs
897+
const codevIds = codevs.map(c => c.id);
898+
899+
const { data: projectMembers, error: pmError } = await supabase
900+
.from("project_members")
901+
.select(`
902+
codev_id,
903+
project_id,
904+
role,
905+
joined_at,
906+
project:project_id (
907+
id,
908+
name,
909+
status,
910+
kanban_display,
911+
public_display
912+
)
913+
`)
914+
.in("codev_id", codevIds);
915+
916+
if (pmError) {
917+
console.error("Error fetching project members:", pmError);
918+
}
919+
920+
// Step 3: Merge project_members data back into codevs
921+
return codevs.map((codev: any) => {
922+
const codevProjectMembers = projectMembers?.filter(pm => pm.codev_id === codev.id) || [];
923+
924+
const projects = codevProjectMembers.map((pm: any) => {
925+
const project: Project & { role: string; joined_at: string } = {
926+
id: pm.project.id,
927+
name: pm.project.name,
928+
role: pm.role,
929+
joined_at: pm.joined_at,
930+
status: pm.project.status,
931+
kanban_display: pm.project.kanban_display,
932+
public_display: pm.project.public_display,
933+
};
934+
return project;
935+
});
936+
937+
return {
938+
...codev,
939+
positions: codev.positions || [],
940+
tech_stacks: codev.tech_stacks || [],
941+
projects,
942+
} as Codev;
943+
});
867944
};
868945

869946
export const getProjectClients = async (): Promise<Client[]> => {

0 commit comments

Comments
 (0)