llzai's picture
Upload 1793 files
9853396 verified
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { graphqlRequest } from '@/gql/graphql';
import { USERS_QUERY, CREATE_USER_MUTATION, UPDATE_USER_MUTATION, UPDATE_USER_STATUS_MUTATION } from '@/gql/users';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { useSelectedProjectId } from '@/stores/projectStore';
import { useErrorHandler } from '@/hooks/use-error-handler';
import { useRequestPermissions } from '@/hooks/useRequestPermissions';
import { User, UserConnection, ProjectUser, CreateUserInput, UpdateUserInput, userSchema, projectUserSchema } from './schema';
// Dynamic GraphQL query builder for project-level users
// This query fetches users with their project-specific information (owner status and scopes)
// Conditionally includes roles based on user permissions
function buildProjectUsersQuery(permissions: { canViewRoles: boolean }) {
const rolesFields = permissions.canViewRoles
? `
roles(where: { projectID: $projectId }) {
edges {
node {
id
name
level
}
}
}`
: '';
return `
query ProjectUsers($projectId: ID!) {
node(id: $projectId) {
... on Project {
id
name
projectUsers {
id
userID
projectID
isOwner
scopes
user {
id
createdAt
updatedAt
email
status
firstName
lastName
preferLanguage${rolesFields}
}
}
}
}
}
`;
}
// Mutation to add a user to a project
export const ADD_USER_TO_PROJECT_MUTATION = `
mutation AddUserToProject($input: AddUserToProjectInput!) {
addUserToProject(input: $input) {
id
userID
projectID
isOwner
scopes
}
}
`;
// Mutation to remove a user from a project
export const REMOVE_USER_FROM_PROJECT_MUTATION = `
mutation RemoveUserFromProject($input: RemoveUserFromProjectInput!) {
removeUserFromProject(input: $input)
}
`;
// Mutation to update user's project membership
export const UPDATE_PROJECT_USER_MUTATION = `
mutation UpdateProjectUser($input: UpdateProjectUserInput!) {
updateProjectUser(input: $input) {
id
userID
projectID
isOwner
scopes
}
}
`;
// Query to get all users (for adding to project)
export const ALL_USERS_QUERY = `
query AllUsers($first: Int, $after: Cursor, $where: UserWhereInput) {
users(first: $first, after: $after, where: $where) {
edges {
node {
id
email
firstName
lastName
status
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`;
// Query hooks - for project-level users
export function useUsers(
variables?: {
first?: number;
after?: string;
orderBy?: { field: 'CREATED_AT'; direction: 'ASC' | 'DESC' };
where?: Record<string, any>;
},
options?: {
disableAutoFetch?: boolean;
}
) {
const { t } = useTranslation();
const { handleError } = useErrorHandler();
const permissions = useRequestPermissions();
const selectedProjectId = useSelectedProjectId();
return useQuery({
queryKey: ['project-users', selectedProjectId, variables, permissions],
queryFn: async () => {
try {
if (!selectedProjectId) {
throw new Error('No project selected');
}
const query = buildProjectUsersQuery(permissions);
const headers = { 'X-Project-ID': selectedProjectId };
const data = await graphqlRequest<{
node: {
id: string;
name: string;
projectUsers: ProjectUser[];
};
}>(query, { projectId: selectedProjectId }, headers);
// Transform projectUsers to User format for display
const projectUsers = data.node.projectUsers || [];
const transformedUsers = projectUsers.map((pu) => {
const parsedPU = projectUserSchema.parse(pu);
return {
id: parsedPU.user.id,
createdAt: parsedPU.user.createdAt,
updatedAt: parsedPU.user.updatedAt,
email: parsedPU.user.email,
status: parsedPU.user.status,
firstName: parsedPU.user.firstName,
lastName: parsedPU.user.lastName,
preferLanguage: parsedPU.user.preferLanguage,
isOwner: parsedPU.isOwner,
scopes: parsedPU.scopes,
roles: parsedPU.user.roles,
projectUserId: parsedPU.id, // Store the project_user ID for removal
};
});
// Sort by createdAt DESC (newest first)
transformedUsers.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
// Return in UserConnection format for compatibility
return {
edges: transformedUsers.map((user) => ({ node: user })),
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
startCursor: null,
endCursor: null,
},
};
} catch (error) {
handleError(error, t('users.messages.loadUsersError'));
throw error;
}
},
enabled: !options?.disableAutoFetch && !!selectedProjectId,
});
}
export function useUser(id: string) {
const { t } = useTranslation();
const { handleError } = useErrorHandler();
const selectedProjectId = useSelectedProjectId();
return useQuery({
queryKey: ['user', id, selectedProjectId],
queryFn: async () => {
try {
const headers = selectedProjectId ? { 'X-Project-ID': selectedProjectId } : undefined;
const data = await graphqlRequest<{ users: UserConnection }>(USERS_QUERY, { where: { id } }, headers);
const user = data.users.edges[0]?.node;
if (!user) {
throw new Error(t('users.messages.userNotFound'));
}
return userSchema.parse(user);
} catch (error) {
handleError(error, t('users.messages.loadUserError'));
throw error;
}
},
enabled: !!id,
});
}
// Mutation hooks
export function useCreateUser() {
const { t } = useTranslation();
const queryClient = useQueryClient();
const selectedProjectId = useSelectedProjectId();
return useMutation({
mutationFn: async (input: CreateUserInput) => {
const headers = selectedProjectId ? { 'X-Project-ID': selectedProjectId } : undefined;
const data = await graphqlRequest<{ createUser: User }>(CREATE_USER_MUTATION, { input }, headers);
return userSchema.parse(data.createUser);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
toast.success(t('users.messages.createSuccess'));
},
onError: (error: any) => {
toast.error(t('users.messages.createError') + `: ${error.message}`);
},
});
}
export function useUpdateUser() {
const { t } = useTranslation();
const queryClient = useQueryClient();
const selectedProjectId = useSelectedProjectId();
return useMutation({
mutationFn: async ({ id, input }: { id: string; input: UpdateUserInput }) => {
const headers = selectedProjectId ? { 'X-Project-ID': selectedProjectId } : undefined;
const data = await graphqlRequest<{ updateUser: User }>(UPDATE_USER_MUTATION, { id, input }, headers);
return userSchema.parse(data.updateUser);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
toast.success(t('users.messages.updateSuccess'));
},
onError: (error: any) => {
toast.error(t('users.messages.updateError') + `: ${error.message}`);
},
});
}
export function useUpdateUserStatus() {
const { t } = useTranslation();
const queryClient = useQueryClient();
const selectedProjectId = useSelectedProjectId();
return useMutation({
mutationFn: async ({ id, status }: { id: string; status: 'activated' | 'deactivated' }) => {
const headers = selectedProjectId ? { 'X-Project-ID': selectedProjectId } : undefined;
const data = await graphqlRequest<{ updateUserStatus: boolean }>(UPDATE_USER_STATUS_MUTATION, { id, status }, headers);
return data.updateUserStatus;
},
onSuccess: (_data, variables) => {
queryClient.invalidateQueries({ queryKey: ['users'] });
queryClient.invalidateQueries({ queryKey: ['user', variables.id] });
const statusText = variables.status === 'activated' ? t('users.status.activated') : t('users.status.deactivated');
toast.success(t('users.messages.statusUpdateSuccess', { status: statusText }));
},
onError: (error: any) => {
toast.error(t('users.messages.statusUpdateError') + `: ${error.message}`);
},
});
}
export function useDeleteUser() {
const { t } = useTranslation();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (_id: string) => {
// This is now deprecated, use useRemoveUserFromProject instead
throw new Error('Direct deletion is not supported. Use removeUserFromProject instead.');
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
toast.success(t('users.messages.deleteSuccess'));
},
onError: (error: any) => {
toast.error(t('users.messages.deleteError') + `: ${error.message}`);
},
});
}
// Add user to project
export function useAddUserToProject() {
const { t } = useTranslation();
const queryClient = useQueryClient();
const selectedProjectId = useSelectedProjectId();
return useMutation({
mutationFn: async (input: { userId: string; isOwner?: boolean; scopes?: string[]; roleIDs?: string[] }) => {
if (!selectedProjectId) {
throw new Error('No project selected');
}
const headers = { 'X-Project-ID': selectedProjectId };
const data = await graphqlRequest<{ addUserToProject: any }>(
ADD_USER_TO_PROJECT_MUTATION,
{
input: {
projectId: selectedProjectId,
userId: input.userId,
isOwner: input.isOwner,
scopes: input.scopes,
roleIDs: input.roleIDs,
},
},
headers
);
return data.addUserToProject;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['project-users', selectedProjectId] });
toast.success(t('users.messages.addToProjectSuccess'));
},
onError: (error: any) => {
toast.error(t('users.messages.addToProjectError') + `: ${error.message}`);
},
});
}
// Remove user from project
export function useRemoveUserFromProject() {
const { t } = useTranslation();
const queryClient = useQueryClient();
const selectedProjectId = useSelectedProjectId();
return useMutation({
mutationFn: async (userId: string) => {
if (!selectedProjectId) {
throw new Error('No project selected');
}
const headers = { 'X-Project-ID': selectedProjectId };
const data = await graphqlRequest<{ removeUserFromProject: boolean }>(
REMOVE_USER_FROM_PROJECT_MUTATION,
{
input: {
projectId: selectedProjectId,
userId: userId,
},
},
headers
);
return data.removeUserFromProject;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['project-users', selectedProjectId] });
toast.success(t('users.messages.removeFromProjectSuccess'));
},
onError: (error: any) => {
toast.error(t('users.messages.removeFromProjectError') + `: ${error.message}`);
},
});
}
// Update project user (for editing roles/scopes)
export function useUpdateProjectUser() {
const { t } = useTranslation();
const queryClient = useQueryClient();
const selectedProjectId = useSelectedProjectId();
return useMutation({
mutationFn: async (input: {
userId: string;
isOwner?: boolean;
scopes?: string[];
addRoleIDs?: string[];
removeRoleIDs?: string[];
}) => {
if (!selectedProjectId) {
throw new Error('No project selected');
}
const headers = { 'X-Project-ID': selectedProjectId };
const data = await graphqlRequest<{ updateProjectUser: any }>(
UPDATE_PROJECT_USER_MUTATION,
{
input: {
projectId: selectedProjectId,
userId: input.userId,
isOwner: input.isOwner,
scopes: input.scopes,
addRoleIDs: input.addRoleIDs,
removeRoleIDs: input.removeRoleIDs,
},
},
headers
);
return data.updateProjectUser;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['project-users', selectedProjectId] });
toast.success(t('users.messages.updateSuccess'));
},
onError: (error: any) => {
toast.error(t('users.messages.updateError') + `: ${error.message}`);
},
});
}
// Get all users (for adding to project)
export function useAllUsers(variables?: { first?: number; after?: string; where?: Record<string, any> }, options?: { enabled?: boolean }) {
const { t } = useTranslation();
const { handleError } = useErrorHandler();
return useQuery({
queryKey: ['all-users', variables],
queryFn: async () => {
try {
const data = await graphqlRequest<{ users: UserConnection }>(ALL_USERS_QUERY, variables);
return data.users;
} catch (error) {
handleError(error, t('users.messages.loadUsersError'));
throw error;
}
},
enabled: options?.enabled ?? true,
});
}
// Export users for compatibility
export const users = {
useUsers,
useUser,
useCreateUser,
useUpdateUser,
useDeleteUser,
};