Compare commits
27 Commits
2023.12.6
...
feat/admin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70665dae03 | ||
|
|
efc98588d9 | ||
|
|
619bdf85f3 | ||
|
|
2509545dea | ||
|
|
df2d5995fd | ||
|
|
3c2b48a635 | ||
|
|
a0d40c8776 | ||
|
|
3c7a2401ae | ||
|
|
9543369ff3 | ||
|
|
fd5abd59fb | ||
|
|
8f6ca169ce | ||
|
|
2eab86476e | ||
|
|
b53cbb093c | ||
|
|
2bde3f8b02 | ||
|
|
da606f5a96 | ||
|
|
2a667a74f0 | ||
|
|
a4c889e38d | ||
|
|
9ceef43c74 | ||
|
|
abaddd94a5 | ||
|
|
88bca2057a | ||
|
|
3ff6cc53bb | ||
|
|
1df2520bf0 | ||
|
|
5368c52aab | ||
|
|
9c6754c70f | ||
|
|
b359650d96 | ||
|
|
3482743782 | ||
|
|
3d6adcc39d |
@@ -118,7 +118,7 @@ services:
|
||||
restart: always
|
||||
environment:
|
||||
# Edit the below line to match your PostgresDB URL if you have an outside DB (make sure to update the .env file as well)
|
||||
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
|
||||
# - DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
|
||||
- PORT=3000
|
||||
volumes:
|
||||
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
||||
|
||||
@@ -27,9 +27,7 @@ import {
|
||||
} from './input-types.args';
|
||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||
import { SkipThrottle } from '@nestjs/throttler';
|
||||
import { User } from 'src/user/user.model';
|
||||
import { PaginationArgs } from 'src/types/input-types.args';
|
||||
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
|
||||
import { UserDeletionResult } from 'src/user/user.model';
|
||||
|
||||
@UseGuards(GqlThrottlerGuard)
|
||||
@Resolver(() => Admin)
|
||||
@@ -49,203 +47,6 @@ export class AdminResolver {
|
||||
return admin;
|
||||
}
|
||||
|
||||
@ResolveField(() => [User], {
|
||||
description: 'Returns a list of all admin users in infra',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async admins() {
|
||||
const admins = await this.adminService.fetchAdmins();
|
||||
return admins;
|
||||
}
|
||||
@ResolveField(() => User, {
|
||||
description: 'Returns a user info by UID',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async userInfo(
|
||||
@Args({
|
||||
name: 'userUid',
|
||||
type: () => ID,
|
||||
description: 'The user UID',
|
||||
})
|
||||
userUid: string,
|
||||
): Promise<AuthUser> {
|
||||
const user = await this.adminService.fetchUserInfo(userUid);
|
||||
if (E.isLeft(user)) throwErr(user.left);
|
||||
return user.right;
|
||||
}
|
||||
|
||||
@ResolveField(() => [User], {
|
||||
description: 'Returns a list of all the users in infra',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async allUsers(
|
||||
@Parent() admin: Admin,
|
||||
@Args() args: PaginationArgs,
|
||||
): Promise<AuthUser[]> {
|
||||
const users = await this.adminService.fetchUsers(args.cursor, args.take);
|
||||
return users;
|
||||
}
|
||||
|
||||
@ResolveField(() => [InvitedUser], {
|
||||
description: 'Returns a list of all the invited users',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async invitedUsers(@Parent() admin: Admin): Promise<InvitedUser[]> {
|
||||
const users = await this.adminService.fetchInvitedUsers();
|
||||
return users;
|
||||
}
|
||||
|
||||
@ResolveField(() => [Team], {
|
||||
description: 'Returns a list of all the teams in the infra',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async allTeams(
|
||||
@Parent() admin: Admin,
|
||||
@Args() args: PaginationArgs,
|
||||
): Promise<Team[]> {
|
||||
const teams = await this.adminService.fetchAllTeams(args.cursor, args.take);
|
||||
return teams;
|
||||
}
|
||||
@ResolveField(() => Team, {
|
||||
description: 'Returns a team info by ID when requested by Admin',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async teamInfo(
|
||||
@Parent() admin: Admin,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
type: () => ID,
|
||||
description: 'Team ID for which info to fetch',
|
||||
})
|
||||
teamID: string,
|
||||
): Promise<Team> {
|
||||
const team = await this.adminService.getTeamInfo(teamID);
|
||||
if (E.isLeft(team)) throwErr(team.left);
|
||||
return team.right;
|
||||
}
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return count of all the members in a team',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async membersCountInTeam(
|
||||
@Parent() admin: Admin,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
type: () => ID,
|
||||
description: 'Team ID for which team members to fetch',
|
||||
nullable: false,
|
||||
})
|
||||
teamID: string,
|
||||
): Promise<number> {
|
||||
const teamMembersCount = await this.adminService.membersCountInTeam(teamID);
|
||||
return teamMembersCount;
|
||||
}
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return count of all the stored collections in a team',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async collectionCountInTeam(
|
||||
@Parent() admin: Admin,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
type: () => ID,
|
||||
description: 'Team ID for which team members to fetch',
|
||||
})
|
||||
teamID: string,
|
||||
): Promise<number> {
|
||||
const teamCollCount = await this.adminService.collectionCountInTeam(teamID);
|
||||
return teamCollCount;
|
||||
}
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return count of all the stored requests in a team',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async requestCountInTeam(
|
||||
@Parent() admin: Admin,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
type: () => ID,
|
||||
description: 'Team ID for which team members to fetch',
|
||||
})
|
||||
teamID: string,
|
||||
): Promise<number> {
|
||||
const teamReqCount = await this.adminService.requestCountInTeam(teamID);
|
||||
return teamReqCount;
|
||||
}
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return count of all the stored environments in a team',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async environmentCountInTeam(
|
||||
@Parent() admin: Admin,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
type: () => ID,
|
||||
description: 'Team ID for which team members to fetch',
|
||||
})
|
||||
teamID: string,
|
||||
): Promise<number> {
|
||||
const envsCount = await this.adminService.environmentCountInTeam(teamID);
|
||||
return envsCount;
|
||||
}
|
||||
|
||||
@ResolveField(() => [TeamInvitation], {
|
||||
description: 'Return all the pending invitations in a team',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async pendingInvitationCountInTeam(
|
||||
@Parent() admin: Admin,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
type: () => ID,
|
||||
description: 'Team ID for which team members to fetch',
|
||||
})
|
||||
teamID: string,
|
||||
) {
|
||||
const invitations = await this.adminService.pendingInvitationCountInTeam(
|
||||
teamID,
|
||||
);
|
||||
return invitations;
|
||||
}
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return total number of Users in organization',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async usersCount() {
|
||||
return this.adminService.getUsersCount();
|
||||
}
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return total number of Teams in organization',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async teamsCount() {
|
||||
return this.adminService.getTeamsCount();
|
||||
}
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return total number of Team Collections in organization',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async teamCollectionsCount() {
|
||||
return this.adminService.getTeamCollectionsCount();
|
||||
}
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return total number of Team Requests in organization',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async teamRequestsCount() {
|
||||
return this.adminService.getTeamRequestsCount();
|
||||
}
|
||||
|
||||
/* Mutations */
|
||||
|
||||
@Mutation(() => InvitedUser, {
|
||||
@@ -269,8 +70,26 @@ export class AdminResolver {
|
||||
return invitedUser.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Revoke a user invites by invitee emails',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async revokeUserInvitationsByAdmin(
|
||||
@Args({
|
||||
name: 'inviteeEmails',
|
||||
description: 'Invitee Emails',
|
||||
type: () => [String],
|
||||
})
|
||||
inviteeEmails: string[],
|
||||
): Promise<boolean> {
|
||||
const invite = await this.adminService.revokeUserInvitations(inviteeEmails);
|
||||
if (E.isLeft(invite)) throwErr(invite.left);
|
||||
return invite.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Delete an user account from infra',
|
||||
deprecationReason: 'Use removeUsersByAdmin instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async removeUserByAdmin(
|
||||
@@ -281,12 +100,33 @@ export class AdminResolver {
|
||||
})
|
||||
userUID: string,
|
||||
): Promise<boolean> {
|
||||
const invitedUser = await this.adminService.removeUserAccount(userUID);
|
||||
if (E.isLeft(invitedUser)) throwErr(invitedUser.left);
|
||||
return invitedUser.right;
|
||||
const removedUser = await this.adminService.removeUserAccount(userUID);
|
||||
if (E.isLeft(removedUser)) throwErr(removedUser.left);
|
||||
return removedUser.right;
|
||||
}
|
||||
|
||||
@Mutation(() => [UserDeletionResult], {
|
||||
description: 'Delete user accounts from infra',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async removeUsersByAdmin(
|
||||
@Args({
|
||||
name: 'userUIDs',
|
||||
description: 'users UID',
|
||||
type: () => [ID],
|
||||
})
|
||||
userUIDs: string[],
|
||||
): Promise<UserDeletionResult[]> {
|
||||
const deletionResults = await this.adminService.removeUserAccounts(
|
||||
userUIDs,
|
||||
);
|
||||
if (E.isLeft(deletionResults)) throwErr(deletionResults.left);
|
||||
return deletionResults.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Make user an admin',
|
||||
deprecationReason: 'Use makeUsersAdmin instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async makeUserAdmin(
|
||||
@@ -302,8 +142,51 @@ export class AdminResolver {
|
||||
return admin.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Make users an admin',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async makeUsersAdmin(
|
||||
@Args({
|
||||
name: 'userUIDs',
|
||||
description: 'users UID',
|
||||
type: () => [ID],
|
||||
})
|
||||
userUIDs: string[],
|
||||
): Promise<boolean> {
|
||||
const isUpdated = await this.adminService.makeUsersAdmin(userUIDs);
|
||||
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
|
||||
return isUpdated.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Update user display name',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async updateUserDisplayNameByAdmin(
|
||||
@Args({
|
||||
name: 'userUID',
|
||||
description: 'users UID',
|
||||
type: () => ID,
|
||||
})
|
||||
userUID: string,
|
||||
@Args({
|
||||
name: 'displayName',
|
||||
description: 'users display name',
|
||||
})
|
||||
displayName: string,
|
||||
): Promise<boolean> {
|
||||
const isUpdated = await this.adminService.updateUserDisplayName(
|
||||
userUID,
|
||||
displayName,
|
||||
);
|
||||
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
|
||||
return isUpdated.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Remove user as admin',
|
||||
deprecationReason: 'Use demoteUsersByAdmin instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async removeUserAsAdmin(
|
||||
@@ -319,6 +202,23 @@ export class AdminResolver {
|
||||
return admin.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
description: 'Remove users as admin',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async demoteUsersByAdmin(
|
||||
@Args({
|
||||
name: 'userUIDs',
|
||||
description: 'users UID',
|
||||
type: () => [ID],
|
||||
})
|
||||
userUIDs: string[],
|
||||
): Promise<boolean> {
|
||||
const isUpdated = await this.adminService.demoteUsersByAdmin(userUIDs);
|
||||
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
|
||||
return isUpdated.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Team, {
|
||||
description:
|
||||
'Create a new team by providing the user uid to nominate as Team owner',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AdminService } from './admin.service';
|
||||
import { PubSubService } from '../pubsub/pubsub.service';
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import { InvitedUsers } from '@prisma/client';
|
||||
import { InvitedUsers, User as DbUser } from '@prisma/client';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { TeamService } from '../team/team.service';
|
||||
import { TeamEnvironmentsService } from '../team-environments/team-environments.service';
|
||||
@@ -13,10 +13,15 @@ import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import {
|
||||
DUPLICATE_EMAIL,
|
||||
INVALID_EMAIL,
|
||||
ONLY_ONE_ADMIN_ACCOUNT,
|
||||
USER_ALREADY_INVITED,
|
||||
USER_INVITATION_DELETION_FAILED,
|
||||
USER_NOT_FOUND,
|
||||
} from '../errors';
|
||||
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { OffsetPaginationArgs } from 'src/types/input-types.args';
|
||||
import * as E from 'fp-ts/Either';
|
||||
|
||||
const mockPrisma = mockDeep<PrismaService>();
|
||||
const mockPubSub = mockDeep<PubSubService>();
|
||||
@@ -58,20 +63,87 @@ const invitedUsers: InvitedUsers[] = [
|
||||
invitedOn: new Date(),
|
||||
},
|
||||
];
|
||||
|
||||
const dbAdminUsers: DbUser[] = [
|
||||
{
|
||||
uid: 'uid 1',
|
||||
displayName: 'displayName',
|
||||
email: 'email@email.com',
|
||||
photoURL: 'photoURL',
|
||||
isAdmin: true,
|
||||
refreshToken: 'refreshToken',
|
||||
currentRESTSession: '',
|
||||
currentGQLSession: '',
|
||||
createdOn: new Date(),
|
||||
},
|
||||
{
|
||||
uid: 'uid 2',
|
||||
displayName: 'displayName',
|
||||
email: 'email@email.com',
|
||||
photoURL: 'photoURL',
|
||||
isAdmin: true,
|
||||
refreshToken: 'refreshToken',
|
||||
currentRESTSession: '',
|
||||
currentGQLSession: '',
|
||||
createdOn: new Date(),
|
||||
},
|
||||
];
|
||||
const dbNonAminUser: DbUser = {
|
||||
uid: 'uid 3',
|
||||
displayName: 'displayName',
|
||||
email: 'email@email.com',
|
||||
photoURL: 'photoURL',
|
||||
isAdmin: false,
|
||||
refreshToken: 'refreshToken',
|
||||
currentRESTSession: '',
|
||||
currentGQLSession: '',
|
||||
createdOn: new Date(),
|
||||
};
|
||||
|
||||
describe('AdminService', () => {
|
||||
describe('fetchInvitedUsers', () => {
|
||||
test('should resolve right and return an array of invited users', async () => {
|
||||
test('should resolve right and apply pagination correctly', async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
mockPrisma.user.findMany.mockResolvedValue([dbAdminUsers[0]]);
|
||||
// @ts-ignore
|
||||
mockPrisma.invitedUsers.findMany.mockResolvedValue(invitedUsers);
|
||||
|
||||
const results = await adminService.fetchInvitedUsers();
|
||||
const paginationArgs: OffsetPaginationArgs = { take: 5, skip: 2 };
|
||||
const results = await adminService.fetchInvitedUsers(paginationArgs);
|
||||
|
||||
expect(mockPrisma.invitedUsers.findMany).toHaveBeenCalledWith({
|
||||
...paginationArgs,
|
||||
orderBy: {
|
||||
invitedOn: 'desc',
|
||||
},
|
||||
where: {
|
||||
NOT: {
|
||||
inviteeEmail: {
|
||||
in: [dbAdminUsers[0].email],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
test('should resolve right and return an array of invited users', async () => {
|
||||
const paginationArgs: OffsetPaginationArgs = { take: 10, skip: 0 };
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
mockPrisma.user.findMany.mockResolvedValue([dbAdminUsers[0]]);
|
||||
// @ts-ignore
|
||||
mockPrisma.invitedUsers.findMany.mockResolvedValue(invitedUsers);
|
||||
|
||||
const results = await adminService.fetchInvitedUsers(paginationArgs);
|
||||
expect(results).toEqual(invitedUsers);
|
||||
});
|
||||
test('should resolve left and return an empty array if invited users not found', async () => {
|
||||
const paginationArgs: OffsetPaginationArgs = { take: 10, skip: 0 };
|
||||
|
||||
mockPrisma.invitedUsers.findMany.mockResolvedValue([]);
|
||||
|
||||
const results = await adminService.fetchInvitedUsers();
|
||||
const results = await adminService.fetchInvitedUsers(paginationArgs);
|
||||
expect(results).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -134,6 +206,58 @@ describe('AdminService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('revokeUserInvitations', () => {
|
||||
test('should resolve left and return error if email not invited', async () => {
|
||||
mockPrisma.invitedUsers.deleteMany.mockRejectedValueOnce(
|
||||
'RecordNotFound',
|
||||
);
|
||||
|
||||
const result = await adminService.revokeUserInvitations([
|
||||
'test@gmail.com',
|
||||
]);
|
||||
|
||||
expect(result).toEqualLeft(USER_INVITATION_DELETION_FAILED);
|
||||
});
|
||||
|
||||
test('should resolve right and return deleted invitee email', async () => {
|
||||
const adminUid = 'adminUid';
|
||||
mockPrisma.invitedUsers.deleteMany.mockResolvedValueOnce({ count: 1 });
|
||||
|
||||
const result = await adminService.revokeUserInvitations([
|
||||
invitedUsers[0].inviteeEmail,
|
||||
]);
|
||||
|
||||
expect(mockPrisma.invitedUsers.deleteMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
inviteeEmail: { in: [invitedUsers[0].inviteeEmail] },
|
||||
},
|
||||
});
|
||||
expect(result).toEqualRight(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeUsersAsAdmin', () => {
|
||||
test('should resolve right and make admins to users', async () => {
|
||||
mockUserService.fetchAdminUsers.mockResolvedValueOnce(dbAdminUsers);
|
||||
mockUserService.removeUsersAsAdmin.mockResolvedValueOnce(E.right(true));
|
||||
|
||||
return expect(
|
||||
await adminService.demoteUsersByAdmin([dbAdminUsers[0].uid]),
|
||||
).toEqualRight(true);
|
||||
});
|
||||
|
||||
test('should resolve left and return error if only one admin in the infra', async () => {
|
||||
mockUserService.fetchAdminUsers.mockResolvedValueOnce(dbAdminUsers);
|
||||
mockUserService.removeUsersAsAdmin.mockResolvedValueOnce(E.right(true));
|
||||
|
||||
return expect(
|
||||
await adminService.demoteUsersByAdmin(
|
||||
dbAdminUsers.map((user) => user.uid),
|
||||
),
|
||||
).toEqualLeft(ONLY_ONE_ADMIN_ACCOUNT);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUsersCount', () => {
|
||||
test('should return count of all users in the organization', async () => {
|
||||
mockUserService.getUsersCount.mockResolvedValueOnce(10);
|
||||
|
||||
@@ -6,13 +6,16 @@ import * as E from 'fp-ts/Either';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import { validateEmail } from '../utils';
|
||||
import {
|
||||
ADMIN_CAN_NOT_BE_DELETED,
|
||||
DUPLICATE_EMAIL,
|
||||
EMAIL_FAILED,
|
||||
INVALID_EMAIL,
|
||||
ONLY_ONE_ADMIN_ACCOUNT,
|
||||
TEAM_INVITE_ALREADY_MEMBER,
|
||||
TEAM_INVITE_NO_INVITE_FOUND,
|
||||
USERS_NOT_FOUND,
|
||||
USER_ALREADY_INVITED,
|
||||
USER_INVITATION_DELETION_FAILED,
|
||||
USER_IS_ADMIN,
|
||||
USER_NOT_FOUND,
|
||||
} from '../errors';
|
||||
@@ -26,6 +29,8 @@ import { TeamInvitationService } from '../team-invitation/team-invitation.servic
|
||||
import { TeamMemberRole } from '../team/team.model';
|
||||
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { OffsetPaginationArgs } from 'src/types/input-types.args';
|
||||
import { UserDeletionResult } from 'src/user/user.model';
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
@@ -48,12 +53,30 @@ export class AdminService {
|
||||
* @param cursorID Users uid
|
||||
* @param take number of users to fetch
|
||||
* @returns an Either of array of user or error
|
||||
* @deprecated use fetchUsersV2 instead
|
||||
*/
|
||||
async fetchUsers(cursorID: string, take: number) {
|
||||
const allUsers = await this.userService.fetchAllUsers(cursorID, take);
|
||||
return allUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the users in the infra.
|
||||
* @param searchString search on users displayName or email
|
||||
* @param paginationOption pagination options
|
||||
* @returns an Either of array of user or error
|
||||
*/
|
||||
async fetchUsersV2(
|
||||
searchString: string,
|
||||
paginationOption: OffsetPaginationArgs,
|
||||
) {
|
||||
const allUsers = await this.userService.fetchAllUsersV2(
|
||||
searchString,
|
||||
paginationOption,
|
||||
);
|
||||
return allUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite a user to join the infra.
|
||||
* @param adminUID Admin's UID
|
||||
@@ -110,14 +133,68 @@ export class AdminService {
|
||||
return E.right(invitedUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the display name of a user
|
||||
* @param userUid Who's display name is being updated
|
||||
* @param displayName New display name of the user
|
||||
* @returns an Either of boolean or error
|
||||
*/
|
||||
async updateUserDisplayName(userUid: string, displayName: string) {
|
||||
const updatedUser = await this.userService.updateUserDisplayName(
|
||||
userUid,
|
||||
displayName,
|
||||
);
|
||||
if (E.isLeft(updatedUser)) return E.left(updatedUser.left);
|
||||
|
||||
return E.right(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke infra level user invitations
|
||||
* @param inviteeEmails Invitee's emails
|
||||
* @param adminUid Admin Uid
|
||||
* @returns an Either of boolean or error string
|
||||
*/
|
||||
async revokeUserInvitations(inviteeEmails: string[]) {
|
||||
try {
|
||||
await this.prisma.invitedUsers.deleteMany({
|
||||
where: {
|
||||
inviteeEmail: { in: inviteeEmails },
|
||||
},
|
||||
});
|
||||
return E.right(true);
|
||||
} catch (error) {
|
||||
return E.left(USER_INVITATION_DELETION_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the list of invited users by the admin.
|
||||
* @returns an Either of array of `InvitedUser` object or error
|
||||
*/
|
||||
async fetchInvitedUsers() {
|
||||
const invitedUsers = await this.prisma.invitedUsers.findMany();
|
||||
async fetchInvitedUsers(paginationOption: OffsetPaginationArgs) {
|
||||
const userEmailObjs = await this.prisma.user.findMany({
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
});
|
||||
|
||||
const users: InvitedUser[] = invitedUsers.map(
|
||||
const pendingInvitedUsers = await this.prisma.invitedUsers.findMany({
|
||||
take: paginationOption.take,
|
||||
skip: paginationOption.skip,
|
||||
orderBy: {
|
||||
invitedOn: 'desc',
|
||||
},
|
||||
where: {
|
||||
NOT: {
|
||||
inviteeEmail: {
|
||||
in: userEmailObjs.map((user) => user.email),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const users: InvitedUser[] = pendingInvitedUsers.map(
|
||||
(user) => <InvitedUser>{ ...user },
|
||||
);
|
||||
|
||||
@@ -337,6 +414,7 @@ export class AdminService {
|
||||
* Remove a user account by UID
|
||||
* @param userUid User UID
|
||||
* @returns an Either of boolean or error
|
||||
* @deprecated use removeUserAccounts instead
|
||||
*/
|
||||
async removeUserAccount(userUid: string) {
|
||||
const user = await this.userService.findUserById(userUid);
|
||||
@@ -349,10 +427,73 @@ export class AdminService {
|
||||
return E.right(delUser.right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove user (not Admin) accounts by UIDs
|
||||
* @param userUIDs User UIDs
|
||||
* @returns an Either of boolean or error
|
||||
*/
|
||||
async removeUserAccounts(userUIDs: string[]) {
|
||||
const userDeleteResult: UserDeletionResult[] = [];
|
||||
|
||||
// step 1: fetch all users
|
||||
const allUsersList = await this.userService.findUsersByIds(userUIDs);
|
||||
if (allUsersList.length === 0) return E.left(USERS_NOT_FOUND);
|
||||
|
||||
// step 2: admin user can not be deleted without removing admin status/role
|
||||
allUsersList.forEach((user) => {
|
||||
if (user.isAdmin) {
|
||||
userDeleteResult.push({
|
||||
userUID: user.uid,
|
||||
isDeleted: false,
|
||||
errorMessage: ADMIN_CAN_NOT_BE_DELETED,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const nonAdminUsers = allUsersList.filter((user) => !user.isAdmin);
|
||||
let deletedUserEmails: string[] = [];
|
||||
|
||||
// step 3: delete non-admin users
|
||||
const deletionPromises = nonAdminUsers.map((user) => {
|
||||
return this.userService
|
||||
.deleteUserByUID(user)()
|
||||
.then((res) => {
|
||||
if (E.isLeft(res)) {
|
||||
return {
|
||||
userUID: user.uid,
|
||||
isDeleted: false,
|
||||
errorMessage: res.left,
|
||||
} as UserDeletionResult;
|
||||
}
|
||||
|
||||
deletedUserEmails.push(user.email);
|
||||
return {
|
||||
userUID: user.uid,
|
||||
isDeleted: true,
|
||||
errorMessage: null,
|
||||
} as UserDeletionResult;
|
||||
});
|
||||
});
|
||||
const promiseResult = await Promise.allSettled(deletionPromises);
|
||||
|
||||
// step 4: revoke all the invites sent to the deleted users
|
||||
await this.revokeUserInvitations(deletedUserEmails);
|
||||
|
||||
// step 5: return the result
|
||||
promiseResult.forEach((result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
userDeleteResult.push(result.value);
|
||||
}
|
||||
});
|
||||
|
||||
return E.right(userDeleteResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a user an admin
|
||||
* @param userUid User UID
|
||||
* @returns an Either of boolean or error
|
||||
* @deprecated use makeUsersAdmin instead
|
||||
*/
|
||||
async makeUserAdmin(userUID: string) {
|
||||
const admin = await this.userService.makeAdmin(userUID);
|
||||
@@ -360,10 +501,22 @@ export class AdminService {
|
||||
return E.right(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make users to admin
|
||||
* @param userUid User UIDs
|
||||
* @returns an Either of boolean or error
|
||||
*/
|
||||
async makeUsersAdmin(userUIDs: string[]) {
|
||||
const isUpdated = await this.userService.makeAdmins(userUIDs);
|
||||
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
|
||||
return E.right(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove user as admin
|
||||
* @param userUid User UID
|
||||
* @returns an Either of boolean or error
|
||||
* @deprecated use demoteUsersByAdmin instead
|
||||
*/
|
||||
async removeUserAsAdmin(userUID: string) {
|
||||
const adminUsers = await this.userService.fetchAdminUsers();
|
||||
@@ -374,6 +527,26 @@ export class AdminService {
|
||||
return E.right(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove users as admin
|
||||
* @param userUIDs User UIDs
|
||||
* @returns an Either of boolean or error
|
||||
*/
|
||||
async demoteUsersByAdmin(userUIDs: string[]) {
|
||||
const adminUsers = await this.userService.fetchAdminUsers();
|
||||
|
||||
const remainingAdmins = adminUsers.filter(
|
||||
(adminUser) => !userUIDs.includes(adminUser.uid),
|
||||
);
|
||||
if (remainingAdmins.length < 1) {
|
||||
return E.left(ONLY_ONE_ADMIN_ACCOUNT);
|
||||
}
|
||||
|
||||
const isUpdated = await this.userService.removeUsersAsAdmin(userUIDs);
|
||||
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
|
||||
return E.right(isUpdated.right);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch list of all the Users in org
|
||||
* @returns number of users in the org
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Injectable, ExecutionContext, CanActivate } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class RESTAdminGuard implements CanActivate {
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
|
||||
return user.isAdmin;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,10 @@ import { AuthUser } from 'src/types/AuthUser';
|
||||
import { throwErr } from 'src/utils';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { Admin } from './admin.model';
|
||||
import { PaginationArgs } from 'src/types/input-types.args';
|
||||
import {
|
||||
OffsetPaginationArgs,
|
||||
PaginationArgs,
|
||||
} from 'src/types/input-types.args';
|
||||
import { InvitedUser } from './invited-user.model';
|
||||
import { Team } from 'src/team/team.model';
|
||||
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
|
||||
@@ -76,6 +79,7 @@ export class InfraResolver {
|
||||
|
||||
@ResolveField(() => [User], {
|
||||
description: 'Returns a list of all the users in infra',
|
||||
deprecationReason: 'Use allUsersV2 instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async allUsers(@Args() args: PaginationArgs): Promise<AuthUser[]> {
|
||||
@@ -83,11 +87,33 @@ export class InfraResolver {
|
||||
return users;
|
||||
}
|
||||
|
||||
@ResolveField(() => [User], {
|
||||
description: 'Returns a list of all the users in infra',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async allUsersV2(
|
||||
@Args({
|
||||
name: 'searchString',
|
||||
nullable: true,
|
||||
description: 'Search on users displayName or email',
|
||||
})
|
||||
searchString: string,
|
||||
@Args() paginationOption: OffsetPaginationArgs,
|
||||
): Promise<AuthUser[]> {
|
||||
const users = await this.adminService.fetchUsersV2(
|
||||
searchString,
|
||||
paginationOption,
|
||||
);
|
||||
return users;
|
||||
}
|
||||
|
||||
@ResolveField(() => [InvitedUser], {
|
||||
description: 'Returns a list of all the invited users',
|
||||
})
|
||||
async invitedUsers(): Promise<InvitedUser[]> {
|
||||
const users = await this.adminService.fetchInvitedUsers();
|
||||
async invitedUsers(
|
||||
@Args() args: OffsetPaginationArgs,
|
||||
): Promise<InvitedUser[]> {
|
||||
const users = await this.adminService.fetchInvitedUsers(args);
|
||||
return users;
|
||||
}
|
||||
|
||||
@@ -306,7 +332,9 @@ export class InfraResolver {
|
||||
})
|
||||
providerInfo: EnableAndDisableSSOArgs[],
|
||||
) {
|
||||
const isUpdated = await this.infraConfigService.enableAndDisableSSO(providerInfo);
|
||||
const isUpdated = await this.infraConfigService.enableAndDisableSSO(
|
||||
providerInfo,
|
||||
);
|
||||
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -10,6 +10,14 @@ export const DUPLICATE_EMAIL = 'email/both_emails_cannot_be_same' as const;
|
||||
export const ONLY_ONE_ADMIN_ACCOUNT =
|
||||
'admin/only_one_admin_account_found' as const;
|
||||
|
||||
/**
|
||||
* Admin user can not be deleted
|
||||
* To delete the admin user, first make the Admin user a normal user
|
||||
* (AdminService)
|
||||
*/
|
||||
export const ADMIN_CAN_NOT_BE_DELETED =
|
||||
'admin/admin_can_not_be_deleted' as const;
|
||||
|
||||
/**
|
||||
* Token Authorization failed (Check 'Authorization' Header)
|
||||
* (GqlAuthGuard)
|
||||
@@ -99,6 +107,13 @@ export const USER_IS_OWNER = 'user/is_owner' as const;
|
||||
*/
|
||||
export const USER_IS_ADMIN = 'user/is_admin' as const;
|
||||
|
||||
/**
|
||||
* User invite deletion failure error due to invitation not found
|
||||
* (AdminService)
|
||||
*/
|
||||
export const USER_INVITATION_DELETION_FAILED =
|
||||
'user/invitation_deletion_failed' as const;
|
||||
|
||||
/**
|
||||
* Teams not found
|
||||
* (TeamsService)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Controller, Get, HttpStatus, Put, UseGuards } from '@nestjs/common';
|
||||
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
|
||||
import { InfraConfigService } from './infra-config.service';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard';
|
||||
import { RESTAdminGuard } from 'src/admin/guards/rest-admin.guard';
|
||||
import { throwHTTPErr } from 'src/auth/helper';
|
||||
import { AuthError } from 'src/types/AuthError';
|
||||
import { InfraConfigEnumForClient } from 'src/types/InfraConfig';
|
||||
|
||||
@UseGuards(ThrottlerBehindProxyGuard)
|
||||
@Controller({ path: 'site', version: '1' })
|
||||
export class SiteController {
|
||||
constructor(private infraConfigService: InfraConfigService) {}
|
||||
|
||||
@Get('setup')
|
||||
@UseGuards(JwtAuthGuard, RESTAdminGuard)
|
||||
async fetchSetupInfo() {
|
||||
const status = await this.infraConfigService.get(
|
||||
InfraConfigEnumForClient.IS_FIRST_TIME_INFRA_SETUP,
|
||||
);
|
||||
|
||||
if (E.isLeft(status))
|
||||
throwHTTPErr(<AuthError>{
|
||||
message: status.left,
|
||||
statusCode: HttpStatus.NOT_FOUND,
|
||||
});
|
||||
return status.right;
|
||||
}
|
||||
|
||||
@Put('setup')
|
||||
@UseGuards(JwtAuthGuard, RESTAdminGuard)
|
||||
async setSetupAsComplete() {
|
||||
const res = await this.infraConfigService.update(
|
||||
InfraConfigEnumForClient.IS_FIRST_TIME_INFRA_SETUP,
|
||||
false.toString(),
|
||||
false,
|
||||
);
|
||||
|
||||
if (E.isLeft(res))
|
||||
throwHTTPErr(<AuthError>{
|
||||
message: res.left,
|
||||
statusCode: HttpStatus.FORBIDDEN,
|
||||
});
|
||||
return res.right;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { InfraConfigService } from './infra-config.service';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { SiteController } from './infra-config.controller';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
providers: [InfraConfigService],
|
||||
exports: [InfraConfigService],
|
||||
controllers: [SiteController],
|
||||
})
|
||||
export class InfraConfigModule {}
|
||||
|
||||
@@ -34,7 +34,9 @@ export class InfraConfigService implements OnModuleInit {
|
||||
await this.initializeInfraConfigTable();
|
||||
}
|
||||
|
||||
getDefaultInfraConfigs(): { name: InfraConfigEnum; value: string }[] {
|
||||
async getDefaultInfraConfigs(): Promise<
|
||||
{ name: InfraConfigEnum; value: string }[]
|
||||
> {
|
||||
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
|
||||
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [
|
||||
{
|
||||
@@ -73,6 +75,10 @@ export class InfraConfigService implements OnModuleInit {
|
||||
name: InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
||||
value: getConfiguredSSOProviders(),
|
||||
},
|
||||
{
|
||||
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||
value: (await this.prisma.infraConfig.count()) === 0 ? 'true' : 'false',
|
||||
},
|
||||
];
|
||||
|
||||
return infraConfigDefaultObjs;
|
||||
@@ -88,7 +94,7 @@ export class InfraConfigService implements OnModuleInit {
|
||||
const enumValues = Object.values(InfraConfigEnum);
|
||||
|
||||
// Fetch the default values (value in .env) for configs to be saved in 'infra_config' table
|
||||
const infraConfigDefaultObjs = this.getDefaultInfraConfigs();
|
||||
const infraConfigDefaultObjs = await this.getDefaultInfraConfigs();
|
||||
|
||||
// Check if all the 'names' are listed in the default values
|
||||
if (enumValues.length !== infraConfigDefaultObjs.length) {
|
||||
@@ -147,11 +153,13 @@ export class InfraConfigService implements OnModuleInit {
|
||||
* Update InfraConfig by name
|
||||
* @param name Name of the InfraConfig
|
||||
* @param value Value of the InfraConfig
|
||||
* @param restartEnabled If true, restart the app after updating the InfraConfig
|
||||
* @returns InfraConfig model
|
||||
*/
|
||||
async update(
|
||||
name: InfraConfigEnumForClient | InfraConfigEnum,
|
||||
value: string,
|
||||
restartEnabled = false,
|
||||
) {
|
||||
const isValidate = this.validateEnvValues([{ name, value }]);
|
||||
if (E.isLeft(isValidate)) return E.left(isValidate.left);
|
||||
@@ -162,7 +170,7 @@ export class InfraConfigService implements OnModuleInit {
|
||||
data: { value },
|
||||
});
|
||||
|
||||
stopApp();
|
||||
if (restartEnabled) stopApp();
|
||||
|
||||
return E.right(this.cast(infraConfig));
|
||||
} catch (e) {
|
||||
@@ -261,6 +269,7 @@ export class InfraConfigService implements OnModuleInit {
|
||||
const isUpdated = await this.update(
|
||||
InfraConfigEnum.VITE_ALLOWED_AUTH_PROVIDERS,
|
||||
updatedAuthProviders.join(','),
|
||||
true,
|
||||
);
|
||||
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
|
||||
|
||||
@@ -316,13 +325,24 @@ export class InfraConfigService implements OnModuleInit {
|
||||
*/
|
||||
async reset() {
|
||||
try {
|
||||
const infraConfigDefaultObjs = this.getDefaultInfraConfigs();
|
||||
const infraConfigDefaultObjs = await this.getDefaultInfraConfigs();
|
||||
|
||||
await this.prisma.infraConfig.deleteMany({
|
||||
where: { name: { in: infraConfigDefaultObjs.map((p) => p.name) } },
|
||||
});
|
||||
|
||||
// Hardcode t
|
||||
const updatedInfraConfigDefaultObjs = infraConfigDefaultObjs.filter(
|
||||
(obj) => obj.name !== InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||
);
|
||||
await this.prisma.infraConfig.createMany({
|
||||
data: infraConfigDefaultObjs,
|
||||
data: [
|
||||
...updatedInfraConfigDefaultObjs,
|
||||
{
|
||||
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
|
||||
value: 'true',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
stopApp();
|
||||
|
||||
@@ -12,6 +12,8 @@ export enum InfraConfigEnum {
|
||||
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
||||
|
||||
VITE_ALLOWED_AUTH_PROVIDERS = 'VITE_ALLOWED_AUTH_PROVIDERS',
|
||||
|
||||
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
|
||||
}
|
||||
|
||||
export enum InfraConfigEnumForClient {
|
||||
@@ -26,4 +28,6 @@ export enum InfraConfigEnumForClient {
|
||||
|
||||
MICROSOFT_CLIENT_ID = 'MICROSOFT_CLIENT_ID',
|
||||
MICROSOFT_CLIENT_SECRET = 'MICROSOFT_CLIENT_SECRET',
|
||||
|
||||
IS_FIRST_TIME_INFRA_SETUP = 'IS_FIRST_TIME_INFRA_SETUP',
|
||||
}
|
||||
|
||||
@@ -17,3 +17,21 @@ export class PaginationArgs {
|
||||
})
|
||||
take: number;
|
||||
}
|
||||
|
||||
@ArgsType()
|
||||
@InputType()
|
||||
export class OffsetPaginationArgs {
|
||||
@Field({
|
||||
nullable: true,
|
||||
defaultValue: 0,
|
||||
description: 'Number of items to skip',
|
||||
})
|
||||
skip: number;
|
||||
|
||||
@Field({
|
||||
nullable: true,
|
||||
defaultValue: 10,
|
||||
description: 'Number of items to fetch',
|
||||
})
|
||||
take: number;
|
||||
}
|
||||
|
||||
@@ -56,3 +56,22 @@ export enum SessionType {
|
||||
registerEnumType(SessionType, {
|
||||
name: 'SessionType',
|
||||
});
|
||||
|
||||
@ObjectType()
|
||||
export class UserDeletionResult {
|
||||
@Field(() => ID, {
|
||||
description: 'UID of the user',
|
||||
})
|
||||
userUID: string;
|
||||
|
||||
@Field(() => Boolean, {
|
||||
description: 'Flag to determine if user deletion was successful or not',
|
||||
})
|
||||
isDeleted: Boolean;
|
||||
|
||||
@Field({
|
||||
nullable: true,
|
||||
description: 'Error message if user deletion was not successful',
|
||||
})
|
||||
errorMessage: String;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { JSON_INVALID, USER_NOT_FOUND } from 'src/errors';
|
||||
import { JSON_INVALID, USERS_NOT_FOUND, USER_NOT_FOUND } from 'src/errors';
|
||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
@@ -176,6 +176,26 @@ describe('UserService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('findUsersByIds', () => {
|
||||
test('should successfully return users given valid user UIDs', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
||||
|
||||
const result = await userService.findUsersByIds([
|
||||
'123344',
|
||||
'5555',
|
||||
'6666',
|
||||
]);
|
||||
expect(result).toEqual(users);
|
||||
});
|
||||
|
||||
test('should return empty array of users given a invalid user UIDs', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await userService.findUsersByIds(['sdcvbdbr']);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createUserViaMagicLink', () => {
|
||||
test('should successfully create user and account for magic-link given valid inputs', async () => {
|
||||
mockPrisma.user.create.mockResolvedValueOnce(user);
|
||||
@@ -414,6 +434,54 @@ describe('UserService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateUserDisplayName', () => {
|
||||
test('should resolve right and update user display name', async () => {
|
||||
const newDisplayName = 'New Name';
|
||||
mockPrisma.user.update.mockResolvedValueOnce({
|
||||
...user,
|
||||
displayName: newDisplayName,
|
||||
});
|
||||
|
||||
const result = await userService.updateUserDisplayName(
|
||||
user.uid,
|
||||
newDisplayName,
|
||||
);
|
||||
expect(result).toEqualRight({
|
||||
...user,
|
||||
displayName: newDisplayName,
|
||||
currentGQLSession: JSON.stringify(user.currentGQLSession),
|
||||
currentRESTSession: JSON.stringify(user.currentRESTSession),
|
||||
});
|
||||
});
|
||||
test('should resolve right and publish user updated subscription', async () => {
|
||||
const newDisplayName = 'New Name';
|
||||
mockPrisma.user.update.mockResolvedValueOnce({
|
||||
...user,
|
||||
displayName: newDisplayName,
|
||||
});
|
||||
|
||||
await userService.updateUserDisplayName(user.uid, user.displayName);
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`user/${user.uid}/updated`,
|
||||
{
|
||||
...user,
|
||||
displayName: newDisplayName,
|
||||
currentGQLSession: JSON.stringify(user.currentGQLSession),
|
||||
currentRESTSession: JSON.stringify(user.currentRESTSession),
|
||||
},
|
||||
);
|
||||
});
|
||||
test('should resolve left and error when invalid user uid is passed', async () => {
|
||||
mockPrisma.user.update.mockRejectedValueOnce('NotFoundError');
|
||||
|
||||
const result = await userService.updateUserDisplayName(
|
||||
'invalidUserUid',
|
||||
user.displayName,
|
||||
);
|
||||
expect(result).toEqualLeft(USER_NOT_FOUND);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchAllUsers', () => {
|
||||
test('should resolve right and return 20 users when cursor is null', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
||||
@@ -435,6 +503,36 @@ describe('UserService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchAllUsersV2', () => {
|
||||
test('should resolve right and return first 20 users when searchString is null', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
||||
|
||||
const result = await userService.fetchAllUsersV2(null, {
|
||||
take: 20,
|
||||
skip: 0,
|
||||
});
|
||||
expect(result).toEqual(users);
|
||||
});
|
||||
test('should resolve right and return next 20 users when searchString is provided', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
||||
|
||||
const result = await userService.fetchAllUsersV2('.com', {
|
||||
take: 20,
|
||||
skip: 0,
|
||||
});
|
||||
expect(result).toEqual(users);
|
||||
});
|
||||
test('should resolve left and return an empty array when users not found', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await userService.fetchAllUsersV2('Unknown entry', {
|
||||
take: 20,
|
||||
skip: 0,
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchAdminUsers', () => {
|
||||
test('should return a list of admin users', async () => {
|
||||
mockPrisma.user.findMany.mockResolvedValueOnce(adminUsers);
|
||||
@@ -556,4 +654,17 @@ describe('UserService', () => {
|
||||
expect(result).toEqual(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeUsersAsAdmin', () => {
|
||||
test('should resolve right and return true for valid user UIDs', async () => {
|
||||
mockPrisma.user.updateMany.mockResolvedValueOnce({ count: 1 });
|
||||
const result = await userService.removeUsersAsAdmin(['123344']);
|
||||
expect(result).toEqualRight(true);
|
||||
});
|
||||
test('should resolve right and return false for invalid user UIDs', async () => {
|
||||
mockPrisma.user.updateMany.mockResolvedValueOnce({ count: 0 });
|
||||
const result = await userService.removeUsersAsAdmin(['123344']);
|
||||
expect(result).toEqualLeft(USERS_NOT_FOUND);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,13 +8,14 @@ import * as T from 'fp-ts/Task';
|
||||
import * as A from 'fp-ts/Array';
|
||||
import { pipe, constVoid } from 'fp-ts/function';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
import { USER_NOT_FOUND } from 'src/errors';
|
||||
import { USERS_NOT_FOUND, USER_NOT_FOUND } from 'src/errors';
|
||||
import { SessionType, User } from './user.model';
|
||||
import { USER_UPDATE_FAILED } from 'src/errors';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { stringToJson, taskEitherValidateArraySeq } from 'src/utils';
|
||||
import { UserDataHandler } from './user.data.handler';
|
||||
import { User as DbUser } from '@prisma/client';
|
||||
import { OffsetPaginationArgs } from 'src/types/input-types.args';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
@@ -88,6 +89,20 @@ export class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find users with given IDs
|
||||
* @param userUIDs User IDs
|
||||
* @returns Array of found Users
|
||||
*/
|
||||
async findUsersByIds(userUIDs: string[]): Promise<AuthUser[]> {
|
||||
const users = await this.prisma.user.findMany({
|
||||
where: {
|
||||
uid: { in: userUIDs },
|
||||
},
|
||||
});
|
||||
return users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update User with new generated hashed refresh token
|
||||
*
|
||||
@@ -269,6 +284,30 @@ export class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a user's data
|
||||
* @param userUID User UID
|
||||
* @param displayName User's displayName
|
||||
* @returns a Either of User or error
|
||||
*/
|
||||
async updateUserDisplayName(userUID: string, displayName: string) {
|
||||
try {
|
||||
const dbUpdatedUser = await this.prisma.user.update({
|
||||
where: { uid: userUID },
|
||||
data: { displayName },
|
||||
});
|
||||
|
||||
const updatedUser = this.convertDbUserToUser(dbUpdatedUser);
|
||||
|
||||
// Publish subscription for user updates
|
||||
await this.pubsub.publish(`user/${updatedUser.uid}/updated`, updatedUser);
|
||||
|
||||
return E.right(updatedUser);
|
||||
} catch (error) {
|
||||
return E.left(USER_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and parse currentRESTSession and currentGQLSession
|
||||
* @param sessionData string of the session
|
||||
@@ -286,6 +325,7 @@ export class UserService {
|
||||
* @param cursorID string of userUID or null
|
||||
* @param take number of users to query
|
||||
* @returns an array of `User` object
|
||||
* @deprecated use fetchAllUsersV2 instead
|
||||
*/
|
||||
async fetchAllUsers(cursorID: string, take: number) {
|
||||
const fetchedUsers = await this.prisma.user.findMany({
|
||||
@@ -296,6 +336,43 @@ export class UserService {
|
||||
return fetchedUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the users in the `User` table based on cursor
|
||||
* @param searchString search on user's displayName or email
|
||||
* @param paginationOption pagination options
|
||||
* @returns an array of `User` object
|
||||
*/
|
||||
async fetchAllUsersV2(
|
||||
searchString: string,
|
||||
paginationOption: OffsetPaginationArgs,
|
||||
) {
|
||||
const fetchedUsers = await this.prisma.user.findMany({
|
||||
skip: paginationOption.skip,
|
||||
take: paginationOption.take,
|
||||
where: searchString
|
||||
? {
|
||||
OR: [
|
||||
{
|
||||
displayName: {
|
||||
contains: searchString,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
{
|
||||
email: {
|
||||
contains: searchString,
|
||||
mode: 'insensitive',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
orderBy: [{ isAdmin: 'desc' }, { displayName: 'asc' }],
|
||||
});
|
||||
|
||||
return fetchedUsers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the number of users in db
|
||||
* @returns a count (Int) of user records in DB
|
||||
@@ -326,6 +403,23 @@ export class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change users to admins by toggling isAdmin param to true
|
||||
* @param userUID user UIDs
|
||||
* @returns a Either of true or error
|
||||
*/
|
||||
async makeAdmins(userUIDs: string[]) {
|
||||
try {
|
||||
await this.prisma.user.updateMany({
|
||||
where: { uid: { in: userUIDs } },
|
||||
data: { isAdmin: true },
|
||||
});
|
||||
return E.right(true);
|
||||
} catch (error) {
|
||||
return E.left(USER_UPDATE_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the admin users
|
||||
* @returns an array of admin users
|
||||
@@ -444,4 +538,22 @@ export class UserService {
|
||||
return E.left(USER_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change users from an admin by toggling isAdmin param to false
|
||||
* @param userUIDs user UIDs
|
||||
* @returns a Either of true or error
|
||||
*/
|
||||
async removeUsersAsAdmin(userUIDs: string[]) {
|
||||
const data = await this.prisma.user.updateMany({
|
||||
where: { uid: { in: userUIDs } },
|
||||
data: { isAdmin: false },
|
||||
});
|
||||
|
||||
if (data.count === 0) {
|
||||
return E.left(USERS_NOT_FOUND);
|
||||
}
|
||||
|
||||
return E.right(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
// * The entry point of the CLI
|
||||
require("../dist").cli(process.argv);
|
||||
6
packages/hoppscotch-cli/bin/hopp.js
Executable file
6
packages/hoppscotch-cli/bin/hopp.js
Executable file
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
// * The entry point of the CLI
|
||||
|
||||
import { cli } from "../dist/index.js";
|
||||
|
||||
cli(process.argv);
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "@hoppscotch/cli",
|
||||
"version": "0.5.2",
|
||||
"version": "0.6.0",
|
||||
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
|
||||
"homepage": "https://hoppscotch.io",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"hopp": "bin/hopp"
|
||||
"hopp": "bin/hopp.js"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -39,27 +40,27 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"private": false,
|
||||
"dependencies": {
|
||||
"axios": "^1.6.6",
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^11.1.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"qs": "^6.11.2",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@hoppscotch/data": "workspace:^",
|
||||
"@hoppscotch/js-sandbox": "workspace:^",
|
||||
"@relmify/jest-fp-ts": "^2.1.1",
|
||||
"@swc/core": "^1.3.92",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/lodash": "^4.14.199",
|
||||
"@types/qs": "^6.9.8",
|
||||
"axios": "^0.21.4",
|
||||
"chalk": "^4.1.2",
|
||||
"commander": "^11.0.0",
|
||||
"esm": "^3.2.25",
|
||||
"fp-ts": "^2.16.1",
|
||||
"io-ts": "^2.2.20",
|
||||
"@swc/core": "^1.3.105",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/qs": "^6.9.11",
|
||||
"fp-ts": "^2.16.2",
|
||||
"jest": "^29.7.0",
|
||||
"lodash": "^4.17.21",
|
||||
"prettier": "^3.0.3",
|
||||
"qs": "^6.11.2",
|
||||
"ts-jest": "^29.1.1",
|
||||
"tsup": "^7.2.0",
|
||||
"typescript": "^5.2.2",
|
||||
"zod": "^3.22.4"
|
||||
"prettier": "^3.2.4",
|
||||
"ts-jest": "^29.1.2",
|
||||
"tsup": "^8.0.1",
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import chalk from "chalk";
|
||||
import { program } from "commander";
|
||||
import { Command } from "commander";
|
||||
import * as E from "fp-ts/Either";
|
||||
import { version } from "../package.json";
|
||||
import { test } from "./commands/test";
|
||||
@@ -20,6 +20,8 @@ const CLI_AFTER_ALL_TXT = `\nFor more help, head on to ${accent(
|
||||
"https://docs.hoppscotch.io/documentation/clients/cli"
|
||||
)}`;
|
||||
|
||||
const program = new Command()
|
||||
|
||||
program
|
||||
.name("hopp")
|
||||
.version(version, "-v, --ver", "see the current version of hopp-cli")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import { bold } from "chalk";
|
||||
import chalk from "chalk";
|
||||
import { log } from "console";
|
||||
import * as A from "fp-ts/Array";
|
||||
import { pipe } from "fp-ts/function";
|
||||
import round from "lodash/round";
|
||||
import { round } from "lodash-es";
|
||||
|
||||
import { CollectionRunnerParam } from "../types/collections";
|
||||
import {
|
||||
@@ -68,7 +68,7 @@ export const collectionsRunner = async (
|
||||
};
|
||||
|
||||
// Request processing initiated message.
|
||||
log(WARN(`\nRunning: ${bold(requestPath)}`));
|
||||
log(WARN(`\nRunning: ${chalk.bold(requestPath)}`));
|
||||
|
||||
// Processing current request.
|
||||
const result = await processRequest(processRequestParams)();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { bold } from "chalk";
|
||||
import chalk from "chalk";
|
||||
import { groupEnd, group, log } from "console";
|
||||
import { handleError } from "../handlers/error";
|
||||
import { RequestConfig } from "../interfaces/request";
|
||||
@@ -120,7 +120,7 @@ export const printErrorsReport = (
|
||||
errorsReport: HoppCLIError[]
|
||||
) => {
|
||||
if (errorsReport.length > 0) {
|
||||
const REPORTED_ERRORS_TITLE = FAIL(`\n${bold(path)} reported errors:`);
|
||||
const REPORTED_ERRORS_TITLE = FAIL(`\n${chalk.bold(path)} reported errors:`);
|
||||
|
||||
group(REPORTED_ERRORS_TITLE);
|
||||
for (const errorReport of errorsReport) {
|
||||
@@ -143,7 +143,7 @@ export const printFailedTestsReport = (
|
||||
|
||||
// Only printing test-reports with failed test-cases.
|
||||
if (failedTestsReport.length > 0) {
|
||||
const FAILED_TESTS_PATH = FAIL(`\n${bold(path)} failed tests:`);
|
||||
const FAILED_TESTS_PATH = FAIL(`\n${chalk.bold(path)} failed tests:`);
|
||||
group(FAILED_TESTS_PATH);
|
||||
|
||||
for (const failedTestReport of failedTestsReport) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clone } from "lodash";
|
||||
import { clone } from "lodash-es";
|
||||
|
||||
/**
|
||||
* Sorts the array based on the sort func.
|
||||
|
||||
@@ -11,7 +11,7 @@ import * as E from "fp-ts/Either";
|
||||
import * as S from "fp-ts/string";
|
||||
import * as O from "fp-ts/Option";
|
||||
import { error } from "../types/errors";
|
||||
import round from "lodash/round";
|
||||
import { round } from "lodash-es";
|
||||
import { DEFAULT_DURATION_PRECISION } from "./constants";
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES6",
|
||||
"module": "commonjs",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"outDir": ".",
|
||||
"rootDir": ".",
|
||||
"strict": true,
|
||||
|
||||
@@ -3,17 +3,14 @@ import { defineConfig } from "tsup";
|
||||
export default defineConfig({
|
||||
entry: [ "./src/index.ts" ],
|
||||
outDir: "./dist/",
|
||||
format: ["cjs"],
|
||||
format: ["esm"],
|
||||
platform: "node",
|
||||
sourcemap: true,
|
||||
bundle: true,
|
||||
target: "node12",
|
||||
target: "esnext",
|
||||
skipNodeModulesBundle: false,
|
||||
esbuildOptions(options) {
|
||||
options.bundle = true
|
||||
},
|
||||
noExternal: [
|
||||
/\w+/
|
||||
],
|
||||
clean: true,
|
||||
});
|
||||
|
||||
37
packages/hoppscotch-sh-admin/assets/images/add_group.svg
Normal file
37
packages/hoppscotch-sh-admin/assets/images/add_group.svg
Normal file
@@ -0,0 +1,37 @@
|
||||
<svg width="117" height="120" viewBox="0 0 117 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M104.634 68.6831C104.634 80.1972 100.53 90.6702 93.6709 98.8771C85.0353 109.228 72.0513 115.781 57.4749 115.781C43.511 115.781 31.0169 109.717 22.3813 100.102C14.9706 91.7726 10.4385 80.7484 10.4385 68.6831C10.4385 42.6538 31.5069 21.5854 57.5361 21.5854C83.5654 21.5854 104.634 42.6538 104.634 68.6831Z" stroke="#737373" stroke-width="2" stroke-miterlimit="10" stroke-dasharray="4 4"/>
|
||||
<path d="M74.8781 71.7725C74.5226 73.2834 73.9893 74.8833 73.3671 76.2164C71.6785 79.5049 69.0121 82.0824 65.7236 83.7711C62.3462 85.4598 58.3467 86.1708 54.3471 85.282C44.926 83.3267 38.8823 74.0833 40.8376 64.6622C42.7929 55.2411 51.9474 49.1085 61.3685 51.1527C64.7459 51.8637 67.6789 53.5524 70.1675 55.8632C74.3448 60.0405 76.1224 66.0843 74.8781 71.7725Z" fill="#737373" stroke="#a3a3a3" stroke-width="2" stroke-miterlimit="10"/>
|
||||
<path d="M63.4127 66.7063H60.4132C59.8609 66.7063 59.4132 66.2586 59.4132 65.7063V62.7067C59.4132 61.9068 58.791 61.1958 57.9023 61.1958C57.1024 61.1958 56.3913 61.818 56.3913 62.7067V65.7063C56.3913 66.2586 55.9436 66.7063 55.3913 66.7063H52.3918C51.5919 66.7063 50.8809 67.3284 50.8809 68.2172C50.8809 69.106 51.503 69.7281 52.3918 69.7281H55.3913C55.9436 69.7281 56.3913 70.1759 56.3913 70.7281V73.7277C56.3913 74.5276 57.0135 75.2386 57.9023 75.2386C58.7022 75.2386 59.4132 74.6165 59.4132 73.7277V70.7281C59.4132 70.1759 59.8609 69.7281 60.4132 69.7281H63.4127C64.2126 69.7281 64.9237 69.106 64.9237 68.2172C64.9237 67.3284 64.2126 66.7063 63.4127 66.7063Z" fill="#525252"/>
|
||||
<path d="M78.3516 21.6057C78.3516 27.1603 76.1318 32.0976 72.6787 35.8007C72.4321 36.171 72.0621 36.4178 71.6921 36.6647C67.9924 40.1209 63.0594 42.2192 57.6332 42.2192C53.3169 42.2192 49.2472 40.8615 45.9174 38.5162C44.9308 37.899 44.0675 37.035 43.2043 36.2944C39.3812 32.5914 37.0381 27.4071 37.0381 21.6057C37.0381 10.2498 46.2874 0.992188 57.6332 0.992188C69.1023 0.992188 78.3516 10.2498 78.3516 21.6057Z" fill="black"/>
|
||||
<path d="M78.3516 21.6043C78.3516 27.6526 75.7618 32.9603 71.6921 36.7868C67.9924 40.2429 63.0594 42.3413 57.6332 42.3413C53.3169 42.3413 49.2472 40.9835 45.9174 38.6383C40.4912 34.9353 37.0381 28.7635 37.0381 21.7278C37.0381 10.3718 46.2874 1.11426 57.6332 1.11426C68.979 1.11426 78.3516 10.2484 78.3516 21.6043Z" fill="#1f1f1f" stroke="#737373" stroke-width="2" stroke-miterlimit="10"/>
|
||||
<path d="M72.679 35.8005C72.4324 36.1708 72.0624 36.4176 71.6924 36.6645C67.9927 40.1207 63.0597 42.219 57.6335 42.219C53.3172 42.219 49.2475 40.8613 45.9177 38.516C44.9311 37.8988 44.0679 37.0348 43.2046 36.2942C43.6979 35.677 44.4378 35.3067 46.041 34.6895L46.6577 34.4427C47.8909 33.9489 49.6174 33.3318 51.8373 32.3443C52.2072 32.2209 52.4539 31.974 52.7005 31.7271C52.8239 31.6037 52.9472 31.4803 52.9472 31.2334C53.0705 30.9865 53.1938 30.6162 53.1938 30.3693V26.1726C53.0705 26.0492 53.0705 26.0492 52.9472 25.9257C52.5772 25.432 52.3306 24.8148 52.3306 24.0742L52.0839 23.9508C50.974 24.1976 51.0973 23.0867 50.8507 20.8649C50.7274 20.0009 50.8507 19.754 51.344 19.6306L51.7139 19.1368C50.974 17.4088 50.604 15.8041 50.604 14.5698C50.604 12.4714 51.4673 11.1136 52.7005 10.4964C51.9606 9.01522 51.9606 8.52148 51.9606 8.52148C51.9606 8.52148 56.2769 9.26209 57.7568 9.01522C59.6067 8.64492 62.5664 9.13866 63.6764 11.6073C65.5262 12.3479 66.1428 13.4589 66.3895 14.6932C66.6361 16.6681 65.5262 18.7665 65.2796 19.6306V19.754C65.5262 19.8774 65.6495 20.1243 65.5262 20.9883C65.2796 23.0867 65.2796 24.3211 64.293 24.0742L63.3064 25.8023C63.3064 26.0492 63.3064 26.0492 63.1831 26.1726C63.1831 26.5429 63.1831 27.1601 63.1831 30.4928C63.1831 30.8631 63.3064 31.3568 63.553 31.6037C63.6764 31.7271 63.6764 31.8506 63.7997 31.8506C64.0463 32.0974 64.293 32.3443 64.5396 32.3443C67.0061 33.3318 68.7326 34.0724 70.0892 34.5661C71.3224 35.0599 72.1857 35.4302 72.679 35.8005Z" fill="#1f1f1f"/>
|
||||
<path d="M72.679 35.8004C72.4324 36.1707 72.0624 36.4176 71.6924 36.6644C67.9927 40.1206 63.0597 42.219 57.6335 42.219C53.3172 42.219 49.2475 40.8612 45.9177 38.5159C44.9311 37.8988 44.0679 37.0347 43.2046 36.2941C43.6979 35.6769 44.4378 35.3066 46.041 34.6895L46.6577 34.4426C47.8909 33.9489 49.6174 33.3317 51.8373 32.3442C52.2072 32.2208 52.4539 31.9739 52.7005 31.7271C53.9338 33.4551 55.907 34.566 58.2501 34.566C60.4699 34.566 62.4431 33.4551 63.6764 31.8505C63.923 32.0974 64.1697 32.3442 64.4163 32.3442C66.8828 33.3317 68.6093 34.0723 69.9659 34.566C71.3224 35.0598 72.1857 35.4301 72.679 35.8004Z" fill="#737373"/>
|
||||
<path d="M65.1564 19.5071C65.2797 19.0134 65.0331 18.2728 64.7864 17.9025C64.7864 17.7791 64.6631 17.7791 64.6631 17.6556C63.7999 15.9275 61.95 15.3104 60.2235 15.1869C55.6605 14.9401 55.2905 15.8041 53.9339 14.5698C54.4272 15.1869 54.4272 16.2978 53.6873 17.5322C53.194 18.3962 52.3307 18.89 51.4675 19.1368C49.371 14.4463 50.4809 11.4839 52.454 10.4964C51.7141 9.01522 51.7141 8.52148 51.7141 8.52148C51.7141 8.52148 56.0304 9.26209 57.5103 9.01522C59.3602 8.64492 62.32 9.13866 63.4299 11.6073C65.2797 12.3479 65.8964 13.4589 66.143 14.6932C66.513 16.5447 65.4031 18.6431 65.1564 19.5071Z" fill="#a3a3a3"/>
|
||||
<path d="M53.317 30.3692V26.1724C53.1936 26.049 53.1936 26.049 53.0703 25.9256V25.6787C53.317 26.049 53.5636 26.4193 53.9336 26.6662L57.2633 29.0114C58.0033 29.6286 59.1132 29.6286 59.8531 29.0114L62.9362 26.2959C63.0595 26.1724 63.1829 26.1724 63.3062 26.049C63.3062 26.4193 63.3062 27.0365 63.3062 30.3692C63.3062 30.6161 63.3062 30.7395 63.4295 30.9864H53.317C53.1936 30.7395 53.317 30.6161 53.317 30.3692Z" fill="url(#paint0_linear)"/>
|
||||
<path d="M115.285 97.8074C115.285 103.362 113.065 108.299 109.612 112.002C109.365 112.373 108.995 112.619 108.625 112.866C104.925 116.323 99.9925 118.421 94.5663 118.421C90.25 118.421 86.1803 117.063 82.8505 114.718C81.8639 114.101 81.0007 113.237 80.1374 112.496C76.3143 108.793 73.9712 103.609 73.9712 97.8074C73.9712 86.4514 83.2205 77.1938 94.5663 77.1938C106.035 77.1938 115.285 86.4514 115.285 97.8074Z" fill="black"/>
|
||||
<path d="M115.285 97.8065C115.285 103.855 112.695 109.162 108.625 112.989C104.925 116.445 99.9925 118.543 94.5663 118.543C90.25 118.543 86.1803 117.186 82.8505 114.84C77.4243 111.137 73.9712 104.966 73.9712 97.9299C73.9712 86.574 83.2205 77.3164 94.5663 77.3164C105.912 77.3164 115.285 86.4505 115.285 97.8065Z" fill="#1f1f1f" stroke="#737373" stroke-width="2" stroke-miterlimit="10"/>
|
||||
<path d="M109.613 112.003C109.366 112.373 108.996 112.62 108.626 112.867C104.927 116.323 99.9938 118.421 94.5676 118.421C90.2512 118.421 86.1815 117.063 82.8518 114.718C81.8652 114.101 81.0019 113.237 80.1387 112.496C80.632 111.879 81.3719 111.509 82.9751 110.892L83.5917 110.645C84.825 110.151 86.5515 109.534 88.7713 108.546C89.1413 108.423 89.388 108.176 89.6346 107.929C89.7579 107.806 89.8813 107.682 89.8813 107.436C90.0046 107.189 90.1279 106.818 90.1279 106.571V102.375C90.0046 102.251 90.0046 102.251 89.8813 102.128C89.5113 101.634 89.2646 101.017 89.2646 100.276L89.018 100.153C87.9081 100.4 88.0314 99.2889 87.7848 97.0671C87.6614 96.203 87.7848 95.9562 88.278 95.8327L88.648 95.339C87.9081 93.6109 87.5381 92.0063 87.5381 90.7719C87.5381 88.6735 88.4014 87.3158 89.6346 86.6986C88.8947 85.2174 88.8947 84.7236 88.8947 84.7236C88.8947 84.7236 93.211 85.4642 94.6909 85.2174C96.5408 84.8471 99.5005 85.3408 100.61 87.8095C102.46 88.5501 103.077 89.661 103.324 90.8953C103.57 92.8703 102.46 94.9687 102.214 95.8327V95.9562C102.46 96.0796 102.584 96.3265 102.46 97.1905C102.214 99.2889 102.214 100.523 101.227 100.276L100.24 102.004C100.24 102.251 100.24 102.251 100.117 102.375C100.117 102.745 100.117 103.362 100.117 106.695C100.117 107.065 100.24 107.559 100.487 107.806C100.61 107.929 100.61 108.053 100.734 108.053C100.98 108.3 101.227 108.546 101.474 108.546C103.94 109.534 105.667 110.275 107.023 110.768C108.257 111.262 109.12 111.632 109.613 112.003Z" fill="#1f1f1f"/>
|
||||
<path d="M109.612 112.003C109.365 112.373 108.995 112.62 108.626 112.867C104.926 116.323 99.9928 118.421 94.5666 118.421C90.2503 118.421 86.1806 117.063 82.8508 114.718C81.8642 114.101 81.001 113.237 80.1377 112.496C80.631 111.879 81.3709 111.509 82.9741 110.892L83.5908 110.645C84.824 110.151 86.5505 109.534 88.7704 108.546C89.1403 108.423 89.387 108.176 89.6336 107.929C90.8669 109.657 92.8401 110.768 95.1832 110.768C97.403 110.768 99.3762 109.657 100.609 108.053C100.856 108.3 101.103 108.546 101.349 108.546C103.816 109.534 105.542 110.274 106.899 110.768C108.256 111.262 109.119 111.632 109.612 112.003Z" fill="#737373"/>
|
||||
<path d="M102.09 95.7093C102.213 95.2155 101.966 94.4749 101.72 94.1046C101.72 93.9812 101.596 93.9812 101.596 93.8578C100.733 92.1297 98.8831 91.5125 97.1566 91.3891C92.5936 91.1422 92.2236 92.0063 90.867 90.7719C91.3603 91.3891 91.3603 92.5 90.6204 93.7343C90.1271 94.5984 89.2638 95.0921 88.4006 95.339C86.3041 90.6485 87.414 87.6861 89.3872 86.6986C88.6472 85.2174 88.6472 84.7236 88.6472 84.7236C88.6472 84.7236 92.9635 85.4642 94.4434 85.2174C96.2933 84.8471 99.2531 85.3408 100.363 87.8095C102.213 88.5501 102.829 89.661 103.076 90.8953C103.446 92.7469 102.336 94.8452 102.09 95.7093Z" fill="#a3a3a3"/>
|
||||
<path d="M90.2501 106.571V102.375C90.1267 102.251 90.1267 102.251 90.0034 102.128V101.881C90.2501 102.251 90.4967 102.621 90.8667 102.868L94.1964 105.214C94.9364 105.831 96.0463 105.831 96.7862 105.214L99.8693 102.498C99.9927 102.375 100.116 102.375 100.239 102.251C100.239 102.621 100.239 103.239 100.239 106.571C100.239 106.818 100.239 106.942 100.363 107.189H90.2501C90.1267 106.942 90.2501 106.818 90.2501 106.571Z" fill="url(#paint1_linear)"/>
|
||||
<path d="M41.2036 98.1168C41.2036 103.918 38.7371 109.102 34.7908 112.805C33.6808 113.793 32.5709 114.657 31.2144 115.398C28.2546 117.126 24.8015 118.113 21.1018 118.113C17.4021 118.113 13.949 117.126 10.9892 115.398C10.4959 115.151 10.126 114.904 9.63268 114.534C4.45307 110.954 1 104.906 1 98.1168C1 87.0077 10.0026 78.1204 20.9785 78.1204C32.201 77.997 41.2036 87.0077 41.2036 98.1168Z" fill="#1f1f1f" stroke="#737373" stroke-width="2" stroke-miterlimit="10"/>
|
||||
<path d="M17.0323 102.56C17.279 102.806 17.5256 103.177 17.8956 103.424C18.1422 103.67 18.3889 103.794 18.6355 104.041C18.7589 104.164 19.0055 104.288 19.1288 104.411C19.1288 104.411 19.2522 104.411 19.2522 104.534L19.3755 104.658V106.386C19.3755 106.386 19.3755 106.386 19.2522 106.263C19.1288 106.139 18.8822 106.016 18.7589 105.892C18.5122 105.769 18.2656 105.522 18.0189 105.399C17.8956 105.399 17.8956 105.275 17.7723 105.275C16.909 104.781 16.1691 104.288 16.1691 103.67C16.2924 103.424 16.539 103.053 17.0323 102.56ZM34.1878 112.107C33.4478 110.502 32.4478 108.978 30.7213 108.114C29.858 107.744 28.8714 107.373 27.8848 107.373C27.6382 107.373 27.2682 107.373 27.0216 107.373C26.8982 107.373 26.7749 107.373 26.6516 107.373C25.295 107.25 25.1717 107.003 25.1717 107.003V104.164C26.035 103.424 26.8982 102.56 27.6382 101.695C28.2548 100.831 28.7481 99.844 28.9947 98.6096C30.1047 98.3628 30.8446 97.3753 30.7213 96.1409C30.7213 95.6472 30.3513 95.1535 30.3513 94.6597C30.3513 94.4129 30.3513 94.166 30.3513 93.9191C30.3513 93.7957 30.3513 93.5488 30.3513 93.4254C30.3513 93.3019 30.3513 93.0551 30.3513 92.9316C30.228 92.0676 29.9813 91.2036 29.488 90.2161C28.0082 87.5005 25.295 85.7725 22.0886 85.7725C21.472 85.7725 20.8554 85.8959 20.2387 86.0193C19.1288 86.2662 18.0189 86.7599 17.1556 87.5005C17.0323 87.624 16.7857 87.7474 16.6624 87.9943L16.539 88.1177C15.5524 89.1052 14.6892 90.2161 14.3192 91.5739C13.8259 92.9316 13.8259 94.2894 13.9492 95.6472C13.9492 95.6472 13.9492 95.6472 13.9492 95.7706V95.8941C13.9492 96.1409 14.0726 96.1409 13.9492 96.2644C13.9492 96.3878 13.8259 96.3878 13.8259 96.5112C13.5793 96.8815 13.4559 97.3753 13.7026 98.1159C14.1959 99.3502 14.9358 99.2268 15.7991 99.844C15.7991 99.844 15.6758 99.844 15.6758 99.9674L14.8125 100.214C10.8661 101.449 9.50957 104.781 11.2361 106.88C11.8527 107.62 12.8393 108.238 14.3192 108.608C13.9492 108.608 13.5793 108.855 13.3326 109.102C11.6061 110.459 10.4962 112.558 10.2495 114.533C10.2495 114.656 10.2495 114.78 10.2495 114.903C10.7428 115.15 11.1128 115.52 11.6061 115.767L30.9175 115.315C32.1507 114.575 32.6963 114.003 33.8062 113.016C33.6829 112.399 34.3111 112.23 34.1878 112.107Z" fill="#1f1f1f"/>
|
||||
<path d="M34.7909 112.805C33.681 113.792 32.5711 114.656 31.2145 115.397C28.2547 117.125 24.8017 118.113 21.1019 118.113C17.4022 118.113 13.9491 117.125 10.9894 115.397C10.4961 115.15 10.1261 114.903 9.63281 114.533C9.63281 114.41 9.63281 114.286 9.63281 114.163C9.87946 112.188 10.9894 110.089 12.7159 108.732C12.9626 108.485 13.3325 108.361 13.7025 108.238C12.2226 107.991 11.236 107.374 10.6194 106.51H15.3057C16.6623 108.361 18.7588 109.472 21.2253 109.472C23.3218 109.472 25.1716 108.608 26.5282 107.25C26.6515 107.25 26.7748 107.25 26.8982 107.25C27.1448 107.25 27.3915 107.25 27.7614 107.25C28.748 107.25 29.7346 107.497 30.5979 107.991C32.3244 108.855 33.5577 110.336 34.4209 112.064C34.6676 112.311 34.6676 112.558 34.7909 112.805Z" fill="#737373"/>
|
||||
<path d="M25.2953 104.165V106.757L17.5259 107.004L17.8958 105.275C18.0192 105.275 18.0192 105.399 18.1425 105.399C18.3891 105.522 18.6358 105.769 18.8824 105.893C19.0058 106.016 19.1291 106.139 19.3757 106.263C19.3757 106.263 19.4991 106.263 19.4991 106.386V104.658L19.3757 104.535C20.7323 105.275 22.5822 105.769 25.2953 104.165Z" fill="url(#paint2_linear)"/>
|
||||
<path d="M30.351 93.4261C28.8711 93.9198 27.1446 94.1667 25.5414 94.0432C22.9516 93.7964 20.4851 92.8089 18.5119 91.0808C17.8953 92.9323 16.2921 94.2901 14.4422 95.1541C14.1956 95.2776 13.949 95.401 13.7023 95.401C13.7023 95.401 13.7023 95.401 13.7023 95.2776C13.579 93.9198 13.579 92.562 14.0723 91.2042C14.4422 89.8465 15.3055 88.7356 16.2921 87.7481L16.4154 87.6247C16.5388 87.5012 16.7854 87.3778 16.9087 87.1309C17.772 86.3903 18.8819 85.8966 19.9918 85.6497C20.6084 85.5263 21.2251 85.4028 21.8417 85.4028C25.0481 85.4028 27.8846 87.1309 29.2411 89.8465C29.7344 90.8339 29.9811 91.8214 30.1044 92.562C30.351 93.0558 30.351 93.3026 30.351 93.4261Z" fill="#a3a3a3"/>
|
||||
<path d="M20.4853 111.694C19.7453 112.558 18.5121 112.558 17.4022 112.558C18.5121 111.447 17.8955 107.868 13.9491 108.238C8.52286 107.251 9.01616 101.573 14.4424 99.8445L15.3057 99.5977L15.429 99.7211C15.799 100.832 16.4156 101.819 17.0322 102.56C14.8124 104.412 17.8955 104.905 19.3754 106.387C20.6086 107.127 21.7185 110.213 20.4853 111.694Z" fill="#a3a3a3"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="58.2299" y1="30.8211" x2="58.2299" y2="28.0409" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1f1f1f"/>
|
||||
<stop offset="0.9913" stop-color="#222427"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="95.163" y1="107.023" x2="95.163" y2="104.243" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1f1f1f"/>
|
||||
<stop offset="0.9913" stop-color="#222427"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear" x1="21.3956" y1="106.915" x2="21.3956" y2="105.428" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1f1f1f"/>
|
||||
<stop offset="0.9913" stop-color="#222427"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 15 KiB |
@@ -15,6 +15,7 @@
|
||||
"update_failure": "Failed to update authentication provider configurations!!"
|
||||
},
|
||||
"confirm_changes": "Hoppscotch server must restart to reflect the new changes. Confirm changes made to the server configurations?",
|
||||
"input_empty": "Please fill all the fields before updating the configurations",
|
||||
"load_error": "Unable to load server configurations",
|
||||
"mail_configs": {
|
||||
"description": " Configure the smtp configurations",
|
||||
@@ -44,18 +45,14 @@
|
||||
"no_metrics": "No metrics found",
|
||||
"total_collections": "Total Collections",
|
||||
"total_requests": "Total Requests",
|
||||
"total_teams": "Total Teams",
|
||||
"total_teams": "Total Workspaces",
|
||||
"total_users": "Total Users"
|
||||
},
|
||||
"role": {
|
||||
"editor": "EDITOR",
|
||||
"owner": "OWNER",
|
||||
"viewer": "VIEWER"
|
||||
},
|
||||
"settings": {
|
||||
"settings": "Settings"
|
||||
},
|
||||
"shared_requests": {
|
||||
"action": "Action",
|
||||
"clear_filter": "Clear Filter",
|
||||
"confirm_request_deletion": "Confirm deletion of the selected shared request?",
|
||||
"copy": "Copy",
|
||||
@@ -75,8 +72,8 @@
|
||||
"url": "URL"
|
||||
},
|
||||
"state": {
|
||||
"add_user_failure": "Failed to add user to the team!!",
|
||||
"add_user_success": "User is now a member of the team!!",
|
||||
"add_user_failure": "Failed to add user to the workspace!!",
|
||||
"add_user_success": "User is now a member of the workspace!!",
|
||||
"admin_failure": "Failed to make user an admin!!",
|
||||
"admin_success": "User is now an admin!!",
|
||||
"and": "and",
|
||||
@@ -87,19 +84,19 @@
|
||||
"continue_google": "Continue with Google",
|
||||
"continue_microsoft": "Continue with Microsoft",
|
||||
"copied_to_clipboard": "Copied to clipboard",
|
||||
"create_team_failure": "Failed to create team!!",
|
||||
"create_team_success": "Team created successfully!!",
|
||||
"create_team_failure": "Failed to create workspace!!",
|
||||
"create_team_success": "Workspace created successfully!!",
|
||||
"delete_request_failure": "Shared Request deletion failed!!",
|
||||
"delete_request_success": "Shared Request deleted successfully!!",
|
||||
"delete_team_failure": "Team deletion failed!!",
|
||||
"delete_team_success": "Team deleted successfully!!",
|
||||
"delete_team_failure": "Workspace deletion failed!!",
|
||||
"delete_team_success": "Workspace deleted successfully!!",
|
||||
"delete_user_failure": "User deletion failed!!",
|
||||
"delete_user_success": "User deleted successfully!!",
|
||||
"email": "Email",
|
||||
"email_failure": "Failed to send invitation",
|
||||
"email_signin_failure": "Failed to login with Email",
|
||||
"email_success": "Email invitation sent successfully",
|
||||
"enter_team_email": "Please enter email of team owner!!",
|
||||
"enter_team_email": "Please enter email of workspace owner!!",
|
||||
"error": "Something went wrong",
|
||||
"error_auth_providers": "Unable to load auth providers",
|
||||
"github_signin_failure": "Failed to login with Github",
|
||||
@@ -113,6 +110,7 @@
|
||||
"microsoft_signin_failure": "Failed to login with Microsoft",
|
||||
"non_admin_logged_in": "Logged in as non admin user.",
|
||||
"non_admin_login": "You are logged in. But you're not an admin",
|
||||
"owner_not_present": "Atleast one owner should be present in the team!!",
|
||||
"privacy_policy": "Privacy Policy",
|
||||
"reenter_email": "Re-enter email",
|
||||
"remove_admin_failure": "Failed to remove admin status!!",
|
||||
@@ -122,8 +120,8 @@
|
||||
"remove_invitee_success": "Removal of invitee is successfull!!",
|
||||
"remove_member_failure": "Member couldn't be removed!!",
|
||||
"remove_member_success": "Member removed successfully!!",
|
||||
"rename_team_failure": "Failed to rename team!!",
|
||||
"rename_team_success": "Team renamed successfully!",
|
||||
"rename_team_failure": "Failed to rename workspace!!",
|
||||
"rename_team_success": "Workspace renamed successfully!",
|
||||
"require_auth_provider": "You need to set atleast one authentication provider to log in.",
|
||||
"role_update_failed": "Roles updation has failed!!",
|
||||
"role_update_success": "Roles updated successfully!!",
|
||||
@@ -132,48 +130,63 @@
|
||||
"sign_in_agreement": "By signing in, you are agreeing to our",
|
||||
"sign_in_options": "All sign in option",
|
||||
"sign_out": "Sign out",
|
||||
"team_name_long": "Team name should be atleast 6 characters long!!",
|
||||
"team_name_long": "Workspace name should be atleast 6 characters long!!",
|
||||
"user_not_found": "User not found in the infra!!"
|
||||
},
|
||||
"teams": {
|
||||
"add_member": "Add Member",
|
||||
"add_members": "Add Members",
|
||||
"add_new": "Add New",
|
||||
"admin": "Admin",
|
||||
"admin_Email": "Admin Email",
|
||||
"admin_id": "Admin ID",
|
||||
"cancel": "Cancel",
|
||||
"confirm_team_deletion": "Confirm Deletion of the team?",
|
||||
"create_team": "Create team",
|
||||
"confirm_team_deletion": "Confirm deletion of the workspace?",
|
||||
"create_team": "Create Workspace",
|
||||
"date": "Date",
|
||||
"delete_team": "Delete Team",
|
||||
"delete_team": "Delete Workspace",
|
||||
"details": "Details",
|
||||
"edit": "Edit",
|
||||
"email": "Team owner email",
|
||||
"editor": "EDITOR",
|
||||
"editor_description": "Editors can add, edit, and delete requests and collections.",
|
||||
"email": "Workspace owner email",
|
||||
"email_address": "Email Address",
|
||||
"email_title": "Email",
|
||||
"empty_name": "Team name cannot be empty!!",
|
||||
"error": "Something went wrong. Please try again later.",
|
||||
"id": "Team ID",
|
||||
"id": "Workspace ID",
|
||||
"invited_email": "Invitee Email",
|
||||
"invited_on": "Invited On",
|
||||
"invites": "Invites",
|
||||
"load_info_error": "Unable to load team info",
|
||||
"load_list_error": "Unable to Load Teams List",
|
||||
"load_info_error": "Unable to load Workspace info",
|
||||
"load_list_error": "Unable to Load Workspace List",
|
||||
"members": "Number of members",
|
||||
"name": "Team Name",
|
||||
"no_members": "No members in this team. Add members to this team to collaborate",
|
||||
"no_invite": "No invites",
|
||||
"owner": "OWNER",
|
||||
"owner_description": " Owners can add, edit, and delete requests, collections and workspace members.",
|
||||
"permissions": "Permissions",
|
||||
"name": "Workspace Name",
|
||||
"no_members": "No members in this workspace. Add members to this workspace to collaborate",
|
||||
"no_pending_invites": "No pending invites",
|
||||
"no_teams": "No teams found",
|
||||
"no_teams": "No workspaces found",
|
||||
"pending_invites": "Pending invites",
|
||||
"roles": "Roles",
|
||||
"roles_description": "Roles are used to control access to the shared collections.",
|
||||
"remove": "Remove",
|
||||
"rename": "Rename",
|
||||
"save": "Save",
|
||||
"save_changes": "Save Changes",
|
||||
"send_invite": "Send Invite",
|
||||
"show_more": "Show more",
|
||||
"team_details": "Team details",
|
||||
"team_details": "Workspace details",
|
||||
"team_members": "Members",
|
||||
"team_members_tab": "Team members",
|
||||
"teams": "Teams",
|
||||
"team_members_tab": "Workspace members",
|
||||
"teams": "Workspace",
|
||||
"uid": "UID",
|
||||
"unnamed": "(Unnamed Team)",
|
||||
"valid_name": "Please enter a valid team name",
|
||||
"unnamed": "(Unnamed Workspace)",
|
||||
"viewer": "VIEWER",
|
||||
"viewer_description": "Viewers can only view and use requests",
|
||||
"valid_name": "Please enter a valid workspace name",
|
||||
"valid_owner_email": "Please enter a valid owner email"
|
||||
},
|
||||
"users": {
|
||||
|
||||
87
packages/hoppscotch-sh-admin/src/components.d.ts
vendored
87
packages/hoppscotch-sh-admin/src/components.d.ts
vendored
@@ -1,51 +1,54 @@
|
||||
// generated by unplugin-vue-components
|
||||
// We suggest you to commit this file into source control
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
import '@vue/runtime-core'
|
||||
import '@vue/runtime-core';
|
||||
|
||||
export {}
|
||||
export {};
|
||||
|
||||
declare module '@vue/runtime-core' {
|
||||
export interface GlobalComponents {
|
||||
AppHeader: typeof import('./components/app/Header.vue')['default']
|
||||
AppLogin: typeof import('./components/app/Login.vue')['default']
|
||||
AppLogout: typeof import('./components/app/Logout.vue')['default']
|
||||
AppModal: typeof import('./components/app/Modal.vue')['default']
|
||||
AppSidebar: typeof import('./components/app/Sidebar.vue')['default']
|
||||
AppToast: typeof import('./components/app/Toast.vue')['default']
|
||||
DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default']
|
||||
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
||||
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
||||
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
||||
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
|
||||
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
||||
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
|
||||
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
||||
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
|
||||
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
||||
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
|
||||
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
||||
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
||||
HoppSmartTable: typeof import('@hoppscotch/ui')['HoppSmartTable']
|
||||
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
|
||||
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
|
||||
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default']
|
||||
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
|
||||
SettingsAuthProvider: typeof import('./components/settings/AuthProvider.vue')['default']
|
||||
SettingsConfigurations: typeof import('./components/settings/Configurations.vue')['default']
|
||||
SettingsReset: typeof import('./components/settings/Reset.vue')['default']
|
||||
SettingsServerRestart: typeof import('./components/settings/ServerRestart.vue')['default']
|
||||
SettingsSmtpConfiguration: typeof import('./components/settings/SmtpConfiguration.vue')['default']
|
||||
TeamsAdd: typeof import('./components/teams/Add.vue')['default']
|
||||
TeamsDetails: typeof import('./components/teams/Details.vue')['default']
|
||||
TeamsInvite: typeof import('./components/teams/Invite.vue')['default']
|
||||
TeamsMembers: typeof import('./components/teams/Members.vue')['default']
|
||||
TeamsPendingInvites: typeof import('./components/teams/PendingInvites.vue')['default']
|
||||
Tippy: typeof import('vue-tippy')['Tippy']
|
||||
UiAutoResetIcon: typeof import('./components/ui/AutoResetIcon.vue')['default']
|
||||
UsersDetails: typeof import('./components/users/Details.vue')['default']
|
||||
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default']
|
||||
UsersSharedRequests: typeof import('./components/users/SharedRequests.vue')['default']
|
||||
AppHeader: typeof import('./components/app/Header.vue')['default'];
|
||||
AppLogin: typeof import('./components/app/Login.vue')['default'];
|
||||
AppLogout: typeof import('./components/app/Logout.vue')['default'];
|
||||
AppModal: typeof import('./components/app/Modal.vue')['default'];
|
||||
AppSidebar: typeof import('./components/app/Sidebar.vue')['default'];
|
||||
AppToast: typeof import('./components/app/Toast.vue')['default'];
|
||||
DashboardMetricsCard: typeof import('./components/dashboard/MetricsCard.vue')['default'];
|
||||
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary'];
|
||||
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary'];
|
||||
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor'];
|
||||
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete'];
|
||||
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal'];
|
||||
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput'];
|
||||
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem'];
|
||||
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal'];
|
||||
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture'];
|
||||
HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder'];
|
||||
HoppSmartSelectWrapper: typeof import('@hoppscotch/ui')['HoppSmartSelectWrapper'];
|
||||
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner'];
|
||||
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab'];
|
||||
HoppSmartTable: typeof import('@hoppscotch/ui')['HoppSmartTable'];
|
||||
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs'];
|
||||
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle'];
|
||||
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default'];
|
||||
IconLucideChevronDown: typeof import('~icons/lucide/chevron-down')['default'];
|
||||
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default'];
|
||||
IconLucideInbox: typeof import('~icons/lucide/inbox')['default'];
|
||||
IconLucideUser: typeof import('~icons/lucide/user')['default'];
|
||||
SettingsAuthProvider: typeof import('./components/settings/AuthProvider.vue')['default'];
|
||||
SettingsConfigurations: typeof import('./components/settings/Configurations.vue')['default'];
|
||||
SettingsReset: typeof import('./components/settings/Reset.vue')['default'];
|
||||
SettingsServerRestart: typeof import('./components/settings/ServerRestart.vue')['default'];
|
||||
SettingsSmtpConfiguration: typeof import('./components/settings/SmtpConfiguration.vue')['default'];
|
||||
TeamsAdd: typeof import('./components/teams/Add.vue')['default'];
|
||||
TeamsDetails: typeof import('./components/teams/Details.vue')['default'];
|
||||
TeamsInvite: typeof import('./components/teams/Invite.vue')['default'];
|
||||
TeamsMembers: typeof import('./components/teams/Members.vue')['default'];
|
||||
TeamsPendingInvites: typeof import('./components/teams/PendingInvites.vue')['default'];
|
||||
Tippy: typeof import('vue-tippy')['Tippy'];
|
||||
UiAutoResetIcon: typeof import('./components/ui/AutoResetIcon.vue')['default'];
|
||||
UsersDetails: typeof import('./components/users/Details.vue')['default'];
|
||||
UsersInviteModal: typeof import('./components/users/InviteModal.vue')['default'];
|
||||
UsersSharedRequests: typeof import('./components/users/SharedRequests.vue')['default'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -72,13 +72,14 @@ onMounted(async () => {
|
||||
success = await resetInfraConfigs(resetInfraConfigsMutation);
|
||||
if (!success) return;
|
||||
} else {
|
||||
const infraResult = await updateInfraConfigs(updateInfraConfigsMutation);
|
||||
|
||||
if (!infraResult) return;
|
||||
|
||||
const authResult = await updateAuthProvider(
|
||||
updateAllowedAuthProviderMutation
|
||||
);
|
||||
const infraResult = await updateInfraConfigs(updateInfraConfigsMutation);
|
||||
|
||||
success = authResult && infraResult;
|
||||
if (!success) return;
|
||||
if (!authResult) return;
|
||||
}
|
||||
|
||||
restart.value = true;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('teams.create_team')"
|
||||
@close="$emit('hide-modal')"
|
||||
@close="emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col space-y-4 relative">
|
||||
@@ -16,12 +16,16 @@
|
||||
class="flex-1 !flex"
|
||||
:source="allUsersEmail"
|
||||
:spellcheck="true"
|
||||
placeholder=""
|
||||
:placeholder="t('teams.email')"
|
||||
@input="(email: string) => getOwnerEmail(email)"
|
||||
/>
|
||||
</div>
|
||||
<label for="teamName"> {{ t('teams.name') }} </label>
|
||||
<HoppSmartInput v-model="teamName" placeholder="" class="!my-2" />
|
||||
<HoppSmartInput
|
||||
v-model="teamName"
|
||||
:placeholder="t('teams.name')"
|
||||
class="!my-2"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #footer>
|
||||
@@ -44,11 +48,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watchEffect } from 'vue';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -82,11 +85,11 @@ const getOwnerEmail = (email: string) => (ownerEmail.value = email);
|
||||
|
||||
const createTeam = () => {
|
||||
if (teamName.value.trim() === '') {
|
||||
toast.error(`${t('teams.valid_name')}`);
|
||||
toast.error(t('teams.valid_name'));
|
||||
return;
|
||||
}
|
||||
if (ownerEmail.value.trim() === '') {
|
||||
toast.error(`${t('teams.valid_owner_email')}`);
|
||||
toast.error(t('teams.valid_owner_email'));
|
||||
return;
|
||||
}
|
||||
emit('create-team', teamName.value, ownerEmail.value);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<div v-if="team" class="flex flex-col">
|
||||
<div class="flex flex-col space-y-8">
|
||||
<div v-if="team.id" class="flex flex-col space-y-3">
|
||||
<div class="flex flex-col space-y-3">
|
||||
<label class="text-accentContrast" for="username"
|
||||
>{{ t('teams.id') }}
|
||||
</label>
|
||||
@@ -10,33 +10,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="teamName" class="flex flex-col space-y-3">
|
||||
<div class="flex flex-col space-y-3">
|
||||
<label class="text-accentContrast" for="teamname"
|
||||
>{{ t('teams.name') }}
|
||||
</label>
|
||||
<div
|
||||
class="flex bg-divider rounded-md items-stretch flex-1 border border-divider"
|
||||
:class="{
|
||||
'!border-accent': showRenameInput,
|
||||
'!border-accent': isTeamNameBeingEdited,
|
||||
}"
|
||||
>
|
||||
<HoppSmartInput
|
||||
v-model="newTeamName"
|
||||
v-model="updatedTeamName"
|
||||
styles="bg-transparent flex-1 rounded-md !rounded-r-none disabled:select-none border-r-0 disabled:cursor-default disabled:opacity-50"
|
||||
placeholder="Team Name"
|
||||
:disabled="!showRenameInput"
|
||||
:disabled="!isTeamNameBeingEdited"
|
||||
>
|
||||
<template #button>
|
||||
<HoppButtonPrimary
|
||||
class="!rounded-l-none"
|
||||
filled
|
||||
:icon="showRenameInput ? IconSave : IconEdit"
|
||||
:icon="isTeamNameBeingEdited ? IconSave : IconEdit"
|
||||
:label="
|
||||
showRenameInput
|
||||
isTeamNameBeingEdited
|
||||
? `${t('teams.rename')}`
|
||||
: `${t('teams.edit')}`
|
||||
"
|
||||
@click="handleNameEdit()"
|
||||
@click="handleTeamNameEdit"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartInput>
|
||||
@@ -58,7 +58,7 @@
|
||||
class="!bg-red-600 !hover:opacity-80"
|
||||
filled
|
||||
:label="t('teams.delete_team')"
|
||||
@click="team && $emit('delete-team', team.id)"
|
||||
@click="emit('delete-team', team.id)"
|
||||
:icon="IconTrash"
|
||||
/>
|
||||
</div>
|
||||
@@ -66,45 +66,89 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useMutation } from '@urql/vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { TeamInfoQuery } from '~/helpers/backend/graphql';
|
||||
import { RenameTeamDocument, TeamInfoQuery } from '~/helpers/backend/graphql';
|
||||
import IconEdit from '~icons/lucide/edit';
|
||||
import IconSave from '~icons/lucide/save';
|
||||
import IconTrash from '~icons/lucide/trash-2';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const props = defineProps<{
|
||||
team: TeamInfoQuery['infra']['teamInfo'];
|
||||
teamName: string;
|
||||
showRenameInput: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'delete-team', teamID: string): void;
|
||||
(event: 'rename-team', teamName: string): void;
|
||||
(event: 'update:showRenameInput', showRenameInput: boolean): void;
|
||||
(event: 'update:team', team: TeamInfoQuery['infra']['teamInfo']): void;
|
||||
(event: 'delete-team', teamId: string): void;
|
||||
}>();
|
||||
|
||||
const newTeamName = ref(props.teamName);
|
||||
const team = useVModel(props, 'team', emit);
|
||||
|
||||
const handleNameEdit = () => {
|
||||
if (props.showRenameInput) {
|
||||
renameTeam();
|
||||
// Contains the actual team name
|
||||
const teamName = computed({
|
||||
get: () => team.value.name,
|
||||
set: (value) => {
|
||||
team.value.name = value;
|
||||
},
|
||||
});
|
||||
|
||||
// Contains the stored team name from the actual team name before being edited
|
||||
const currentTeamName = ref('');
|
||||
|
||||
// Contains the team name that is being edited
|
||||
const updatedTeamName = computed({
|
||||
get: () => currentTeamName.value,
|
||||
set: (value) => {
|
||||
currentTeamName.value = value;
|
||||
},
|
||||
});
|
||||
|
||||
// Set the current team name to the actual team name
|
||||
onMounted(() => {
|
||||
currentTeamName.value = teamName.value;
|
||||
});
|
||||
|
||||
// Rename the team name
|
||||
const isTeamNameBeingEdited = ref(false);
|
||||
const teamRename = useMutation(RenameTeamDocument);
|
||||
|
||||
const handleTeamNameEdit = () => {
|
||||
if (isTeamNameBeingEdited.value) {
|
||||
// If the team name is not changed, then return control
|
||||
if (teamName.value !== updatedTeamName.value) {
|
||||
renameTeamName();
|
||||
} else isTeamNameBeingEdited.value = false;
|
||||
} else {
|
||||
emit('update:showRenameInput', true);
|
||||
isTeamNameBeingEdited.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
const renameTeam = () => {
|
||||
if (newTeamName.value.trim() === '') {
|
||||
toast.error(`${t('teams.empty_name')}`);
|
||||
const renameTeamName = async () => {
|
||||
if (updatedTeamName.value.trim() === '') {
|
||||
toast.error(t('teams.empty_name'));
|
||||
return;
|
||||
}
|
||||
emit('rename-team', newTeamName.value);
|
||||
|
||||
if (updatedTeamName.value.length < 6) {
|
||||
toast.error(t('state.team_name_too_short'));
|
||||
return;
|
||||
}
|
||||
|
||||
const variables = { uid: team.value.id, name: updatedTeamName.value };
|
||||
const result = await teamRename.executeMutation(variables);
|
||||
|
||||
if (result.error) {
|
||||
toast.error(t('state.rename_team_failure'));
|
||||
} else {
|
||||
isTeamNameBeingEdited.value = false;
|
||||
toast.success(t('state.rename_team_success'));
|
||||
teamName.value = updatedTeamName.value;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
<template>
|
||||
<HoppSmartModal v-if="show" dialog title="Add Member" @close="hideModal">
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('teams.add_member')"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<div v-if="addingUserToTeam" class="flex items-center justify-center p-4">
|
||||
<HoppSmartSpinner />
|
||||
</div>
|
||||
<div v-else class="flex flex-col">
|
||||
<div class="flex items-center justify-between flex-1 pt-4">
|
||||
<label for="memberList" class="p-4"> Add members </label>
|
||||
<label for="memberList" class="p-4">
|
||||
{{ t('teams.add_members') }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
:icon="IconPlus"
|
||||
label="Add new"
|
||||
:label="t('teams.add_new')"
|
||||
filled
|
||||
@click="addNewMember"
|
||||
/>
|
||||
@@ -23,8 +30,8 @@
|
||||
class="flex divide-x divide-dividerLight"
|
||||
>
|
||||
<HoppSmartAutoComplete
|
||||
v-model="member.key"
|
||||
placeholder="Email"
|
||||
:value="member.key"
|
||||
:placeholder="t('state.email')"
|
||||
:source="allUsersEmail"
|
||||
:name="'member' + index"
|
||||
:spellcheck="true"
|
||||
@@ -44,7 +51,7 @@
|
||||
<HoppSmartSelectWrapper>
|
||||
<input
|
||||
class="flex flex-1 px-4 py-2 bg-transparent cursor-pointer"
|
||||
placeholder="Permissions"
|
||||
:placeholder="t('teams.permissions')"
|
||||
:name="'value' + index"
|
||||
:value="member.value"
|
||||
readonly
|
||||
@@ -58,7 +65,7 @@
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
label="OWNER"
|
||||
:label="t('teams.owner')"
|
||||
:icon="
|
||||
member.value === 'OWNER' ? IconCircleDot : IconCircle
|
||||
"
|
||||
@@ -71,7 +78,7 @@
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
label="EDITOR"
|
||||
:label="t('teams.editor')"
|
||||
:icon="
|
||||
member.value === 'EDITOR' ? IconCircleDot : IconCircle
|
||||
"
|
||||
@@ -84,7 +91,7 @@
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
label="VIEWER"
|
||||
:label="t('teams.viewer')"
|
||||
:icon="
|
||||
member.value === 'VIEWER' ? IconCircleDot : IconCircle
|
||||
"
|
||||
@@ -104,7 +111,7 @@
|
||||
<HoppButtonSecondary
|
||||
id="member"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
title="Remove"
|
||||
:title="t('teams.remove')"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
@click="removeNewMember(index)"
|
||||
@@ -113,13 +120,13 @@
|
||||
</div>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="newMembersList.length === 0"
|
||||
:src="`/images/states/dark/add_group.svg`"
|
||||
alt="No invites"
|
||||
text="No invites"
|
||||
:src="addGroupImagePath"
|
||||
:alt="t('teams.no_members')"
|
||||
:text="t('teams.no_members')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppButtonSecondary
|
||||
label="Add new"
|
||||
:label="t('teams.add_new')"
|
||||
filled
|
||||
@click="addNewMember"
|
||||
/>
|
||||
@@ -136,11 +143,12 @@
|
||||
<icon-lucide-help-circle
|
||||
class="mr-2 text-secondaryLight svg-icons"
|
||||
/>
|
||||
Roles
|
||||
|
||||
{{ t('teams.roles') }}
|
||||
</span>
|
||||
<p>
|
||||
<span class="text-secondaryLight">
|
||||
Roles are used to control access to the shared collections.
|
||||
{{ t('teams.roles_description') }}
|
||||
</span>
|
||||
</p>
|
||||
<ul class="mt-4 space-y-4">
|
||||
@@ -148,31 +156,30 @@
|
||||
<span
|
||||
class="w-1/4 font-semibold uppercase truncate text-secondaryDark max-w-[4rem]"
|
||||
>
|
||||
Owner
|
||||
{{ t('teams.owner') }}
|
||||
</span>
|
||||
<span class="flex flex-1">
|
||||
Owners can add, edit, and delete requests, collections and team
|
||||
members.
|
||||
{{ t('teams.owner_description') }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex">
|
||||
<span
|
||||
class="w-1/4 font-semibold uppercase truncate text-secondaryDark max-w-[4rem]"
|
||||
>
|
||||
Editor
|
||||
{{ t('teams.editor') }}
|
||||
</span>
|
||||
<span class="flex flex-1">
|
||||
Editors can add, edit, and delete requests.
|
||||
{{ t('teams.editor_description') }}
|
||||
</span>
|
||||
</li>
|
||||
<li class="flex">
|
||||
<span
|
||||
class="w-1/4 font-semibold uppercase truncate text-secondaryDark max-w-[4rem]"
|
||||
>
|
||||
Viewer
|
||||
{{ t('teams.viewer') }}
|
||||
</span>
|
||||
<span class="flex flex-1">
|
||||
Viewers can only view and use requests.
|
||||
{{ t('teams.viewer_description') }}
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -183,41 +190,61 @@
|
||||
<template #footer>
|
||||
<span class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
label="Add Member"
|
||||
:label="t('teams.add_member')"
|
||||
outline
|
||||
@click="addUserasTeamMember"
|
||||
/>
|
||||
<HoppButtonSecondary label="Cancel" outline filled @click="hideModal" />
|
||||
<HoppButtonSecondary
|
||||
:label="t('teams.cancel')"
|
||||
outline
|
||||
filled
|
||||
@click="hideModal"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useMutation, useQuery } from '@urql/vue';
|
||||
import * as A from 'fp-ts/Array';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import { pipe } from 'fp-ts/function';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { usePagedQuery } from '~/composables/usePagedQuery';
|
||||
import { Email, EmailCodec } from '~/helpers/Email';
|
||||
import IconCircle from '~icons/lucide/circle';
|
||||
import IconCircleDot from '~icons/lucide/circle-dot';
|
||||
import IconPlus from '~icons/lucide/plus';
|
||||
import IconTrash from '~icons/lucide/trash';
|
||||
import {
|
||||
AddUserToTeamByAdminDocument,
|
||||
TeamMemberRole,
|
||||
MetricsDocument,
|
||||
TeamMemberRole,
|
||||
UsersListDocument,
|
||||
} from '../../helpers/backend/graphql';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { useMutation, useQuery } from '@urql/vue';
|
||||
import { Email, EmailCodec } from '~/helpers/Email';
|
||||
import IconTrash from '~icons/lucide/trash';
|
||||
import IconPlus from '~icons/lucide/plus';
|
||||
import IconCircleDot from '~icons/lucide/circle-dot';
|
||||
import IconCircle from '~icons/lucide/circle';
|
||||
import { computed } from 'vue';
|
||||
import { usePagedQuery } from '~/composables/usePagedQuery';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
|
||||
const t = useI18n();
|
||||
const toast = useToast();
|
||||
const tippyActions = ref<any | null>(null);
|
||||
|
||||
// Get Users List
|
||||
const props = defineProps<{
|
||||
show: boolean;
|
||||
editingTeamID: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'hide-modal'): void;
|
||||
(e: 'member'): void;
|
||||
}>();
|
||||
|
||||
const addGroupImagePath = `${
|
||||
import.meta.env.VITE_ADMIN_URL
|
||||
}/assets/images/add_group.svg`;
|
||||
|
||||
// Get Users List to extract email ids of all users
|
||||
const { data } = useQuery({ query: MetricsDocument });
|
||||
const usersPerPage = computed(() => data.value?.infra.usersCount || 10000);
|
||||
|
||||
@@ -231,21 +258,6 @@ const { list: usersList } = usePagedQuery(
|
||||
|
||||
const allUsersEmail = computed(() => usersList.value.map((user) => user.email));
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null);
|
||||
|
||||
const props = defineProps({
|
||||
show: Boolean,
|
||||
editingTeamID: { type: String, default: null },
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'hide-modal'): void;
|
||||
(e: 'member'): void;
|
||||
}>();
|
||||
|
||||
const newMembersList = ref<Array<{ key: string; value: TeamMemberRole }>>([
|
||||
{
|
||||
key: '',
|
||||
@@ -264,12 +276,13 @@ const updateNewMemberRole = (index: number, role: TeamMemberRole) => {
|
||||
newMembersList.value[index].value = role;
|
||||
};
|
||||
|
||||
const removeNewMember = (id: number) => {
|
||||
newMembersList.value.splice(id, 1);
|
||||
const removeNewMember = (index: number) => {
|
||||
newMembersList.value.splice(index, 1);
|
||||
};
|
||||
|
||||
const addingUserToTeam = ref<boolean>(false);
|
||||
|
||||
// Checks if the member invites are of valid email format and then adds the users to the team
|
||||
const addUserasTeamMember = async () => {
|
||||
addingUserToTeam.value = true;
|
||||
|
||||
@@ -293,7 +306,7 @@ const addUserasTeamMember = async () => {
|
||||
|
||||
if (O.isNone(validationResult)) {
|
||||
// Error handling for no validation
|
||||
toast.error(`${t('users.invalid_user')}`);
|
||||
toast.error(t('users.invalid_user'));
|
||||
addingUserToTeam.value = false;
|
||||
return;
|
||||
}
|
||||
@@ -320,20 +333,18 @@ const addUserToTeam = async (
|
||||
) => {
|
||||
const variables = { userEmail: email, role: userRole, teamID: teamID };
|
||||
|
||||
const res = await addUserToTeamMutation
|
||||
.executeMutation(variables)
|
||||
.then((result) => {
|
||||
if (result.error) {
|
||||
if (result.error.toString() == '[GraphQL] user/not_found') {
|
||||
toast.error(`${t('state.user_not_found')}`);
|
||||
} else {
|
||||
toast.error(`${t('state.add_user_failure')}`);
|
||||
}
|
||||
} else {
|
||||
toast.success(`${t('state.add_user_success')}`);
|
||||
emit('member');
|
||||
}
|
||||
});
|
||||
return res;
|
||||
const result = await addUserToTeamMutation.executeMutation(variables);
|
||||
|
||||
if (result.error) {
|
||||
if (result.error.toString() == '[GraphQL] user/not_found') {
|
||||
toast.error(t('state.user_not_found'));
|
||||
} else {
|
||||
toast.error(t('state.add_user_failure'));
|
||||
}
|
||||
} else {
|
||||
toast.success(t('state.add_user_success'));
|
||||
emit('member');
|
||||
}
|
||||
return result;
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<div class="border rounded border-divider my-8">
|
||||
<HoppSmartPlaceholder
|
||||
v-if="team?.teamMembers?.length === 0"
|
||||
text="No members in this team. Add members to this team to collaborate"
|
||||
:text="t('teams.no_members')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppButtonSecondary
|
||||
@@ -35,7 +35,7 @@
|
||||
>
|
||||
<input
|
||||
class="flex flex-1 px-4 py-3 bg-transparent"
|
||||
placeholder="Email"
|
||||
:placeholder="t('teams.email_title')"
|
||||
:name="'param' + index"
|
||||
:value="member.email"
|
||||
readonly
|
||||
@@ -50,7 +50,7 @@
|
||||
<span class="relative">
|
||||
<input
|
||||
class="flex flex-1 px-4 py-3 bg-transparent cursor-pointer"
|
||||
placeholder="Permissions"
|
||||
:placeholder="t('teams.permissions')"
|
||||
:name="'value' + index"
|
||||
:value="member.role"
|
||||
readonly
|
||||
@@ -69,7 +69,7 @@
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
label="OWNER"
|
||||
:label="t('teams.owner')"
|
||||
:icon="
|
||||
member.role === 'OWNER' ? IconCircleDot : IconCircle
|
||||
"
|
||||
@@ -82,7 +82,7 @@
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
label="EDITOR"
|
||||
:label="t('teams.editor')"
|
||||
:icon="
|
||||
member.role === 'EDITOR' ? IconCircleDot : IconCircle
|
||||
"
|
||||
@@ -98,7 +98,7 @@
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
label="VIEWER"
|
||||
:label="t('teams.viewer')"
|
||||
:icon="
|
||||
member.role === 'VIEWER' ? IconCircleDot : IconCircle
|
||||
"
|
||||
@@ -131,7 +131,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!fetching && !team" class="flex flex-col items-center">
|
||||
<div v-if="!team" class="flex flex-col items-center">
|
||||
<icon-lucide-help-circle class="mb-4 svg-icons" />
|
||||
{{ t('teams.error') }}
|
||||
</div>
|
||||
@@ -139,13 +139,15 @@
|
||||
|
||||
<div class="flex">
|
||||
<HoppButtonPrimary
|
||||
:label="t('teams.save')"
|
||||
v-if="areRolesUpdated"
|
||||
:label="t('teams.save_changes')"
|
||||
outline
|
||||
@click="saveUpdatedTeam"
|
||||
/>
|
||||
</div>
|
||||
<TeamsInvite
|
||||
:show="showInvite"
|
||||
:team="team"
|
||||
:editingTeamID="route.params.id.toString()"
|
||||
@member="updateMembers"
|
||||
@hide-modal="
|
||||
@@ -158,102 +160,101 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconCircleDot from '~icons/lucide/circle-dot';
|
||||
import IconCircle from '~icons/lucide/circle';
|
||||
import IconUserPlus from '~icons/lucide/user-plus';
|
||||
import IconUserMinus from '~icons/lucide/user-minus';
|
||||
import IconChevronDown from '~icons/lucide/chevron-down';
|
||||
import { useClientHandle, useMutation } from '@urql/vue';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useMutation } from '@urql/vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { cloneDeep, isEqual } from 'lodash-es';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { useClientHandler } from '~/composables/useClientHandler';
|
||||
import IconChevronDown from '~icons/lucide/chevron-down';
|
||||
import IconCircle from '~icons/lucide/circle';
|
||||
import IconCircleDot from '~icons/lucide/circle-dot';
|
||||
import IconUserMinus from '~icons/lucide/user-minus';
|
||||
import IconUserPlus from '~icons/lucide/user-plus';
|
||||
import {
|
||||
ChangeUserRoleInTeamByAdminDocument,
|
||||
TeamInfoDocument,
|
||||
TeamMemberRole,
|
||||
RemoveUserFromTeamByAdminDocument,
|
||||
TeamInfoDocument,
|
||||
TeamInfoQuery,
|
||||
TeamMemberRole,
|
||||
} from '../../helpers/backend/graphql';
|
||||
import { HoppButtonPrimary, HoppButtonSecondary } from '@hoppscotch/ui';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const toast = useToast();
|
||||
const route = useRoute();
|
||||
|
||||
const props = defineProps<{
|
||||
team: TeamInfoQuery['infra']['teamInfo'];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update-team'): void;
|
||||
(event: 'update:team', team: TeamInfoQuery['infra']['teamInfo']): void;
|
||||
}>();
|
||||
|
||||
const teamDetails = useVModel(props, 'team', emit);
|
||||
|
||||
// Used to Invoke the Invite Members Modal
|
||||
const showInvite = ref(false);
|
||||
|
||||
// Get Team Details
|
||||
const team = ref<TeamInfoQuery['infra']['teamInfo'] | undefined>();
|
||||
const fetching = ref(true);
|
||||
const route = useRoute();
|
||||
const { client } = useClientHandle();
|
||||
|
||||
const getTeamInfo = async () => {
|
||||
fetching.value = true;
|
||||
const result = await client
|
||||
.query(TeamInfoDocument, { teamID: route.params.id.toString() })
|
||||
.toPromise();
|
||||
|
||||
if (result.error) {
|
||||
return toast.error(`${t('teams.load_info_error')}`);
|
||||
const { fetchData: getTeamInfo, data: teamInfo } = useClientHandler(
|
||||
TeamInfoDocument,
|
||||
{
|
||||
teamID: route.params.id.toString(),
|
||||
}
|
||||
if (result.data?.infra.teamInfo) {
|
||||
team.value = result.data.infra.teamInfo;
|
||||
}
|
||||
fetching.value = false;
|
||||
};
|
||||
);
|
||||
|
||||
onMounted(async () => await getTeamInfo());
|
||||
onUnmounted(() => emit('update-team'));
|
||||
onMounted(async () => {
|
||||
await getTeamInfo();
|
||||
});
|
||||
|
||||
const team = computed(() => teamInfo.value?.infra.teamInfo);
|
||||
|
||||
// Update members tab after a change in the members list or member roles
|
||||
const updateMembers = () => {
|
||||
getTeamInfo();
|
||||
emit('update-team');
|
||||
const updateMembers = async () => {
|
||||
if (!team.value) return;
|
||||
await getTeamInfo();
|
||||
teamDetails.value = team.value;
|
||||
};
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null);
|
||||
|
||||
const roleUpdates = ref<
|
||||
// Roles of the members in the team
|
||||
const currentMemberRoles = ref<
|
||||
{
|
||||
userID: string;
|
||||
role: TeamMemberRole;
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
watch(
|
||||
() => team.value,
|
||||
(teamDetails) => {
|
||||
const members = teamDetails?.teamMembers ?? [];
|
||||
// Roles of the members in the team after the updates but before saving
|
||||
const updatedMemberRoles = ref<
|
||||
{
|
||||
userID: string;
|
||||
role: TeamMemberRole;
|
||||
}[]
|
||||
>(cloneDeep(currentMemberRoles.value));
|
||||
|
||||
// Remove deleted members
|
||||
roleUpdates.value = roleUpdates.value.filter(
|
||||
(update) =>
|
||||
members.findIndex(
|
||||
(y: { user: { uid: string } }) => y.user.uid === update.userID
|
||||
) !== -1
|
||||
);
|
||||
}
|
||||
// Check if the roles of the members have been updated
|
||||
const areRolesUpdated = computed(() =>
|
||||
currentMemberRoles.value && updatedMemberRoles.value
|
||||
? !isEqual(currentMemberRoles.value, updatedMemberRoles.value)
|
||||
: false
|
||||
);
|
||||
|
||||
// Update the role of the member selected in the UI
|
||||
const updateMemberRole = (userID: string, role: TeamMemberRole) => {
|
||||
const updateIndex = roleUpdates.value.findIndex(
|
||||
const updateIndex = updatedMemberRoles.value.findIndex(
|
||||
(item) => item.userID === userID
|
||||
);
|
||||
if (updateIndex !== -1) {
|
||||
// Role Update exists
|
||||
roleUpdates.value[updateIndex].role = role;
|
||||
updatedMemberRoles.value[updateIndex].role = role;
|
||||
} else {
|
||||
// Role Update does not exist
|
||||
roleUpdates.value.push({
|
||||
updatedMemberRoles.value.push({
|
||||
userID,
|
||||
role,
|
||||
});
|
||||
@@ -264,7 +265,7 @@ const updateMemberRole = (userID: string, role: TeamMemberRole) => {
|
||||
const membersList = computed(() => {
|
||||
if (!team.value) return [];
|
||||
const members = (team.value.teamMembers ?? []).map((member) => {
|
||||
const updatedRole = roleUpdates.value.find(
|
||||
const updatedRole = updatedMemberRoles.value.find(
|
||||
(update) => update.userID === member.user.uid
|
||||
);
|
||||
|
||||
@@ -299,19 +300,31 @@ const isLoading = ref(false);
|
||||
|
||||
const saveUpdatedTeam = async () => {
|
||||
isLoading.value = true;
|
||||
roleUpdates.value.forEach(async (update) => {
|
||||
|
||||
const isOwnerPresent = membersList.value.some(
|
||||
(member) => member.role === TeamMemberRole.Owner
|
||||
);
|
||||
|
||||
if (!isOwnerPresent) {
|
||||
toast.error(t('state.owner_not_present'));
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
updatedMemberRoles.value.forEach(async (update) => {
|
||||
if (!team.value) return;
|
||||
|
||||
const updateMemberRoleResult = await changeUserRoleInTeam(
|
||||
update.userID,
|
||||
team.value.id,
|
||||
update.role
|
||||
);
|
||||
if (updateMemberRoleResult.error) {
|
||||
toast.error(`${t('state.role_update_failed')}`);
|
||||
roleUpdates.value = [];
|
||||
toast.error(t('state.role_update_failed'));
|
||||
} else {
|
||||
toast.success(`${t('state.role_update_success')}`);
|
||||
roleUpdates.value = [];
|
||||
toast.success(t('state.role_update_success'));
|
||||
currentMemberRoles.value = updatedMemberRoles.value;
|
||||
updatedMemberRoles.value = cloneDeep(currentMemberRoles.value);
|
||||
}
|
||||
isLoading.value = false;
|
||||
});
|
||||
@@ -340,14 +353,14 @@ const removeExistingTeamMember = async (userID: string, index: number) => {
|
||||
team.value.id
|
||||
)();
|
||||
if (removeTeamMemberResult.error) {
|
||||
toast.error(`${t('state.remove_member_failure')}`);
|
||||
toast.error(t('state.remove_member_failure'));
|
||||
} else {
|
||||
team.value.teamMembers = team.value.teamMembers?.filter(
|
||||
(member: any) => member.user.uid !== userID
|
||||
);
|
||||
toast.success(`${t('state.remove_member_success')}`);
|
||||
teamDetails.value = team.value;
|
||||
toast.success(t('state.remove_member_success'));
|
||||
}
|
||||
isLoadingIndex.value = null;
|
||||
emit('update-team');
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,103 +1,76 @@
|
||||
<template>
|
||||
<div class="border rounded divide-y divide-dividerLight border-divider my-8">
|
||||
<div v-if="fetching" class="flex items-center justify-center p-4">
|
||||
<HoppSmartSpinner />
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="team" class="divide-y divide-dividerLight">
|
||||
<div
|
||||
v-for="(invitee, index) in pendingInvites"
|
||||
:key="`invitee-${index}`"
|
||||
class="flex divide-x divide-dividerLight"
|
||||
>
|
||||
<input
|
||||
v-if="invitee"
|
||||
class="flex flex-1 px-4 py-2 bg-transparent text-secondaryLight"
|
||||
placeholder="Email"
|
||||
:name="'param' + index"
|
||||
:value="invitee.inviteeEmail"
|
||||
readonly
|
||||
<HoppSmartPlaceholder
|
||||
v-if="team && pendingInvites?.length === 0"
|
||||
text="No pending invites"
|
||||
/>
|
||||
|
||||
<div v-else class="divide-y divide-dividerLight">
|
||||
<div
|
||||
v-for="(invitee, index) in pendingInvites"
|
||||
:key="`invitee-${index}`"
|
||||
class="flex divide-x divide-dividerLight"
|
||||
>
|
||||
<input
|
||||
v-if="invitee"
|
||||
class="flex flex-1 px-4 py-2 bg-transparent text-secondaryLight"
|
||||
:placeholder="t('teams.email_title')"
|
||||
:name="'param' + index"
|
||||
:value="invitee.inviteeEmail"
|
||||
readonly
|
||||
/>
|
||||
<input
|
||||
class="flex flex-1 px-4 py-2 bg-transparent text-secondaryLight"
|
||||
:placeholder="t('teams.permission')"
|
||||
:name="'value' + index"
|
||||
:value="invitee.inviteeRole"
|
||||
readonly
|
||||
/>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('teams.remove')"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
:loading="isLoadingIndex === index"
|
||||
@click="removeInvitee(invitee.id, index)"
|
||||
/>
|
||||
<input
|
||||
class="flex flex-1 px-4 py-2 bg-transparent text-secondaryLight"
|
||||
placeholder="Permissions"
|
||||
:name="'value' + index"
|
||||
:value="invitee.inviteeRole"
|
||||
readonly
|
||||
/>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('teams.remove')"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
:loading="isLoadingIndex === index"
|
||||
@click="removeInvitee(invitee.id, index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="team && pendingInvites?.length === 0"
|
||||
text="No pending invites"
|
||||
>
|
||||
<template #body>
|
||||
<div v-if="!fetching && error" class="flex flex-col items-center p-4">
|
||||
<icon-lucide-help-circle class="mb-4 svg-icons" />
|
||||
{{ t('error.something_went_wrong') }}
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconTrash from '~icons/lucide/trash';
|
||||
import { useMutation, useClientHandle } from '@urql/vue';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useMutation } from '@urql/vue';
|
||||
import { useVModel } from '@vueuse/core';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import {
|
||||
RevokeTeamInvitationDocument,
|
||||
TeamInfoDocument,
|
||||
TeamInfoQuery,
|
||||
} from '~/helpers/backend/graphql';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import IconTrash from '~icons/lucide/trash';
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
// Get details of the team
|
||||
const fetching = ref(true);
|
||||
const error = ref(false);
|
||||
const { client } = useClientHandle();
|
||||
const route = useRoute();
|
||||
const team = ref<TeamInfoQuery['infra']['teamInfo'] | undefined>();
|
||||
const pendingInvites = ref<
|
||||
TeamInfoQuery['infra']['teamInfo']['teamInvitations'] | undefined
|
||||
>();
|
||||
const props = defineProps<{
|
||||
team: TeamInfoQuery['infra']['teamInfo'];
|
||||
}>();
|
||||
|
||||
const getTeamInfo = async () => {
|
||||
fetching.value = true;
|
||||
const result = await client
|
||||
.query(TeamInfoDocument, { teamID: route.params.id.toString() })
|
||||
.toPromise();
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:team', team: TeamInfoQuery['infra']['teamInfo']): void;
|
||||
}>();
|
||||
|
||||
if (result.error) {
|
||||
error.value = true;
|
||||
return toast.error(`${t('teams.load_info_error')}`);
|
||||
}
|
||||
|
||||
if (result.data?.infra.teamInfo) {
|
||||
team.value = result.data.infra.teamInfo;
|
||||
pendingInvites.value = team.value.teamInvitations;
|
||||
}
|
||||
fetching.value = false;
|
||||
};
|
||||
|
||||
onMounted(async () => await getTeamInfo());
|
||||
const team = useVModel(props, 'team', emit);
|
||||
const pendingInvites = computed({
|
||||
get: () => team.value?.teamInvitations,
|
||||
set: (value) => {
|
||||
team.value.teamInvitations = value;
|
||||
},
|
||||
});
|
||||
|
||||
// Remove Invitation
|
||||
const isLoadingIndex = ref<null | number>(null);
|
||||
@@ -110,7 +83,7 @@ const removeInvitee = async (id: string, index: number) => {
|
||||
isLoadingIndex.value = index;
|
||||
const result = await revokeTeamInvitation(id);
|
||||
if (result.error) {
|
||||
toast.error(`${t('state.remove_invitee_failure')}`);
|
||||
toast.error(t('state.remove_invitee_failure'));
|
||||
} else {
|
||||
if (pendingInvites.value) {
|
||||
pendingInvites.value = pendingInvites.value.filter(
|
||||
@@ -118,7 +91,7 @@ const removeInvitee = async (id: string, index: number) => {
|
||||
return invite.id !== id;
|
||||
}
|
||||
);
|
||||
toast.success(`${t('state.remove_invitee_success')}`);
|
||||
toast.success(t('state.remove_invitee_success'));
|
||||
}
|
||||
}
|
||||
isLoadingIndex.value = null;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="rounded-md">
|
||||
<div class="grid gap-6 mt-4">
|
||||
<div class="grid gap-6">
|
||||
<div
|
||||
class="relative"
|
||||
:class="
|
||||
@@ -71,18 +71,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { format } from 'date-fns';
|
||||
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
|
||||
import { UserInfoQuery } from '~/helpers/backend/graphql';
|
||||
import IconTrash from '~icons/lucide/trash';
|
||||
import IconUserCheck from '~icons/lucide/user-check';
|
||||
import IconUserMinus from '~icons/lucide/user-minus';
|
||||
|
||||
import { UserInfoQuery } from '~/helpers/backend/graphql';
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="t('users.invite_user')"
|
||||
@close="$emit('hide-modal')"
|
||||
@close="emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<HoppSmartInput
|
||||
@@ -16,7 +16,6 @@
|
||||
<span class="flex space-x-2">
|
||||
<HoppButtonPrimary
|
||||
:label="t('users.send_invite')"
|
||||
:loading="loadingState"
|
||||
@click="sendInvite"
|
||||
/>
|
||||
<HoppButtonSecondary label="Cancel" outline filled @click="hideModal" />
|
||||
@@ -27,21 +26,18 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
show: boolean;
|
||||
loadingState: boolean;
|
||||
}>(),
|
||||
{
|
||||
show: false,
|
||||
loadingState: false,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -54,7 +50,7 @@ const email = ref('');
|
||||
|
||||
const sendInvite = () => {
|
||||
if (email.value.trim() === '') {
|
||||
toast.error(`${t('users.valid_email')}`);
|
||||
toast.error(t('users.valid_email'));
|
||||
return;
|
||||
}
|
||||
emit('send-invite', email.value);
|
||||
|
||||
@@ -1,116 +1,116 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="px-4">
|
||||
<div v-if="fetching" class="flex justify-center">
|
||||
<HoppSmartSpinner />
|
||||
</div>
|
||||
|
||||
<div v-else-if="error">{{ t('shared_requests.load_list_error') }}</div>
|
||||
|
||||
<div v-else-if="sharedRequests.length === 0" class="ml-3 mt-5 text-lg">
|
||||
<div v-else-if="sharedRequests.length === 0" class="mt-5">
|
||||
{{ t('users.no_shared_requests') }}
|
||||
</div>
|
||||
|
||||
<div v-else class="mt-10">
|
||||
<HoppSmartTable :list="sharedRequests">
|
||||
<template #head>
|
||||
<tr
|
||||
class="text-secondary border-b border-dividerDark text-sm text-left bg-primaryLight"
|
||||
>
|
||||
<th class="px-6 py-2">{{ t('shared_requests.id') }}</th>
|
||||
<th class="px-6 py-2 w-30">{{ t('shared_requests.url') }}</th>
|
||||
<th class="px-6 py-2">{{ t('shared_requests.created_on') }}</th>
|
||||
<!-- Empty Heading for the Action Button -->
|
||||
<th class="px-6 py-2 text-center">Actions</th>
|
||||
</tr>
|
||||
</template>
|
||||
<template #body="{ list: sharedRequests }">
|
||||
<tr
|
||||
v-for="request in sharedRequests"
|
||||
:key="request.id"
|
||||
class="text-secondaryDark hover:bg-divider hover:cursor-pointer rounded-xl"
|
||||
>
|
||||
<td class="flex py-4 px-7 max-w-50">
|
||||
<span class="truncate">
|
||||
{{ request.id }}
|
||||
</span>
|
||||
</td>
|
||||
<HoppSmartTable v-else class="mt-8" :list="sharedRequests">
|
||||
<template #head>
|
||||
<tr
|
||||
class="text-secondary border-b border-dividerDark text-sm text-left bg-primaryLight"
|
||||
>
|
||||
<th class="px-6 py-2">{{ t('shared_requests.id') }}</th>
|
||||
<th class="px-6 py-2 w-30">{{ t('shared_requests.url') }}</th>
|
||||
<th class="px-6 py-2">{{ t('shared_requests.created_on') }}</th>
|
||||
<!-- Empty Heading for the Action Button -->
|
||||
<th class="px-6 py-2 text-center">
|
||||
{{ t('shared_requests.action') }}
|
||||
</th>
|
||||
</tr>
|
||||
</template>
|
||||
<template #body="{ list: sharedRequests }">
|
||||
<tr
|
||||
v-for="request in sharedRequests"
|
||||
:key="request.id"
|
||||
class="text-secondaryDark hover:bg-divider hover:cursor-pointer rounded-xl"
|
||||
>
|
||||
<td class="flex py-4 px-7 max-w-50">
|
||||
<span class="truncate">
|
||||
{{ request.id }}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="py-4 px-7 w-96">
|
||||
{{ sharedRequestURL(request.request) }}
|
||||
</td>
|
||||
<td class="py-4 px-7 w-96">
|
||||
{{ sharedRequestURL(request.request) }}
|
||||
</td>
|
||||
|
||||
<td class="py-2 px-7">
|
||||
{{ getCreatedDate(request.createdOn) }}
|
||||
<div class="text-gray-400 text-tiny">
|
||||
{{ getCreatedTime(request.createdOn) }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="py-2 px-7">
|
||||
{{ getCreatedDate(request.createdOn) }}
|
||||
<div class="text-gray-400 text-tiny">
|
||||
{{ getCreatedTime(request.createdOn) }}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="flex justify-center">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('shared_requests.open_request')"
|
||||
:to="`${shortcodeBaseURL}/r/${request.id}`"
|
||||
:blank="true"
|
||||
:icon="IconExternalLink"
|
||||
class="px-3 text-emerald-500 hover:text-accent"
|
||||
/>
|
||||
<td class="flex justify-center">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('shared_requests.open_request')"
|
||||
:to="`${shortcodeBaseURL}/r/${request.id}`"
|
||||
:blank="true"
|
||||
:icon="IconExternalLink"
|
||||
class="px-3 text-emerald-500 hover:text-accent"
|
||||
/>
|
||||
|
||||
<UiAutoResetIcon
|
||||
:title="t('shared_requests.copy')"
|
||||
:icon="{ default: IconCopy, temporary: IconCheck }"
|
||||
@click="copySharedRequest(request.id)"
|
||||
/>
|
||||
<UiAutoResetIcon
|
||||
:title="t('shared_requests.copy')"
|
||||
:icon="{ default: IconCopy, temporary: IconCheck }"
|
||||
@click="copySharedRequest(request.id)"
|
||||
/>
|
||||
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('shared_requests.delete')"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
class="px-3"
|
||||
@click="deleteSharedRequest(request.id)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</HoppSmartTable>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('shared_requests.delete')"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
class="px-3"
|
||||
@click="deleteSharedRequest(request.id)"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</HoppSmartTable>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div
|
||||
v-if="hasNextPage && sharedRequests.length >= sharedRequestsPerPage"
|
||||
class="flex items-center w-28 px-3 py-2 mt-5 mx-auto font-semibold text-secondaryDark bg-divider hover:bg-dividerDark rounded-3xl cursor-pointer"
|
||||
@click="fetchNextSharedRequests"
|
||||
>
|
||||
<span class="mr-2">{{ t('shared_requests.show_more') }}</span>
|
||||
<icon-lucide-chevron-down />
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
<div
|
||||
v-if="hasNextPage && sharedRequests.length >= sharedRequestsPerPage"
|
||||
class="flex items-center w-28 px-3 py-2 mt-5 mx-auto font-semibold text-secondaryDark bg-divider hover:bg-dividerDark rounded-3xl cursor-pointer"
|
||||
@click="fetchNextSharedRequests"
|
||||
>
|
||||
<span class="mr-2">{{ t('shared_requests.show_more') }}</span>
|
||||
<icon-lucide-chevron-down />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmDeletion"
|
||||
:title="t('shared_requests.confirm_request_deletion')"
|
||||
@hide-modal="confirmDeletion = false"
|
||||
@resolve="deleteSharedRequestMutation(deleteSharedRequestID)"
|
||||
/>
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmDeletion"
|
||||
:title="t('shared_requests.confirm_request_deletion')"
|
||||
@hide-modal="confirmDeletion = false"
|
||||
@resolve="deleteSharedRequestMutation(deleteSharedRequestID)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMutation } from '@urql/vue';
|
||||
import { format } from 'date-fns';
|
||||
import { ref } from 'vue';
|
||||
import { useMutation } from '@urql/vue';
|
||||
import {
|
||||
SharedRequestsDocument,
|
||||
RevokeShortcodeByAdminDocument,
|
||||
} from '../../helpers/backend/graphql';
|
||||
import { usePagedQuery } from '~/composables/usePagedQuery';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { usePagedQuery } from '~/composables/usePagedQuery';
|
||||
import { copyToClipboard } from '~/helpers/utils/clipboard';
|
||||
import IconTrash from '~icons/lucide/trash';
|
||||
import IconCopy from '~icons/lucide/copy';
|
||||
import IconCheck from '~icons/lucide/check';
|
||||
import IconCopy from '~icons/lucide/copy';
|
||||
import IconExternalLink from '~icons/lucide/external-link';
|
||||
import IconTrash from '~icons/lucide/trash';
|
||||
import {
|
||||
RevokeShortcodeByAdminDocument,
|
||||
SharedRequestsDocument,
|
||||
} from '~/helpers/backend/graphql';
|
||||
|
||||
const t = useI18n();
|
||||
const toast = useToast();
|
||||
@@ -154,7 +154,7 @@ const shortcodeBaseURL =
|
||||
// Copy Shared Request to Clipboard
|
||||
const copySharedRequest = (requestID: string) => {
|
||||
copyToClipboard(`${shortcodeBaseURL}/r/${requestID}`);
|
||||
toast.success(`${t('state.copied_to_clipboard')}`);
|
||||
toast.success(t('state.copied_to_clipboard'));
|
||||
};
|
||||
|
||||
// Shared Request Deletion
|
||||
@@ -170,19 +170,19 @@ const deleteSharedRequest = (id: string) => {
|
||||
const deleteSharedRequestMutation = async (id: string | null) => {
|
||||
if (!id) {
|
||||
confirmDeletion.value = false;
|
||||
toast.error(`${t('state.delete_request_failure')}`);
|
||||
toast.error(t('state.delete_request_failure'));
|
||||
return;
|
||||
}
|
||||
const variables = { codeID: id };
|
||||
await sharedRequestDeletion.executeMutation(variables).then((result) => {
|
||||
if (result.error) {
|
||||
toast.error(`${t('state.delete_request_failure')}`);
|
||||
toast.error(t('state.delete_request_failure'));
|
||||
} else {
|
||||
sharedRequests.value = sharedRequests.value.filter(
|
||||
(request) => request.id !== id
|
||||
);
|
||||
refetch();
|
||||
toast.success(`${t('state.delete_request_success')}`);
|
||||
toast.success(t('state.delete_request_success'));
|
||||
}
|
||||
});
|
||||
confirmDeletion.value = false;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TypedDocumentNode, useClientHandle } from '@urql/vue';
|
||||
import { DocumentNode } from 'graphql';
|
||||
import { ref } from 'vue';
|
||||
import { Ref, ref } from 'vue';
|
||||
|
||||
/** A composable function to handle grapqhl requests
|
||||
* using urql's useClientHandle
|
||||
@@ -14,15 +14,16 @@ export function useClientHandler<
|
||||
ListItem
|
||||
>(
|
||||
query: string | TypedDocumentNode<Result, Vars> | DocumentNode,
|
||||
getList: (result: Result) => ListItem[],
|
||||
variables: Vars
|
||||
variables: Vars,
|
||||
getList?: (result: Result) => ListItem[]
|
||||
) {
|
||||
const { client } = useClientHandle();
|
||||
const fetching = ref(true);
|
||||
const error = ref(false);
|
||||
const list = ref<ListItem[]>([]);
|
||||
const data = ref<Result>();
|
||||
const dataAsList: Ref<ListItem[]> = ref([]);
|
||||
|
||||
const fetchList = async () => {
|
||||
const fetchData = async () => {
|
||||
fetching.value = true;
|
||||
try {
|
||||
const result = await client
|
||||
@@ -31,9 +32,12 @@ export function useClientHandler<
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
const resultList = getList(result.data!);
|
||||
|
||||
list.value.push(...resultList);
|
||||
if (getList) {
|
||||
const resultList = getList(result.data!);
|
||||
dataAsList.value.push(...resultList);
|
||||
} else {
|
||||
data.value = result.data;
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = true;
|
||||
}
|
||||
@@ -43,7 +47,8 @@ export function useClientHandler<
|
||||
return {
|
||||
fetching,
|
||||
error,
|
||||
list,
|
||||
fetchList,
|
||||
data,
|
||||
dataAsList,
|
||||
fetchData,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,31 +72,35 @@ export function useConfigHandler(updatedConfigs?: Config) {
|
||||
const {
|
||||
fetching: fetchingInfraConfigs,
|
||||
error: infraConfigsError,
|
||||
list: infraConfigs,
|
||||
fetchList: fetchInfraConfigs,
|
||||
} = useClientHandler(InfraConfigsDocument, (x) => x.infraConfigs, {
|
||||
configNames: [
|
||||
'GOOGLE_CLIENT_ID',
|
||||
'GOOGLE_CLIENT_SECRET',
|
||||
'MICROSOFT_CLIENT_ID',
|
||||
'MICROSOFT_CLIENT_SECRET',
|
||||
'GITHUB_CLIENT_ID',
|
||||
'GITHUB_CLIENT_SECRET',
|
||||
'MAILER_SMTP_URL',
|
||||
'MAILER_ADDRESS_FROM',
|
||||
] as InfraConfigEnum[],
|
||||
});
|
||||
dataAsList: infraConfigs,
|
||||
fetchData: fetchInfraConfigs,
|
||||
} = useClientHandler(
|
||||
InfraConfigsDocument,
|
||||
{
|
||||
configNames: [
|
||||
'GOOGLE_CLIENT_ID',
|
||||
'GOOGLE_CLIENT_SECRET',
|
||||
'MICROSOFT_CLIENT_ID',
|
||||
'MICROSOFT_CLIENT_SECRET',
|
||||
'GITHUB_CLIENT_ID',
|
||||
'GITHUB_CLIENT_SECRET',
|
||||
'MAILER_SMTP_URL',
|
||||
'MAILER_ADDRESS_FROM',
|
||||
] as InfraConfigEnum[],
|
||||
},
|
||||
(x) => x.infraConfigs
|
||||
);
|
||||
|
||||
// Fetching allowed auth providers
|
||||
const {
|
||||
fetching: fetchingAllowedAuthProviders,
|
||||
error: allowedAuthProvidersError,
|
||||
list: allowedAuthProviders,
|
||||
fetchList: fetchAllowedAuthProviders,
|
||||
dataAsList: allowedAuthProviders,
|
||||
fetchData: fetchAllowedAuthProviders,
|
||||
} = useClientHandler(
|
||||
AllowedAuthProvidersDocument,
|
||||
(x) => x.allowedAuthProviders,
|
||||
{}
|
||||
{},
|
||||
(x) => x.allowedAuthProviders
|
||||
);
|
||||
|
||||
// Current and working configs
|
||||
@@ -255,7 +259,21 @@ export function useConfigHandler(updatedConfigs?: Config) {
|
||||
return config;
|
||||
});
|
||||
|
||||
// Trasforming the working configs back into the format required by the mutations
|
||||
// Checking if any of the config fields are empty
|
||||
const isFieldEmpty = (field: string) => field.trim() === '';
|
||||
|
||||
const AreAnyConfigFieldsEmpty = (config: Config): boolean => {
|
||||
const providerFieldsEmpty = Object.values(config.providers).some(
|
||||
(provider) => Object.values(provider.fields).some(isFieldEmpty)
|
||||
);
|
||||
const mailFieldsEmpty = Object.values(config.mailConfigs.fields).some(
|
||||
isFieldEmpty
|
||||
);
|
||||
|
||||
return providerFieldsEmpty || mailFieldsEmpty;
|
||||
};
|
||||
|
||||
// Transforming the working configs back into the format required by the mutations
|
||||
const updatedAllowedAuthProviders = computed(() => {
|
||||
return [
|
||||
{
|
||||
@@ -341,5 +359,6 @@ export function useConfigHandler(updatedConfigs?: Config) {
|
||||
fetchingAllowedAuthProviders,
|
||||
infraConfigsError,
|
||||
allowedAuthProvidersError,
|
||||
AreAnyConfigFieldsEmpty,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -16,15 +16,15 @@
|
||||
{{ t('configs.load_error') }}
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col py-8">
|
||||
<HoppSmartTabs v-model="selectedOptionTab" render-inactive-tabs>
|
||||
<HoppSmartTab :id="'config'" :label="t('configs.title')">
|
||||
<SettingsConfigurations
|
||||
v-model:config="workingConfigs"
|
||||
class="py-8 px-4"
|
||||
/>
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
<div v-else-if="workingConfigs" class="flex flex-col py-8">
|
||||
<HoppSmartTabs v-model="selectedOptionTab" render-inactive-tabs>
|
||||
<HoppSmartTab :id="'config'" :label="t('configs.title')">
|
||||
<SettingsConfigurations
|
||||
v-model:config="workingConfigs"
|
||||
class="py-8 px-4"
|
||||
/>
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</div>
|
||||
|
||||
<div v-if="isConfigUpdated" class="fixed bottom-0 right-0 m-10">
|
||||
@@ -43,7 +43,7 @@
|
||||
:show="showSaveChangesModal"
|
||||
:title="t('configs.confirm_changes')"
|
||||
@hide-modal="showSaveChangesModal = false"
|
||||
@resolve="initiateServerRestart = true"
|
||||
@resolve="restartServer"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -51,9 +51,11 @@
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { useConfigHandler } from '~/composables/useConfigHandler';
|
||||
|
||||
const t = useI18n();
|
||||
const toast = useToast();
|
||||
|
||||
const showSaveChangesModal = ref(false);
|
||||
const initiateServerRestart = ref(false);
|
||||
@@ -70,6 +72,7 @@ const {
|
||||
infraConfigsError,
|
||||
fetchingAllowedAuthProviders,
|
||||
allowedAuthProvidersError,
|
||||
AreAnyConfigFieldsEmpty,
|
||||
} = useConfigHandler();
|
||||
|
||||
// Check if the configs have been updated
|
||||
@@ -78,4 +81,17 @@ const isConfigUpdated = computed(() =>
|
||||
? !isEqual(currentConfigs.value, workingConfigs.value)
|
||||
: false
|
||||
);
|
||||
|
||||
// Check if any of the fields in workingConfigs are empty
|
||||
const areAnyFieldsEmpty = computed(() =>
|
||||
workingConfigs.value ? AreAnyConfigFieldsEmpty(workingConfigs.value) : false
|
||||
);
|
||||
|
||||
const restartServer = () => {
|
||||
if (areAnyFieldsEmpty.value) {
|
||||
return toast.error(t('configs.input_empty'));
|
||||
}
|
||||
initiateServerRestart.value = true;
|
||||
showSaveChangesModal.value = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
<HoppSmartSpinner />
|
||||
</div>
|
||||
|
||||
<div v-if="team" class="flex flex-col">
|
||||
<div v-else-if="error">{{ t('teams.load_info_error') }}</div>
|
||||
|
||||
<div v-else-if="team" class="flex flex-col">
|
||||
<div class="flex items-center space-x-4">
|
||||
<button
|
||||
class="p-2 rounded-3xl bg-divider hover:bg-dividerDark transition flex justify-center items-center"
|
||||
@@ -27,19 +29,16 @@
|
||||
<HoppSmartTabs v-model="selectedOptionTab" render-inactive-tabs>
|
||||
<HoppSmartTab :id="'details'" :label="t('teams.details')">
|
||||
<TeamsDetails
|
||||
:team="team"
|
||||
:teamName="teamName"
|
||||
v-model:showRenameInput="showRenameInput"
|
||||
@rename-team="renameTeamName"
|
||||
v-model:team="team"
|
||||
@delete-team="deleteTeam"
|
||||
class="py-8 px-4"
|
||||
/>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab :id="'members'" :label="t('teams.team_members')">
|
||||
<TeamsMembers @update-team="updateTeam()" class="py-8 px-4" />
|
||||
<TeamsMembers v-model:team="team" class="py-8 px-4" />
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab :id="'invites'" :label="t('teams.invites')">
|
||||
<TeamsPendingInvites :editingTeamID="team.id" />
|
||||
<TeamsPendingInvites v-model:team="team" />
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
|
||||
@@ -55,24 +54,24 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useClientHandle, useMutation } from '@urql/vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useMutation } from '@urql/vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { useClientHandler } from '~/composables/useClientHandler';
|
||||
import {
|
||||
RemoveTeamDocument,
|
||||
RenameTeamDocument,
|
||||
TeamInfoDocument,
|
||||
TeamMemberRole,
|
||||
TeamInfoQuery,
|
||||
} from '../../helpers/backend/graphql';
|
||||
import { HoppSmartTabs } from '@hoppscotch/ui';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const toast = useToast();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
// Tabs
|
||||
type OptionTabs = 'details' | 'members' | 'invites';
|
||||
|
||||
const selectedOptionTab = ref<OptionTabs>('details');
|
||||
@@ -90,59 +89,24 @@ const currentTabName = computed(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get the details of the team
|
||||
// Get Team Info
|
||||
const {
|
||||
fetching,
|
||||
error,
|
||||
data: teamInfo,
|
||||
fetchData: getTeamInfo,
|
||||
} = useClientHandler(TeamInfoDocument, {
|
||||
teamID: route.params.id.toString(),
|
||||
});
|
||||
|
||||
const team = ref<TeamInfoQuery['infra']['teamInfo'] | undefined>();
|
||||
const teamName = ref('');
|
||||
const route = useRoute();
|
||||
const fetching = ref(true);
|
||||
const { client } = useClientHandle();
|
||||
|
||||
const getTeamInfo = async () => {
|
||||
fetching.value = true;
|
||||
const result = await client
|
||||
.query(TeamInfoDocument, { teamID: route.params.id.toString() })
|
||||
.toPromise();
|
||||
if (result.error) {
|
||||
return toast.error(`${t('team.load_info_error')}`);
|
||||
}
|
||||
if (result.data?.infra.teamInfo) {
|
||||
team.value = result.data.infra.teamInfo;
|
||||
teamName.value = team.value.name;
|
||||
}
|
||||
fetching.value = false;
|
||||
};
|
||||
|
||||
onMounted(async () => await getTeamInfo());
|
||||
|
||||
const updateTeam = async () => await getTeamInfo();
|
||||
|
||||
// Rename the team name
|
||||
const showRenameInput = ref(false);
|
||||
const teamRename = useMutation(RenameTeamDocument);
|
||||
|
||||
const renameTeamName = async (teamName: string) => {
|
||||
if (!team.value) return;
|
||||
|
||||
if (team.value.name === teamName) {
|
||||
showRenameInput.value = false;
|
||||
return;
|
||||
}
|
||||
const variables = { uid: team.value.id, name: teamName };
|
||||
await teamRename.executeMutation(variables).then((result) => {
|
||||
if (result.error) {
|
||||
toast.error(`${t('state.rename_team_failure')}`);
|
||||
} else {
|
||||
showRenameInput.value = false;
|
||||
if (team.value) {
|
||||
team.value.name = teamName;
|
||||
toast.success(`${t('state.rename_team_success')}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
onMounted(async () => {
|
||||
await getTeamInfo();
|
||||
team.value = teamInfo.value?.infra.teamInfo;
|
||||
});
|
||||
|
||||
// Delete team from the infra
|
||||
const router = useRouter();
|
||||
const confirmDeletion = ref(false);
|
||||
const teamDeletion = useMutation(RemoveTeamDocument);
|
||||
const deleteTeamUID = ref<string | null>(null);
|
||||
@@ -155,42 +119,18 @@ const deleteTeam = (id: string) => {
|
||||
const deleteTeamMutation = async (id: string | null) => {
|
||||
if (!id) {
|
||||
confirmDeletion.value = false;
|
||||
toast.error(`${t('state.delete_team_failure')}`);
|
||||
toast.error(t('state.delete_team_failure'));
|
||||
return;
|
||||
}
|
||||
const variables = { uid: id };
|
||||
await teamDeletion.executeMutation(variables).then((result) => {
|
||||
if (result.error) {
|
||||
toast.error(`${t('state.delete_team_failure')}`);
|
||||
} else {
|
||||
toast.success(`${t('state.delete_team_success')}`);
|
||||
}
|
||||
});
|
||||
const result = await teamDeletion.executeMutation(variables);
|
||||
if (result.error) {
|
||||
toast.error(t('state.delete_team_failure'));
|
||||
} else {
|
||||
toast.success(t('state.delete_team_success'));
|
||||
}
|
||||
confirmDeletion.value = false;
|
||||
deleteTeamUID.value = null;
|
||||
router.push('/teams');
|
||||
};
|
||||
|
||||
// Update Roles of Members
|
||||
const roleUpdates = ref<
|
||||
{
|
||||
userID: string;
|
||||
role: TeamMemberRole;
|
||||
}[]
|
||||
>([]);
|
||||
|
||||
watch(
|
||||
() => team.value,
|
||||
(teamDetails) => {
|
||||
const members = teamDetails?.teamMembers ?? [];
|
||||
|
||||
// Remove deleted members
|
||||
roleUpdates.value = roleUpdates.value.filter(
|
||||
(update) =>
|
||||
members.findIndex(
|
||||
(y: { user: { uid: string } }) => y.user.uid === update.userID
|
||||
) !== -1
|
||||
);
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -122,7 +122,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useMutation, useQuery } from '@urql/vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { usePagedQuery } from '~/composables/usePagedQuery';
|
||||
import IconMoreHorizontal from '~icons/lucide/more-horizontal';
|
||||
import IconAddUsers from '~icons/lucide/plus';
|
||||
import IconTrash from '~icons/lucide/trash';
|
||||
import {
|
||||
CreateTeamDocument,
|
||||
MetricsDocument,
|
||||
@@ -130,18 +138,10 @@ import {
|
||||
TeamListDocument,
|
||||
UsersListDocument,
|
||||
} from '../../helpers/backend/graphql';
|
||||
import { usePagedQuery } from '~/composables/usePagedQuery';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useMutation, useQuery } from '@urql/vue';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import IconAddUsers from '~icons/lucide/plus';
|
||||
import IconTrash from '~icons/lucide/trash';
|
||||
import IconMoreHorizontal from '~icons/lucide/more-horizontal';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
// Get Users List
|
||||
const { data } = useQuery({ query: MetricsDocument });
|
||||
const usersPerPage = computed(() => data.value?.infra.usersCount || 10000);
|
||||
@@ -174,55 +174,50 @@ const {
|
||||
);
|
||||
|
||||
// Create Team
|
||||
const createTeamMutation = useMutation(CreateTeamDocument);
|
||||
const showCreateTeamModal = ref(false);
|
||||
const createTeamLoading = ref(false);
|
||||
const createTeamMutation = useMutation(CreateTeamDocument);
|
||||
|
||||
const createTeam = async (newTeamName: string, ownerEmail: string) => {
|
||||
if (newTeamName.length < 6) {
|
||||
toast.error(`${t('state.team_name_long')}`);
|
||||
toast.error(t('state.team_name_too_short'));
|
||||
return;
|
||||
}
|
||||
if (ownerEmail.length === 0) {
|
||||
toast.error(`${t('state.enter_team_email')}`);
|
||||
toast.error(t('state.enter_team_email'));
|
||||
return;
|
||||
}
|
||||
|
||||
createTeamLoading.value = true;
|
||||
const userUid =
|
||||
usersList.value.find((user) => user.email === ownerEmail)?.uid || '';
|
||||
|
||||
const variables = { name: newTeamName.trim(), userUid: userUid };
|
||||
await createTeamMutation.executeMutation(variables).then((result) => {
|
||||
if (result.error) {
|
||||
if (result.error.toString() == '[GraphQL] user/not_found') {
|
||||
toast.error(`${t('state.user_not_found')}`);
|
||||
} else {
|
||||
toast.error(`${t('state.create_team_failure')}`);
|
||||
}
|
||||
createTeamLoading.value = false;
|
||||
|
||||
const result = await createTeamMutation.executeMutation(variables);
|
||||
if (result.error) {
|
||||
if (result.error.toString() == '[GraphQL] user/not_found') {
|
||||
toast.error(t('state.user_not_found'));
|
||||
} else {
|
||||
toast.success(`${t('state.create_team_success')}`);
|
||||
showCreateTeamModal.value = false;
|
||||
createTeamLoading.value = false;
|
||||
refetch();
|
||||
toast.error(t('state.create_team_failure'));
|
||||
}
|
||||
});
|
||||
createTeamLoading.value = false;
|
||||
} else {
|
||||
toast.success(t('state.create_team_success'));
|
||||
showCreateTeamModal.value = false;
|
||||
createTeamLoading.value = false;
|
||||
refetch();
|
||||
}
|
||||
};
|
||||
|
||||
// Go To Individual Team Details Page
|
||||
const router = useRouter();
|
||||
const goToTeamDetails = (teamId: string) => router.push('/teams/' + teamId);
|
||||
|
||||
// Reload Teams Page when routed back to the teams page
|
||||
const route = useRoute();
|
||||
watch(
|
||||
() => route.params.id,
|
||||
() => window.location.reload()
|
||||
);
|
||||
|
||||
// Team Deletion
|
||||
const teamDeletion = useMutation(RemoveTeamDocument);
|
||||
const confirmDeletion = ref(false);
|
||||
const deleteTeamID = ref<string | null>(null);
|
||||
const teamDeletion = useMutation(RemoveTeamDocument);
|
||||
|
||||
const deleteTeam = (id: string) => {
|
||||
confirmDeletion.value = true;
|
||||
@@ -232,20 +227,19 @@ const deleteTeam = (id: string) => {
|
||||
const deleteTeamMutation = async (id: string | null) => {
|
||||
if (!id) {
|
||||
confirmDeletion.value = false;
|
||||
toast.error(`${t('state.delete_team_failure')}`);
|
||||
toast.error(t('state.delete_team_failure'));
|
||||
return;
|
||||
}
|
||||
const variables = { uid: id };
|
||||
await teamDeletion.executeMutation(variables).then((result) => {
|
||||
if (result.error) {
|
||||
toast.error(`${t('state.delete_team_failure')}`);
|
||||
} else {
|
||||
teamsList.value = teamsList.value.filter((team) => team.id !== id);
|
||||
toast.success(`${t('state.delete_team_success')}`);
|
||||
}
|
||||
});
|
||||
const result = await teamDeletion.executeMutation(variables);
|
||||
if (result.error) {
|
||||
toast.error(t('state.delete_team_failure'));
|
||||
} else {
|
||||
teamsList.value = teamsList.value.filter((team) => team.id !== id);
|
||||
toast.success(t('state.delete_team_success'));
|
||||
}
|
||||
|
||||
confirmDeletion.value = false;
|
||||
deleteTeamID.value = null;
|
||||
router.push('/teams');
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div v-if="fetching" class="flex justify-center"><HoppSmartSpinner /></div>
|
||||
<div v-else class="flex flex-col space-y-4">
|
||||
<div v-else-if="error">{{ t('users.load_info_error') }}</div>
|
||||
<div v-else-if="user" class="flex flex-col space-y-4">
|
||||
<div class="flex gap-x-3">
|
||||
<button
|
||||
class="p-2 mb-2 rounded-3xl bg-divider"
|
||||
@@ -32,7 +33,7 @@
|
||||
/>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab :id="'requests'" :label="t('shared_requests.title')">
|
||||
<UsersSharedRequests :email="user.email" class="py-8 px-4 mt-10" />
|
||||
<UsersSharedRequests :email="user.email" />
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</div>
|
||||
@@ -59,21 +60,20 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, computed } from 'vue';
|
||||
import { useMutation } from '@urql/vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { useClientHandler } from '~/composables/useClientHandler';
|
||||
import {
|
||||
MakeUserAdminDocument,
|
||||
UserInfoDocument,
|
||||
RemoveUserByAdminDocument,
|
||||
RemoveUserAsAdminDocument,
|
||||
RemoveUserByAdminDocument,
|
||||
UserInfoDocument,
|
||||
} from '~/helpers/backend/graphql';
|
||||
import { useClientHandle } from '@urql/vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
// Tabs
|
||||
@@ -91,23 +91,26 @@ const currentTabName = computed(() => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get User Info
|
||||
const user = ref();
|
||||
const { client } = useClientHandle();
|
||||
const fetching = ref(true);
|
||||
const route = useRoute();
|
||||
|
||||
onMounted(async () => {
|
||||
fetching.value = true;
|
||||
const result = await client
|
||||
.query(UserInfoDocument, { uid: route.params.id.toString() })
|
||||
.toPromise();
|
||||
|
||||
if (result.error) {
|
||||
toast.error(`${t('users.load_info_error')}`);
|
||||
const { fetching, error, data, fetchData } = useClientHandler(
|
||||
UserInfoDocument,
|
||||
{
|
||||
uid: route.params.id.toString(),
|
||||
}
|
||||
user.value = result.data?.infra.userInfo ?? {};
|
||||
fetching.value = false;
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData();
|
||||
});
|
||||
|
||||
const user = computed({
|
||||
get: () => data.value?.infra.userInfo,
|
||||
set: (value) => {
|
||||
if (value) {
|
||||
data.value!.infra.userInfo = value;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// User Deletion
|
||||
@@ -124,17 +127,18 @@ const deleteUser = (id: string) => {
|
||||
const deleteUserMutation = async (id: string | null) => {
|
||||
if (!id) {
|
||||
confirmDeletion.value = false;
|
||||
toast.error(`${t('state.delete_user_failure')}`);
|
||||
toast.error(t('state.delete_user_failure'));
|
||||
return;
|
||||
}
|
||||
const variables = { uid: id };
|
||||
await userDeletion.executeMutation(variables).then((result) => {
|
||||
if (result.error) {
|
||||
toast.error(`${t('state.delete_user_failure')}`);
|
||||
} else {
|
||||
toast.success(`${t('state.delete_user_success')}`);
|
||||
}
|
||||
});
|
||||
const result = await userDeletion.executeMutation(variables);
|
||||
|
||||
if (result.error) {
|
||||
toast.error(t('state.delete_user_failure'));
|
||||
} else {
|
||||
toast.success(t('state.delete_user_success'));
|
||||
}
|
||||
|
||||
confirmDeletion.value = false;
|
||||
deleteUserUID.value = null;
|
||||
router.push('/users');
|
||||
@@ -153,18 +157,17 @@ const makeUserAdmin = (id: string) => {
|
||||
const makeUserAdminMutation = async (id: string | null) => {
|
||||
if (!id) {
|
||||
confirmUserToAdmin.value = false;
|
||||
toast.error(`${t('state.admin_failure')}`);
|
||||
toast.error(t('state.admin_failure'));
|
||||
return;
|
||||
}
|
||||
const variables = { uid: id };
|
||||
await userToAdmin.executeMutation(variables).then((result) => {
|
||||
if (result.error) {
|
||||
toast.error(`${t('state.admin_failure')}`);
|
||||
} else {
|
||||
user.value.isAdmin = true;
|
||||
toast.success(`${t('state.admin_success')}`);
|
||||
}
|
||||
});
|
||||
const result = await userToAdmin.executeMutation(variables);
|
||||
if (result.error) {
|
||||
toast.error(t('state.admin_failure'));
|
||||
} else {
|
||||
user.value!.isAdmin = true;
|
||||
toast.success(t('state.admin_success'));
|
||||
}
|
||||
confirmUserToAdmin.value = false;
|
||||
userToAdminUID.value = null;
|
||||
};
|
||||
@@ -182,18 +185,17 @@ const makeAdminToUser = (id: string) => {
|
||||
const makeAdminToUserMutation = async (id: string | null) => {
|
||||
if (!id) {
|
||||
confirmAdminToUser.value = false;
|
||||
toast.error(`${t('state.remove_admin_failure')}`);
|
||||
toast.error(t('state.remove_admin_failure'));
|
||||
return;
|
||||
}
|
||||
const variables = { uid: id };
|
||||
await adminToUser.executeMutation(variables).then((result) => {
|
||||
if (result.error) {
|
||||
toast.error(`${t('state.remove_admin_failure')}`);
|
||||
} else {
|
||||
user.value.isAdmin = false;
|
||||
toast.error(`${t('state.remove_admin_success')}`);
|
||||
}
|
||||
});
|
||||
const result = await adminToUser.executeMutation(variables);
|
||||
if (result.error) {
|
||||
toast.error(t('state.remove_admin_failure'));
|
||||
} else {
|
||||
user.value!.isAdmin = false;
|
||||
toast.error(t('state.remove_admin_success'));
|
||||
}
|
||||
confirmAdminToUser.value = false;
|
||||
adminToUserUID.value = null;
|
||||
};
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
@click="showInviteUserModal = true"
|
||||
:icon="IconAddUser"
|
||||
/>
|
||||
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
outline
|
||||
@@ -173,35 +172,33 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { format } from 'date-fns';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useMutation } from '@urql/vue';
|
||||
import { format } from 'date-fns';
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { usePagedQuery } from '~/composables/usePagedQuery';
|
||||
import IconMoreHorizontal from '~icons/lucide/more-horizontal';
|
||||
import IconTrash from '~icons/lucide/trash';
|
||||
import IconUserCheck from '~icons/lucide/user-check';
|
||||
import IconUserMinus from '~icons/lucide/user-minus';
|
||||
import IconAddUser from '~icons/lucide/user-plus';
|
||||
import {
|
||||
InviteNewUserDocument,
|
||||
MakeUserAdminDocument,
|
||||
RemoveUserByAdminDocument,
|
||||
RemoveUserAsAdminDocument,
|
||||
RemoveUserByAdminDocument,
|
||||
UsersListDocument,
|
||||
} from '../../helpers/backend/graphql';
|
||||
import { usePagedQuery } from '~/composables/usePagedQuery';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useToast } from '~/composables/toast';
|
||||
import { HoppButtonSecondary } from '@hoppscotch/ui';
|
||||
import IconAddUser from '~icons/lucide/user-plus';
|
||||
import IconTrash from '~icons/lucide/trash';
|
||||
import IconUserMinus from '~icons/lucide/user-minus';
|
||||
import IconUserCheck from '~icons/lucide/user-check';
|
||||
import IconMoreHorizontal from '~icons/lucide/more-horizontal';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
} from '~/helpers/backend/graphql';
|
||||
|
||||
// Get Proper Date Formats
|
||||
const t = useI18n();
|
||||
const toast = useToast();
|
||||
|
||||
const getCreatedDate = (date: string) => format(new Date(date), 'dd-MM-yyyy');
|
||||
const getCreatedTime = (date: string) => format(new Date(date), 'hh:mm a');
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
// Get Paginated Results of all the users in the infra
|
||||
const usersPerPage = 20;
|
||||
const {
|
||||
@@ -224,30 +221,23 @@ const showInviteUserModal = ref(false);
|
||||
|
||||
const sendInvite = async (email: string) => {
|
||||
if (!email.trim()) {
|
||||
toast.error(`${t('state.invalid_email')}`);
|
||||
toast.error(t('state.invalid_email'));
|
||||
return;
|
||||
}
|
||||
const variables = { inviteeEmail: email.trim() };
|
||||
await sendInvitation.executeMutation(variables).then((result) => {
|
||||
if (result.error) {
|
||||
toast.error(`${t('state.email_failure')}`);
|
||||
} else {
|
||||
toast.success(`${t('state.email_success')}`);
|
||||
showInviteUserModal.value = false;
|
||||
}
|
||||
});
|
||||
const result = await sendInvitation.executeMutation(variables);
|
||||
if (result.error) {
|
||||
toast.error(t('state.email_failure'));
|
||||
} else {
|
||||
toast.success(t('state.email_success'));
|
||||
showInviteUserModal.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Go to Individual User Details Page
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const goToUserDetails = (uid: string) => router.push('/users/' + uid);
|
||||
|
||||
watch(
|
||||
() => route.params.id,
|
||||
() => window.location.reload()
|
||||
);
|
||||
|
||||
// User Deletion
|
||||
const userDeletion = useMutation(RemoveUserByAdminDocument);
|
||||
const confirmDeletion = ref(false);
|
||||
@@ -256,18 +246,17 @@ const deleteUserUID = ref<string | null>(null);
|
||||
const deleteUserMutation = async (id: string | null) => {
|
||||
if (!id) {
|
||||
confirmDeletion.value = false;
|
||||
toast.error(`${t('state.delete_user_failure')}`);
|
||||
toast.error(t('state.delete_user_failure'));
|
||||
return;
|
||||
}
|
||||
const variables = { uid: id };
|
||||
await userDeletion.executeMutation(variables).then((result) => {
|
||||
if (result.error) {
|
||||
toast.error(`${t('state.delete_user_failure')}`);
|
||||
} else {
|
||||
toast.success(`${t('state.delete_user_success')}`);
|
||||
usersList.value = usersList.value.filter((user) => user.uid !== id);
|
||||
}
|
||||
});
|
||||
const result = await userDeletion.executeMutation(variables);
|
||||
if (result.error) {
|
||||
toast.error(t('state.delete_user_failure'));
|
||||
} else {
|
||||
toast.success(t('state.delete_user_success'));
|
||||
usersList.value = usersList.value.filter((user) => user.uid !== id);
|
||||
}
|
||||
confirmDeletion.value = false;
|
||||
deleteUserUID.value = null;
|
||||
};
|
||||
@@ -285,23 +274,20 @@ const makeUserAdmin = (id: string) => {
|
||||
const makeUserAdminMutation = async (id: string | null) => {
|
||||
if (!id) {
|
||||
confirmUserToAdmin.value = false;
|
||||
toast.error(`${t('state.admin_failure')}`);
|
||||
toast.error(t('state.admin_failure'));
|
||||
return;
|
||||
}
|
||||
const variables = { uid: id };
|
||||
await userToAdmin.executeMutation(variables).then((result) => {
|
||||
if (result.error) {
|
||||
toast.error(`${t('state.admin_failure')}`);
|
||||
} else {
|
||||
toast.success(`${t('state.admin_success')}`);
|
||||
usersList.value = usersList.value.map((user) => {
|
||||
if (user.uid === id) {
|
||||
user.isAdmin = true;
|
||||
}
|
||||
return user;
|
||||
});
|
||||
}
|
||||
});
|
||||
const result = await userToAdmin.executeMutation(variables);
|
||||
if (result.error) {
|
||||
toast.error(t('state.admin_failure'));
|
||||
} else {
|
||||
toast.success(t('state.admin_success'));
|
||||
usersList.value = usersList.value.map((user) => ({
|
||||
...user,
|
||||
isAdmin: user.uid === id ? true : user.isAdmin,
|
||||
}));
|
||||
}
|
||||
confirmUserToAdmin.value = false;
|
||||
userToAdminUID.value = null;
|
||||
};
|
||||
@@ -324,23 +310,20 @@ const deleteUser = (id: string) => {
|
||||
const makeAdminToUserMutation = async (id: string | null) => {
|
||||
if (!id) {
|
||||
confirmAdminToUser.value = false;
|
||||
toast.error(`${t('state.remove_admin_failure')}`);
|
||||
toast.error(t('state.remove_admin_failure'));
|
||||
return;
|
||||
}
|
||||
const variables = { uid: id };
|
||||
await adminToUser.executeMutation(variables).then((result) => {
|
||||
if (result.error) {
|
||||
toast.error(`${t('state.remove_admin_failure')}`);
|
||||
} else {
|
||||
toast.success(`${t('state.remove_admin_success')}`);
|
||||
usersList.value = usersList.value.map((user) => {
|
||||
if (user.uid === id) {
|
||||
user.isAdmin = false;
|
||||
}
|
||||
return user;
|
||||
});
|
||||
}
|
||||
});
|
||||
const result = await adminToUser.executeMutation(variables);
|
||||
if (result.error) {
|
||||
toast.error(t('state.remove_admin_failure'));
|
||||
} else {
|
||||
toast.success(t('state.remove_admin_success'));
|
||||
usersList.value = usersList.value.map((user) => ({
|
||||
...user,
|
||||
isAdmin: user.uid === id ? false : user.isAdmin,
|
||||
}));
|
||||
}
|
||||
confirmAdminToUser.value = false;
|
||||
adminToUserUID.value = null;
|
||||
};
|
||||
|
||||
@@ -46,16 +46,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useQuery } from '@urql/vue';
|
||||
import { InvitedUsersDocument } from '../../helpers/backend/graphql';
|
||||
import { format } from 'date-fns';
|
||||
import { HoppSmartSpinner } from '@hoppscotch/ui';
|
||||
import { computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from '~/composables/i18n';
|
||||
import { InvitedUsersDocument } from '~/helpers/backend/graphql';
|
||||
|
||||
const t = useI18n();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Get Proper Date Formats
|
||||
|
||||
13992
pnpm-lock.yaml
generated
13992
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user