Compare commits

..

1 Commits

Author SHA1 Message Date
nivedin
00588bcc0a fix: interceptor error from extension issue 2023-11-14 17:17:07 +05:30
389 changed files with 10130 additions and 18827 deletions

View File

@@ -12,8 +12,8 @@ SESSION_SECRET='add some secret here'
# Hoppscotch App Domain Config # Hoppscotch App Domain Config
REDIRECT_URL="http://localhost:3000" REDIRECT_URL="http://localhost:3000"
WHITELISTED_ORIGINS="http://localhost:3170,http://localhost:3000,http://localhost:3100" WHITELISTED_ORIGINS = "http://localhost:3170,http://localhost:3000,http://localhost:3100"
VITE_ALLOWED_AUTH_PROVIDERS=GOOGLE,GITHUB,MICROSOFT,EMAIL VITE_ALLOWED_AUTH_PROVIDERS = GOOGLE,GITHUB,MICROSOFT,EMAIL
# Google Auth Config # Google Auth Config
GOOGLE_CLIENT_ID="************************************************" GOOGLE_CLIENT_ID="************************************************"
@@ -59,6 +59,3 @@ VITE_BACKEND_API_URL=http://localhost:3170/v1
# Terms Of Service And Privacy Policy Links (Optional) # Terms Of Service And Privacy Policy Links (Optional)
VITE_APP_TOS_LINK=https://docs.hoppscotch.io/support/terms VITE_APP_TOS_LINK=https://docs.hoppscotch.io/support/terms
VITE_APP_PRIVACY_POLICY_LINK=https://docs.hoppscotch.io/support/privacy VITE_APP_PRIVACY_POLICY_LINK=https://docs.hoppscotch.io/support/privacy
# Set to `true` for subpath based access
ENABLE_SUBPATH_BASED_ACCESS=false

14
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,14 @@
{
"recommendations": [
"antfu.iconify",
"vue.volar",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"csstools.postcss",
"folke.vscode-monorepo-workspace"
],
"unwantedRecommendations": [
"octref.vetur"
]
}

View File

@@ -1,19 +0,0 @@
:3000 {
try_files {path} /
root * /site/selfhost-web
file_server
}
:3100 {
try_files {path} /
root * /site/sh-admin-multiport-setup
file_server
}
:3170 {
reverse_proxy localhost:8080
}
:80 {
respond 404
}

View File

@@ -1,37 +0,0 @@
:3000 {
respond 404
}
:3100 {
respond 404
}
:3170 {
reverse_proxy localhost:8080
}
:80 {
# Serve the `selfhost-web` SPA by default
root * /site/selfhost-web
file_server
handle_path /admin* {
root * /site/sh-admin-subpath-access
file_server
# Ensures any non-existent file in the server is routed to the SPA
try_files {path} /
}
# Handle requests under `/backend*` path
handle_path /backend* {
reverse_proxy localhost:8080
}
# Catch-all route for unknown paths, serves `selfhost-web` SPA
handle {
root * /site/selfhost-web
file_server
try_files {path} /
}
}

11
aio.Caddyfile Normal file
View File

@@ -0,0 +1,11 @@
:3000 {
try_files {path} /
root * /site/selfhost-web
file_server
}
:3100 {
try_files {path} /
root * /site/sh-admin
file_server
}

View File

@@ -49,8 +49,7 @@ execSync(`npx import-meta-env -x build.env -e build.env -p "/site/**/*"`)
fs.rmSync("build.env") fs.rmSync("build.env")
const caddyFileName = process.env.ENABLE_SUBPATH_BASED_ACCESS === 'true' ? 'aio-subpath-access.Caddyfile' : 'aio-multiport-setup.Caddyfile' const caddyProcess = runChildProcessWithPrefix("caddy", ["run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"], "App/Admin Dashboard Caddy")
const caddyProcess = runChildProcessWithPrefix("caddy", ["run", "--config", `/etc/caddy/${caddyFileName}`, "--adapter", "caddyfile"], "App/Admin Dashboard Caddy")
const backendProcess = runChildProcessWithPrefix("pnpm", ["run", "start:prod"], "Backend Server") const backendProcess = runChildProcessWithPrefix("pnpm", ["run", "start:prod"], "Backend Server")
caddyProcess.on("exit", (code) => { caddyProcess.on("exit", (code) => {

View File

@@ -17,7 +17,7 @@ services:
environment: 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) # 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=8080 - PORT=3170
volumes: volumes:
# Uncomment the line below when modifying code. Only applicable when using the "dev" target. # Uncomment the line below when modifying code. Only applicable when using the "dev" target.
# - ./packages/hoppscotch-backend/:/usr/src/app # - ./packages/hoppscotch-backend/:/usr/src/app
@@ -26,7 +26,6 @@ services:
hoppscotch-db: hoppscotch-db:
condition: service_healthy condition: service_healthy
ports: ports:
- "3180:80"
- "3170:3170" - "3170:3170"
# The main hoppscotch app. This will be hosted at port 3000 # The main hoppscotch app. This will be hosted at port 3000
@@ -43,8 +42,7 @@ services:
depends_on: depends_on:
- hoppscotch-backend - hoppscotch-backend
ports: ports:
- "3080:80" - "3000:8080"
- "3000:3000"
# The Self Host dashboard for managing the app. This will be hosted at port 3100 # The Self Host dashboard for managing the app. This will be hosted at port 3100
# NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for # NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for
@@ -60,8 +58,7 @@ services:
depends_on: depends_on:
- hoppscotch-backend - hoppscotch-backend
ports: ports:
- "3280:80" - "3100:8080"
- "3100:3100"
# The service that spins up all 3 services at once in one container # The service that spins up all 3 services at once in one container
hoppscotch-aio: hoppscotch-aio:
@@ -79,7 +76,6 @@ services:
- "3000:3000" - "3000:3000"
- "3100:3100" - "3100:3100"
- "3170:3170" - "3170:3170"
- "3080:80"
# The preset DB service, you can delete/comment the below lines if # The preset DB service, you can delete/comment the below lines if
# you are using an external postgres instance # you are using an external postgres instance

View File

@@ -1,3 +0,0 @@
:80 :3170 {
reverse_proxy localhost:8080
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "hoppscotch-backend", "name": "hoppscotch-backend",
"version": "2023.8.4-1", "version": "2023.8.3-1",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,

View File

@@ -1,15 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[id]` on the table `Shortcode` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "Shortcode" ADD COLUMN "embedProperties" JSONB,
ADD COLUMN "updatedOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- CreateIndex
CREATE UNIQUE INDEX "Shortcode_id_key" ON "Shortcode"("id");
-- AddForeignKey
ALTER TABLE "Shortcode" ADD CONSTRAINT "Shortcode_creatorUid_fkey" FOREIGN KEY ("creatorUid") REFERENCES "User"("uid") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -1,5 +0,0 @@
-- AlterTable
ALTER TABLE "TeamCollection" ADD COLUMN "data" JSONB;
-- AlterTable
ALTER TABLE "UserCollection" ADD COLUMN "data" JSONB;

View File

@@ -43,7 +43,6 @@ model TeamInvitation {
model TeamCollection { model TeamCollection {
id String @id @default(cuid()) id String @id @default(cuid())
parentID String? parentID String?
data Json?
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id]) parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
children TeamCollection[] @relation("TeamCollectionChildParent") children TeamCollection[] @relation("TeamCollectionChildParent")
requests TeamRequest[] requests TeamRequest[]
@@ -69,13 +68,10 @@ model TeamRequest {
} }
model Shortcode { model Shortcode {
id String @id @unique id String @id
request Json request Json
embedProperties Json? creatorUid String?
creatorUid String? createdOn DateTime @default(now())
User User? @relation(fields: [creatorUid], references: [uid])
createdOn DateTime @default(now())
updatedOn DateTime @default(now()) @updatedAt
@@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique") @@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique")
} }
@@ -106,7 +102,6 @@ model User {
currentGQLSession Json? currentGQLSession Json?
createdOn DateTime @default(now()) @db.Timestamp(3) createdOn DateTime @default(now()) @db.Timestamp(3)
invitedUsers InvitedUsers[] invitedUsers InvitedUsers[]
shortcodes Shortcode[]
} }
model Account { model Account {
@@ -197,7 +192,6 @@ model UserCollection {
userUid String userUid String
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade) user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
title String title String
data Json?
orderIndex Int orderIndex Int
type ReqType type ReqType
createdOn DateTime @default(now()) @db.Timestamp(3) createdOn DateTime @default(now()) @db.Timestamp(3)

View File

@@ -1,66 +0,0 @@
#!/usr/local/bin/node
// @ts-check
import { spawn } from 'child_process';
import process from 'process';
function runChildProcessWithPrefix(command, args, prefix) {
const childProcess = spawn(command, args);
childProcess.stdout.on('data', (data) => {
const output = data.toString().trim().split('\n');
output.forEach((line) => {
console.log(`${prefix} | ${line}`);
});
});
childProcess.stderr.on('data', (data) => {
const error = data.toString().trim().split('\n');
error.forEach((line) => {
console.error(`${prefix} | ${line}`);
});
});
childProcess.on('close', (code) => {
console.log(`${prefix} Child process exited with code ${code}`);
});
childProcess.on('error', (stuff) => {
console.error('error');
console.error(stuff);
});
return childProcess;
}
const caddyProcess = runChildProcessWithPrefix(
'caddy',
['run', '--config', '/etc/caddy/backend.Caddyfile', '--adapter', 'caddyfile'],
'App/Admin Dashboard Caddy',
);
const backendProcess = runChildProcessWithPrefix(
'pnpm',
['run', 'start:prod'],
'Backend Server',
);
caddyProcess.on('exit', (code) => {
console.log(`Exiting process because Caddy Server exited with code ${code}`);
process.exit(code);
});
backendProcess.on('exit', (code) => {
console.log(
`Exiting process because Backend Server exited with code ${code}`,
);
process.exit(code);
});
process.on('SIGINT', () => {
console.log('SIGINT received, exiting...');
caddyProcess.kill('SIGINT');
backendProcess.kill('SIGINT');
process.exit(0);
});

View File

@@ -1,9 +1,4 @@
import { ObjectType, OmitType } from '@nestjs/graphql'; import { ObjectType } from '@nestjs/graphql';
import { User } from 'src/user/user.model';
@ObjectType() @ObjectType()
export class Admin extends OmitType(User, [ export class Admin {}
'isAdmin',
'currentRESTSession',
'currentGQLSession',
]) {}

View File

@@ -10,8 +10,6 @@ import { TeamInvitationModule } from '../team-invitation/team-invitation.module'
import { TeamEnvironmentsModule } from '../team-environments/team-environments.module'; import { TeamEnvironmentsModule } from '../team-environments/team-environments.module';
import { TeamCollectionModule } from '../team-collection/team-collection.module'; import { TeamCollectionModule } from '../team-collection/team-collection.module';
import { TeamRequestModule } from '../team-request/team-request.module'; import { TeamRequestModule } from '../team-request/team-request.module';
import { InfraResolver } from './infra.resolver';
import { ShortcodeModule } from 'src/shortcode/shortcode.module';
@Module({ @Module({
imports: [ imports: [
@@ -24,9 +22,8 @@ import { ShortcodeModule } from 'src/shortcode/shortcode.module';
TeamEnvironmentsModule, TeamEnvironmentsModule,
TeamCollectionModule, TeamCollectionModule,
TeamRequestModule, TeamRequestModule,
ShortcodeModule,
], ],
providers: [InfraResolver, AdminResolver, AdminService], providers: [AdminResolver, AdminService],
exports: [AdminService], exports: [AdminService],
}) })
export class AdminModule {} export class AdminModule {}

View File

@@ -21,15 +21,15 @@ import { InvitedUser } from './invited-user.model';
import { GqlUser } from '../decorators/gql-user.decorator'; import { GqlUser } from '../decorators/gql-user.decorator';
import { PubSubService } from '../pubsub/pubsub.service'; import { PubSubService } from '../pubsub/pubsub.service';
import { Team, TeamMember } from '../team/team.model'; import { Team, TeamMember } from '../team/team.model';
import { User } from '../user/user.model';
import { TeamInvitation } from '../team-invitation/team-invitation.model';
import { PaginationArgs } from '../types/input-types.args';
import { import {
AddUserToTeamArgs, AddUserToTeamArgs,
ChangeUserRoleInTeamArgs, ChangeUserRoleInTeamArgs,
} from './input-types.args'; } from './input-types.args';
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard'; import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
import { SkipThrottle } from '@nestjs/throttler'; 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';
@UseGuards(GqlThrottlerGuard) @UseGuards(GqlThrottlerGuard)
@Resolver(() => Admin) @Resolver(() => Admin)
@@ -51,7 +51,6 @@ export class AdminResolver {
@ResolveField(() => [User], { @ResolveField(() => [User], {
description: 'Returns a list of all admin users in infra', description: 'Returns a list of all admin users in infra',
deprecationReason: 'Use `infra` query instead',
}) })
@UseGuards(GqlAuthGuard, GqlAdminGuard) @UseGuards(GqlAuthGuard, GqlAdminGuard)
async admins() { async admins() {
@@ -60,7 +59,6 @@ export class AdminResolver {
} }
@ResolveField(() => User, { @ResolveField(() => User, {
description: 'Returns a user info by UID', description: 'Returns a user info by UID',
deprecationReason: 'Use `infra` query instead',
}) })
@UseGuards(GqlAuthGuard, GqlAdminGuard) @UseGuards(GqlAuthGuard, GqlAdminGuard)
async userInfo( async userInfo(
@@ -78,7 +76,6 @@ export class AdminResolver {
@ResolveField(() => [User], { @ResolveField(() => [User], {
description: 'Returns a list of all the users in infra', description: 'Returns a list of all the users in infra',
deprecationReason: 'Use `infra` query instead',
}) })
@UseGuards(GqlAuthGuard, GqlAdminGuard) @UseGuards(GqlAuthGuard, GqlAdminGuard)
async allUsers( async allUsers(
@@ -91,7 +88,6 @@ export class AdminResolver {
@ResolveField(() => [InvitedUser], { @ResolveField(() => [InvitedUser], {
description: 'Returns a list of all the invited users', description: 'Returns a list of all the invited users',
deprecationReason: 'Use `infra` query instead',
}) })
async invitedUsers(@Parent() admin: Admin): Promise<InvitedUser[]> { async invitedUsers(@Parent() admin: Admin): Promise<InvitedUser[]> {
const users = await this.adminService.fetchInvitedUsers(); const users = await this.adminService.fetchInvitedUsers();
@@ -100,7 +96,6 @@ export class AdminResolver {
@ResolveField(() => [Team], { @ResolveField(() => [Team], {
description: 'Returns a list of all the teams in the infra', description: 'Returns a list of all the teams in the infra',
deprecationReason: 'Use `infra` query instead',
}) })
async allTeams( async allTeams(
@Parent() admin: Admin, @Parent() admin: Admin,
@@ -111,7 +106,6 @@ export class AdminResolver {
} }
@ResolveField(() => Team, { @ResolveField(() => Team, {
description: 'Returns a team info by ID when requested by Admin', description: 'Returns a team info by ID when requested by Admin',
deprecationReason: 'Use `infra` query instead',
}) })
async teamInfo( async teamInfo(
@Parent() admin: Admin, @Parent() admin: Admin,
@@ -129,7 +123,6 @@ export class AdminResolver {
@ResolveField(() => Number, { @ResolveField(() => Number, {
description: 'Return count of all the members in a team', description: 'Return count of all the members in a team',
deprecationReason: 'Use `infra` query instead',
}) })
async membersCountInTeam( async membersCountInTeam(
@Parent() admin: Admin, @Parent() admin: Admin,
@@ -147,7 +140,6 @@ export class AdminResolver {
@ResolveField(() => Number, { @ResolveField(() => Number, {
description: 'Return count of all the stored collections in a team', description: 'Return count of all the stored collections in a team',
deprecationReason: 'Use `infra` query instead',
}) })
async collectionCountInTeam( async collectionCountInTeam(
@Parent() admin: Admin, @Parent() admin: Admin,
@@ -163,7 +155,6 @@ export class AdminResolver {
} }
@ResolveField(() => Number, { @ResolveField(() => Number, {
description: 'Return count of all the stored requests in a team', description: 'Return count of all the stored requests in a team',
deprecationReason: 'Use `infra` query instead',
}) })
async requestCountInTeam( async requestCountInTeam(
@Parent() admin: Admin, @Parent() admin: Admin,
@@ -180,7 +171,6 @@ export class AdminResolver {
@ResolveField(() => Number, { @ResolveField(() => Number, {
description: 'Return count of all the stored environments in a team', description: 'Return count of all the stored environments in a team',
deprecationReason: 'Use `infra` query instead',
}) })
async environmentCountInTeam( async environmentCountInTeam(
@Parent() admin: Admin, @Parent() admin: Admin,
@@ -197,7 +187,6 @@ export class AdminResolver {
@ResolveField(() => [TeamInvitation], { @ResolveField(() => [TeamInvitation], {
description: 'Return all the pending invitations in a team', description: 'Return all the pending invitations in a team',
deprecationReason: 'Use `infra` query instead',
}) })
async pendingInvitationCountInTeam( async pendingInvitationCountInTeam(
@Parent() admin: Admin, @Parent() admin: Admin,
@@ -216,7 +205,6 @@ export class AdminResolver {
@ResolveField(() => Number, { @ResolveField(() => Number, {
description: 'Return total number of Users in organization', description: 'Return total number of Users in organization',
deprecationReason: 'Use `infra` query instead',
}) })
async usersCount() { async usersCount() {
return this.adminService.getUsersCount(); return this.adminService.getUsersCount();
@@ -224,7 +212,6 @@ export class AdminResolver {
@ResolveField(() => Number, { @ResolveField(() => Number, {
description: 'Return total number of Teams in organization', description: 'Return total number of Teams in organization',
deprecationReason: 'Use `infra` query instead',
}) })
async teamsCount() { async teamsCount() {
return this.adminService.getTeamsCount(); return this.adminService.getTeamsCount();
@@ -232,7 +219,6 @@ export class AdminResolver {
@ResolveField(() => Number, { @ResolveField(() => Number, {
description: 'Return total number of Team Collections in organization', description: 'Return total number of Team Collections in organization',
deprecationReason: 'Use `infra` query instead',
}) })
async teamCollectionsCount() { async teamCollectionsCount() {
return this.adminService.getTeamCollectionsCount(); return this.adminService.getTeamCollectionsCount();
@@ -240,7 +226,6 @@ export class AdminResolver {
@ResolveField(() => Number, { @ResolveField(() => Number, {
description: 'Return total number of Team Requests in organization', description: 'Return total number of Team Requests in organization',
deprecationReason: 'Use `infra` query instead',
}) })
async teamRequestsCount() { async teamRequestsCount() {
return this.adminService.getTeamRequestsCount(); return this.adminService.getTeamRequestsCount();
@@ -443,23 +428,6 @@ export class AdminResolver {
return true; return true;
} }
@Mutation(() => Boolean, {
description: 'Revoke Shortcode by ID',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async revokeShortcodeByAdmin(
@Args({
name: 'code',
description: 'The shortcode to delete',
type: () => ID,
})
code: string,
): Promise<boolean> {
const res = await this.adminService.deleteShortcode(code);
if (E.isLeft(res)) throwErr(res.left);
return true;
}
/* Subscriptions */ /* Subscriptions */
@Subscription(() => InvitedUser, { @Subscription(() => InvitedUser, {

View File

@@ -15,7 +15,6 @@ import {
INVALID_EMAIL, INVALID_EMAIL,
USER_ALREADY_INVITED, USER_ALREADY_INVITED,
} from '../errors'; } from '../errors';
import { ShortcodeService } from 'src/shortcode/shortcode.service';
const mockPrisma = mockDeep<PrismaService>(); const mockPrisma = mockDeep<PrismaService>();
const mockPubSub = mockDeep<PubSubService>(); const mockPubSub = mockDeep<PubSubService>();
@@ -26,7 +25,6 @@ const mockTeamRequestService = mockDeep<TeamRequestService>();
const mockTeamInvitationService = mockDeep<TeamInvitationService>(); const mockTeamInvitationService = mockDeep<TeamInvitationService>();
const mockTeamCollectionService = mockDeep<TeamCollectionService>(); const mockTeamCollectionService = mockDeep<TeamCollectionService>();
const mockMailerService = mockDeep<MailerService>(); const mockMailerService = mockDeep<MailerService>();
const mockShortcodeService = mockDeep<ShortcodeService>();
const adminService = new AdminService( const adminService = new AdminService(
mockUserService, mockUserService,
@@ -38,7 +36,6 @@ const adminService = new AdminService(
mockPubSub as any, mockPubSub as any,
mockPrisma as any, mockPrisma as any,
mockMailerService, mockMailerService,
mockShortcodeService,
); );
const invitedUsers: InvitedUsers[] = [ const invitedUsers: InvitedUsers[] = [

View File

@@ -24,7 +24,6 @@ import { TeamRequestService } from '../team-request/team-request.service';
import { TeamEnvironmentsService } from '../team-environments/team-environments.service'; import { TeamEnvironmentsService } from '../team-environments/team-environments.service';
import { TeamInvitationService } from '../team-invitation/team-invitation.service'; import { TeamInvitationService } from '../team-invitation/team-invitation.service';
import { TeamMemberRole } from '../team/team.model'; import { TeamMemberRole } from '../team/team.model';
import { ShortcodeService } from 'src/shortcode/shortcode.service';
@Injectable() @Injectable()
export class AdminService { export class AdminService {
@@ -38,7 +37,6 @@ export class AdminService {
private readonly pubsub: PubSubService, private readonly pubsub: PubSubService,
private readonly prisma: PrismaService, private readonly prisma: PrismaService,
private readonly mailerService: MailerService, private readonly mailerService: MailerService,
private readonly shortcodeService: ShortcodeService,
) {} ) {}
/** /**
@@ -434,35 +432,4 @@ export class AdminService {
return E.right(teamInvite.right); return E.right(teamInvite.right);
} }
/**
* Fetch all created ShortCodes
*
* @param args Pagination arguments
* @param userEmail User email
* @returns ShortcodeWithUserEmail
*/
async fetchAllShortcodes(
cursorID: string,
take: number,
userEmail: string = null,
) {
return this.shortcodeService.fetchAllShortcodes(
{ cursor: cursorID, take },
userEmail,
);
}
/**
* Delete a Shortcode
*
* @param shortcodeID ID of Shortcode being deleted
* @returns Boolean on successful deletion
*/
async deleteShortcode(shortcodeID: string) {
const result = await this.shortcodeService.deleteShortcode(shortcodeID);
if (E.isLeft(result)) return E.left(result.left);
return E.right(result.right);
}
} }

View File

@@ -1,10 +0,0 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { Admin } from './admin.model';
@ObjectType()
export class Infra {
@Field(() => Admin, {
description: 'Admin who executed the action',
})
executedBy: Admin;
}

View File

@@ -1,225 +0,0 @@
import { UseGuards } from '@nestjs/common';
import { Args, ID, Query, ResolveField, Resolver } from '@nestjs/graphql';
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
import { Infra } from './infra.model';
import { AdminService } from './admin.service';
import { GqlAuthGuard } from 'src/guards/gql-auth.guard';
import { GqlAdminGuard } from './guards/gql-admin.guard';
import { User } from 'src/user/user.model';
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 { InvitedUser } from './invited-user.model';
import { Team } from 'src/team/team.model';
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
import { GqlAdmin } from './decorators/gql-admin.decorator';
import { ShortcodeWithUserEmail } from 'src/shortcode/shortcode.model';
@UseGuards(GqlThrottlerGuard)
@Resolver(() => Infra)
export class InfraResolver {
constructor(private adminService: AdminService) {}
@Query(() => Infra, {
description: 'Fetch details of the Infrastructure',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
infra(@GqlAdmin() admin: Admin) {
const infra: Infra = { executedBy: admin };
return infra;
}
@ResolveField(() => [User], {
description: 'Returns a list of all admin users in infra',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async admins() {
const admins = await this.adminService.fetchAdmins();
return admins;
}
@ResolveField(() => User, {
description: 'Returns a user info by UID',
})
@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',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async allUsers(@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',
})
async invitedUsers(): Promise<InvitedUser[]> {
const users = await this.adminService.fetchInvitedUsers();
return users;
}
@ResolveField(() => [Team], {
description: 'Returns a list of all the teams in the infra',
})
async allTeams(@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',
})
async teamInfo(
@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',
})
async membersCountInTeam(
@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',
})
async collectionCountInTeam(
@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',
})
async requestCountInTeam(
@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',
})
async environmentCountInTeam(
@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',
})
async pendingInvitationCountInTeam(
@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',
})
async usersCount() {
return this.adminService.getUsersCount();
}
@ResolveField(() => Number, {
description: 'Return total number of Teams in organization',
})
async teamsCount() {
return this.adminService.getTeamsCount();
}
@ResolveField(() => Number, {
description: 'Return total number of Team Collections in organization',
})
async teamCollectionsCount() {
return this.adminService.getTeamCollectionsCount();
}
@ResolveField(() => Number, {
description: 'Return total number of Team Requests in organization',
})
async teamRequestsCount() {
return this.adminService.getTeamRequestsCount();
}
@ResolveField(() => [ShortcodeWithUserEmail], {
description: 'Returns a list of all the shortcodes in the infra',
})
async allShortcodes(
@Args() args: PaginationArgs,
@Args({
name: 'userEmail',
nullable: true,
description: 'Users email to filter shortcodes by',
})
userEmail: string,
) {
return await this.adminService.fetchAllShortcodes(
args.cursor,
args.take,
userEmail,
);
}
}

View File

@@ -254,13 +254,6 @@ export const TEAM_COLL_INVALID_JSON = 'team_coll/invalid_json';
*/ */
export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const; export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const;
/**
* The Team Collection data is not valid
* (TeamCollectionService)
*/
export const TEAM_COLL_DATA_INVALID =
'team_coll/team_coll_data_invalid' as const;
/** /**
* Tried to perform an action on a request that doesn't accept their member role level * Tried to perform an action on a request that doesn't accept their member role level
* (GqlRequestTeamMemberGuard) * (GqlRequestTeamMemberGuard)
@@ -325,6 +318,18 @@ export const TEAM_INVITATION_NOT_FOUND =
*/ */
export const SHORTCODE_NOT_FOUND = 'shortcode/not_found' as const; export const SHORTCODE_NOT_FOUND = 'shortcode/not_found' as const;
/**
* Invalid ShortCode format
* (ShortcodeService)
*/
export const SHORTCODE_INVALID_JSON = 'shortcode/invalid_json' as const;
/**
* ShortCode already exists in DB
* (ShortcodeService)
*/
export const SHORTCODE_ALREADY_EXISTS = 'shortcode/already_exists' as const;
/** /**
* Invalid or non-existent TEAM ENVIRONMENT ID * Invalid or non-existent TEAM ENVIRONMENT ID
* (TeamEnvironmentsService) * (TeamEnvironmentsService)
@@ -592,13 +597,6 @@ export const USER_COLL_REORDERING_FAILED =
export const USER_COLL_SAME_NEXT_COLL = export const USER_COLL_SAME_NEXT_COLL =
'user_coll/user_collection_and_next_user_collection_are_same' as const; 'user_coll/user_collection_and_next_user_collection_are_same' as const;
/**
* The User Collection data is not valid
* (UserCollectionService)
*/
export const USER_COLL_DATA_INVALID =
'user_coll/user_coll_data_invalid' as const;
/** /**
* The User Collection does not belong to the logged-in user * The User Collection does not belong to the logged-in user
* (UserCollectionService) * (UserCollectionService)
@@ -623,24 +621,3 @@ export const MAILER_SMTP_URL_UNDEFINED = 'mailer/smtp_url_undefined' as const;
*/ */
export const MAILER_FROM_ADDRESS_UNDEFINED = export const MAILER_FROM_ADDRESS_UNDEFINED =
'mailer/from_address_undefined' as const; 'mailer/from_address_undefined' as const;
/**
* SharedRequest invalid request JSON format
* (ShortcodeService)
*/
export const SHORTCODE_INVALID_REQUEST_JSON =
'shortcode/request_invalid_format' as const;
/**
* SharedRequest invalid properties JSON format
* (ShortcodeService)
*/
export const SHORTCODE_INVALID_PROPERTIES_JSON =
'shortcode/properties_invalid_format' as const;
/**
* SharedRequest invalid properties not found
* (ShortcodeService)
*/
export const SHORTCODE_PROPERTIES_NOT_FOUND =
'shortcode/properties_not_found' as const;

View File

@@ -27,7 +27,6 @@ import { UserRequestUserCollectionResolver } from './user-request/resolvers/user
import { UserEnvsUserResolver } from './user-environment/user.resolver'; import { UserEnvsUserResolver } from './user-environment/user.resolver';
import { UserHistoryUserResolver } from './user-history/user.resolver'; import { UserHistoryUserResolver } from './user-history/user.resolver';
import { UserSettingsUserResolver } from './user-settings/user.resolver'; import { UserSettingsUserResolver } from './user-settings/user.resolver';
import { InfraResolver } from './admin/infra.resolver';
/** /**
* All the resolvers present in the application. * All the resolvers present in the application.
@@ -35,7 +34,6 @@ import { InfraResolver } from './admin/infra.resolver';
* NOTE: This needs to be KEPT UP-TO-DATE to keep the schema accurate * NOTE: This needs to be KEPT UP-TO-DATE to keep the schema accurate
*/ */
const RESOLVERS = [ const RESOLVERS = [
InfraResolver,
AdminResolver, AdminResolver,
ShortcodeResolver, ShortcodeResolver,
TeamResolver, TeamResolver,

View File

@@ -21,8 +21,8 @@ import {
} from 'src/team-request/team-request.model'; } from 'src/team-request/team-request.model';
import { TeamInvitation } from 'src/team-invitation/team-invitation.model'; import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
import { InvitedUser } from '../admin/invited-user.model'; import { InvitedUser } from '../admin/invited-user.model';
import { UserCollection } from '@prisma/client';
import { import {
UserCollection,
UserCollectionRemovedData, UserCollectionRemovedData,
UserCollectionReorderData, UserCollectionReorderData,
} from 'src/user-collection/user-collections.model'; } from 'src/user-collection/user-collections.model';
@@ -69,7 +69,5 @@ export type TopicDef = {
[topic: `team_req/${string}/req_deleted`]: string; [topic: `team_req/${string}/req_deleted`]: string;
[topic: `team/${string}/invite_added`]: TeamInvitation; [topic: `team/${string}/invite_added`]: TeamInvitation;
[topic: `team/${string}/invite_removed`]: string; [topic: `team/${string}/invite_removed`]: string;
[ [topic: `shortcode/${string}/${'created' | 'revoked'}`]: Shortcode;
topic: `shortcode/${string}/${'created' | 'revoked' | 'updated'}`
]: Shortcode;
}; };

View File

@@ -1,10 +1,9 @@
import { Field, ID, ObjectType } from '@nestjs/graphql'; import { Field, ID, ObjectType } from '@nestjs/graphql';
import { User } from 'src/user/user.model';
@ObjectType() @ObjectType()
export class Shortcode { export class Shortcode {
@Field(() => ID, { @Field(() => ID, {
description: 'The 12 digit alphanumeric code', description: 'The shortcode. 12 digit alphanumeric.',
}) })
id: string; id: string;
@@ -13,57 +12,8 @@ export class Shortcode {
}) })
request: string; request: string;
@Field({
description: 'JSON string representing the properties for an embed',
nullable: true,
})
properties: string;
@Field({ @Field({
description: 'Timestamp of when the Shortcode was created', description: 'Timestamp of when the Shortcode was created',
}) })
createdOn: Date; createdOn: Date;
} }
@ObjectType()
export class ShortcodeCreator {
@Field({
description: 'Uid of user who created the shortcode',
})
uid: string;
@Field({
description: 'Email of user who created the shortcode',
})
email: string;
}
@ObjectType()
export class ShortcodeWithUserEmail {
@Field(() => ID, {
description: 'The 12 digit alphanumeric code',
})
id: string;
@Field({
description: 'JSON string representing the request data',
})
request: string;
@Field({
description: 'JSON string representing the properties for an embed',
nullable: true,
})
properties: string;
@Field({
description: 'Timestamp of when the Shortcode was created',
})
createdOn: Date;
@Field({
description: 'Details of user who created the shortcode',
nullable: true,
})
creator: ShortcodeCreator;
}

View File

@@ -1,4 +1,5 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PrismaModule } from 'src/prisma/prisma.module'; import { PrismaModule } from 'src/prisma/prisma.module';
import { PubSubModule } from 'src/pubsub/pubsub.module'; import { PubSubModule } from 'src/pubsub/pubsub.module';
import { UserModule } from 'src/user/user.module'; import { UserModule } from 'src/user/user.module';
@@ -6,7 +7,14 @@ import { ShortcodeResolver } from './shortcode.resolver';
import { ShortcodeService } from './shortcode.service'; import { ShortcodeService } from './shortcode.service';
@Module({ @Module({
imports: [PrismaModule, UserModule, PubSubModule], imports: [
PrismaModule,
UserModule,
PubSubModule,
JwtModule.register({
secret: process.env.JWT_SECRET,
}),
],
providers: [ShortcodeService, ShortcodeResolver], providers: [ShortcodeService, ShortcodeResolver],
exports: [ShortcodeService], exports: [ShortcodeService],
}) })

View File

@@ -1,5 +1,6 @@
import { import {
Args, Args,
Context,
ID, ID,
Mutation, Mutation,
Query, Query,
@@ -8,25 +9,28 @@ import {
} from '@nestjs/graphql'; } from '@nestjs/graphql';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
import { UseGuards } from '@nestjs/common'; import { UseGuards } from '@nestjs/common';
import { Shortcode, ShortcodeWithUserEmail } from './shortcode.model'; import { Shortcode } from './shortcode.model';
import { ShortcodeService } from './shortcode.service'; import { ShortcodeService } from './shortcode.service';
import { UserService } from 'src/user/user.service';
import { throwErr } from 'src/utils'; import { throwErr } from 'src/utils';
import { GqlUser } from 'src/decorators/gql-user.decorator'; import { GqlUser } from 'src/decorators/gql-user.decorator';
import { GqlAuthGuard } from 'src/guards/gql-auth.guard'; import { GqlAuthGuard } from 'src/guards/gql-auth.guard';
import { User } from 'src/user/user.model'; import { User } from 'src/user/user.model';
import { PubSubService } from 'src/pubsub/pubsub.service'; import { PubSubService } from 'src/pubsub/pubsub.service';
import { AuthUser } from '../types/AuthUser'; import { AuthUser } from '../types/AuthUser';
import { JwtService } from '@nestjs/jwt';
import { PaginationArgs } from 'src/types/input-types.args'; import { PaginationArgs } from 'src/types/input-types.args';
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard'; import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
import { SkipThrottle } from '@nestjs/throttler'; import { SkipThrottle } from '@nestjs/throttler';
import { GqlAdminGuard } from 'src/admin/guards/gql-admin.guard';
@UseGuards(GqlThrottlerGuard) @UseGuards(GqlThrottlerGuard)
@Resolver(() => Shortcode) @Resolver(() => Shortcode)
export class ShortcodeResolver { export class ShortcodeResolver {
constructor( constructor(
private readonly shortcodeService: ShortcodeService, private readonly shortcodeService: ShortcodeService,
private readonly userService: UserService,
private readonly pubsub: PubSubService, private readonly pubsub: PubSubService,
private jwtService: JwtService,
) {} ) {}
/* Queries */ /* Queries */
@@ -60,53 +64,20 @@ export class ShortcodeResolver {
@Mutation(() => Shortcode, { @Mutation(() => Shortcode, {
description: 'Create a shortcode for the given request.', description: 'Create a shortcode for the given request.',
}) })
@UseGuards(GqlAuthGuard)
async createShortcode( async createShortcode(
@GqlUser() user: AuthUser,
@Args({ @Args({
name: 'request', name: 'request',
description: 'JSON string of the request object', description: 'JSON string of the request object',
}) })
request: string, request: string,
@Args({ @Context() ctx: any,
name: 'properties',
description: 'JSON string of the properties of the embed',
nullable: true,
})
properties: string,
) { ) {
const decodedAccessToken = this.jwtService.verify(
ctx.req.cookies['access_token'],
);
const result = await this.shortcodeService.createShortcode( const result = await this.shortcodeService.createShortcode(
request, request,
properties, decodedAccessToken?.sub,
user,
);
if (E.isLeft(result)) throwErr(result.left);
return result.right;
}
@Mutation(() => Shortcode, {
description: 'Update a user generated Shortcode',
})
@UseGuards(GqlAuthGuard)
async updateEmbedProperties(
@GqlUser() user: AuthUser,
@Args({
name: 'code',
type: () => ID,
description: 'The Shortcode to update',
})
code: string,
@Args({
name: 'properties',
description: 'JSON string of the properties of the embed',
})
properties: string,
) {
const result = await this.shortcodeService.updateEmbedProperties(
code,
user.uid,
properties,
); );
if (E.isLeft(result)) throwErr(result.left); if (E.isLeft(result)) throwErr(result.left);
@@ -122,7 +93,7 @@ export class ShortcodeResolver {
@Args({ @Args({
name: 'code', name: 'code',
type: () => ID, type: () => ID,
description: 'The shortcode to remove', description: 'The shortcode to resolve',
}) })
code: string, code: string,
) { ) {
@@ -143,16 +114,6 @@ export class ShortcodeResolver {
return this.pubsub.asyncIterator(`shortcode/${user.uid}/created`); return this.pubsub.asyncIterator(`shortcode/${user.uid}/created`);
} }
@Subscription(() => Shortcode, {
description: 'Listen for Shortcode updates',
resolve: (value) => value,
})
@SkipThrottle()
@UseGuards(GqlAuthGuard)
myShortcodesUpdated(@GqlUser() user: AuthUser) {
return this.pubsub.asyncIterator(`shortcode/${user.uid}/updated`);
}
@Subscription(() => Shortcode, { @Subscription(() => Shortcode, {
description: 'Listen for shortcode deletion', description: 'Listen for shortcode deletion',
resolve: (value) => value, resolve: (value) => value,

View File

@@ -1,16 +1,13 @@
import { mockDeep, mockReset } from 'jest-mock-extended'; import { mockDeep, mockReset } from 'jest-mock-extended';
import { PrismaService } from '../prisma/prisma.service'; import { PrismaService } from '../prisma/prisma.service';
import { import {
INVALID_EMAIL, SHORTCODE_ALREADY_EXISTS,
SHORTCODE_INVALID_PROPERTIES_JSON, SHORTCODE_INVALID_JSON,
SHORTCODE_INVALID_REQUEST_JSON,
SHORTCODE_NOT_FOUND, SHORTCODE_NOT_FOUND,
SHORTCODE_PROPERTIES_NOT_FOUND,
} from 'src/errors'; } from 'src/errors';
import { Shortcode, ShortcodeWithUserEmail } from './shortcode.model'; import { Shortcode } from './shortcode.model';
import { ShortcodeService } from './shortcode.service'; import { ShortcodeService } from './shortcode.service';
import { UserService } from 'src/user/user.service'; import { UserService } from 'src/user/user.service';
import { AuthUser } from 'src/types/AuthUser';
const mockPrisma = mockDeep<PrismaService>(); const mockPrisma = mockDeep<PrismaService>();
@@ -25,7 +22,7 @@ const mockFB = {
doc: mockDocFunc, doc: mockDocFunc,
}, },
}; };
const mockUserService = new UserService(mockPrisma as any, mockPubSub as any); const mockUserService = new UserService(mockFB as any, mockPubSub as any);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
@@ -41,34 +38,18 @@ beforeEach(() => {
}); });
const createdOn = new Date(); const createdOn = new Date();
const user: AuthUser = { const shortCodeWithOutUser = {
uid: '123344',
email: 'dwight@dundermifflin.com',
displayName: 'Dwight Schrute',
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
isAdmin: false,
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
createdOn: createdOn,
currentGQLSession: {},
currentRESTSession: {},
};
const mockEmbed = {
id: '123', id: '123',
request: '{}', request: '{}',
embedProperties: '{}',
createdOn: createdOn, createdOn: createdOn,
creatorUid: user.uid, creatorUid: null,
updatedOn: createdOn,
}; };
const mockShortcode = { const shortCodeWithUser = {
id: '123', id: '123',
request: '{}', request: '{}',
embedProperties: null,
createdOn: createdOn, createdOn: createdOn,
creatorUid: user.uid, creatorUid: 'user_uid_1',
updatedOn: createdOn,
}; };
const shortcodes = [ const shortcodes = [
@@ -77,67 +58,33 @@ const shortcodes = [
request: { request: {
hello: 'there', hello: 'there',
}, },
embedProperties: { creatorUid: 'testuser',
foo: 'bar',
},
creatorUid: user.uid,
createdOn: new Date(), createdOn: new Date(),
updatedOn: createdOn,
}, },
{ {
id: 'blablabla1', id: 'blablabla1',
request: { request: {
hello: 'there', hello: 'there',
}, },
embedProperties: { creatorUid: 'testuser',
foo: 'bar',
},
creatorUid: user.uid,
createdOn: new Date(), createdOn: new Date(),
updatedOn: createdOn,
},
];
const shortcodesWithUserEmail = [
{
id: 'blablabla',
request: {
hello: 'there',
},
embedProperties: {
foo: 'bar',
},
creatorUid: user.uid,
createdOn: new Date(),
updatedOn: createdOn,
User: user,
},
{
id: 'blablabla1',
request: {
hello: 'there',
},
embedProperties: {
foo: 'bar',
},
creatorUid: user.uid,
createdOn: new Date(),
updatedOn: createdOn,
User: user,
}, },
]; ];
describe('ShortcodeService', () => { describe('ShortcodeService', () => {
describe('getShortCode', () => { describe('getShortCode', () => {
test('should return a valid Shortcode with valid Shortcode ID', async () => { test('should return a valid shortcode with valid shortcode ID', async () => {
mockPrisma.shortcode.findFirstOrThrow.mockResolvedValueOnce(mockEmbed); mockPrisma.shortcode.findFirstOrThrow.mockResolvedValueOnce(
shortCodeWithOutUser,
);
const result = await shortcodeService.getShortCode(mockEmbed.id); const result = await shortcodeService.getShortCode(
shortCodeWithOutUser.id,
);
expect(result).toEqualRight(<Shortcode>{ expect(result).toEqualRight(<Shortcode>{
id: mockEmbed.id, id: shortCodeWithOutUser.id,
createdOn: mockEmbed.createdOn, createdOn: shortCodeWithOutUser.createdOn,
request: JSON.stringify(mockEmbed.request), request: JSON.stringify(shortCodeWithOutUser.request),
properties: JSON.stringify(mockEmbed.embedProperties),
}); });
}); });
@@ -152,10 +99,10 @@ describe('ShortcodeService', () => {
}); });
describe('fetchUserShortCodes', () => { describe('fetchUserShortCodes', () => {
test('should return list of Shortcode with valid inputs and no cursor', async () => { test('should return list of shortcodes with valid inputs and no cursor', async () => {
mockPrisma.shortcode.findMany.mockResolvedValueOnce(shortcodes); mockPrisma.shortcode.findMany.mockResolvedValueOnce(shortcodes);
const result = await shortcodeService.fetchUserShortCodes(user.uid, { const result = await shortcodeService.fetchUserShortCodes('testuser', {
cursor: null, cursor: null,
take: 10, take: 10,
}); });
@@ -163,22 +110,20 @@ describe('ShortcodeService', () => {
{ {
id: shortcodes[0].id, id: shortcodes[0].id,
request: JSON.stringify(shortcodes[0].request), request: JSON.stringify(shortcodes[0].request),
properties: JSON.stringify(shortcodes[0].embedProperties),
createdOn: shortcodes[0].createdOn, createdOn: shortcodes[0].createdOn,
}, },
{ {
id: shortcodes[1].id, id: shortcodes[1].id,
request: JSON.stringify(shortcodes[1].request), request: JSON.stringify(shortcodes[1].request),
properties: JSON.stringify(shortcodes[1].embedProperties),
createdOn: shortcodes[1].createdOn, createdOn: shortcodes[1].createdOn,
}, },
]); ]);
}); });
test('should return list of Shortcode with valid inputs and cursor', async () => { test('should return list of shortcodes with valid inputs and cursor', async () => {
mockPrisma.shortcode.findMany.mockResolvedValue([shortcodes[1]]); mockPrisma.shortcode.findMany.mockResolvedValue([shortcodes[1]]);
const result = await shortcodeService.fetchUserShortCodes(user.uid, { const result = await shortcodeService.fetchUserShortCodes('testuser', {
cursor: 'blablabla', cursor: 'blablabla',
take: 10, take: 10,
}); });
@@ -186,7 +131,6 @@ describe('ShortcodeService', () => {
{ {
id: shortcodes[1].id, id: shortcodes[1].id,
request: JSON.stringify(shortcodes[1].request), request: JSON.stringify(shortcodes[1].request),
properties: JSON.stringify(shortcodes[1].embedProperties),
createdOn: shortcodes[1].createdOn, createdOn: shortcodes[1].createdOn,
}, },
]); ]);
@@ -195,7 +139,7 @@ describe('ShortcodeService', () => {
test('should return an empty array for an invalid cursor', async () => { test('should return an empty array for an invalid cursor', async () => {
mockPrisma.shortcode.findMany.mockResolvedValue([]); mockPrisma.shortcode.findMany.mockResolvedValue([]);
const result = await shortcodeService.fetchUserShortCodes(user.uid, { const result = await shortcodeService.fetchUserShortCodes('testuser', {
cursor: 'invalidcursor', cursor: 'invalidcursor',
take: 10, take: 10,
}); });
@@ -227,111 +171,77 @@ describe('ShortcodeService', () => {
}); });
describe('createShortcode', () => { describe('createShortcode', () => {
test('should throw SHORTCODE_INVALID_REQUEST_JSON error if incoming request data is invalid', async () => { test('should throw SHORTCODE_INVALID_JSON error if incoming request data is invalid', async () => {
const result = await shortcodeService.createShortcode( const result = await shortcodeService.createShortcode(
'invalidRequest', 'invalidRequest',
null, 'user_uid_1',
user,
); );
expect(result).toEqualLeft(SHORTCODE_INVALID_REQUEST_JSON); expect(result).toEqualLeft(SHORTCODE_INVALID_JSON);
}); });
test('should throw SHORTCODE_INVALID_PROPERTIES_JSON error if incoming properties data is invalid', async () => { test('should successfully create a new shortcode with valid user uid', async () => {
const result = await shortcodeService.createShortcode( // generateUniqueShortCodeID --> getShortCode
'{}',
'invalid_data',
user,
);
expect(result).toEqualLeft(SHORTCODE_INVALID_PROPERTIES_JSON);
});
test('should successfully create a new Embed with valid user uid', async () => {
// generateUniqueShortCodeID --> getShortcode
mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce( mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce(
'NotFoundError', 'NotFoundError',
); );
mockPrisma.shortcode.create.mockResolvedValueOnce(mockEmbed); mockPrisma.shortcode.create.mockResolvedValueOnce(shortCodeWithUser);
const result = await shortcodeService.createShortcode('{}', '{}', user); const result = await shortcodeService.createShortcode('{}', 'user_uid_1');
expect(result).toEqualRight(<Shortcode>{ expect(result).toEqualRight({
id: mockEmbed.id, id: shortCodeWithUser.id,
createdOn: mockEmbed.createdOn, createdOn: shortCodeWithUser.createdOn,
request: JSON.stringify(mockEmbed.request), request: JSON.stringify(shortCodeWithUser.request),
properties: JSON.stringify(mockEmbed.embedProperties),
}); });
}); });
test('should successfully create a new ShortCode with valid user uid', async () => { test('should successfully create a new shortcode with null user uid', async () => {
// generateUniqueShortCodeID --> getShortcode // generateUniqueShortCodeID --> getShortCode
mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce( mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce(
'NotFoundError', 'NotFoundError',
); );
mockPrisma.shortcode.create.mockResolvedValueOnce(mockShortcode); mockPrisma.shortcode.create.mockResolvedValueOnce(shortCodeWithUser);
const result = await shortcodeService.createShortcode('{}', null, user); const result = await shortcodeService.createShortcode('{}', null);
expect(result).toEqualRight(<Shortcode>{ expect(result).toEqualRight({
id: mockShortcode.id, id: shortCodeWithUser.id,
createdOn: mockShortcode.createdOn, createdOn: shortCodeWithUser.createdOn,
request: JSON.stringify(mockShortcode.request), request: JSON.stringify(shortCodeWithOutUser.request),
properties: mockShortcode.embedProperties,
}); });
}); });
test('should send pubsub message to `shortcode/{uid}/created` on successful creation of a Shortcode', async () => { test('should send pubsub message to `shortcode/{uid}/created` on successful creation of shortcode', async () => {
// generateUniqueShortCodeID --> getShortcode // generateUniqueShortCodeID --> getShortCode
mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce( mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce(
'NotFoundError', 'NotFoundError',
); );
mockPrisma.shortcode.create.mockResolvedValueOnce(mockShortcode); mockPrisma.shortcode.create.mockResolvedValueOnce(shortCodeWithUser);
const result = await shortcodeService.createShortcode('{}', null, user);
const result = await shortcodeService.createShortcode('{}', 'user_uid_1');
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`shortcode/${mockShortcode.creatorUid}/created`, `shortcode/${shortCodeWithUser.creatorUid}/created`,
<Shortcode>{ {
id: mockShortcode.id, id: shortCodeWithUser.id,
createdOn: mockShortcode.createdOn, createdOn: shortCodeWithUser.createdOn,
request: JSON.stringify(mockShortcode.request), request: JSON.stringify(shortCodeWithUser.request),
properties: mockShortcode.embedProperties,
},
);
});
test('should send pubsub message to `shortcode/{uid}/created` on successful creation of an Embed', async () => {
// generateUniqueShortCodeID --> getShortcode
mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce(
'NotFoundError',
);
mockPrisma.shortcode.create.mockResolvedValueOnce(mockEmbed);
const result = await shortcodeService.createShortcode('{}', '{}', user);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`shortcode/${mockEmbed.creatorUid}/created`,
<Shortcode>{
id: mockEmbed.id,
createdOn: mockEmbed.createdOn,
request: JSON.stringify(mockEmbed.request),
properties: JSON.stringify(mockEmbed.embedProperties),
}, },
); );
}); });
}); });
describe('revokeShortCode', () => { describe('revokeShortCode', () => {
test('should return true on successful deletion of Shortcode with valid inputs', async () => { test('should return true on successful deletion of shortcode with valid inputs', async () => {
mockPrisma.shortcode.delete.mockResolvedValueOnce(mockEmbed); mockPrisma.shortcode.delete.mockResolvedValueOnce(shortCodeWithUser);
const result = await shortcodeService.revokeShortCode( const result = await shortcodeService.revokeShortCode(
mockEmbed.id, shortCodeWithUser.id,
mockEmbed.creatorUid, shortCodeWithUser.creatorUid,
); );
expect(mockPrisma.shortcode.delete).toHaveBeenCalledWith({ expect(mockPrisma.shortcode.delete).toHaveBeenCalledWith({
where: { where: {
creator_uid_shortcode_unique: { creator_uid_shortcode_unique: {
creatorUid: mockEmbed.creatorUid, creatorUid: shortCodeWithUser.creatorUid,
id: mockEmbed.id, id: shortCodeWithUser.id,
}, },
}, },
}); });
@@ -339,53 +249,52 @@ describe('ShortcodeService', () => {
expect(result).toEqualRight(true); expect(result).toEqualRight(true);
}); });
test('should return SHORTCODE_NOT_FOUND error when Shortcode is invalid and user uid is valid', async () => { test('should return SHORTCODE_NOT_FOUND error when shortcode is invalid and user uid is valid', async () => {
mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound'); mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound');
expect( expect(
shortcodeService.revokeShortCode('invalid', 'testuser'), shortcodeService.revokeShortCode('invalid', 'testuser'),
).resolves.toEqualLeft(SHORTCODE_NOT_FOUND); ).resolves.toEqualLeft(SHORTCODE_NOT_FOUND);
}); });
test('should return SHORTCODE_NOT_FOUND error when Shortcode is valid and user uid is invalid', async () => { test('should return SHORTCODE_NOT_FOUND error when shortcode is valid and user uid is invalid', async () => {
mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound'); mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound');
expect( expect(
shortcodeService.revokeShortCode('blablablabla', 'invalidUser'), shortcodeService.revokeShortCode('blablablabla', 'invalidUser'),
).resolves.toEqualLeft(SHORTCODE_NOT_FOUND); ).resolves.toEqualLeft(SHORTCODE_NOT_FOUND);
}); });
test('should return SHORTCODE_NOT_FOUND error when both Shortcode and user uid are invalid', async () => { test('should return SHORTCODE_NOT_FOUND error when both shortcode and user uid are invalid', async () => {
mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound'); mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound');
expect( expect(
shortcodeService.revokeShortCode('invalid', 'invalid'), shortcodeService.revokeShortCode('invalid', 'invalid'),
).resolves.toEqualLeft(SHORTCODE_NOT_FOUND); ).resolves.toEqualLeft(SHORTCODE_NOT_FOUND);
}); });
test('should send pubsub message to `shortcode/{uid}/revoked` on successful deletion of Shortcode', async () => { test('should send pubsub message to `shortcode/{uid}/revoked` on successful deletion of shortcode', async () => {
mockPrisma.shortcode.delete.mockResolvedValueOnce(mockEmbed); mockPrisma.shortcode.delete.mockResolvedValueOnce(shortCodeWithUser);
const result = await shortcodeService.revokeShortCode( const result = await shortcodeService.revokeShortCode(
mockEmbed.id, shortCodeWithUser.id,
mockEmbed.creatorUid, shortCodeWithUser.creatorUid,
); );
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`shortcode/${mockEmbed.creatorUid}/revoked`, `shortcode/${shortCodeWithUser.creatorUid}/revoked`,
{ {
id: mockEmbed.id, id: shortCodeWithUser.id,
createdOn: mockEmbed.createdOn, createdOn: shortCodeWithUser.createdOn,
request: JSON.stringify(mockEmbed.request), request: JSON.stringify(shortCodeWithUser.request),
properties: JSON.stringify(mockEmbed.embedProperties),
}, },
); );
}); });
}); });
describe('deleteUserShortCodes', () => { describe('deleteUserShortCodes', () => {
test('should successfully delete all users Shortcodes with valid user uid', async () => { test('should successfully delete all users shortcodes with valid user uid', async () => {
mockPrisma.shortcode.deleteMany.mockResolvedValueOnce({ count: 1 }); mockPrisma.shortcode.deleteMany.mockResolvedValueOnce({ count: 1 });
const result = await shortcodeService.deleteUserShortCodes( const result = await shortcodeService.deleteUserShortCodes(
mockEmbed.creatorUid, shortCodeWithUser.creatorUid,
); );
expect(result).toEqual(1); expect(result).toEqual(1);
}); });
@@ -394,176 +303,9 @@ describe('ShortcodeService', () => {
mockPrisma.shortcode.deleteMany.mockResolvedValueOnce({ count: 0 }); mockPrisma.shortcode.deleteMany.mockResolvedValueOnce({ count: 0 });
const result = await shortcodeService.deleteUserShortCodes( const result = await shortcodeService.deleteUserShortCodes(
mockEmbed.creatorUid, shortCodeWithUser.creatorUid,
); );
expect(result).toEqual(0); expect(result).toEqual(0);
}); });
}); });
describe('updateShortcode', () => {
test('should return SHORTCODE_PROPERTIES_NOT_FOUND error when updatedProps in invalid', async () => {
const result = await shortcodeService.updateEmbedProperties(
mockEmbed.id,
user.uid,
'',
);
expect(result).toEqualLeft(SHORTCODE_PROPERTIES_NOT_FOUND);
});
test('should return SHORTCODE_PROPERTIES_NOT_FOUND error when updatedProps in invalid JSON format', async () => {
const result = await shortcodeService.updateEmbedProperties(
mockEmbed.id,
user.uid,
'{kk',
);
expect(result).toEqualLeft(SHORTCODE_INVALID_PROPERTIES_JSON);
});
test('should return SHORTCODE_NOT_FOUND error when Shortcode ID is invalid', async () => {
mockPrisma.shortcode.update.mockRejectedValue('RecordNotFound');
const result = await shortcodeService.updateEmbedProperties(
'invalidID',
user.uid,
'{}',
);
expect(result).toEqualLeft(SHORTCODE_NOT_FOUND);
});
test('should successfully update a Shortcodes with valid inputs', async () => {
mockPrisma.shortcode.update.mockResolvedValueOnce({
...mockEmbed,
embedProperties: '{"foo":"bar"}',
});
const result = await shortcodeService.updateEmbedProperties(
mockEmbed.id,
user.uid,
'{"foo":"bar"}',
);
expect(result).toEqualRight({
id: mockEmbed.id,
createdOn: mockEmbed.createdOn,
request: JSON.stringify(mockEmbed.request),
properties: JSON.stringify('{"foo":"bar"}'),
});
});
test('should send pubsub message to `shortcode/{uid}/updated` on successful Update of Shortcode', async () => {
mockPrisma.shortcode.update.mockResolvedValueOnce({
...mockEmbed,
embedProperties: '{"foo":"bar"}',
});
const result = await shortcodeService.updateEmbedProperties(
mockEmbed.id,
user.uid,
'{"foo":"bar"}',
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`shortcode/${mockEmbed.creatorUid}/updated`,
{
id: mockEmbed.id,
createdOn: mockEmbed.createdOn,
request: JSON.stringify(mockEmbed.request),
properties: JSON.stringify('{"foo":"bar"}'),
},
);
});
});
describe('deleteShortcode', () => {
test('should return true on successful deletion of Shortcode with valid inputs', async () => {
mockPrisma.shortcode.delete.mockResolvedValueOnce(mockEmbed);
const result = await shortcodeService.deleteShortcode(mockEmbed.id);
expect(result).toEqualRight(true);
});
test('should return SHORTCODE_NOT_FOUND error when Shortcode is invalid', async () => {
mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound');
expect(shortcodeService.deleteShortcode('invalid')).resolves.toEqualLeft(
SHORTCODE_NOT_FOUND,
);
});
});
describe('fetchAllShortcodes', () => {
test('should return list of Shortcodes with valid inputs and no cursor', async () => {
mockPrisma.shortcode.findMany.mockResolvedValueOnce(
shortcodesWithUserEmail,
);
const result = await shortcodeService.fetchAllShortcodes(
{
cursor: null,
take: 10,
},
user.email,
);
expect(result).toEqual(<ShortcodeWithUserEmail[]>[
{
id: shortcodes[0].id,
request: JSON.stringify(shortcodes[0].request),
properties: JSON.stringify(shortcodes[0].embedProperties),
createdOn: shortcodes[0].createdOn,
creator: {
uid: user.uid,
email: user.email,
},
},
{
id: shortcodes[1].id,
request: JSON.stringify(shortcodes[1].request),
properties: JSON.stringify(shortcodes[1].embedProperties),
createdOn: shortcodes[1].createdOn,
creator: {
uid: user.uid,
email: user.email,
},
},
]);
});
test('should return list of Shortcode with valid inputs and cursor', async () => {
mockPrisma.shortcode.findMany.mockResolvedValue([
shortcodesWithUserEmail[1],
]);
const result = await shortcodeService.fetchAllShortcodes(
{
cursor: 'blablabla',
take: 10,
},
user.email,
);
expect(result).toEqual(<ShortcodeWithUserEmail[]>[
{
id: shortcodes[1].id,
request: JSON.stringify(shortcodes[1].request),
properties: JSON.stringify(shortcodes[1].embedProperties),
createdOn: shortcodes[1].createdOn,
creator: {
uid: user.uid,
email: user.email,
},
},
]);
});
test('should return an empty array for an invalid cursor', async () => {
mockPrisma.shortcode.findMany.mockResolvedValue([]);
const result = await shortcodeService.fetchAllShortcodes(
{
cursor: 'invalidcursor',
take: 10,
},
user.email,
);
expect(result).toHaveLength(0);
});
});
}); });

View File

@@ -1,16 +1,12 @@
import { Injectable, OnModuleInit } from '@nestjs/common'; import { Injectable, OnModuleInit } from '@nestjs/common';
import * as T from 'fp-ts/Task'; import * as T from 'fp-ts/Task';
import * as O from 'fp-ts/Option';
import * as TO from 'fp-ts/TaskOption'; import * as TO from 'fp-ts/TaskOption';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
import { PrismaService } from 'src/prisma/prisma.service'; import { PrismaService } from 'src/prisma/prisma.service';
import { import { SHORTCODE_INVALID_JSON, SHORTCODE_NOT_FOUND } from 'src/errors';
SHORTCODE_INVALID_PROPERTIES_JSON,
SHORTCODE_INVALID_REQUEST_JSON,
SHORTCODE_NOT_FOUND,
SHORTCODE_PROPERTIES_NOT_FOUND,
} from 'src/errors';
import { UserDataHandler } from 'src/user/user.data.handler'; import { UserDataHandler } from 'src/user/user.data.handler';
import { Shortcode, ShortcodeWithUserEmail } from './shortcode.model'; import { Shortcode } from './shortcode.model';
import { Shortcode as DBShortCode } from '@prisma/client'; import { Shortcode as DBShortCode } from '@prisma/client';
import { PubSubService } from 'src/pubsub/pubsub.service'; import { PubSubService } from 'src/pubsub/pubsub.service';
import { UserService } from 'src/user/user.service'; import { UserService } from 'src/user/user.service';
@@ -50,14 +46,10 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
* @param shortcodeInfo Prisma Shortcode type * @param shortcodeInfo Prisma Shortcode type
* @returns GQL Shortcode * @returns GQL Shortcode
*/ */
private cast(shortcodeInfo: DBShortCode): Shortcode { private returnShortCode(shortcodeInfo: DBShortCode): Shortcode {
return <Shortcode>{ return <Shortcode>{
id: shortcodeInfo.id, id: shortcodeInfo.id,
request: JSON.stringify(shortcodeInfo.request), request: JSON.stringify(shortcodeInfo.request),
properties:
shortcodeInfo.embedProperties != null
? JSON.stringify(shortcodeInfo.embedProperties)
: null,
createdOn: shortcodeInfo.createdOn, createdOn: shortcodeInfo.createdOn,
}; };
} }
@@ -102,7 +94,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
const shortcodeInfo = await this.prisma.shortcode.findFirstOrThrow({ const shortcodeInfo = await this.prisma.shortcode.findFirstOrThrow({
where: { id: shortcode }, where: { id: shortcode },
}); });
return E.right(this.cast(shortcodeInfo)); return E.right(this.returnShortCode(shortcodeInfo));
} catch (error) { } catch (error) {
return E.left(SHORTCODE_NOT_FOUND); return E.left(SHORTCODE_NOT_FOUND);
} }
@@ -112,22 +104,14 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
* Create a new ShortCode * Create a new ShortCode
* *
* @param request JSON string of request details * @param request JSON string of request details
* @param userInfo user UI * @param userUID user UID, if present
* @param properties JSON string of embed properties, if present
* @returns Either of ShortCode or error * @returns Either of ShortCode or error
*/ */
async createShortcode( async createShortcode(request: string, userUID: string | null) {
request: string, const shortcodeData = stringToJson(request);
properties: string | null = null, if (E.isLeft(shortcodeData)) return E.left(SHORTCODE_INVALID_JSON);
userInfo: AuthUser,
) {
const requestData = stringToJson(request);
if (E.isLeft(requestData) || !requestData.right)
return E.left(SHORTCODE_INVALID_REQUEST_JSON);
const parsedProperties = stringToJson(properties); const user = await this.userService.findUserById(userUID);
if (E.isLeft(parsedProperties))
return E.left(SHORTCODE_INVALID_PROPERTIES_JSON);
const generatedShortCode = await this.generateUniqueShortCodeID(); const generatedShortCode = await this.generateUniqueShortCodeID();
if (E.isLeft(generatedShortCode)) return E.left(generatedShortCode.left); if (E.isLeft(generatedShortCode)) return E.left(generatedShortCode.left);
@@ -135,9 +119,8 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
const createdShortCode = await this.prisma.shortcode.create({ const createdShortCode = await this.prisma.shortcode.create({
data: { data: {
id: generatedShortCode.right, id: generatedShortCode.right,
request: requestData.right, request: shortcodeData.right,
embedProperties: parsedProperties.right ?? undefined, creatorUid: O.isNone(user) ? null : user.value.uid,
creatorUid: userInfo.uid,
}, },
}); });
@@ -145,11 +128,11 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
if (createdShortCode.creatorUid) { if (createdShortCode.creatorUid) {
this.pubsub.publish( this.pubsub.publish(
`shortcode/${createdShortCode.creatorUid}/created`, `shortcode/${createdShortCode.creatorUid}/created`,
this.cast(createdShortCode), this.returnShortCode(createdShortCode),
); );
} }
return E.right(this.cast(createdShortCode)); return E.right(this.returnShortCode(createdShortCode));
} }
/** /**
@@ -173,14 +156,14 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
}); });
const fetchedShortCodes: Shortcode[] = shortCodes.map((code) => const fetchedShortCodes: Shortcode[] = shortCodes.map((code) =>
this.cast(code), this.returnShortCode(code),
); );
return fetchedShortCodes; return fetchedShortCodes;
} }
/** /**
* Delete a ShortCode created by User of uid * Delete a ShortCode
* *
* @param shortcode ShortCode * @param shortcode ShortCode
* @param uid User Uid * @param uid User Uid
@@ -199,7 +182,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
this.pubsub.publish( this.pubsub.publish(
`shortcode/${deletedShortCodes.creatorUid}/revoked`, `shortcode/${deletedShortCodes.creatorUid}/revoked`,
this.cast(deletedShortCodes), this.returnShortCode(deletedShortCodes),
); );
return E.right(true); return E.right(true);
@@ -222,118 +205,4 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
return deletedShortCodes.count; return deletedShortCodes.count;
} }
/**
* Delete a Shortcode
*
* @param shortcodeID ID of Shortcode being deleted
* @returns Boolean on successful deletion
*/
async deleteShortcode(shortcodeID: string) {
try {
await this.prisma.shortcode.delete({
where: {
id: shortcodeID,
},
});
return E.right(true);
} catch (error) {
return E.left(SHORTCODE_NOT_FOUND);
}
}
/**
* Update a created Shortcode
* @param shortcodeID Shortcode ID
* @param uid User Uid
* @returns Updated Shortcode
*/
async updateEmbedProperties(
shortcodeID: string,
uid: string,
updatedProps: string,
) {
if (!updatedProps) return E.left(SHORTCODE_PROPERTIES_NOT_FOUND);
const parsedProperties = stringToJson(updatedProps);
if (E.isLeft(parsedProperties) || !parsedProperties.right)
return E.left(SHORTCODE_INVALID_PROPERTIES_JSON);
try {
const updatedShortcode = await this.prisma.shortcode.update({
where: {
creator_uid_shortcode_unique: {
creatorUid: uid,
id: shortcodeID,
},
},
data: {
embedProperties: parsedProperties.right,
},
});
this.pubsub.publish(
`shortcode/${updatedShortcode.creatorUid}/updated`,
this.cast(updatedShortcode),
);
return E.right(this.cast(updatedShortcode));
} catch (error) {
return E.left(SHORTCODE_NOT_FOUND);
}
}
/**
* Fetch all created ShortCodes
*
* @param args Pagination arguments
* @param userEmail User email
* @returns ShortcodeWithUserEmail
*/
async fetchAllShortcodes(
args: PaginationArgs,
userEmail: string | null = null,
) {
const shortCodes = await this.prisma.shortcode.findMany({
where: userEmail
? {
User: {
email: userEmail,
},
}
: undefined,
orderBy: {
createdOn: 'desc',
},
skip: args.cursor ? 1 : 0,
take: args.take,
cursor: args.cursor ? { id: args.cursor } : undefined,
include: {
User: true,
},
});
const fetchedShortCodes: ShortcodeWithUserEmail[] = shortCodes.map(
(code) => {
return <ShortcodeWithUserEmail>{
id: code.id,
request: JSON.stringify(code.request),
properties:
code.embedProperties != null
? JSON.stringify(code.embedProperties)
: null,
createdOn: code.createdOn,
creator: code.User
? {
uid: code.User.uid,
email: code.User.email,
}
: null,
};
},
);
return fetchedShortCodes;
}
} }

View File

@@ -14,13 +14,6 @@ export class CreateRootTeamCollectionArgs {
@Field({ name: 'title', description: 'Title of the new collection' }) @Field({ name: 'title', description: 'Title of the new collection' })
title: string; title: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
} }
@ArgsType() @ArgsType()
@@ -33,13 +26,6 @@ export class CreateChildTeamCollectionArgs {
@Field({ name: 'childTitle', description: 'Title of the new collection' }) @Field({ name: 'childTitle', description: 'Title of the new collection' })
childTitle: string; childTitle: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
} }
@ArgsType() @ArgsType()
@@ -47,14 +33,12 @@ export class RenameTeamCollectionArgs {
@Field(() => ID, { @Field(() => ID, {
name: 'collectionID', name: 'collectionID',
description: 'ID of the collection', description: 'ID of the collection',
deprecationReason: 'Switch to updateTeamCollection mutation instead',
}) })
collectionID: string; collectionID: string;
@Field({ @Field({
name: 'newTitle', name: 'newTitle',
description: 'The updated title of the collection', description: 'The updated title of the collection',
deprecationReason: 'Switch to updateTeamCollection mutation instead',
}) })
newTitle: string; newTitle: string;
} }
@@ -114,26 +98,3 @@ export class ReplaceTeamCollectionArgs {
}) })
parentCollectionID?: string; parentCollectionID?: string;
} }
@ArgsType()
export class UpdateTeamCollectionArgs {
@Field(() => ID, {
name: 'collectionID',
description: 'ID of the collection',
})
collectionID: string;
@Field({
name: 'newTitle',
description: 'The updated title of the collection',
nullable: true,
})
newTitle: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
}

View File

@@ -12,17 +12,12 @@ export class TeamCollection {
}) })
title: string; title: string;
@Field({
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
@Field(() => ID, { @Field(() => ID, {
description: 'ID of the collection', description: 'ID of the collection',
nullable: true, nullable: true,
}) })
parentID: string; parentID: string;
teamID: string;
} }
@ObjectType() @ObjectType()

View File

@@ -25,7 +25,6 @@ import {
MoveTeamCollectionArgs, MoveTeamCollectionArgs,
RenameTeamCollectionArgs, RenameTeamCollectionArgs,
ReplaceTeamCollectionArgs, ReplaceTeamCollectionArgs,
UpdateTeamCollectionArgs,
UpdateTeamCollectionOrderArgs, UpdateTeamCollectionOrderArgs,
} from './input-type.args'; } from './input-type.args';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
@@ -142,14 +141,7 @@ export class TeamCollectionResolver {
); );
if (E.isLeft(teamCollections)) throwErr(teamCollections.left); if (E.isLeft(teamCollections)) throwErr(teamCollections.left);
return <TeamCollection>{ return teamCollections.right;
id: teamCollections.right.id,
title: teamCollections.right.title,
parentID: teamCollections.right.parentID,
data: !teamCollections.right.data
? null
: JSON.stringify(teamCollections.right.data),
};
} }
// Mutations // Mutations
@@ -163,7 +155,6 @@ export class TeamCollectionResolver {
const teamCollection = await this.teamCollectionService.createCollection( const teamCollection = await this.teamCollectionService.createCollection(
args.teamID, args.teamID,
args.title, args.title,
args.data,
null, null,
); );
@@ -239,7 +230,6 @@ export class TeamCollectionResolver {
const teamCollection = await this.teamCollectionService.createCollection( const teamCollection = await this.teamCollectionService.createCollection(
team.right.id, team.right.id,
args.childTitle, args.childTitle,
args.data,
args.collectionID, args.collectionID,
); );
@@ -249,7 +239,6 @@ export class TeamCollectionResolver {
@Mutation(() => TeamCollection, { @Mutation(() => TeamCollection, {
description: 'Rename a collection', description: 'Rename a collection',
deprecationReason: 'Switch to updateTeamCollection mutation instead',
}) })
@UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard) @UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard)
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR) @RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
@@ -314,23 +303,6 @@ export class TeamCollectionResolver {
return request.right; return request.right;
} }
@Mutation(() => TeamCollection, {
description: 'Update Team Collection details',
})
@UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard)
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
async updateTeamCollection(@Args() args: UpdateTeamCollectionArgs) {
const updatedTeamCollection =
await this.teamCollectionService.updateTeamCollection(
args.collectionID,
args.data,
args.newTitle,
);
if (E.isLeft(updatedTeamCollection)) throwErr(updatedTeamCollection.left);
return updatedTeamCollection.right;
}
// Subscriptions // Subscriptions
@Subscription(() => TeamCollection, { @Subscription(() => TeamCollection, {

View File

@@ -1,7 +1,6 @@
import { Team, TeamCollection as DBTeamCollection } from '@prisma/client'; import { Team, TeamCollection as DBTeamCollection } from '@prisma/client';
import { mockDeep, mockReset } from 'jest-mock-extended'; import { mockDeep, mockReset } from 'jest-mock-extended';
import { import {
TEAM_COLL_DATA_INVALID,
TEAM_COLL_DEST_SAME, TEAM_COLL_DEST_SAME,
TEAM_COLL_INVALID_JSON, TEAM_COLL_INVALID_JSON,
TEAM_COLL_IS_PARENT_COLL, TEAM_COLL_IS_PARENT_COLL,
@@ -18,7 +17,6 @@ import { PrismaService } from 'src/prisma/prisma.service';
import { PubSubService } from 'src/pubsub/pubsub.service'; import { PubSubService } from 'src/pubsub/pubsub.service';
import { AuthUser } from 'src/types/AuthUser'; import { AuthUser } from 'src/types/AuthUser';
import { TeamCollectionService } from './team-collection.service'; import { TeamCollectionService } from './team-collection.service';
import { TeamCollection } from './team-collection.model';
const mockPrisma = mockDeep<PrismaService>(); const mockPrisma = mockDeep<PrismaService>();
const mockPubSub = mockDeep<PubSubService>(); const mockPubSub = mockDeep<PubSubService>();
@@ -53,60 +51,35 @@ const rootTeamCollection: DBTeamCollection = {
id: '123', id: '123',
orderIndex: 1, orderIndex: 1,
parentID: null, parentID: null,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
}; };
const rootTeamCollectionsCasted: TeamCollection = {
id: rootTeamCollection.id,
title: rootTeamCollection.title,
parentID: rootTeamCollection.parentID,
data: JSON.stringify(rootTeamCollection.data),
};
const rootTeamCollection_2: DBTeamCollection = { const rootTeamCollection_2: DBTeamCollection = {
id: 'erv', id: 'erv',
orderIndex: 2, orderIndex: 2,
parentID: null, parentID: null,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
}; };
const rootTeamCollection_2Casted: TeamCollection = {
id: 'erv',
parentID: null,
data: JSON.stringify(rootTeamCollection_2.data),
title: 'Root Collection 1',
};
const childTeamCollection: DBTeamCollection = { const childTeamCollection: DBTeamCollection = {
id: 'rfe', id: 'rfe',
orderIndex: 1, orderIndex: 1,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
data: {},
title: 'Child Collection 1', title: 'Child Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
}; };
const childTeamCollectionCasted: TeamCollection = {
id: 'rfe',
parentID: rootTeamCollection.id,
data: JSON.stringify(childTeamCollection.data),
title: 'Child Collection 1',
};
const childTeamCollection_2: DBTeamCollection = { const childTeamCollection_2: DBTeamCollection = {
id: 'bgdz', id: 'bgdz',
orderIndex: 1, orderIndex: 1,
data: {},
parentID: rootTeamCollection_2.id, parentID: rootTeamCollection_2.id,
title: 'Child Collection 1', title: 'Child Collection 1',
teamID: team.id, teamID: team.id,
@@ -114,20 +87,11 @@ const childTeamCollection_2: DBTeamCollection = {
updatedOn: currentTime, updatedOn: currentTime,
}; };
const childTeamCollection_2Casted: TeamCollection = {
id: 'bgdz',
data: JSON.stringify(childTeamCollection_2.data),
parentID: rootTeamCollection_2.id,
title: 'Child Collection 1',
};
const rootTeamCollectionList: DBTeamCollection[] = [ const rootTeamCollectionList: DBTeamCollection[] = [
{ {
id: 'fdv', id: 'fdv',
orderIndex: 1, orderIndex: 1,
parentID: null, parentID: null,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
@@ -138,8 +102,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 2, orderIndex: 2,
parentID: null, parentID: null,
title: 'Root Collection 1', title: 'Root Collection 1',
data: {},
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
@@ -149,8 +111,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 3, orderIndex: 3,
parentID: null, parentID: null,
title: 'Root Collection 1', title: 'Root Collection 1',
data: {},
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
@@ -159,8 +119,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
id: 'bre3', id: 'bre3',
orderIndex: 4, orderIndex: 4,
parentID: null, parentID: null,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
@@ -171,8 +129,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 5, orderIndex: 5,
parentID: null, parentID: null,
title: 'Root Collection 1', title: 'Root Collection 1',
data: {},
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
@@ -183,8 +139,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
parentID: null, parentID: null,
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
data: {},
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
}, },
@@ -194,8 +148,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
parentID: null, parentID: null,
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
data: {},
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
}, },
@@ -204,7 +156,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 8, orderIndex: 8,
parentID: null, parentID: null,
title: 'Root Collection 1', title: 'Root Collection 1',
data: {},
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
@@ -214,7 +165,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 9, orderIndex: 9,
parentID: null, parentID: null,
title: 'Root Collection 1', title: 'Root Collection 1',
data: {},
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
@@ -225,83 +175,17 @@ const rootTeamCollectionList: DBTeamCollection[] = [
parentID: null, parentID: null,
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
data: {},
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
}, },
]; ];
const rootTeamCollectionListCasted: TeamCollection[] = [
{
id: 'fdv',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: 'fbbg',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: 'fgbfg',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: 'bre3',
parentID: null,
data: JSON.stringify(rootTeamCollection.data),
title: 'Root Collection 1',
},
{
id: 'hghgf',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: '123',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: '54tyh',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: '234re',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: '34rtg',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: '45tgh',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
];
const childTeamCollectionList: DBTeamCollection[] = [ const childTeamCollectionList: DBTeamCollection[] = [
{ {
id: '123', id: '123',
orderIndex: 1, orderIndex: 1,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
title: 'Root Collection 1', title: 'Root Collection 1',
data: {},
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
@@ -311,8 +195,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
orderIndex: 2, orderIndex: 2,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
title: 'Root Collection 1', title: 'Root Collection 1',
data: {},
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
@@ -322,8 +204,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
orderIndex: 3, orderIndex: 3,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
title: 'Root Collection 1', title: 'Root Collection 1',
data: {},
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
updatedOn: currentTime, updatedOn: currentTime,
@@ -332,8 +212,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '567', id: '567',
orderIndex: 4, orderIndex: 4,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
@@ -343,8 +221,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '123', id: '123',
orderIndex: 5, orderIndex: 5,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
@@ -354,8 +230,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '678', id: '678',
orderIndex: 6, orderIndex: 6,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
@@ -365,8 +239,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '789', id: '789',
orderIndex: 7, orderIndex: 7,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
@@ -376,8 +248,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '890', id: '890',
orderIndex: 8, orderIndex: 8,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
@@ -387,7 +257,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '012', id: '012',
orderIndex: 9, orderIndex: 9,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
@@ -397,8 +266,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '0bhu', id: '0bhu',
orderIndex: 10, orderIndex: 10,
parentID: rootTeamCollection.id, parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1', title: 'Root Collection 1',
teamID: team.id, teamID: team.id,
createdOn: currentTime, createdOn: currentTime,
@@ -406,75 +273,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
}, },
]; ];
const childTeamCollectionListCasted: TeamCollection[] = [
{
id: '123',
parentID: rootTeamCollection.id,
title: 'Root Collection 1',
data: JSON.stringify({}),
},
{
id: '345',
parentID: rootTeamCollection.id,
title: 'Root Collection 1',
data: JSON.stringify({}),
},
{
id: '456',
parentID: rootTeamCollection.id,
title: 'Root Collection 1',
data: JSON.stringify({}),
},
{
id: '567',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '123',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '678',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '789',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '890',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '012',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '0bhu',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
];
beforeEach(() => { beforeEach(() => {
mockReset(mockPrisma); mockReset(mockPrisma);
mockPubSub.publish.mockClear(); mockPubSub.publish.mockClear();
@@ -513,7 +311,7 @@ describe('getParentOfCollection', () => {
const result = await teamCollectionService.getParentOfCollection( const result = await teamCollectionService.getParentOfCollection(
childTeamCollection.id, childTeamCollection.id,
); );
expect(result).toEqual(rootTeamCollectionsCasted); expect(result).toEqual(rootTeamCollection);
}); });
test('should return null successfully for a root collection with valid collectionID', async () => { test('should return null successfully for a root collection with valid collectionID', async () => {
@@ -549,7 +347,7 @@ describe('getChildrenOfCollection', () => {
null, null,
10, 10,
); );
expect(result).toEqual(childTeamCollectionListCasted); expect(result).toEqual(childTeamCollectionList);
}); });
test('should return a list of 3 child collections successfully with cursor being equal to the 7th item in the list', async () => { test('should return a list of 3 child collections successfully with cursor being equal to the 7th item in the list', async () => {
@@ -565,9 +363,9 @@ describe('getChildrenOfCollection', () => {
10, 10,
); );
expect(result).toEqual([ expect(result).toEqual([
{ ...childTeamCollectionListCasted[7] }, { ...childTeamCollectionList[7] },
{ ...childTeamCollectionListCasted[8] }, { ...childTeamCollectionList[8] },
{ ...childTeamCollectionListCasted[9] }, { ...childTeamCollectionList[9] },
]); ]);
}); });
@@ -594,7 +392,7 @@ describe('getTeamRootCollections', () => {
null, null,
10, 10,
); );
expect(result).toEqual(rootTeamCollectionListCasted); expect(result).toEqual(rootTeamCollectionList);
}); });
test('should return a list of 3 root collections successfully with cursor being equal to the 7th item in the list', async () => { test('should return a list of 3 root collections successfully with cursor being equal to the 7th item in the list', async () => {
@@ -610,9 +408,9 @@ describe('getTeamRootCollections', () => {
10, 10,
); );
expect(result).toEqual([ expect(result).toEqual([
{ ...rootTeamCollectionListCasted[7] }, { ...rootTeamCollectionList[7] },
{ ...rootTeamCollectionListCasted[8] }, { ...rootTeamCollectionList[8] },
{ ...rootTeamCollectionListCasted[9] }, { ...rootTeamCollectionList[9] },
]); ]);
}); });
@@ -666,7 +464,6 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection( const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID, rootTeamCollection.teamID,
'ab', 'ab',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id, rootTeamCollection.id,
); );
expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE); expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE);
@@ -681,27 +478,11 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection( const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID, rootTeamCollection.teamID,
'abcd', 'abcd',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id, rootTeamCollection.id,
); );
expect(result).toEqualLeft(TEAM_NOT_OWNER); expect(result).toEqualLeft(TEAM_NOT_OWNER);
}); });
test('should throw TEAM_COLL_DATA_INVALID when parent TeamCollection does not belong to the team', async () => {
// isOwnerCheck
mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce(
rootTeamCollection,
);
const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID,
'abcd',
'{',
rootTeamCollection.id,
);
expect(result).toEqualLeft(TEAM_COLL_DATA_INVALID);
});
test('should successfully create a new root TeamCollection with valid inputs', async () => { test('should successfully create a new root TeamCollection with valid inputs', async () => {
// isOwnerCheck // isOwnerCheck
mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce( mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce(
@@ -715,10 +496,9 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection( const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID, rootTeamCollection.teamID,
'abcdefg', 'abcdefg',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id, rootTeamCollection.id,
); );
expect(result).toEqualRight(rootTeamCollectionsCasted); expect(result).toEqualRight(rootTeamCollection);
}); });
test('should successfully create a new child TeamCollection with valid inputs', async () => { test('should successfully create a new child TeamCollection with valid inputs', async () => {
@@ -734,10 +514,9 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection( const result = await teamCollectionService.createCollection(
childTeamCollection.teamID, childTeamCollection.teamID,
childTeamCollection.title, childTeamCollection.title,
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id, rootTeamCollection.id,
); );
expect(result).toEqualRight(childTeamCollectionCasted); expect(result).toEqualRight(childTeamCollection);
}); });
test('should send pubsub message to "team_coll/<teamID>/coll_added" if child TeamCollection is created successfully', async () => { test('should send pubsub message to "team_coll/<teamID>/coll_added" if child TeamCollection is created successfully', async () => {
@@ -753,13 +532,11 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection( const result = await teamCollectionService.createCollection(
childTeamCollection.teamID, childTeamCollection.teamID,
childTeamCollection.title, childTeamCollection.title,
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id, rootTeamCollection.id,
); );
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollection.teamID}/coll_added`, `team_coll/${childTeamCollection.teamID}/coll_added`,
childTeamCollectionCasted, childTeamCollection,
); );
}); });
@@ -776,13 +553,11 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection( const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID, rootTeamCollection.teamID,
'abcdefg', 'abcdefg',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id, rootTeamCollection.id,
); );
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_added`, `team_coll/${rootTeamCollection.teamID}/coll_added`,
rootTeamCollectionsCasted, rootTeamCollection,
); );
}); });
}); });
@@ -812,7 +587,7 @@ describe('renameCollection', () => {
'NewTitle', 'NewTitle',
); );
expect(result).toEqualRight({ expect(result).toEqualRight({
...rootTeamCollectionsCasted, ...rootTeamCollection,
title: 'NewTitle', title: 'NewTitle',
}); });
}); });
@@ -850,7 +625,7 @@ describe('renameCollection', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_updated`, `team_coll/${rootTeamCollection.teamID}/coll_updated`,
{ {
...rootTeamCollectionsCasted, ...rootTeamCollection,
title: 'NewTitle', title: 'NewTitle',
}, },
); );
@@ -1057,8 +832,9 @@ describe('moveCollection', () => {
null, null,
); );
expect(result).toEqualRight({ expect(result).toEqualRight({
...childTeamCollectionCasted, ...childTeamCollection,
parentID: null, parentID: null,
orderIndex: 2,
}); });
}); });
@@ -1114,8 +890,9 @@ describe('moveCollection', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollection.teamID}/coll_moved`, `team_coll/${childTeamCollection.teamID}/coll_moved`,
{ {
...childTeamCollectionCasted, ...childTeamCollection,
parentID: null, parentID: null,
orderIndex: 2,
}, },
); );
}); });
@@ -1154,8 +931,9 @@ describe('moveCollection', () => {
childTeamCollection_2.id, childTeamCollection_2.id,
); );
expect(result).toEqualRight({ expect(result).toEqualRight({
...rootTeamCollectionsCasted, ...rootTeamCollection,
parentID: childTeamCollection_2Casted.id, parentID: childTeamCollection_2.id,
orderIndex: 1,
}); });
}); });
@@ -1195,8 +973,9 @@ describe('moveCollection', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollection_2.teamID}/coll_moved`, `team_coll/${childTeamCollection_2.teamID}/coll_moved`,
{ {
...rootTeamCollectionsCasted, ...rootTeamCollection,
parentID: childTeamCollection_2Casted.id, parentID: childTeamCollection_2.id,
orderIndex: 1,
}, },
); );
}); });
@@ -1235,8 +1014,9 @@ describe('moveCollection', () => {
childTeamCollection_2.id, childTeamCollection_2.id,
); );
expect(result).toEqualRight({ expect(result).toEqualRight({
...childTeamCollectionCasted, ...childTeamCollection,
parentID: childTeamCollection_2Casted.id, parentID: childTeamCollection_2.id,
orderIndex: 1,
}); });
}); });
@@ -1276,8 +1056,9 @@ describe('moveCollection', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollection.teamID}/coll_moved`, `team_coll/${childTeamCollection.teamID}/coll_moved`,
{ {
...childTeamCollectionCasted, ...childTeamCollection,
parentID: childTeamCollection_2Casted.id, parentID: childTeamCollection_2.id,
orderIndex: 1,
}, },
); );
}); });
@@ -1373,7 +1154,7 @@ describe('updateCollectionOrder', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollectionList[4].teamID}/coll_order_updated`, `team_coll/${childTeamCollectionList[4].teamID}/coll_order_updated`,
{ {
collection: rootTeamCollectionListCasted[4], collection: rootTeamCollectionList[4],
nextCollection: null, nextCollection: null,
}, },
); );
@@ -1454,8 +1235,8 @@ describe('updateCollectionOrder', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollectionList[2].teamID}/coll_order_updated`, `team_coll/${childTeamCollectionList[2].teamID}/coll_order_updated`,
{ {
collection: childTeamCollectionListCasted[4], collection: childTeamCollectionList[4],
nextCollection: childTeamCollectionListCasted[2], nextCollection: childTeamCollectionList[2],
}, },
); );
}); });
@@ -1521,7 +1302,7 @@ describe('importCollectionsFromJSON', () => {
); );
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_added`, `team_coll/${rootTeamCollection.teamID}/coll_added`,
rootTeamCollectionsCasted, rootTeamCollection,
); );
}); });
}); });
@@ -1640,7 +1421,7 @@ describe('replaceCollectionsWithJSON', () => {
); );
expect(mockPubSub.publish).toHaveBeenCalledWith( expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_added`, `team_coll/${rootTeamCollection.teamID}/coll_added`,
rootTeamCollectionsCasted, rootTeamCollection,
); );
}); });
}); });
@@ -1677,64 +1458,4 @@ describe('totalCollectionsInTeam', () => {
}); });
}); });
describe('updateTeamCollection', () => {
test('should throw TEAM_COLL_SHORT_TITLE if title is invalid', async () => {
const result = await teamCollectionService.updateTeamCollection(
rootTeamCollection.id,
JSON.stringify(rootTeamCollection.data),
'de',
);
expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE);
});
test('should throw TEAM_COLL_DATA_INVALID is collection data is invalid', async () => {
const result = await teamCollectionService.updateTeamCollection(
rootTeamCollection.id,
'{',
rootTeamCollection.title,
);
expect(result).toEqualLeft(TEAM_COLL_DATA_INVALID);
});
test('should throw TEAM_COLL_NOT_FOUND is collectionID is invalid', async () => {
mockPrisma.teamCollection.update.mockRejectedValueOnce('RecordNotFound');
const result = await teamCollectionService.updateTeamCollection(
'invalid_id',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.title,
);
expect(result).toEqualLeft(TEAM_COLL_NOT_FOUND);
});
test('should successfully update a collection', async () => {
mockPrisma.teamCollection.update.mockResolvedValueOnce(rootTeamCollection);
const result = await teamCollectionService.updateTeamCollection(
rootTeamCollection.id,
JSON.stringify({ foo: 'bar' }),
'new_title',
);
expect(result).toEqualRight({
data: JSON.stringify({ foo: 'bar' }),
title: 'new_title',
...rootTeamCollectionsCasted,
});
});
test('should send pubsub message to "team_coll/<teamID>/coll_updated" if TeamCollection is updated successfully', async () => {
mockPrisma.teamCollection.update.mockResolvedValueOnce(rootTeamCollection);
const result = await teamCollectionService.updateTeamCollection(
rootTeamCollection.id,
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.title,
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_updated`,
rootTeamCollectionsCasted,
);
});
});
//ToDo: write test cases for exportCollectionsToJSON //ToDo: write test cases for exportCollectionsToJSON

View File

@@ -13,7 +13,6 @@ import {
TEAM_COLL_IS_PARENT_COLL, TEAM_COLL_IS_PARENT_COLL,
TEAM_COL_SAME_NEXT_COLL, TEAM_COL_SAME_NEXT_COLL,
TEAM_COL_REORDERING_FAILED, TEAM_COL_REORDERING_FAILED,
TEAM_COLL_DATA_INVALID,
} from '../errors'; } from '../errors';
import { PubSubService } from '../pubsub/pubsub.service'; import { PubSubService } from '../pubsub/pubsub.service';
import { isValidLength } from 'src/utils'; import { isValidLength } from 'src/utils';
@@ -70,7 +69,6 @@ export class TeamCollectionService {
this.generatePrismaQueryObjForFBCollFolder(f, teamID, index + 1), this.generatePrismaQueryObjForFBCollFolder(f, teamID, index + 1),
), ),
}, },
data: folder.data ?? undefined,
}; };
} }
@@ -120,7 +118,6 @@ export class TeamCollectionService {
name: collection.right.title, name: collection.right.title,
folders: childrenCollectionObjects, folders: childrenCollectionObjects,
requests: requests.map((x) => x.request), requests: requests.map((x) => x.request),
data: JSON.stringify(collection.right.data),
}; };
return E.right(result); return E.right(result);
@@ -201,11 +198,8 @@ export class TeamCollectionService {
), ),
); );
teamCollections.forEach((collection) => teamCollections.forEach((x) =>
this.pubsub.publish( this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x),
`team_coll/${destTeamID}/coll_added`,
this.cast(collection),
),
); );
return E.right(true); return E.right(true);
@@ -274,11 +268,8 @@ export class TeamCollectionService {
), ),
); );
teamCollections.forEach((collections) => teamCollections.forEach((x) =>
this.pubsub.publish( this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x),
`team_coll/${destTeamID}/coll_added`,
this.cast(collections),
),
); );
return E.right(true); return E.right(true);
@@ -286,17 +277,11 @@ export class TeamCollectionService {
/** /**
* Typecast a database TeamCollection to a TeamCollection model * Typecast a database TeamCollection to a TeamCollection model
*
* @param teamCollection database TeamCollection * @param teamCollection database TeamCollection
* @returns TeamCollection model * @returns TeamCollection model
*/ */
private cast(teamCollection: DBTeamCollection): TeamCollection { private cast(teamCollection: DBTeamCollection): TeamCollection {
return <TeamCollection>{ return <TeamCollection>{ ...teamCollection };
id: teamCollection.id,
title: teamCollection.title,
parentID: teamCollection.parentID,
data: !teamCollection.data ? null : JSON.stringify(teamCollection.data),
};
} }
/** /**
@@ -339,7 +324,7 @@ export class TeamCollectionService {
}); });
if (!teamCollection) return null; if (!teamCollection) return null;
return !teamCollection.parent ? null : this.cast(teamCollection.parent); return teamCollection.parent;
} }
/** /**
@@ -350,12 +335,12 @@ export class TeamCollectionService {
* @param take Number of items we want returned * @param take Number of items we want returned
* @returns A list of child collections * @returns A list of child collections
*/ */
async getChildrenOfCollection( getChildrenOfCollection(
collectionID: string, collectionID: string,
cursor: string | null, cursor: string | null,
take: number, take: number,
) { ) {
const res = await this.prisma.teamCollection.findMany({ return this.prisma.teamCollection.findMany({
where: { where: {
parentID: collectionID, parentID: collectionID,
}, },
@@ -366,12 +351,6 @@ export class TeamCollectionService {
skip: cursor ? 1 : 0, skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined, cursor: cursor ? { id: cursor } : undefined,
}); });
const childCollections = res.map((teamCollection) =>
this.cast(teamCollection),
);
return childCollections;
} }
/** /**
@@ -387,7 +366,7 @@ export class TeamCollectionService {
cursor: string | null, cursor: string | null,
take: number, take: number,
) { ) {
const res = await this.prisma.teamCollection.findMany({ return this.prisma.teamCollection.findMany({
where: { where: {
teamID, teamID,
parentID: null, parentID: null,
@@ -399,12 +378,6 @@ export class TeamCollectionService {
skip: cursor ? 1 : 0, skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined, cursor: cursor ? { id: cursor } : undefined,
}); });
const teamCollections = res.map((teamCollection) =>
this.cast(teamCollection),
);
return teamCollections;
} }
/** /**
@@ -497,7 +470,6 @@ export class TeamCollectionService {
async createCollection( async createCollection(
teamID: string, teamID: string,
title: string, title: string,
data: string | null = null,
parentTeamCollectionID: string | null, parentTeamCollectionID: string | null,
) { ) {
const isTitleValid = isValidLength(title, this.TITLE_LENGTH); const isTitleValid = isValidLength(title, this.TITLE_LENGTH);
@@ -509,13 +481,6 @@ export class TeamCollectionService {
if (O.isNone(isOwner)) return E.left(TEAM_NOT_OWNER); if (O.isNone(isOwner)) return E.left(TEAM_NOT_OWNER);
} }
if (data === '') return E.left(TEAM_COLL_DATA_INVALID);
if (data) {
const jsonReq = stringToJson(data);
if (E.isLeft(jsonReq)) return E.left(TEAM_COLL_DATA_INVALID);
data = jsonReq.right;
}
const isParent = parentTeamCollectionID const isParent = parentTeamCollectionID
? { ? {
connect: { connect: {
@@ -533,23 +498,18 @@ export class TeamCollectionService {
}, },
}, },
parent: isParent, parent: isParent,
data: data ?? undefined,
orderIndex: !parentTeamCollectionID orderIndex: !parentTeamCollectionID
? (await this.getRootCollectionsCount(teamID)) + 1 ? (await this.getRootCollectionsCount(teamID)) + 1
: (await this.getChildCollectionsCount(parentTeamCollectionID)) + 1, : (await this.getChildCollectionsCount(parentTeamCollectionID)) + 1,
}, },
}); });
this.pubsub.publish( this.pubsub.publish(`team_coll/${teamID}/coll_added`, teamCollection);
`team_coll/${teamID}/coll_added`,
this.cast(teamCollection),
);
return E.right(this.cast(teamCollection)); return E.right(this.cast(teamCollection));
} }
/** /**
* @deprecated Use updateTeamCollection method instead
* Update the title of a TeamCollection * Update the title of a TeamCollection
* *
* @param collectionID The Collection ID * @param collectionID The Collection ID
@@ -572,10 +532,10 @@ export class TeamCollectionService {
this.pubsub.publish( this.pubsub.publish(
`team_coll/${updatedTeamCollection.teamID}/coll_updated`, `team_coll/${updatedTeamCollection.teamID}/coll_updated`,
this.cast(updatedTeamCollection), updatedTeamCollection,
); );
return E.right(this.cast(updatedTeamCollection)); return E.right(updatedTeamCollection);
} catch (error) { } catch (error) {
return E.left(TEAM_COLL_NOT_FOUND); return E.left(TEAM_COLL_NOT_FOUND);
} }
@@ -734,8 +694,8 @@ export class TeamCollectionService {
* @returns An Option of boolean, is parent or not * @returns An Option of boolean, is parent or not
*/ */
private async isParent( private async isParent(
collection: DBTeamCollection, collection: TeamCollection,
destCollection: DBTeamCollection, destCollection: TeamCollection,
): Promise<O.Option<boolean>> { ): Promise<O.Option<boolean>> {
//* Recursively check if collection is a parent by going up the tree of child-parent collections until we reach a root collection i.e parentID === null //* Recursively check if collection is a parent by going up the tree of child-parent collections until we reach a root collection i.e parentID === null
//* Valid condition, isParent returns false //* Valid condition, isParent returns false
@@ -1011,49 +971,4 @@ export class TeamCollectionService {
const teamCollectionsCount = this.prisma.teamCollection.count(); const teamCollectionsCount = this.prisma.teamCollection.count();
return teamCollectionsCount; return teamCollectionsCount;
} }
/**
* Update Team Collection details
*
* @param collectionID Collection ID
* @param collectionData new header data in a JSONified string form
* @param newTitle New title of the collection
* @returns Updated TeamCollection
*/
async updateTeamCollection(
collectionID: string,
collectionData: string = null,
newTitle: string = null,
) {
try {
if (newTitle != null) {
const isTitleValid = isValidLength(newTitle, this.TITLE_LENGTH);
if (!isTitleValid) return E.left(TEAM_COLL_SHORT_TITLE);
}
if (collectionData === '') return E.left(TEAM_COLL_DATA_INVALID);
if (collectionData) {
const jsonReq = stringToJson(collectionData);
if (E.isLeft(jsonReq)) return E.left(TEAM_COLL_DATA_INVALID);
collectionData = jsonReq.right;
}
const updatedTeamCollection = await this.prisma.teamCollection.update({
where: { id: collectionID },
data: {
data: collectionData ?? undefined,
title: newTitle ?? undefined,
},
});
this.pubsub.publish(
`team_coll/${updatedTeamCollection.teamID}/coll_updated`,
this.cast(updatedTeamCollection),
);
return E.right(this.cast(updatedTeamCollection));
} catch (e) {
return E.left(TEAM_COLL_NOT_FOUND);
}
}
} }

View File

@@ -42,7 +42,6 @@ const teamCollection: DbTeamCollection = {
id: 'team-coll-1', id: 'team-coll-1',
parentID: null, parentID: null,
teamID: team.id, teamID: team.id,
data: {},
title: 'Team Collection 1', title: 'Team Collection 1',
orderIndex: 1, orderIndex: 1,
createdOn: new Date(), createdOn: new Date(),

View File

@@ -1,8 +1,6 @@
// This interface defines how data will be received from the app when we are importing Hoppscotch collections
export interface CollectionFolder { export interface CollectionFolder {
id?: string; id?: string;
folders: CollectionFolder[]; folders: CollectionFolder[];
requests: any[]; requests: any[];
name: string; name: string;
data?: string;
} }

View File

@@ -6,13 +6,6 @@ import { PaginationArgs } from 'src/types/input-types.args';
export class CreateRootUserCollectionArgs { export class CreateRootUserCollectionArgs {
@Field({ name: 'title', description: 'Title of the new user collection' }) @Field({ name: 'title', description: 'Title of the new user collection' })
title: string; title: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
} }
@ArgsType() @ArgsType()
export class CreateChildUserCollectionArgs { export class CreateChildUserCollectionArgs {
@@ -24,13 +17,6 @@ export class CreateChildUserCollectionArgs {
description: 'ID of the parent to the new user collection', description: 'ID of the parent to the new user collection',
}) })
parentUserCollectionID: string; parentUserCollectionID: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
} }
@ArgsType() @ArgsType()
@@ -109,26 +95,3 @@ export class ImportUserCollectionsFromJSONArgs {
}) })
parentCollectionID?: string; parentCollectionID?: string;
} }
@ArgsType()
export class UpdateUserCollectionsArgs {
@Field(() => ID, {
name: 'userCollectionID',
description: 'ID of the user collection',
})
userCollectionID: string;
@Field({
name: 'newTitle',
description: 'The updated title of the user collection',
nullable: true,
})
newTitle: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
}

View File

@@ -30,7 +30,6 @@ import {
MoveUserCollectionArgs, MoveUserCollectionArgs,
RenameUserCollectionsArgs, RenameUserCollectionsArgs,
UpdateUserCollectionArgs, UpdateUserCollectionArgs,
UpdateUserCollectionsArgs,
} from './input-type.args'; } from './input-type.args';
import { ReqType } from 'src/types/RequestTypes'; import { ReqType } from 'src/types/RequestTypes';
import * as E from 'fp-ts/Either'; import * as E from 'fp-ts/Either';
@@ -143,13 +142,7 @@ export class UserCollectionResolver {
); );
if (E.isLeft(userCollection)) throwErr(userCollection.left); if (E.isLeft(userCollection)) throwErr(userCollection.left);
return <UserCollection>{ return userCollection.right;
...userCollection.right,
userID: userCollection.right.userUid,
data: !userCollection.right.data
? null
: JSON.stringify(userCollection.right.data),
};
} }
@Query(() => UserCollectionExportJSONData, { @Query(() => UserCollectionExportJSONData, {
@@ -198,7 +191,6 @@ export class UserCollectionResolver {
await this.userCollectionService.createUserCollection( await this.userCollectionService.createUserCollection(
user, user,
args.title, args.title,
args.data,
null, null,
ReqType.REST, ReqType.REST,
); );
@@ -220,7 +212,6 @@ export class UserCollectionResolver {
await this.userCollectionService.createUserCollection( await this.userCollectionService.createUserCollection(
user, user,
args.title, args.title,
args.data,
null, null,
ReqType.GQL, ReqType.GQL,
); );
@@ -241,7 +232,6 @@ export class UserCollectionResolver {
await this.userCollectionService.createUserCollection( await this.userCollectionService.createUserCollection(
user, user,
args.title, args.title,
args.data,
args.parentUserCollectionID, args.parentUserCollectionID,
ReqType.GQL, ReqType.GQL,
); );
@@ -262,7 +252,6 @@ export class UserCollectionResolver {
await this.userCollectionService.createUserCollection( await this.userCollectionService.createUserCollection(
user, user,
args.title, args.title,
args.data,
args.parentUserCollectionID, args.parentUserCollectionID,
ReqType.REST, ReqType.REST,
); );
@@ -370,26 +359,6 @@ export class UserCollectionResolver {
return importedCollection.right; return importedCollection.right;
} }
@Mutation(() => UserCollection, {
description: 'Update a UserCollection',
})
@UseGuards(GqlAuthGuard)
async updateUserCollection(
@GqlUser() user: AuthUser,
@Args() args: UpdateUserCollectionsArgs,
) {
const updatedUserCollection =
await this.userCollectionService.updateUserCollection(
args.newTitle,
args.data,
args.userCollectionID,
user.uid,
);
if (E.isLeft(updatedUserCollection)) throwErr(updatedUserCollection.left);
return updatedUserCollection.right;
}
// Subscriptions // Subscriptions
@Subscription(() => UserCollection, { @Subscription(() => UserCollection, {
description: 'Listen for User Collection Creation', description: 'Listen for User Collection Creation',

View File

@@ -12,7 +12,6 @@ import {
USER_NOT_FOUND, USER_NOT_FOUND,
USER_NOT_OWNER, USER_NOT_OWNER,
USER_COLL_INVALID_JSON, USER_COLL_INVALID_JSON,
USER_COLL_DATA_INVALID,
} from 'src/errors'; } from 'src/errors';
import { PrismaService } from 'src/prisma/prisma.service'; import { PrismaService } from 'src/prisma/prisma.service';
import { AuthUser } from 'src/types/AuthUser'; import { AuthUser } from 'src/types/AuthUser';
@@ -44,12 +43,8 @@ export class UserCollectionService {
*/ */
private cast(collection: UserCollection) { private cast(collection: UserCollection) {
return <UserCollectionModel>{ return <UserCollectionModel>{
id: collection.id, ...collection,
title: collection.title,
type: collection.type,
parentID: collection.parentID,
userID: collection.userUid, userID: collection.userUid,
data: !collection.data ? null : JSON.stringify(collection.data),
}; };
} }
@@ -151,7 +146,7 @@ export class UserCollectionService {
}, },
}); });
return !parent ? null : this.cast(parent); return parent;
} }
/** /**
@@ -169,7 +164,7 @@ export class UserCollectionService {
take: number, take: number,
type: ReqType, type: ReqType,
) { ) {
const res = await this.prisma.userCollection.findMany({ return this.prisma.userCollection.findMany({
where: { where: {
parentID: collectionID, parentID: collectionID,
type: type, type: type,
@@ -181,12 +176,6 @@ export class UserCollectionService {
skip: cursor ? 1 : 0, skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined, cursor: cursor ? { id: cursor } : undefined,
}); });
const childCollections = res.map((childCollection) =>
this.cast(childCollection),
);
return childCollections;
} }
/** /**
@@ -222,20 +211,12 @@ export class UserCollectionService {
async createUserCollection( async createUserCollection(
user: AuthUser, user: AuthUser,
title: string, title: string,
data: string | null = null,
parentUserCollectionID: string | null, parentUserCollectionID: string | null,
type: ReqType, type: ReqType,
) { ) {
const isTitleValid = isValidLength(title, this.TITLE_LENGTH); const isTitleValid = isValidLength(title, this.TITLE_LENGTH);
if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE); if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE);
if (data === '') return E.left(USER_COLL_DATA_INVALID);
if (data) {
const jsonReq = stringToJson(data);
if (E.isLeft(jsonReq)) return E.left(USER_COLL_DATA_INVALID);
data = jsonReq.right;
}
// If creating a child collection // If creating a child collection
if (parentUserCollectionID !== null) { if (parentUserCollectionID !== null) {
const parentCollection = await this.getUserCollection( const parentCollection = await this.getUserCollection(
@@ -270,19 +251,15 @@ export class UserCollectionService {
}, },
}, },
parent: isParent, parent: isParent,
data: data ?? undefined,
orderIndex: !parentUserCollectionID orderIndex: !parentUserCollectionID
? (await this.getRootCollectionsCount(user.uid)) + 1 ? (await this.getRootCollectionsCount(user.uid)) + 1
: (await this.getChildCollectionsCount(parentUserCollectionID)) + 1, : (await this.getChildCollectionsCount(parentUserCollectionID)) + 1,
}, },
}); });
await this.pubsub.publish( await this.pubsub.publish(`user_coll/${user.uid}/created`, userCollection);
`user_coll/${user.uid}/created`,
this.cast(userCollection),
);
return E.right(this.cast(userCollection)); return E.right(userCollection);
} }
/** /**
@@ -299,7 +276,7 @@ export class UserCollectionService {
take: number, take: number,
type: ReqType, type: ReqType,
) { ) {
const res = await this.prisma.userCollection.findMany({ return this.prisma.userCollection.findMany({
where: { where: {
userUid: user.uid, userUid: user.uid,
parentID: null, parentID: null,
@@ -312,12 +289,6 @@ export class UserCollectionService {
skip: cursor ? 1 : 0, skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined, cursor: cursor ? { id: cursor } : undefined,
}); });
const userCollections = res.map((childCollection) =>
this.cast(childCollection),
);
return userCollections;
} }
/** /**
@@ -336,7 +307,7 @@ export class UserCollectionService {
take: number, take: number,
type: ReqType, type: ReqType,
) { ) {
const res = await this.prisma.userCollection.findMany({ return this.prisma.userCollection.findMany({
where: { where: {
userUid: user.uid, userUid: user.uid,
parentID: userCollectionID, parentID: userCollectionID,
@@ -346,16 +317,9 @@ export class UserCollectionService {
skip: cursor ? 1 : 0, skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined, cursor: cursor ? { id: cursor } : undefined,
}); });
const childCollections = res.map((childCollection) =>
this.cast(childCollection),
);
return childCollections;
} }
/** /**
* @deprecated Use updateUserCollection method instead
* Update the title of a UserCollection * Update the title of a UserCollection
* *
* @param newTitle The new title of collection * @param newTitle The new title of collection
@@ -387,10 +351,10 @@ export class UserCollectionService {
this.pubsub.publish( this.pubsub.publish(
`user_coll/${updatedUserCollection.userUid}/updated`, `user_coll/${updatedUserCollection.userUid}/updated`,
this.cast(updatedUserCollection), updatedUserCollection,
); );
return E.right(this.cast(updatedUserCollection)); return E.right(updatedUserCollection);
} catch (error) { } catch (error) {
return E.left(USER_COLL_NOT_FOUND); return E.left(USER_COLL_NOT_FOUND);
} }
@@ -627,10 +591,10 @@ export class UserCollectionService {
this.pubsub.publish( this.pubsub.publish(
`user_coll/${collection.right.userUid}/moved`, `user_coll/${collection.right.userUid}/moved`,
this.cast(updatedCollection.right), updatedCollection.right,
); );
return E.right(this.cast(updatedCollection.right)); return E.right(updatedCollection.right);
} }
// destCollectionID != null i.e move into another collection // destCollectionID != null i.e move into another collection
@@ -678,10 +642,10 @@ export class UserCollectionService {
this.pubsub.publish( this.pubsub.publish(
`user_coll/${collection.right.userUid}/moved`, `user_coll/${collection.right.userUid}/moved`,
this.cast(updatedCollection.right), updatedCollection.right,
); );
return E.right(this.cast(updatedCollection.right)); return E.right(updatedCollection.right);
} }
/** /**
@@ -882,7 +846,6 @@ export class UserCollectionService {
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread ...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
}; };
}), }),
data: JSON.stringify(collection.right.data),
}; };
return E.right(result); return E.right(result);
@@ -955,7 +918,6 @@ export class UserCollectionService {
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread ...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
}; };
}), }),
data: JSON.stringify(parentCollection.right.data),
}), }),
collectionType: parentCollection.right.type, collectionType: parentCollection.right.type,
}); });
@@ -1009,7 +971,6 @@ export class UserCollectionService {
this.generatePrismaQueryObj(f, userID, index + 1, reqType), this.generatePrismaQueryObj(f, userID, index + 1, reqType),
), ),
}, },
data: folder.data ?? undefined,
}; };
} }
@@ -1079,63 +1040,10 @@ export class UserCollectionService {
), ),
); );
userCollections.forEach((collection) => userCollections.forEach((x) =>
this.pubsub.publish(`user_coll/${userID}/created`, this.cast(collection)), this.pubsub.publish(`user_coll/${userID}/created`, x),
); );
return E.right(true); return E.right(true);
} }
/**
* Update a UserCollection
*
* @param newTitle The new title of collection
* @param userCollectionID The Collection Id
* @param userID The User UID
* @returns An Either of the updated UserCollection
*/
async updateUserCollection(
newTitle: string = null,
collectionData: string | null = null,
userCollectionID: string,
userID: string,
) {
if (collectionData === '') return E.left(USER_COLL_DATA_INVALID);
if (collectionData) {
const jsonReq = stringToJson(collectionData);
if (E.isLeft(jsonReq)) return E.left(USER_COLL_DATA_INVALID);
collectionData = jsonReq.right;
}
if (newTitle != null) {
const isTitleValid = isValidLength(newTitle, this.TITLE_LENGTH);
if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE);
}
// Check to see is the collection belongs to the user
const isOwner = await this.isOwnerCheck(userCollectionID, userID);
if (O.isNone(isOwner)) return E.left(USER_NOT_OWNER);
try {
const updatedUserCollection = await this.prisma.userCollection.update({
where: {
id: userCollectionID,
},
data: {
data: collectionData ?? undefined,
title: newTitle ?? undefined,
},
});
this.pubsub.publish(
`user_coll/${updatedUserCollection.userUid}/updated`,
this.cast(updatedUserCollection),
);
return E.right(this.cast(updatedUserCollection));
} catch (error) {
return E.left(USER_COLL_NOT_FOUND);
}
}
} }

View File

@@ -13,12 +13,6 @@ export class UserCollection {
}) })
title: string; title: string;
@Field({
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
@Field(() => ReqType, { @Field(() => ReqType, {
description: 'Type of the user collection', description: 'Type of the user collection',
}) })

View File

@@ -1,6 +1,6 @@
{ {
"name": "@hoppscotch/cli", "name": "@hoppscotch/cli",
"version": "0.4.0", "version": "0.3.3",
"description": "A CLI to run Hoppscotch test scripts in CI environments.", "description": "A CLI to run Hoppscotch test scripts in CI environments.",
"homepage": "https://hoppscotch.io", "homepage": "https://hoppscotch.io",
"main": "dist/index.js", "main": "dist/index.js",
@@ -10,9 +10,6 @@
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
"engines": {
"node": ">=18"
},
"scripts": { "scripts": {
"build": "pnpm exec tsup", "build": "pnpm exec tsup",
"dev": "pnpm exec tsup --watch", "dev": "pnpm exec tsup --watch",
@@ -41,24 +38,24 @@
"devDependencies": { "devDependencies": {
"@hoppscotch/data": "workspace:^", "@hoppscotch/data": "workspace:^",
"@hoppscotch/js-sandbox": "workspace:^", "@hoppscotch/js-sandbox": "workspace:^",
"@relmify/jest-fp-ts": "^2.1.1", "@relmify/jest-fp-ts": "^2.0.2",
"@swc/core": "^1.3.92", "@swc/core": "^1.2.181",
"@types/jest": "^29.5.5", "@types/jest": "^27.4.1",
"@types/lodash": "^4.14.199", "@types/lodash": "^4.14.181",
"@types/qs": "^6.9.8", "@types/qs": "^6.9.7",
"axios": "^0.21.4", "axios": "^0.21.4",
"chalk": "^4.1.2", "chalk": "^4.1.1",
"commander": "^11.0.0", "commander": "^8.0.0",
"esm": "^3.2.25", "esm": "^3.2.25",
"fp-ts": "^2.16.1", "fp-ts": "^2.12.1",
"io-ts": "^2.2.20", "io-ts": "^2.2.16",
"jest": "^29.7.0", "jest": "^27.5.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"prettier": "^3.0.3", "prettier": "^2.8.4",
"qs": "^6.11.2", "qs": "^6.10.3",
"ts-jest": "^29.1.1", "ts-jest": "^27.1.4",
"tsup": "^7.2.0", "tsup": "^5.12.7",
"typescript": "^5.2.2", "typescript": "^4.6.4",
"zod": "^3.22.4" "zod": "^3.22.2"
} }
} }

View File

@@ -57,7 +57,7 @@ module.exports = {
{ {
name: "localStorage", name: "localStorage",
message: message:
"Do not use 'localStorage' directly. Please use the PersistenceService", "Do not use 'localStorage' directly. Please use localpersistence.ts functions or stores",
}, },
], ],
// window.localStorage block // window.localStorage block
@@ -66,10 +66,8 @@ module.exports = {
{ {
selector: "CallExpression[callee.object.property.name='localStorage']", selector: "CallExpression[callee.object.property.name='localStorage']",
message: message:
"Do not use 'localStorage' directly. Please use the PersistenceService", "Do not use 'localStorage' directly. Please use localpersistence.ts functions or stores",
}, },
], ],
eqeqeq: 1,
"no-else-return": 1,
}, },
} }

View File

@@ -4,5 +4,5 @@ module.exports = {
singleQuote: false, singleQuote: false,
printWidth: 80, printWidth: 80,
useTabs: false, useTabs: false,
tabWidth: 2, tabWidth: 2
} }

View File

@@ -1,25 +1,7 @@
/*
* Write hoppscotch-common related custom styles in this file.
* If styles are sharable across all package then write into hoppscotch-ui/assets/scss/styles.scss file.
*/
* { * {
backface-visibility: hidden; @apply backface-hidden;
-moz-backface-visibility: hidden; @apply before:backface-hidden;
-webkit-backface-visibility: hidden; @apply after:backface-hidden;
&::before {
backface-visibility: hidden;
-moz-backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
&::after {
backface-visibility: hidden;
-moz-backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
@apply selection:bg-accentDark; @apply selection:bg-accentDark;
@apply selection:text-accentContrast; @apply selection:text-accentContrast;
@apply overscroll-none; @apply overscroll-none;
@@ -33,13 +15,13 @@
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
@apply bg-transparent; @apply bg-transparent;
@apply border-b-0 border-l border-r-0 border-t-0 border-solid border-dividerLight; @apply border-solid border-l border-dividerLight border-t-0 border-b-0 border-r-0;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
@apply bg-divider bg-clip-content; @apply bg-divider bg-clip-content;
@apply rounded-full; @apply rounded-full;
@apply border-4 border-solid border-transparent; @apply border-solid border-transparent border-4;
@apply hover:bg-dividerDark; @apply hover:bg-dividerDark;
@apply hover:bg-clip-content; @apply hover:bg-clip-content;
} }
@@ -57,7 +39,7 @@ input::placeholder,
textarea::placeholder, textarea::placeholder,
.cm-placeholder { .cm-placeholder {
@apply text-secondary; @apply text-secondary;
@apply opacity-50 #{!important}; @apply opacity-50;
} }
input, input,
@@ -72,11 +54,11 @@ html {
body { body {
@apply bg-primary; @apply bg-primary;
@apply text-body text-secondary; @apply text-secondary text-body;
@apply font-medium; @apply font-medium;
@apply select-none; @apply select-none;
@apply overflow-x-hidden; @apply overflow-x-hidden;
@apply leading-body #{!important}; @apply leading-body;
animation: fade 300ms forwards; animation: fade 300ms forwards;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none; -webkit-touch-callout: none;
@@ -142,8 +124,8 @@ a {
&.link { &.link {
@apply items-center; @apply items-center;
@apply px-1 py-0.5; @apply py-0.5 px-1;
@apply -mx-1 -my-0.5; @apply -my-0.5 -mx-1;
@apply text-accent; @apply text-accent;
@apply rounded; @apply rounded;
@apply hover:text-accentDark; @apply hover:text-accentDark;
@@ -158,7 +140,7 @@ a {
@apply shadow-none #{!important}; @apply shadow-none #{!important};
@apply fixed; @apply fixed;
@apply inline-flex; @apply inline-flex;
@apply -mt-8; @apply -mt-7.5;
} }
} }
@@ -172,15 +154,15 @@ a {
@apply flex; @apply flex;
@apply text-tiny text-primary; @apply text-tiny text-primary;
@apply font-semibold; @apply font-semibold;
@apply px-2 py-1; @apply py-1 px-2;
@apply truncate; @apply truncate;
@apply leading-body; @apply leading-normal;
@apply items-center; @apply items-center;
kbd { kbd {
@apply hidden; @apply hidden;
@apply font-sans; @apply font-sans;
background-color: rgba(107, 114, 128, 0.45); @apply bg-gray-500/45;
@apply text-primaryLight; @apply text-primaryLight;
@apply rounded-sm; @apply rounded-sm;
@apply px-1; @apply px-1;
@@ -188,12 +170,6 @@ a {
@apply truncate; @apply truncate;
@apply sm:inline-flex; @apply sm:inline-flex;
} }
.env-icon {
@apply transition;
@apply inline-flex;
@apply items-center;
}
} }
.tippy-svg-arrow { .tippy-svg-arrow {
@@ -219,9 +195,9 @@ a {
@apply max-h-[45vh]; @apply max-h-[45vh];
@apply items-stretch; @apply items-stretch;
@apply overflow-y-auto; @apply overflow-y-auto;
@apply text-body text-secondary; @apply text-secondary text-body;
@apply p-2; @apply p-2;
@apply leading-body; @apply leading-normal;
@apply focus:outline-none; @apply focus:outline-none;
scroll-behavior: smooth; scroll-behavior: smooth;
@@ -253,12 +229,12 @@ a {
hr { hr {
@apply border-b border-dividerLight; @apply border-b border-dividerLight;
@apply my-2 #{!important}; @apply my-2;
} }
.heading { .heading {
@apply font-bold; @apply font-bold;
@apply text-lg text-secondaryDark; @apply text-secondaryDark text-lg;
@apply tracking-tight; @apply tracking-tight;
} }
@@ -267,7 +243,7 @@ hr {
.textarea { .textarea {
@apply flex; @apply flex;
@apply w-full; @apply w-full;
@apply px-4 py-2; @apply py-2 px-4;
@apply bg-transparent; @apply bg-transparent;
@apply rounded; @apply rounded;
@apply text-secondaryDark; @apply text-secondaryDark;
@@ -308,7 +284,7 @@ button {
@apply transform; @apply transform;
@apply origin-top-left; @apply origin-top-left;
@apply scale-75; @apply scale-75;
@apply -translate-y-4 translate-x-1; @apply translate-x-1 -translate-y-4;
} }
.floating-input:focus-within ~ label { .floating-input:focus-within ~ label {
@@ -317,7 +293,7 @@ button {
.floating-input ~ .end-actions { .floating-input ~ .end-actions {
@apply absolute; @apply absolute;
@apply right-[.05rem]; @apply right-0.2;
@apply inset-y-0; @apply inset-y-0;
@apply flex; @apply flex;
@apply items-center; @apply items-center;
@@ -342,28 +318,44 @@ pre.ace_editor {
} }
} }
.select-wrapper {
@apply flex flex-1;
@apply relative;
@apply after:absolute;
@apply after:flex;
@apply after:inset-y-0;
@apply after:items-center;
@apply after:justify-center;
@apply after:pointer-events-none;
@apply after:font-icon;
@apply after:text-current;
@apply after:right-3;
@apply after:content-["\e5cf"];
@apply after:text-lg;
}
.info-response { .info-response {
color: var(--status-info-color); @apply text-pink-500;
} }
.success-response { .success-response {
color: var(--status-success-color); @apply text-green-500;
} }
.redirect-response { .redir-response {
color: var(--status-redirect-color); @apply text-yellow-500;
} }
.critical-error-response { .cl-error-response {
color: var(--status-critical-error-color); @apply text-red-500;
} }
.server-error-response { .sv-error-response {
color: var(--status-server-error-color); @apply text-red-600;
} }
.missing-data-response { .missing-data-response {
color: var(--status-missing-data-color); @apply text-secondaryLight;
} }
.toasted-container { .toasted-container {
@@ -374,7 +366,7 @@ pre.ace_editor {
@apply px-4 py-2; @apply px-4 py-2;
@apply bg-tooltip; @apply bg-tooltip;
@apply border-secondaryDark; @apply border-secondaryDark;
@apply text-body text-primary; @apply text-primary text-body;
@apply justify-between; @apply justify-between;
@apply shadow-lg; @apply shadow-lg;
@apply font-semibold; @apply font-semibold;
@@ -402,7 +394,7 @@ pre.ace_editor {
@apply before:opacity-10; @apply before:opacity-10;
@apply before:inset-0; @apply before:inset-0;
@apply before:transition; @apply before:transition;
@apply before:content-['']; @apply before:content-DEFAULT;
@apply hover:no-underline; @apply hover:no-underline;
@apply hover:before:opacity-20; @apply hover:before:opacity-20;
} }
@@ -436,7 +428,7 @@ pre.ace_editor {
@apply before:opacity-0; @apply before:opacity-0;
@apply before:z-20; @apply before:z-20;
@apply before:transition; @apply before:transition;
@apply before:content-['']; @apply before:content-DEFAULT;
@apply hover:before:opacity-100; @apply hover:before:opacity-100;
} }
@@ -509,16 +501,32 @@ pre.ace_editor {
} }
} }
.cm-panel.cm-search [name="close"] {
@apply flex;
@apply items-center;
@apply justify-center;
@apply min-h-5;
@apply min-w-5;
@apply bg-primaryDark #{!important};
@apply sticky #{!important};
@apply right-0 #{!important};
@apply ml-auto #{!important};
@apply my-auto #{!important};
@apply rounded #{!important};
@apply outline #{!important};
@apply outline-divider #{!important};
}
.shortcut-key { .shortcut-key {
@apply inline-flex; @apply inline-flex;
@apply font-sans; @apply font-sans;
@apply text-tiny; @apply text-tiny;
@apply bg-dividerLight; @apply bg-divider;
@apply rounded; @apply rounded;
@apply ml-2; @apply ml-2;
@apply px-1; @apply px-1;
@apply min-w-[1.25rem]; @apply min-w-5;
@apply min-h-[1.25rem]; @apply min-h-5;
@apply items-center; @apply items-center;
@apply justify-center; @apply justify-center;
@apply border border-dividerDark; @apply border border-dividerDark;

View File

@@ -1,3 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,274 @@
@mixin base-theme {
--font-sans: "Inter Variable", sans-serif;
--font-icon: "Material Symbols Rounded Variable";
--font-mono: "Roboto Mono Variable", monospace;
--font-size-body: 0.75rem;
--font-size-tiny: 0.688rem;
--line-height-body: 1rem;
--upper-primary-sticky-fold: 4.125rem;
--upper-secondary-sticky-fold: 6.188rem;
--upper-tertiary-sticky-fold: 8.25rem;
--upper-fourth-sticky-fold: 10.2rem;
--upper-mobile-primary-sticky-fold: 6.625rem;
--upper-mobile-secondary-sticky-fold: 8.688rem;
--upper-mobile-sticky-fold: 10.75rem;
--upper-mobile-tertiary-sticky-fold: 8.25rem;
--lower-primary-sticky-fold: 3rem;
--lower-secondary-sticky-fold: 5.063rem;
--lower-tertiary-sticky-fold: 7.125rem;
--lower-fourth-sticky-fold: 9.188rem;
--sidebar-primary-sticky-fold: 2rem;
}
@mixin dark-theme {
--primary-color: theme("colors.dark.800");
--primary-light-color: theme("colors.dark.600");
--primary-dark-color: theme("colors.neutral.800");
--primary-contrast-color: theme("colors.neutral.900");
--secondary-color: theme("colors.neutral.400");
--secondary-light-color: theme("colors.neutral.500");
--secondary-dark-color: theme("colors.neutral.50");
--divider-color: theme("colors.neutral.800");
--divider-light-color: theme("colors.dark.500");
--divider-dark-color: theme("colors.dark.300");
--error-color: theme("colors.stone.800");
--tooltip-color: theme("colors.neutral.100");
--popover-color: theme("colors.dark.700");
--editor-theme: "merbivore_soft";
}
@mixin light-theme {
--primary-color: theme("colors.white");
--primary-light-color: theme("colors.gray.50");
--primary-dark-color: theme("colors.gray.100");
--primary-contrast-color: theme("colors.light.50");
--secondary-color: theme("colors.gray.500");
--secondary-light-color: theme("colors.gray.400");
--secondary-dark-color: theme("colors.gray.900");
--divider-color: theme("colors.gray.100");
--divider-light-color: theme("colors.gray.100");
--divider-dark-color: theme("colors.gray.300");
--error-color: theme("colors.yellow.100");
--tooltip-color: theme("colors.neutral.800");
--popover-color: theme("colors.white");
--editor-theme: "textmate";
}
@mixin black-theme {
--primary-color: theme("colors.dark.900");
--primary-light-color: theme("colors.neutral.900");
--primary-dark-color: theme("colors.dark.800");
--primary-contrast-color: theme("colors.dark.900");
--secondary-color: theme("colors.neutral.400");
--secondary-light-color: theme("colors.neutral.500");
--secondary-dark-color: theme("colors.neutral.100");
--divider-color: theme("colors.dark.600");
--divider-light-color: theme("colors.dark.800");
--divider-dark-color: theme("colors.dark.200");
--error-color: theme("colors.stone.900");
--tooltip-color: theme("colors.neutral.100");
--popover-color: theme("colors.dark.900");
--editor-theme: "twilight";
}
@mixin dark-editor-theme {
--editor-type-color: theme("colors.purple.400");
--editor-name-color: theme("colors.blue.400");
--editor-operator-color: theme("colors.indigo.400");
--editor-invalid-color: theme("colors.red.400");
--editor-separator-color: theme("colors.gray.400");
--editor-meta-color: theme("colors.gray.400");
--editor-variable-color: theme("colors.green.400");
--editor-link-color: theme("colors.cyan.400");
--editor-process-color: theme("colors.fuchsia.400");
--editor-constant-color: theme("colors.violet.400");
--editor-keyword-color: theme("colors.pink.400");
}
@mixin light-editor-theme {
--editor-type-color: theme("colors.purple.600");
--editor-name-color: theme("colors.red.600");
--editor-operator-color: theme("colors.indigo.600");
--editor-invalid-color: theme("colors.red.600");
--editor-separator-color: theme("colors.gray.600");
--editor-meta-color: theme("colors.gray.600");
--editor-variable-color: theme("colors.green.600");
--editor-link-color: theme("colors.cyan.600");
--editor-process-color: theme("colors.blue.600");
--editor-constant-color: theme("colors.fuchsia.600");
--editor-keyword-color: theme("colors.pink.600");
}
@mixin black-editor-theme {
--editor-type-color: theme("colors.purple.400");
--editor-name-color: theme("colors.fuchsia.400");
--editor-operator-color: theme("colors.indigo.400");
--editor-invalid-color: theme("colors.red.400");
--editor-separator-color: theme("colors.gray.400");
--editor-meta-color: theme("colors.gray.400");
--editor-variable-color: theme("colors.green.400");
--editor-link-color: theme("colors.cyan.400");
--editor-process-color: theme("colors.violet.400");
--editor-constant-color: theme("colors.blue.400");
--editor-keyword-color: theme("colors.pink.400");
}
@mixin green-theme {
--accent-color: theme("colors.green.500");
--accent-light-color: theme("colors.green.400");
--accent-dark-color: theme("colors.green.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.green.200");
--gradient-via-color: theme("colors.green.400");
--gradient-to-color: theme("colors.green.600");
}
@mixin teal-theme {
--accent-color: theme("colors.teal.500");
--accent-light-color: theme("colors.teal.400");
--accent-dark-color: theme("colors.teal.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.teal.200");
--gradient-via-color: theme("colors.teal.400");
--gradient-to-color: theme("colors.teal.600");
}
@mixin blue-theme {
--accent-color: theme("colors.blue.500");
--accent-light-color: theme("colors.blue.400");
--accent-dark-color: theme("colors.blue.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.blue.200");
--gradient-via-color: theme("colors.blue.400");
--gradient-to-color: theme("colors.blue.600");
}
@mixin indigo-theme {
--accent-color: theme("colors.indigo.500");
--accent-light-color: theme("colors.indigo.400");
--accent-dark-color: theme("colors.indigo.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.indigo.200");
--gradient-via-color: theme("colors.indigo.400");
--gradient-to-color: theme("colors.indigo.600");
}
@mixin purple-theme {
--accent-color: theme("colors.purple.500");
--accent-light-color: theme("colors.purple.400");
--accent-dark-color: theme("colors.purple.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.purple.200");
--gradient-via-color: theme("colors.purple.400");
--gradient-to-color: theme("colors.purple.600");
}
@mixin yellow-theme {
--accent-color: theme("colors.yellow.500");
--accent-light-color: theme("colors.yellow.400");
--accent-dark-color: theme("colors.yellow.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.yellow.200");
--gradient-via-color: theme("colors.yellow.400");
--gradient-to-color: theme("colors.yellow.600");
}
@mixin orange-theme {
--accent-color: theme("colors.orange.500");
--accent-light-color: theme("colors.orange.400");
--accent-dark-color: theme("colors.orange.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.orange.200");
--gradient-via-color: theme("colors.orange.400");
--gradient-to-color: theme("colors.orange.600");
}
@mixin red-theme {
--accent-color: theme("colors.red.500");
--accent-light-color: theme("colors.red.400");
--accent-dark-color: theme("colors.red.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.red.200");
--gradient-via-color: theme("colors.red.400");
--gradient-to-color: theme("colors.red.600");
}
@mixin pink-theme {
--accent-color: theme("colors.pink.500");
--accent-light-color: theme("colors.pink.400");
--accent-dark-color: theme("colors.pink.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.pink.200");
--gradient-via-color: theme("colors.pink.400");
--gradient-to-color: theme("colors.pink.600");
}
:root {
@include base-theme;
@include dark-theme;
@include dark-editor-theme;
@include green-theme;
}
:root.light {
@include light-theme;
@include light-editor-theme;
color-scheme: light;
}
:root.dark {
@include dark-theme;
@include dark-editor-theme;
color-scheme: dark;
}
:root.black {
@include black-theme;
@include black-editor-theme;
color-scheme: dark;
}
:root[data-accent="blue"] {
@include blue-theme;
}
:root[data-accent="green"] {
@include green-theme;
}
:root[data-accent="teal"] {
@include teal-theme;
}
:root[data-accent="indigo"] {
@include indigo-theme;
}
:root[data-accent="purple"] {
@include purple-theme;
}
:root[data-accent="orange"] {
@include orange-theme;
}
:root[data-accent="pink"] {
@include pink-theme;
}
:root[data-accent="red"] {
@include red-theme;
}
:root[data-accent="yellow"] {
@include yellow-theme;
}

View File

@@ -1,89 +0,0 @@
@mixin green-theme {
--accent-color: theme("colors.emerald.500");
--accent-light-color: theme("colors.emerald.400");
--accent-dark-color: theme("colors.emerald.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.emerald.400");
--gradient-via-color: theme("colors.emerald.500");
--gradient-to-color: theme("colors.emerald.600");
}
@mixin teal-theme {
--accent-color: theme("colors.teal.500");
--accent-light-color: theme("colors.teal.400");
--accent-dark-color: theme("colors.teal.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.teal.400");
--gradient-via-color: theme("colors.teal.500");
--gradient-to-color: theme("colors.teal.600");
}
@mixin blue-theme {
--accent-color: theme("colors.blue.500");
--accent-light-color: theme("colors.blue.400");
--accent-dark-color: theme("colors.blue.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.blue.400");
--gradient-via-color: theme("colors.blue.500");
--gradient-to-color: theme("colors.blue.600");
}
@mixin indigo-theme {
--accent-color: theme("colors.indigo.500");
--accent-light-color: theme("colors.indigo.400");
--accent-dark-color: theme("colors.indigo.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.indigo.400");
--gradient-via-color: theme("colors.indigo.500");
--gradient-to-color: theme("colors.indigo.600");
}
@mixin purple-theme {
--accent-color: theme("colors.purple.500");
--accent-light-color: theme("colors.purple.400");
--accent-dark-color: theme("colors.purple.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.purple.400");
--gradient-via-color: theme("colors.purple.500");
--gradient-to-color: theme("colors.purple.600");
}
@mixin yellow-theme {
--accent-color: theme("colors.amber.500");
--accent-light-color: theme("colors.amber.400");
--accent-dark-color: theme("colors.amber.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.amber.400");
--gradient-via-color: theme("colors.amber.500");
--gradient-to-color: theme("colors.amber.600");
}
@mixin orange-theme {
--accent-color: theme("colors.orange.500");
--accent-light-color: theme("colors.orange.400");
--accent-dark-color: theme("colors.orange.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.orange.400");
--gradient-via-color: theme("colors.orange.500");
--gradient-to-color: theme("colors.orange.600");
}
@mixin red-theme {
--accent-color: theme("colors.red.500");
--accent-light-color: theme("colors.red.400");
--accent-dark-color: theme("colors.red.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.red.400");
--gradient-via-color: theme("colors.red.500");
--gradient-to-color: theme("colors.red.600");
}
@mixin pink-theme {
--accent-color: theme("colors.pink.500");
--accent-light-color: theme("colors.pink.400");
--accent-dark-color: theme("colors.pink.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.pink.400");
--gradient-via-color: theme("colors.pink.500");
--gradient-to-color: theme("colors.pink.600");
}

View File

@@ -1,140 +0,0 @@
@mixin base-theme {
--font-sans: "Inter Variable", sans-serif;
--font-mono: "Roboto Mono Variable", monospace;
--font-size-body: 0.75rem;
--font-size-tiny: 0.625rem;
--line-height-body: 1rem;
--upper-primary-sticky-fold: 4.125rem;
--upper-secondary-sticky-fold: 6.188rem;
--upper-tertiary-sticky-fold: 8.25rem;
--upper-fourth-sticky-fold: 10.2rem;
--upper-mobile-primary-sticky-fold: 6.75rem;
--upper-mobile-secondary-sticky-fold: 8.813rem;
--upper-mobile-sticky-fold: 10.875rem;
--upper-mobile-tertiary-sticky-fold: 8.25rem;
--lower-primary-sticky-fold: 3rem;
--lower-secondary-sticky-fold: 5.063rem;
--lower-tertiary-sticky-fold: 7.125rem;
--lower-fourth-sticky-fold: 9.188rem;
--sidebar-primary-sticky-fold: 2rem;
}
@mixin light-theme {
--primary-color: theme("colors.white");
--primary-light-color: theme("colors.gray.50");
--primary-dark-color: theme("colors.gray.100");
--primary-contrast-color: #fdfdfd;
--secondary-color: theme("colors.gray.500");
--secondary-light-color: theme("colors.gray.400");
--secondary-dark-color: theme("colors.gray.900");
--divider-color: theme("colors.gray.100");
--divider-light-color: theme("colors.gray.100");
--divider-dark-color: theme("colors.gray.300");
--banner-info-color: theme("colors.stone.100");
--banner-warning-color: theme("colors.yellow.100");
--banner-error-color: theme("colors.red.100");
--tooltip-color: theme("colors.neutral.800");
--popover-color: theme("colors.white");
--method-get-color: theme("colors.green.500");
--method-post-color: theme("colors.amber.500");
--method-put-color: theme("colors.blue.500");
--method-patch-color: theme("colors.purple.500");
--method-delete-color: theme("colors.red.500");
--method-head-color: theme("colors.lime.500");
--method-options-color: theme("colors.pink.500");
--method-default-color: theme("colors.gray.500");
--status-info-color: theme("colors.blue.500");
--status-success-color: theme("colors.green.500");
--status-redirect-color: theme("colors.amber.500");
--status-critical-error-color: theme("colors.red.500");
--status-server-error-color: theme("colors.rose.500");
--status-missing-data-color: theme("colors.slate.500");
--editor-theme: "textmate";
}
@mixin dark-theme {
--primary-color: #181818;
--primary-light-color: #1c1c1e;
--primary-dark-color: theme("colors.neutral.800");
--primary-contrast-color: theme("colors.neutral.900");
--secondary-color: theme("colors.neutral.400");
--secondary-light-color: theme("colors.neutral.500");
--secondary-dark-color: theme("colors.zinc.50");
--divider-color: #1f1f1f;
--divider-light-color: #1f1f1f;
--divider-dark-color: theme("colors.zinc.800");
--banner-info-color: theme("colors.stone.800");
--banner-warning-color: theme("colors.yellow.800");
--banner-error-color: theme("colors.red.800");
--tooltip-color: theme("colors.neutral.100");
--popover-color: #1b1b1b;
--method-get-color: theme("colors.emerald.500");
--method-post-color: theme("colors.yellow.500");
--method-put-color: theme("colors.sky.500");
--method-patch-color: theme("colors.violet.500");
--method-delete-color: theme("colors.rose.500");
--method-head-color: theme("colors.teal.500");
--method-options-color: theme("colors.indigo.500");
--method-default-color: theme("colors.neutral.500");
--status-info-color: theme("colors.blue.500");
--status-success-color: theme("colors.green.500");
--status-redirect-color: theme("colors.amber.500");
--status-critical-error-color: theme("colors.red.500");
--status-server-error-color: theme("colors.rose.500");
--status-missing-data-color: theme("colors.slate.500");
--editor-theme: "merbivore_soft";
}
@mixin black-theme {
--primary-color: #0f0f0f;
--primary-light-color: theme("colors.neutral.900");
--primary-dark-color: #181818;
--primary-contrast-color: #0f0f0f;
--secondary-color: theme("colors.neutral.400");
--secondary-light-color: theme("colors.neutral.500");
--secondary-dark-color: theme("colors.neutral.50");
--divider-color: theme("colors.neutral.900");
--divider-light-color: theme("colors.neutral.900");
--divider-dark-color: theme("colors.zinc.800");
--banner-info-color: theme("colors.stone.900");
--banner-warning-color: theme("colors.yellow.900");
--banner-error-color: theme("colors.red.900");
--tooltip-color: theme("colors.neutral.100");
--popover-color: theme("colors.stone.950");
--method-get-color: theme("colors.emerald.500");
--method-post-color: theme("colors.yellow.500");
--method-put-color: theme("colors.sky.500");
--method-patch-color: theme("colors.violet.500");
--method-delete-color: theme("colors.rose.500");
--method-head-color: theme("colors.teal.500");
--method-options-color: theme("colors.indigo.500");
--method-default-color: theme("colors.zinc.500");
--status-info-color: theme("colors.blue.500");
--status-success-color: theme("colors.green.500");
--status-redirect-color: theme("colors.amber.500");
--status-critical-error-color: theme("colors.red.500");
--status-server-error-color: theme("colors.rose.500");
--status-missing-data-color: theme("colors.slate.500");
--editor-theme: "twilight";
}

View File

@@ -1,41 +0,0 @@
@mixin light-editor-theme {
--editor-type-color: theme("colors.violet.600");
--editor-name-color: theme("colors.red.600");
--editor-operator-color: theme("colors.indigo.600");
--editor-invalid-color: theme("colors.red.600");
--editor-separator-color: theme("colors.gray.600");
--editor-meta-color: theme("colors.gray.600");
--editor-variable-color: theme("colors.emerald.600");
--editor-link-color: theme("colors.cyan.600");
--editor-process-color: theme("colors.blue.600");
--editor-constant-color: theme("colors.fuchsia.600");
--editor-keyword-color: theme("colors.pink.600");
}
@mixin dark-editor-theme {
--editor-type-color: theme("colors.violet.400");
--editor-name-color: theme("colors.blue.400");
--editor-operator-color: theme("colors.indigo.400");
--editor-invalid-color: theme("colors.red.400");
--editor-separator-color: theme("colors.gray.400");
--editor-meta-color: theme("colors.gray.400");
--editor-variable-color: theme("colors.emerald.400");
--editor-link-color: theme("colors.cyan.400");
--editor-process-color: theme("colors.fuchsia.400");
--editor-constant-color: theme("colors.violet.400");
--editor-keyword-color: theme("colors.pink.400");
}
@mixin black-editor-theme {
--editor-type-color: theme("colors.violet.400");
--editor-name-color: theme("colors.fuchsia.400");
--editor-operator-color: theme("colors.indigo.400");
--editor-invalid-color: theme("colors.red.400");
--editor-separator-color: theme("colors.gray.400");
--editor-meta-color: theme("colors.gray.400");
--editor-variable-color: theme("colors.emerald.400");
--editor-link-color: theme("colors.cyan.400");
--editor-process-color: theme("colors.violet.400");
--editor-constant-color: theme("colors.blue.400");
--editor-keyword-color: theme("colors.pink.400");
}

View File

@@ -1,64 +0,0 @@
@import "./base-themes.scss";
@import "./editor-themes.scss";
@import "./accent-themes.scss";
:root {
@include base-theme;
@include dark-theme;
@include green-theme;
@include dark-editor-theme;
}
:root.light {
@include light-theme;
@include light-editor-theme;
color-scheme: light;
}
:root.dark {
@include dark-theme;
@include dark-editor-theme;
color-scheme: dark;
}
:root.black {
@include black-theme;
@include black-editor-theme;
color-scheme: dark;
}
:root[data-accent="blue"] {
@include blue-theme;
}
:root[data-accent="green"] {
@include green-theme;
}
:root[data-accent="teal"] {
@include teal-theme;
}
:root[data-accent="indigo"] {
@include indigo-theme;
}
:root[data-accent="purple"] {
@include purple-theme;
}
:root[data-accent="orange"] {
@include orange-theme;
}
:root[data-accent="pink"] {
@include pink-theme;
}
:root[data-accent="red"] {
@include red-theme;
}
:root[data-accent="yellow"] {
@include yellow-theme;
}

View File

@@ -11,7 +11,6 @@
"connect": "Connect", "connect": "Connect",
"connecting": "Connecting", "connecting": "Connecting",
"copy": "Copy", "copy": "Copy",
"create": "Create",
"delete": "Delete", "delete": "Delete",
"disconnect": "Disconnect", "disconnect": "Disconnect",
"dismiss": "Dismiss", "dismiss": "Dismiss",
@@ -41,7 +40,6 @@
"scroll_to_top": "Scroll to top", "scroll_to_top": "Scroll to top",
"search": "Search", "search": "Search",
"send": "Send", "send": "Send",
"share": "Share",
"start": "Start", "start": "Start",
"starting": "Starting", "starting": "Starting",
"stop": "Stop", "stop": "Stop",
@@ -80,7 +78,6 @@
"contact_us": "Contact us", "contact_us": "Contact us",
"cookies": "Cookies", "cookies": "Cookies",
"copy": "Copy", "copy": "Copy",
"copy_interface_type": "Copy interface type",
"copy_user_id": "Copy User Auth Token", "copy_user_id": "Copy User Auth Token",
"developer_option": "Developer options", "developer_option": "Developer options",
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.", "developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
@@ -96,7 +93,6 @@
"keyboard_shortcuts": "Keyboard shortcuts", "keyboard_shortcuts": "Keyboard shortcuts",
"name": "Hoppscotch", "name": "Hoppscotch",
"new_version_found": "New version found. Refresh to update.", "new_version_found": "New version found. Refresh to update.",
"open_in_hoppscotch": "Open in Hoppscotch",
"options": "Options", "options": "Options",
"proxy_privacy_policy": "Proxy privacy policy", "proxy_privacy_policy": "Proxy privacy policy",
"reload": "Reload", "reload": "Reload",
@@ -143,21 +139,7 @@
"password": "Password", "password": "Password",
"token": "Token", "token": "Token",
"type": "Authorization Type", "type": "Authorization Type",
"username": "Username", "username": "Username"
"oauth": {
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed",
"something_went_wrong_on_token_generation": "Something went wrong on token generation",
"redirect_auth_server_returned_error": "Auth Server returned an error state",
"redirect_no_auth_code": "No Authorization Code present in the redirect",
"redirect_invalid_state": "Invalid State value present in the redirect",
"redirect_no_token_endpoint": "No Token Endpoint Defined",
"redirect_no_client_id": "No Client ID defined",
"redirect_no_client_secret": "No Client Secret Defined",
"redirect_no_code_verifier": "No Code Verifier Defined",
"redirect_auth_token_request_failed": "Request to get the auth token failed",
"redirect_auth_token_request_invalid_response": "Invalid Response from the Token Endpoint when requesting for an auth token",
"something_went_wrong_on_oauth_redirect": "Something went wrong during OAuth Redirect"
}
}, },
"collection": { "collection": {
"created": "Collection created", "created": "Collection created",
@@ -191,7 +173,6 @@
"remove_folder": "Are you sure you want to permanently delete this folder?", "remove_folder": "Are you sure you want to permanently delete this folder?",
"remove_history": "Are you sure you want to permanently delete all history?", "remove_history": "Are you sure you want to permanently delete all history?",
"remove_request": "Are you sure you want to permanently delete this request?", "remove_request": "Are you sure you want to permanently delete this request?",
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
"remove_team": "Are you sure you want to delete this team?", "remove_team": "Are you sure you want to delete this team?",
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?", "remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.", "request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
@@ -233,8 +214,7 @@
"profile": "Login to view your profile", "profile": "Login to view your profile",
"protocols": "Protocols are empty", "protocols": "Protocols are empty",
"schema": "Connect to a GraphQL endpoint to view schema", "schema": "Connect to a GraphQL endpoint to view schema",
"shared_requests_logout": "Login to view your shared requests or create a new one", "shortcodes": "Shortcodes are empty",
"shared_requests": "Shared requests are empty",
"subscription": "Subscriptions are empty", "subscription": "Subscriptions are empty",
"team_name": "Team name empty", "team_name": "Team name empty",
"teams": "You don't belong to any teams", "teams": "You don't belong to any teams",
@@ -274,9 +254,6 @@
"variable": "Variable", "variable": "Variable",
"variable_list": "Variable List" "variable_list": "Variable List"
}, },
"graphql_collections": {
"title": "GraphQL Collections"
},
"error": { "error": {
"browser_support_sse": "This browser doesn't seems to have Server Sent Events support.", "browser_support_sse": "This browser doesn't seems to have Server Sent Events support.",
"check_console_details": "Check console log for details.", "check_console_details": "Check console log for details.",
@@ -312,8 +289,7 @@
"create_secret_gist": "Create secret Gist", "create_secret_gist": "Create secret Gist",
"gist_created": "Gist created", "gist_created": "Gist created",
"require_github": "Login with GitHub to create secret gist", "require_github": "Login with GitHub to create secret gist",
"title": "Export", "title": "Export"
"failed": "Something went wrong while exporting"
}, },
"filter": { "filter": {
"all": "All", "all": "All",
@@ -350,8 +326,8 @@
"authorization": "The authorization header will be automatically generated when you send the request.", "authorization": "The authorization header will be automatically generated when you send the request.",
"generate_documentation_first": "Generate documentation first", "generate_documentation_first": "Generate documentation first",
"network_fail": "Unable to reach the API endpoint. Check your network connection or select a different Interceptor and try again.", "network_fail": "Unable to reach the API endpoint. Check your network connection or select a different Interceptor and try again.",
"offline": "You're using Hoppscotch offline. Updates will sync when you're online, based on workspace settings.", "offline": "You seem to be offline. Data in this workspace might not be up to date.",
"offline_short": "You're using Hoppscotch offline.", "offline_short": "You seem to be offline.",
"post_request_tests": "Test scripts are written in JavaScript, and are run after the response is received.", "post_request_tests": "Test scripts are written in JavaScript, and are run after the response is received.",
"pre_request_script": "Pre-request scripts are written in JavaScript, and are run before the request is sent.", "pre_request_script": "Pre-request scripts are written in JavaScript, and are run before the request is sent.",
"script_fail": "It seems there is a glitch in the pre-request script. Check the error below and fix the script accordingly.", "script_fail": "It seems there is a glitch in the pre-request script. Check the error below and fix the script accordingly.",
@@ -380,7 +356,6 @@
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)", "from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
"from_postman": "Import from Postman", "from_postman": "Import from Postman",
"from_postman_description": "Import from Postman collection", "from_postman_description": "Import from Postman collection",
"from_file": "Import from File",
"from_url": "Import from URL", "from_url": "Import from URL",
"gist_url": "Enter Gist URL", "gist_url": "Enter Gist URL",
"import_from_url_invalid_fetch": "Couldn't get data from the url", "import_from_url_invalid_fetch": "Couldn't get data from the url",
@@ -388,14 +363,7 @@
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'", "import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
"import_from_url_success": "Collections Imported", "import_from_url_success": "Collections Imported",
"json_description": "Import collections from a Hoppscotch Collections JSON file", "json_description": "Import collections from a Hoppscotch Collections JSON file",
"title": "Import", "title": "Import"
"hoppscotch_environment": "Hoppscotch Environment",
"hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
"postman_environment": "Postman Environment",
"postman_environment_description": "Import Postman Environment JSON file",
"environments_from_gist": "Import From Gist",
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
"gql_collections_from_gist_description": "Import GraphQL Collections From Gist"
}, },
"inspections": { "inspections": {
"description": "Inspect possible errors", "description": "Inspect possible errors",
@@ -432,9 +400,7 @@
"close_unsaved_tab": "You have unsaved changes", "close_unsaved_tab": "You have unsaved changes",
"collections": "Collections", "collections": "Collections",
"confirm": "Confirm", "confirm": "Confirm",
"customize_request": "Customize Request",
"edit_request": "Edit Request", "edit_request": "Edit Request",
"share_request": "Share Request",
"import_export": "Import / Export" "import_export": "Import / Export"
}, },
"mqtt": { "mqtt": {
@@ -510,14 +476,14 @@
"structured": "Structured", "structured": "Structured",
"text": "Text" "text": "Text"
}, },
"copy_link": "Copy link",
"different_collection": "Cannot reorder requests from different collections", "different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated", "duplicated": "Request duplicated",
"duration": "Duration", "duration": "Duration",
"enter_curl": "Enter cURL command", "enter_curl": "Enter cURL command",
"generate_code": "Generate code", "generate_code": "Generate code",
"generated_code": "Generated code", "generated_code": "Generated code",
"go_to_authorization_tab": "Go to Authorization tab", "go_to_authorization_tab": "Go to Authorization",
"go_to_body_tab": "Go to Body tab",
"header_list": "Header List", "header_list": "Header List",
"invalid_name": "Please provide a name for the request", "invalid_name": "Please provide a name for the request",
"method": "Method", "method": "Method",
@@ -542,7 +508,6 @@
"saved": "Request saved", "saved": "Request saved",
"share": "Share", "share": "Share",
"share_description": "Share Hoppscotch with your friends", "share_description": "Share Hoppscotch with your friends",
"share_request": "Share Request",
"stop": "Stop", "stop": "Stop",
"title": "Request", "title": "Request",
"type": "Request type", "type": "Request type",
@@ -620,34 +585,16 @@
"use_experimental_url_bar": "Use experimental URL bar with environment highlighting", "use_experimental_url_bar": "Use experimental URL bar with environment highlighting",
"user": "User", "user": "User",
"verified_email": "Verified email", "verified_email": "Verified email",
"additional": "Additional Settings",
"verify_email": "Verify email" "verify_email": "Verify email"
}, },
"shared_requests": { "shortcodes": {
"button": "Button", "actions": "Actions",
"button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.", "created_on": "Created on",
"customize": "Customize", "deleted": "Shortcode deleted",
"creating_widget": "Creating widget", "method": "Method",
"copy_html": "Copy HTML", "not_found": "Shortcode not found",
"copy_link": "Copy Link", "short_code": "Short code",
"copy_markdown": "Copy Markdown", "url": "URL"
"deleted": "Shared request deleted",
"description": "Select a widget, you can change and customize this later",
"embed": "Embed",
"embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.",
"link": "Link",
"link_info": "Create a shareable link to share with anyone on the internet with view access.",
"modified": "Shared request modified",
"not_found": "Shared request not found",
"open_new_tab": "Open in new tab",
"preview": "Preview",
"run_in_hoppscotch": "Run in Hoppscotch",
"theme": {
"dark": "Dark",
"light": "Light",
"system": "System",
"title": "Theme"
}
}, },
"shortcut": { "shortcut": {
"general": { "general": {
@@ -677,6 +624,7 @@
"title": "Others" "title": "Others"
}, },
"request": { "request": {
"copy_request_link": "Copy Request Link",
"delete_method": "Select DELETE method", "delete_method": "Select DELETE method",
"get_method": "Select GET method", "get_method": "Select GET method",
"head_method": "Select HEAD method", "head_method": "Select HEAD method",
@@ -692,7 +640,6 @@
"save_to_collections": "Save to Collections", "save_to_collections": "Save to Collections",
"send_request": "Send Request", "send_request": "Send Request",
"show_code": "Generate code snippet", "show_code": "Generate code snippet",
"share_request": "Share Request",
"title": "Request" "title": "Request"
}, },
"response": { "response": {
@@ -817,7 +764,6 @@
"connection_failed": "Connection failed", "connection_failed": "Connection failed",
"connection_lost": "Connection lost", "connection_lost": "Connection lost",
"copied_to_clipboard": "Copied to clipboard", "copied_to_clipboard": "Copied to clipboard",
"copied_interface_to_clipboard": "Copied {language} interface type to clipboard",
"deleted": "Deleted", "deleted": "Deleted",
"deprecated": "DEPRECATED", "deprecated": "DEPRECATED",
"disabled": "Disabled", "disabled": "Disabled",
@@ -876,7 +822,6 @@
"queries": "Queries", "queries": "Queries",
"query": "Query", "query": "Query",
"schema": "Schema", "schema": "Schema",
"shared_requests": "Shared Requests",
"socketio": "Socket.IO", "socketio": "Socket.IO",
"sse": "SSE", "sse": "SSE",
"tests": "Tests", "tests": "Tests",

View File

@@ -1,7 +1,7 @@
{ {
"name": "@hoppscotch/common", "name": "@hoppscotch/common",
"private": true, "private": true,
"version": "2023.8.4-1", "version": "2023.8.3-1",
"scripts": { "scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*", "dev": "pnpm exec npm-run-all -p -l dev:*",
"test": "vitest --run", "test": "vitest --run",
@@ -22,41 +22,45 @@
}, },
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "^10.1.0", "@apidevtools/swagger-parser": "^10.1.0",
"@codemirror/autocomplete": "^6.11.0", "@codemirror/autocomplete": "^6.10.2",
"@codemirror/commands": "^6.3.0", "@codemirror/commands": "^6.3.0",
"@codemirror/lang-javascript": "^6.2.1", "@codemirror/lang-javascript": "^6.2.1",
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-xml": "^6.0.2", "@codemirror/lang-xml": "^6.0.2",
"@codemirror/language": "6.9.2", "@codemirror/language": "6.9.0",
"@codemirror/legacy-modes": "^6.3.3", "@codemirror/legacy-modes": "^6.3.3",
"@codemirror/lint": "^6.4.2", "@codemirror/lint": "^6.4.2",
"@codemirror/search": "^6.5.4", "@codemirror/search": "^6.5.4",
"@codemirror/state": "^6.3.1", "@codemirror/state": "^6.3.1",
"@codemirror/view": "^6.22.0", "@codemirror/view": "^6.22.0",
"@fontsource-variable/inter": "^5.0.8",
"@fontsource-variable/material-symbols-rounded": "^5.0.7",
"@fontsource-variable/roboto-mono": "^5.0.9",
"@hoppscotch/codemirror-lang-graphql": "workspace:^", "@hoppscotch/codemirror-lang-graphql": "workspace:^",
"@hoppscotch/data": "workspace:^", "@hoppscotch/data": "workspace:^",
"@hoppscotch/js-sandbox": "workspace:^", "@hoppscotch/js-sandbox": "workspace:^",
"@hoppscotch/ui": "workspace:^", "@hoppscotch/ui": "workspace:^",
"@hoppscotch/vue-toasted": "^0.1.0", "@hoppscotch/vue-toasted": "^0.1.0",
"@lezer/highlight": "1.2.0", "@lezer/highlight": "1.1.4",
"@unhead/vue": "^1.8.8", "@urql/core": "^4.1.1",
"@urql/core": "^4.2.0",
"@urql/devtools": "^2.0.3", "@urql/devtools": "^2.0.3",
"@urql/exchange-auth": "^2.1.6", "@urql/exchange-auth": "^2.1.6",
"@urql/exchange-graphcache": "^6.3.3", "@urql/exchange-graphcache": "^6.3.2",
"@vitejs/plugin-legacy": "^4.1.1", "@vitejs/plugin-legacy": "^4.1.1",
"@vueuse/core": "^10.6.1", "@vueuse/core": "^10.3.0",
"acorn-walk": "^8.3.0", "@vueuse/head": "^1.3.1",
"axios": "^1.6.2", "acorn-walk": "^8.2.0",
"axios": "^1.4.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"cookie-es": "^1.0.0", "cookie-es": "^1.0.0",
"dioc": "workspace:^", "dioc": "workspace:^",
"esprima": "^4.0.1", "esprima": "^4.0.1",
"events": "^3.3.0", "events": "^3.3.0",
"fp-ts": "^2.16.1", "fp-ts": "^2.16.1",
"fuse.js": "^6.6.2",
"globalthis": "^1.0.3", "globalthis": "^1.0.3",
"graphql": "^16.8.1", "graphql": "^16.8.0",
"graphql-language-service-interface": "^2.10.2", "graphql-language-service-interface": "^2.9.1",
"graphql-tag": "^2.12.6", "graphql-tag": "^2.12.6",
"httpsnippet": "^3.0.1", "httpsnippet": "^3.0.1",
"insomnia-importers": "^3.6.0", "insomnia-importers": "^3.6.0",
@@ -64,15 +68,14 @@
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonpath-plus": "^7.2.0", "jsonpath-plus": "^7.2.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lossless-json": "^3.0.2", "lossless-json": "^2.0.11",
"minisearch": "^6.3.0", "minisearch": "^6.1.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"paho-mqtt": "^1.1.0", "paho-mqtt": "^1.1.0",
"path": "^0.12.7", "path": "^0.12.7",
"postman-collection": "^4.3.0", "postman-collection": "^4.2.0",
"process": "^0.11.10", "process": "^0.11.10",
"qs": "^6.11.2", "qs": "^6.11.2",
"quicktype-core": "^23.0.79",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"set-cookie-parser": "^2.6.0", "set-cookie-parser": "^2.6.0",
"set-cookie-parser-es": "^1.0.5", "set-cookie-parser-es": "^1.0.5",
@@ -86,19 +89,18 @@
"tern": "^0.24.3", "tern": "^0.24.3",
"timers": "^0.1.1", "timers": "^0.1.1",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"url": "^0.11.3", "url": "^0.11.1",
"util": "^0.12.5", "util": "^0.12.5",
"verzod": "^0.2.0", "uuid": "^9.0.0",
"uuid": "^9.0.1", "vue": "^3.3.4",
"vue": "^3.3.8", "vue-i18n": "^9.2.2",
"vue-i18n": "^9.7.1", "vue-pdf-embed": "^1.1.6",
"vue-pdf-embed": "^1.2.1", "vue-router": "^4.2.4",
"vue-router": "^4.2.5",
"vue-tippy": "6.3.1", "vue-tippy": "6.3.1",
"vuedraggable-es": "^4.1.1", "vuedraggable-es": "^4.1.1",
"wonka": "^6.3.4", "wonka": "^6.3.4",
"workbox-window": "^7.0.0", "workbox-window": "^7.0.0",
"xml-formatter": "^3.6.0", "xml-formatter": "^3.5.0",
"yargs-parser": "^21.1.1", "yargs-parser": "^21.1.1",
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
@@ -110,58 +112,54 @@
"@graphql-codegen/typed-document-node": "^5.0.1", "@graphql-codegen/typed-document-node": "^5.0.1",
"@graphql-codegen/typescript": "^4.0.1", "@graphql-codegen/typescript": "^4.0.1",
"@graphql-codegen/typescript-operations": "^4.0.1", "@graphql-codegen/typescript-operations": "^4.0.1",
"@graphql-codegen/typescript-urql-graphcache": "^3.0.0", "@graphql-codegen/typescript-urql-graphcache": "^2.4.5",
"@graphql-codegen/urql-introspection": "^3.0.0", "@graphql-codegen/urql-introspection": "^2.2.1",
"@graphql-typed-document-node/core": "^3.2.0", "@graphql-typed-document-node/core": "^3.2.0",
"@iconify-json/lucide": "^1.1.141", "@iconify-json/lucide": "^1.1.119",
"@intlify/vite-plugin-vue-i18n": "^7.0.0", "@intlify/vite-plugin-vue-i18n": "^7.0.0",
"@relmify/jest-fp-ts": "^2.1.1", "@relmify/jest-fp-ts": "^2.1.1",
"@rushstack/eslint-patch": "^1.6.0", "@rushstack/eslint-patch": "^1.3.3",
"@types/har-format": "^1.2.15", "@types/har-format": "^1.2.12",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.5",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.8",
"@types/lossless-json": "^1.0.4", "@types/lossless-json": "^1.0.1",
"@types/nprogress": "^0.2.3", "@types/nprogress": "^0.2.0",
"@types/paho-mqtt": "^1.0.10", "@types/paho-mqtt": "^1.0.7",
"@types/postman-collection": "^3.5.10", "@types/postman-collection": "^3.5.7",
"@types/splitpanes": "^2.2.6", "@types/splitpanes": "^2.2.1",
"@types/uuid": "^9.0.7", "@types/uuid": "^9.0.2",
"@types/yargs-parser": "^21.0.3", "@types/yargs-parser": "^21.0.0",
"@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.12.0", "@typescript-eslint/parser": "^6.4.0",
"@vitejs/plugin-vue": "^4.5.0", "@vitejs/plugin-vue": "^4.3.1",
"@vue/compiler-sfc": "^3.3.8", "@vue/compiler-sfc": "^3.3.4",
"@vue/eslint-config-typescript": "^12.0.0", "@vue/eslint-config-typescript": "^11.0.3",
"@vue/runtime-core": "^3.3.8", "@vue/runtime-core": "^3.3.4",
"autoprefixer": "^10.4.14",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"eslint": "^8.54.0", "eslint": "^8.47.0",
"eslint-plugin-prettier": "^5.0.1", "eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.18.1", "eslint-plugin-vue": "^9.17.0",
"glob": "^10.3.10", "glob": "^10.3.10",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"openapi-types": "^12.1.3", "openapi-types": "^12.1.3",
"postcss": "^8.4.23", "rollup-plugin-polyfill-node": "^0.12.0",
"prettier": "^3.1.0", "sass": "^1.66.0",
"prettier-plugin-tailwindcss": "^0.5.7", "typescript": "^5.1.6",
"rollup-plugin-polyfill-node": "^0.13.0", "unplugin-fonts": "^1.0.3",
"sass": "^1.69.5", "unplugin-icons": "^0.16.5",
"tailwindcss": "^3.3.2", "unplugin-vue-components": "^0.25.1",
"typescript": "^5.3.2", "vite": "^4.4.9",
"unplugin-fonts": "^1.1.1", "vite-plugin-checker": "^0.6.1",
"unplugin-icons": "^0.17.4",
"unplugin-vue-components": "^0.25.2",
"vite": "^4.5.0",
"vite-plugin-checker": "^0.6.2",
"vite-plugin-fonts": "^0.7.0",
"vite-plugin-html-config": "^1.0.11", "vite-plugin-html-config": "^1.0.11",
"vite-plugin-inspect": "^0.7.42", "vite-plugin-inspect": "^0.7.38",
"vite-plugin-pages": "^0.31.0", "vite-plugin-pages": "^0.31.0",
"vite-plugin-pages-sitemap": "^1.6.1", "vite-plugin-pages-sitemap": "^1.6.1",
"vite-plugin-pwa": "^0.17.0", "vite-plugin-pwa": "^0.16.4",
"vite-plugin-vue-layouts": "^0.8.0", "vite-plugin-vue-layouts": "^0.8.0",
"vitest": "^0.34.6", "vite-plugin-windicss": "^1.9.1",
"vue-tsc": "^1.8.22" "vitest": "^0.34.2",
"vue-tsc": "^1.8.8",
"windicss": "^3.5.6"
} }
} }

View File

@@ -2,7 +2,7 @@
<div> <div>
<div <div
v-if="isLoadingInitialRoute" v-if="isLoadingInitialRoute"
class="flex min-h-screen flex-col items-center justify-center" class="flex flex-col items-center justify-center min-h-screen"
> >
<HoppSmartSpinner /> <HoppSmartSpinner />
</div> </div>

View File

@@ -9,7 +9,6 @@ declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default'] AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default'] AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
AppBanner: typeof import('./components/app/Banner.vue')['default']
AppContextMenu: typeof import('./components/app/ContextMenu.vue')['default'] AppContextMenu: typeof import('./components/app/ContextMenu.vue')['default']
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default'] AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
AppFooter: typeof import('./components/app/Footer.vue')['default'] AppFooter: typeof import('./components/app/Footer.vue')['default']
@@ -61,7 +60,6 @@ declare module 'vue' {
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default'] CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default'] CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default'] CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default']
Embeds: typeof import('./components/embeds/index.vue')['default']
Environments: typeof import('./components/environments/index.vue')['default'] Environments: typeof import('./components/environments/index.vue')['default']
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default'] EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default'] EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
@@ -94,13 +92,11 @@ declare module 'vue' {
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary'] HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary'] HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor'] HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox'] HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal'] HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand'] HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip'] HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput'] HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem'] HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink'] HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal'] HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
@@ -109,7 +105,6 @@ declare module 'vue' {
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing'] HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio'] HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup'] HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
HoppSmartSelectWrapper: typeof import('@hoppscotch/ui')['HoppSmartSelectWrapper']
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver'] HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner'] HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab'] HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
@@ -145,11 +140,9 @@ declare module 'vue' {
HttpTests: typeof import('./components/http/Tests.vue')['default'] HttpTests: typeof import('./components/http/Tests.vue')['default']
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default'] HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
IconLucideActivity: typeof import('~icons/lucide/activity')['default'] IconLucideActivity: typeof import('~icons/lucide/activity')['default']
IconLucideAlertCircle: typeof import('~icons/lucide/alert-circle')['default']
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default'] IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default'] IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default'] IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
IconLucideBrush: typeof import('~icons/lucide/brush')['default']
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default'] IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default'] IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
IconLucideGlobe: typeof import('~icons/lucide/globe')['default'] IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
@@ -159,11 +152,8 @@ declare module 'vue' {
IconLucideLayers: typeof import('~icons/lucide/layers')['default'] IconLucideLayers: typeof import('~icons/lucide/layers')['default']
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default'] IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
IconLucideMinus: typeof import('~icons/lucide/minus')['default'] IconLucideMinus: typeof import('~icons/lucide/minus')['default']
IconLucideRss: typeof import('~icons/lucide/rss')['default']
IconLucideSearch: typeof import('~icons/lucide/search')['default'] IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideUsers: typeof import('~icons/lucide/users')['default'] IconLucideUsers: typeof import('~icons/lucide/users')['default']
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
IconLucideX: typeof import('~icons/lucide/x')['default']
InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default'] InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default']
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default'] InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default'] LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
@@ -187,16 +177,6 @@ declare module 'vue' {
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default'] RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
SettingsExtension: typeof import('./components/settings/Extension.vue')['default'] SettingsExtension: typeof import('./components/settings/Extension.vue')['default']
SettingsProxy: typeof import('./components/settings/Proxy.vue')['default'] SettingsProxy: typeof import('./components/settings/Proxy.vue')['default']
Share: typeof import('./components/share/index.vue')['default']
ShareCreateModal: typeof import('./components/share/CreateModal.vue')['default']
ShareCustomizeModal: typeof import('./components/share/CustomizeModal.vue')['default']
ShareModal: typeof import('./components/share/Modal.vue')['default']
ShareRequest: typeof import('./components/share/Request.vue')['default']
ShareRequestModal: typeof import('./components/share/RequestModal.vue')['default']
ShareShareRequestModal: typeof import('./components/share/ShareRequestModal.vue')['default']
ShareTemplatesButton: typeof import('./components/share/templates/Button.vue')['default']
ShareTemplatesEmbeds: typeof import('./components/share/templates/Embeds.vue')['default']
ShareTemplatesLink: typeof import('./components/share/templates/Link.vue')['default']
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default'] SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default'] SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default'] SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
@@ -217,11 +197,9 @@ declare module 'vue' {
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default'] SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default'] SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default'] SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
SmartSelectWrapper: typeof import('./../../hoppscotch-ui/src/components/smart/SelectWrapper.vue')['default']
SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default'] SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default']
SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.vue')['default'] SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.vue')['default']
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default'] SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
SmartTable: typeof import('./../../hoppscotch-ui/src/components/smart/Table.vue')['default']
SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default'] SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default']
SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default'] SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
SmartTree: typeof import('./../../hoppscotch-ui/src/components/smart/Tree.vue')['default'] SmartTree: typeof import('./../../hoppscotch-ui/src/components/smart/Tree.vue')['default']

View File

@@ -0,0 +1,22 @@
<template>
<div
class="relative flex items-center px-4 py-2 transition bg-error text-tiny group"
role="alert"
>
<icon-lucide-info class="mr-2" />
<span class="text-secondaryDark">
<span class="md:hidden">
{{ t("helpers.offline_short") }}
</span>
<span class="<md:hidden">
{{ t("helpers.offline") }}
</span>
</span>
</div>
</template>
<script setup lang="ts">
import { useI18n } from "~/composables/i18n"
const t = useI18n()
</script>

View File

@@ -1,70 +0,0 @@
<template>
<div
:role="bannerRole"
class="flex items-center justify-between px-4 py-2 text-tiny text-secondaryDark"
:class="bannerColor"
>
<div class="flex items-center">
<component :is="bannerIcon" class="mr-2" />
<span :class="{ 'hidden sm:inline-flex': banner.alternateText }">
{{ banner.text(t) }}
</span>
<span v-if="banner.alternateText" class="inline-flex sm:hidden">
{{ banner.alternateText(t) }}
</span>
</div>
<icon-lucide-x
v-if="dismissible"
class="opacity-50 hover:cursor-pointer hover:opacity-100"
@click="emit('dismiss')"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue"
import { BannerContent, BannerType } from "~/services/banner.service"
import { useI18n } from "@composables/i18n"
import IconAlertCircle from "~icons/lucide/alert-circle"
import IconAlertTriangle from "~icons/lucide/alert-triangle"
import IconInfo from "~icons/lucide/info"
const props = withDefaults(
defineProps<{
banner: BannerContent
dismissible?: boolean
}>(),
{
dismissible: false,
}
)
const t = useI18n()
const emit = defineEmits<{
(e: "dismiss"): void
}>()
const ariaRoles: Record<BannerType, string> = {
info: "status",
warning: "status",
error: "alert",
}
const bgColors: Record<BannerType, string> = {
info: "bg-bannerInfo",
warning: "bg-bannerWarning",
error: "bg-bannerError",
}
const icons = {
info: IconInfo,
warning: IconAlertCircle,
error: IconAlertTriangle,
}
const bannerColor = computed(() => bgColors[props.banner.type])
const bannerIcon = computed(() => icons[props.banner.type])
const bannerRole = computed(() => ariaRoles[props.banner.type])
</script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div <div
ref="contextMenuRef" ref="contextMenuRef"
class="fixed translate-y-8 transform rounded border border-dividerDark bg-popover p-2 shadow-lg" class="fixed bg-popover shadow-lg transform translate-y-8 border border-dividerDark p-2 rounded"
:style="`top: ${position.top}px; left: ${position.left}px; z-index: 1000;`" :style="`top: ${position.top}px; left: ${position.left}px; z-index: 1000;`"
> >
<div v-if="contextMenuOptions" class="flex flex-col"> <div v-if="contextMenuOptions" class="flex flex-col">

View File

@@ -6,7 +6,7 @@
@close="hideModal" @close="hideModal"
> >
<template #body> <template #body>
<p class="mb-4 px-2 text-secondaryLight"> <p class="px-2 mb-4 text-secondaryLight">
{{ t("app.developer_option_description") }} {{ t("app.developer_option_description") }}
</p> </p>
<div class="flex flex-1"> <div class="flex flex-1">

View File

@@ -181,7 +181,7 @@
@click="COLUMN_LAYOUT = !COLUMN_LAYOUT" @click="COLUMN_LAYOUT = !COLUMN_LAYOUT"
/> />
<span <span
class="transform transition" class="transition transform"
:class="{ :class="{
'rotate-180': SIDEBAR_ON_LEFT, 'rotate-180': SIDEBAR_ON_LEFT,
}" }"

View File

@@ -2,31 +2,29 @@
<div> <div>
<header <header
ref="headerRef" ref="headerRef"
class="grid grid-cols-5 grid-rows-1 gap-2 overflow-x-auto overflow-y-hidden p-2" class="flex items-center justify-between flex-1 flex-shrink-0 px-2 py-2 space-x-2 overflow-x-auto overflow-y-hidden"
@mousedown.prevent="platform.ui?.appHeader?.onHeaderAreaClick?.()" @mousedown.prevent="platform.ui?.appHeader?.onHeaderAreaClick?.()"
> >
<div <div
class="col-span-2 flex items-center justify-between space-x-2" class="inline-flex items-center justify-start flex-1 space-x-2"
:style="{ :style="{
paddingTop: platform.ui?.appHeader?.paddingTop?.value, paddingTop: platform.ui?.appHeader?.paddingTop?.value,
paddingLeft: platform.ui?.appHeader?.paddingLeft?.value, paddingLeft: platform.ui?.appHeader?.paddingLeft?.value,
}" }"
> >
<div class="flex"> <HoppButtonSecondary
<HoppButtonSecondary class="tracking-wide !font-bold !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark uppercase"
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark" :label="t('app.name')"
:label="t('app.name')" to="/"
to="/" />
/>
</div>
</div> </div>
<div class="col-span-1 flex items-center justify-between space-x-2"> <div class="inline-flex items-center justify-center flex-1 space-x-2">
<button <button
class="flex h-full flex-1 cursor-text items-center justify-between self-stretch rounded border border-dividerDark bg-primaryDark px-2 text-secondaryLight transition hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary" class="flex flex-1 items-center justify-between px-2 py-1 self-stretch bg-primaryDark transition text-secondaryLight cursor-text rounded border border-dividerDark max-w-60 hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
@click="invokeAction('modals.search.toggle')" @click="invokeAction('modals.search.toggle')"
> >
<span class="inline-flex flex-1 items-center"> <span class="inline-flex flex-1 items-center">
<icon-lucide-search class="svg-icons mr-2" /> <icon-lucide-search class="mr-2 svg-icons" />
{{ t("app.search") }} {{ t("app.search") }}
</span> </span>
<span class="flex space-x-1"> <span class="flex space-x-1">
@@ -34,189 +32,192 @@
<kbd class="shortcut-key">K</kbd> <kbd class="shortcut-key">K</kbd>
</span> </span>
</button> </button>
<HoppButtonSecondary
v-if="showInstallButton"
v-tippy="{ theme: 'tooltip' }"
:title="t('header.install_pwa')"
:icon="IconDownload"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
@click="installPWA()"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${
mdAndLarger ? t('support.title') : t('app.options')
} <kbd>?</kbd>`"
:icon="IconLifeBuoy"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
@click="invokeAction('modals.support.toggle')"
/>
</div> </div>
<div class="col-span-2 flex items-center justify-between space-x-2"> <div class="inline-flex items-center justify-end flex-1 space-x-2">
<div class="flex"> <div
v-if="currentUser === null"
class="inline-flex items-center space-x-2"
>
<HoppButtonSecondary <HoppButtonSecondary
v-if="showInstallButton" :icon="IconUploadCloud"
v-tippy="{ theme: 'tooltip' }" :label="t('header.save_workspace')"
:title="t('header.install_pwa')" class="hidden md:flex bg-green-500/15 py-1.75 border border-green-600/25 !text-green-500 hover:bg-green-400/10 focus-visible:bg-green-400/10 focus-visible:border-green-800/50 !focus-visible:text-green-600 hover:border-green-800/50 !hover:text-green-600"
:icon="IconDownload" @click="invokeAction('modals.login.toggle')"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
@click="installPWA()"
/> />
<HoppButtonSecondary <HoppButtonPrimary
v-tippy="{ theme: 'tooltip', allowHTML: true }" :label="t('header.login')"
:title="`${ @click="invokeAction('modals.login.toggle')"
mdAndLarger ? t('support.title') : t('app.options')
} <kbd>?</kbd>`"
:icon="IconLifeBuoy"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
@click="invokeAction('modals.support.toggle')"
/> />
</div> </div>
<div class="flex"> <div v-else class="inline-flex items-center space-x-2">
<TeamsMemberStack
v-if="
workspace.type === 'team' &&
selectedTeam &&
selectedTeam.teamMembers.length > 1
"
:team-members="selectedTeam.teamMembers"
show-count
class="mx-2"
@handle-click="handleTeamEdit()"
/>
<div <div
v-if="currentUser === null" class="flex border divide-x rounded bg-green-500/15 divide-green-600/25 border-green-600/25 focus-within:bg-green-400/10 focus-within:border-green-800/50 focus-within:divide-green-800/50 hover:bg-green-400/10 hover:border-green-800/50 hover:divide-green-800/50"
class="inline-flex items-center space-x-2"
> >
<HoppButtonSecondary <HoppButtonSecondary
:icon="IconUploadCloud" v-tippy="{ theme: 'tooltip' }"
:label="t('header.save_workspace')" :title="t('team.invite_tooltip')"
class="!focus-visible:text-emerald-600 !hover:text-emerald-600 hidden h-8 border border-emerald-600/25 bg-emerald-500/10 !text-emerald-500 hover:border-emerald-600/20 hover:bg-emerald-600/20 focus-visible:border-emerald-600/20 focus-visible:bg-emerald-600/20 md:flex" :icon="IconUserPlus"
@click="invokeAction('modals.login.toggle')" class="py-1.75 !text-green-500 !focus-visible:text-green-600 !hover:text-green-600"
@click="handleInvite()"
/> />
<HoppButtonPrimary <HoppButtonSecondary
:label="t('header.login')"
class="h-8"
@click="invokeAction('modals.login.toggle')"
/>
</div>
<div v-else class="inline-flex items-center space-x-2">
<TeamsMemberStack
v-if=" v-if="
workspace.type === 'team' && workspace.type === 'team' &&
selectedTeam && selectedTeam &&
selectedTeam.teamMembers.length > 1 selectedTeam?.myRole === 'OWNER'
" "
:team-members="selectedTeam.teamMembers" v-tippy="{ theme: 'tooltip' }"
show-count :title="t('team.edit')"
class="mx-2" :icon="IconSettings"
@handle-click="handleTeamEdit()" class="py-1.75 !text-green-500 !focus-visible:text-green-600 !hover:text-green-600"
@click="handleTeamEdit()"
/> />
<div </div>
class="flex h-8 divide-x divide-emerald-600/25 rounded border border-emerald-600/25 bg-emerald-500/10 focus-within:divide-emerald-600/20 focus-within:border-emerald-600/20 focus-within:bg-emerald-600/20 hover:divide-emerald-600/20 hover:border-emerald-600/20 hover:bg-emerald-600/20" <tippy
> interactive
<HoppButtonSecondary trigger="click"
v-tippy="{ theme: 'tooltip' }" theme="popover"
:title="t('team.invite_tooltip')" :on-shown="() => accountActions.focus()"
:icon="IconUserPlus" >
class="!focus-visible:text-emerald-600 !hover:text-emerald-600 !text-emerald-500" <HoppButtonSecondary
@click="handleInvite()" v-tippy="{ theme: 'tooltip' }"
/> :title="t('workspace.change')"
<HoppButtonSecondary :label="mdAndLarger ? workspaceName : ``"
v-if=" :icon="workspace.type === 'personal' ? IconUser : IconUsers"
workspace.type === 'team' && class="pr-8 select-wrapper rounded bg-blue-500/15 py-1.75 border border-blue-600/25 !text-blue-500 focus-visible:bg-blue-400/10 focus-visible:border-blue-800/50 !focus-visible:text-blue-600 hover:bg-blue-400/10 hover:border-blue-800/50 !hover:text-blue-600"
selectedTeam && />
selectedTeam?.myRole === 'OWNER' <template #content="{ hide }">
" <div
v-tippy="{ theme: 'tooltip' }" ref="accountActions"
:title="t('team.edit')" class="flex flex-col focus:outline-none"
:icon="IconSettings" tabindex="0"
class="!focus-visible:text-emerald-600 !hover:text-emerald-600 !text-emerald-500" @keyup.escape="hide()"
@click="handleTeamEdit()" @click="hide()"
/> >
</div> <WorkspaceSelector />
</div>
</template>
</tippy>
<span class="px-2">
<tippy <tippy
interactive interactive
trigger="click" trigger="click"
theme="popover" theme="popover"
:on-shown="() => accountActions.focus()" :on-shown="() => tippyActions.focus()"
> >
<HoppSmartSelectWrapper <HoppSmartPicture
class="!text-blue-500 !focus-visible:text-blue-600 !hover:text-blue-600" v-if="currentUser.photoURL"
> v-tippy="{
<HoppButtonSecondary theme: 'tooltip',
v-tippy="{ theme: 'tooltip' }" }"
:title="t('workspace.change')" :url="currentUser.photoURL"
:label="mdAndLarger ? workspaceName : ``" :alt="
:icon="workspace.type === 'personal' ? IconUser : IconUsers" currentUser.displayName ||
class="!focus-visible:text-blue-600 !hover:text-blue-600 h-8 rounded border border-blue-600/25 bg-blue-500/10 pr-8 !text-blue-500 hover:border-blue-600/20 hover:bg-blue-600/20 focus-visible:border-blue-600/20 focus-visible:bg-blue-600/20" t('profile.default_hopp_displayname')
/> "
</HoppSmartSelectWrapper> :title="
currentUser.displayName ||
currentUser.email ||
t('profile.default_hopp_displayname')
"
indicator
:indicator-styles="
network.isOnline ? 'bg-green-500' : 'bg-red-500'
"
/>
<HoppSmartPicture
v-else
v-tippy="{ theme: 'tooltip' }"
:title="
currentUser.displayName ||
currentUser.email ||
t('profile.default_hopp_displayname')
"
:initial="currentUser.displayName || currentUser.email"
indicator
:indicator-styles="
network.isOnline ? 'bg-green-500' : 'bg-red-500'
"
/>
<template #content="{ hide }"> <template #content="{ hide }">
<div <div
ref="accountActions" ref="tippyActions"
class="flex flex-col focus:outline-none" class="flex flex-col focus:outline-none"
tabindex="0" tabindex="0"
@keyup.p="profile.$el.click()"
@keyup.s="settings.$el.click()"
@keyup.l="logout.$el.click()"
@keyup.escape="hide()" @keyup.escape="hide()"
@click="hide()"
> >
<WorkspaceSelector /> <div class="flex flex-col px-2 text-tiny">
<span class="inline-flex font-semibold truncate">
{{
currentUser.displayName ||
t("profile.default_hopp_displayname")
}}
</span>
<span class="inline-flex truncate text-secondaryLight">
{{ currentUser.email }}
</span>
</div>
<hr />
<HoppSmartItem
ref="profile"
to="/profile"
:icon="IconUser"
:label="t('navigation.profile')"
:shortcut="['P']"
@click="hide()"
/>
<HoppSmartItem
ref="settings"
to="/settings"
:icon="IconSettings"
:label="t('navigation.settings')"
:shortcut="['S']"
@click="hide()"
/>
<FirebaseLogout
ref="logout"
:shortcut="['L']"
@confirm-logout="hide()"
/>
</div> </div>
</template> </template>
</tippy> </tippy>
<span class="px-2"> </span>
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => tippyActions.focus()"
>
<HoppSmartPicture
v-tippy="{
theme: 'tooltip',
}"
:name="currentUser.uid"
:title="
currentUser.displayName ||
currentUser.email ||
t('profile.default_hopp_displayname')
"
indicator
:indicator-styles="
network.isOnline ? 'bg-green-500' : 'bg-red-500'
"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.p="profile.$el.click()"
@keyup.s="settings.$el.click()"
@keyup.l="logout.$el.click()"
@keyup.escape="hide()"
>
<div class="flex flex-col px-2">
<span class="inline-flex truncate font-semibold">
{{
currentUser.displayName ||
t("profile.default_hopp_displayname")
}}
</span>
<span
class="inline-flex truncate text-secondaryLight text-tiny"
>
{{ currentUser.email }}
</span>
</div>
<hr />
<HoppSmartItem
ref="profile"
to="/profile"
:icon="IconUser"
:label="t('navigation.profile')"
:shortcut="['P']"
@click="hide()"
/>
<HoppSmartItem
ref="settings"
to="/settings"
:icon="IconSettings"
:label="t('navigation.settings')"
:shortcut="['S']"
@click="hide()"
/>
<FirebaseLogout
ref="logout"
:shortcut="['L']"
@confirm-logout="hide()"
/>
</div>
</template>
</tippy>
</span>
</div>
</div> </div>
</div> </div>
</header> </header>
<AppBanner <AppAnnouncement v-if="!network.isOnline" />
v-if="bannerContent"
:banner="bannerContent"
:dismissible="true"
@dismiss="dismissOfflineBanner"
/>
<TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" /> <TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" />
<TeamsInvite <TeamsInvite
v-if="workspace.type === 'team' && workspace.teamID" v-if="workspace.type === 'team' && workspace.teamID"
@@ -232,6 +233,7 @@
@invite-team="inviteTeam(editingTeamName, editingTeamID)" @invite-team="inviteTeam(editingTeamName, editingTeamID)"
@refetch-teams="refetchTeams" @refetch-teams="refetchTeams"
/> />
<HoppSmartConfirmModal <HoppSmartConfirmModal
:show="confirmRemove" :show="confirmRemove"
:title="t('confirm.remove_team')" :title="t('confirm.remove_team')"
@@ -264,11 +266,6 @@ import IconUsers from "~icons/lucide/users"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither" import * as TE from "fp-ts/TaskEither"
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team" import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
import {
BannerService,
BannerContent,
BANNER_PRIORITY_HIGH,
} from "~/services/banner.service"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -286,33 +283,7 @@ const showTeamsModal = ref(false)
const breakpoints = useBreakpoints(breakpointsTailwind) const breakpoints = useBreakpoints(breakpointsTailwind)
const mdAndLarger = breakpoints.greater("md") const mdAndLarger = breakpoints.greater("md")
const banner = useService(BannerService)
const bannerContent = computed(() => banner.content.value?.content)
let bannerID: number | null = null
const offlineBanner: BannerContent = {
type: "warning",
text: (t) => t("helpers.offline"),
alternateText: (t) => t("helpers.offline_short"),
score: BANNER_PRIORITY_HIGH,
}
const network = reactive(useNetwork()) const network = reactive(useNetwork())
const isOnline = computed(() => network.isOnline)
// Show the offline banner if the user is offline
watch(isOnline, () => {
if (!isOnline.value) {
bannerID = banner.showBanner(offlineBanner)
return
} else {
if (banner.content && bannerID) {
banner.removeBanner(bannerID)
}
}
})
const dismissOfflineBanner = () => banner.removeBanner(bannerID!)
const currentUser = useReadonlyStream( const currentUser = useReadonlyStream(
platform.auth.getProbableUserStream(), platform.auth.getProbableUserStream(),

View File

@@ -1,7 +1,7 @@
<template> <template>
<div v-if="inspectionResults && inspectionResults.length > 0"> <div v-if="inspectionResults && inspectionResults.length > 0">
<tippy interactive trigger="click" theme="popover"> <tippy interactive trigger="click" theme="popover">
<div class="flex flex-1 flex-col items-center justify-center"> <div class="flex justify-center items-center flex-1 flex-col">
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:icon="IconAlertTriangle" :icon="IconAlertTriangle"
@@ -10,12 +10,12 @@
/> />
</div> </div>
<template #content="{ hide }"> <template #content="{ hide }">
<div class="flex flex-1 flex-col items-start space-y-2"> <div class="flex flex-col space-y-2 items-start flex-1">
<div <div
class="sticky top-0 flex justify-between self-stretch rounded border border-divider bg-popover pl-2" class="flex justify-between border rounded pl-2 border-divider bg-popover sticky top-0 self-stretch"
> >
<span class="flex flex-1 items-center"> <span class="flex items-center flex-1">
<icon-lucide-activity class="svg-icons mr-2 text-accent" /> <icon-lucide-activity class="mr-2 svg-icons text-accent" />
<span class="font-bold"> <span class="font-bold">
{{ t("inspections.title") }} {{ t("inspections.title") }}
</span> </span>
@@ -31,10 +31,10 @@
<div <div
v-for="(inspector, index) in inspectionResults" v-for="(inspector, index) in inspectionResults"
:key="index" :key="index"
class="flex w-full max-w-md self-stretch" class="flex self-stretch max-w-md w-full"
> >
<div <div
class="flex flex-1 flex-col divide-y divide-dashed divide-dividerDark rounded border border-dashed border-dividerDark" class="flex flex-col flex-1 rounded border border-dashed border-dividerDark divide-y divide-dashed divide-dividerDark"
> >
<span <span
v-if="inspector.text.type === 'text'" v-if="inspector.text.type === 'text'"
@@ -44,13 +44,13 @@
<HoppSmartLink <HoppSmartLink
blank blank
:to="inspector.doc.link" :to="inspector.doc.link"
class="text-accent transition hover:text-accentDark" class="text-accent hover:text-accentDark transition"
> >
{{ inspector.doc.text }} {{ inspector.doc.text }}
<icon-lucide-arrow-up-right class="svg-icons" /> <icon-lucide-arrow-up-right class="svg-icons" />
</HoppSmartLink> </HoppSmartLink>
</span> </span>
<span v-if="inspector.action" class="flex space-x-2 p-2"> <span v-if="inspector.action" class="flex p-2 space-x-2">
<HoppButtonSecondary <HoppButtonSecondary
:label="inspector.action.text" :label="inspector.action.text"
outline outline
@@ -92,8 +92,9 @@ const getHighestSeverity = computed(() => {
}, },
{ severity: 0 } { severity: 0 }
) )
} else {
return { severity: 0 }
} }
return { severity: 0 }
}) })
const severityColor = (severity: number) => { const severityColor = (severity: number) => {

View File

@@ -8,7 +8,7 @@
> >
<template #body> <template #body>
<div class="flex flex-col space-y-2"> <div class="flex flex-col space-y-2">
<h2 class="p-4 font-bold font-semibold text-secondaryDark"> <h2 class="p-4 font-semibold font-bold text-secondaryDark">
{{ t("layout.name") }} {{ t("layout.name") }}
</h2> </h2>
<HoppSmartItem <HoppSmartItem
@@ -27,7 +27,7 @@
active active
@click="expandCollection" @click="expandCollection"
/> />
<h2 class="p-4 font-bold font-semibold text-secondaryDark"> <h2 class="p-4 font-semibold font-bold text-secondaryDark">
{{ t("support.title") }} {{ t("support.title") }}
</h2> </h2>
<template <template

View File

@@ -47,15 +47,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Pane, Splitpanes } from "splitpanes" import { Splitpanes, Pane } from "splitpanes"
import "splitpanes/dist/splitpanes.css" import "splitpanes/dist/splitpanes.css"
import { useSetting } from "@composables/settings"
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core" import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
import { useService } from "dioc/vue" import { computed, useSlots, ref } from "vue"
import { computed, ref, useSlots } from "vue" import { useSetting } from "@composables/settings"
import { PersistenceService } from "~/services/persistence" import { setLocalConfig, getLocalConfig } from "~/newstore/localpersistence"
const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT") const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT")
@@ -68,8 +67,6 @@ const SIDEBAR = useSetting("SIDEBAR")
const slots = useSlots() const slots = useSlots()
const persistenceService = useService(PersistenceService)
const hasSidebar = computed(() => !!slots.sidebar) const hasSidebar = computed(() => !!slots.sidebar)
const hasSecondary = computed(() => !!slots.secondary) const hasSecondary = computed(() => !!slots.secondary)
@@ -99,7 +96,7 @@ if (!COLUMN_LAYOUT.value) {
function setPaneEvent(event: PaneEvent[], type: "vertical" | "horizontal") { function setPaneEvent(event: PaneEvent[], type: "vertical" | "horizontal") {
if (!props.layoutId) return if (!props.layoutId) return
const storageKey = `${props.layoutId}-pane-config-${type}` const storageKey = `${props.layoutId}-pane-config-${type}`
persistenceService.setLocalConfig(storageKey, JSON.stringify(event)) setLocalConfig(storageKey, JSON.stringify(event))
} }
function populatePaneEvent() { function populatePaneEvent() {
@@ -122,7 +119,7 @@ function populatePaneEvent() {
function getPaneData(type: "vertical" | "horizontal"): PaneEvent[] | null { function getPaneData(type: "vertical" | "horizontal"): PaneEvent[] | null {
const storageKey = `${props.layoutId}-pane-config-${type}` const storageKey = `${props.layoutId}-pane-config-${type}`
const paneEvent = persistenceService.getLocalConfig(storageKey) const paneEvent = getLocalConfig(storageKey)
if (!paneEvent) return null if (!paneEvent) return null
return JSON.parse(paneEvent) return JSON.parse(paneEvent)
} }

View File

@@ -16,13 +16,13 @@
class="share-link" class="share-link"
tabindex="0" tabindex="0"
> >
<component :is="platform.icon" class="h-6 w-6" /> <component :is="platform.icon" class="w-6 h-6" />
<span class="mt-3"> <span class="mt-3">
{{ platform.name }} {{ platform.name }}
</span> </span>
</a> </a>
<button class="share-link" @click="copyAppLink"> <button class="share-link" @click="copyAppLink">
<component :is="copyIcon" class="h-6 w-6 text-xl" /> <component :is="copyIcon" class="w-6 h-6 text-xl" />
<span class="mt-3"> <span class="mt-3">
{{ t("app.copy") }} {{ t("app.copy") }}
</span> </span>
@@ -119,14 +119,14 @@ const hideModal = () => {
.share-link { .share-link {
@apply border border-dividerLight; @apply border border-dividerLight;
@apply rounded; @apply rounded;
@apply flex flex-col; @apply flex-col flex;
@apply p-4; @apply p-4;
@apply items-center; @apply items-center;
@apply justify-center; @apply justify-center;
@apply font-semibold; @apply font-semibold;
@apply hover:bg-primaryLight hover:text-secondaryDark; @apply hover: (bg-primaryLight text-secondaryDark);
@apply focus:outline-none; @apply focus: outline-none;
@apply focus-visible:border-divider; @apply focus-visible: border-divider;
svg { svg {
@apply opacity-80; @apply opacity-80;

View File

@@ -2,7 +2,7 @@
<HoppSmartSlideOver :show="show" :title="t('app.shortcuts')" @close="close()"> <HoppSmartSlideOver :show="show" :title="t('app.shortcuts')" @close="close()">
<template #content> <template #content>
<div <div
class="sticky top-0 z-10 flex flex-shrink-0 flex-col overflow-x-auto bg-primary" class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary"
> >
<HoppSmartInput <HoppSmartInput
v-model="filterText" v-model="filterText"
@@ -17,7 +17,7 @@
v-if="isEmpty(shortcutsResults)" v-if="isEmpty(shortcutsResults)"
:text="`${t('state.nothing_found')} ‟${filterText}”`" :text="`${t('state.nothing_found')} ‟${filterText}”`"
> >
<icon-lucide-search class="svg-icons pb-2 opacity-75" /> <icon-lucide-search class="pb-2 opacity-75 svg-icons" />
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<details <details
@@ -28,16 +28,16 @@
open open
> >
<summary <summary
class="flex min-w-0 flex-1 cursor-pointer items-center px-6 py-4 font-semibold text-secondaryLight transition hover:text-secondaryDark focus:outline-none" class="flex items-center flex-1 min-w-0 px-6 py-4 font-semibold transition cursor-pointer focus:outline-none text-secondaryLight hover:text-secondaryDark"
> >
<icon-lucide-chevron-right class="indicator mr-2" /> <icon-lucide-chevron-right class="mr-2 indicator" />
<span <span
class="capitalize-first truncate font-semibold text-secondaryDark" class="font-semibold truncate capitalize-first text-secondaryDark"
> >
{{ sectionTitle }} {{ sectionTitle }}
</span> </span>
</summary> </summary>
<div class="flex flex-col space-y-2 px-6 pb-4"> <div class="flex flex-col px-6 pb-4 space-y-2">
<AppShortcutsEntry <AppShortcutsEntry
v-for="(shortcut, index) in sectionResults" v-for="(shortcut, index) in sectionResults"
:key="`shortcut-${index}`" :key="`shortcut-${index}`"

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="flex items-center py-1"> <div class="flex items-center py-1">
<span class="mr-4 flex flex-1"> <span class="flex flex-1 mr-4">
{{ shortcut.label }} {{ shortcut.label }}
</span> </span>
<kbd <kbd

View File

@@ -1,17 +1,17 @@
<template> <template>
<div class="flex flex-col items-center justify-center text-secondaryLight"> <div class="flex flex-col items-center justify-center text-secondaryLight">
<div class="mb-4 flex space-x-2"> <div class="flex mb-4 space-x-2">
<div class="flex flex-col items-end space-y-4 text-right"> <div class="flex flex-col items-end space-y-4 text-right">
<span class="flex flex-1 items-center"> <span class="flex items-center flex-1">
{{ t("shortcut.request.send_request") }} {{ t("shortcut.request.send_request") }}
</span> </span>
<span class="flex flex-1 items-center"> <span class="flex items-center flex-1">
{{ t("shortcut.general.show_all") }} {{ t("shortcut.general.show_all") }}
</span> </span>
<span class="flex flex-1 items-center"> <span class="flex items-center flex-1">
{{ t("shortcut.general.command_menu") }} {{ t("shortcut.general.command_menu") }}
</span> </span>
<span class="flex flex-1 items-center"> <span class="flex items-center flex-1">
{{ t("shortcut.general.help_menu") }} {{ t("shortcut.general.help_menu") }}
</span> </span>
</div> </div>

View File

@@ -1,6 +1,6 @@
<template> <template>
<aside class="flex h-full justify-between md:flex-col"> <aside class="flex justify-between h-full md:flex-col">
<nav class="flex flex-1 flex-nowrap bg-primary md:flex-none md:flex-col"> <nav class="flex flex-1 flex-nowrap md:flex-col md:flex-none bg-primary">
<HoppSmartLink <HoppSmartLink
v-for="(navigation, index) in primaryNavigation" v-for="(navigation, index) in primaryNavigation"
:key="`navigation-${index}`" :key="`navigation-${index}`"
@@ -73,25 +73,25 @@ const primaryNavigation = [
.nav-link { .nav-link {
@apply relative; @apply relative;
@apply p-4; @apply p-4;
@apply flex flex-1 flex-col; @apply flex flex-col flex-1;
@apply items-center; @apply items-center;
@apply justify-center; @apply justify-center;
@apply hover:bg-primaryDark hover:text-secondaryDark; @apply hover: (bg-primaryDark text-secondaryDark);
@apply focus-visible:text-secondaryDark; @apply focus-visible: text-secondaryDark;
@apply after:absolute; @apply after:absolute;
@apply after:inset-x-0; @apply after:inset-x-0;
@apply after:md:inset-x-auto; @apply after:md: inset-x-auto;
@apply after:md:inset-y-0; @apply after:md: inset-y-0;
@apply after:bottom-0; @apply after:bottom-0;
@apply after:md:bottom-auto; @apply after:md: bottom-auto;
@apply after:md:left-0; @apply after:md: left-0;
@apply after:z-10; @apply after:z-2;
@apply after:h-0.5; @apply after:h-0.5;
@apply after:md:h-full; @apply after:md: h-full;
@apply after:w-full; @apply after:w-full;
@apply after:md:w-0.5; @apply after:md: w-0.5;
@apply after:content-[""]; @apply after:content-DEFAULT;
@apply focus:after:bg-divider; @apply focus: after: bg-divider;
.svg-icons { .svg-icons {
@apply opacity-75; @apply opacity-75;
@@ -105,7 +105,7 @@ const primaryNavigation = [
&.router-link-active { &.router-link-active {
@apply text-secondaryDark; @apply text-secondaryDark;
@apply bg-primaryLight; @apply bg-primaryLight;
@apply hover:text-secondaryDark; @apply hover: text-secondaryDark;
@apply after:bg-accent; @apply after:bg-accent;
.svg-icons { .svg-icons {
@@ -116,7 +116,7 @@ const primaryNavigation = [
&.exact-active-link { &.exact-active-link {
@apply text-secondaryDark; @apply text-secondaryDark;
@apply bg-primaryLight; @apply bg-primaryLight;
@apply hover:text-secondaryDark; @apply hover: text-secondaryDark;
@apply after:bg-accent; @apply after:bg-accent;
.svg-icons { .svg-icons {

View File

@@ -1,7 +1,7 @@
<template> <template>
<button <button
ref="el" ref="el"
class="search-entry relative flex flex-1 cursor-pointer items-center space-x-4 px-6 py-4 font-medium transition focus:outline-none" class="flex items-center flex-1 px-6 py-4 font-medium space-x-4 transition cursor-pointer relative search-entry focus:outline-none"
:class="{ 'active bg-primaryLight text-secondaryDark': active }" :class="{ 'active bg-primaryLight text-secondaryDark': active }"
tabindex="-1" tabindex="-1"
@click="emit('action')" @click="emit('action')"
@@ -9,8 +9,8 @@
> >
<component <component
:is="entry.icon" :is="entry.icon"
class="svg-icons opacity-80" class="opacity-50 svg-icons"
:class="{ 'opacity-25': active }" :class="{ 'opacity-100': active }"
/> />
<template <template
v-if="entry.text.type === 'text' && typeof entry.text.text === 'string'" v-if="entry.text.type === 'text' && typeof entry.text.text === 'string'"
@@ -82,9 +82,9 @@ const props = defineProps<{
const formattedShortcutKeys = computed( const formattedShortcutKeys = computed(
() => () =>
props.entry.meta?.keyboardShortcut?.map( props.entry.meta?.keyboardShortcut?.map((key) => {
(key) => SPECIAL_KEY_CHARS[key] ?? capitalize(key) return SPECIAL_KEY_CHARS[key] ?? capitalize(key)
) })
) )
const emit = defineEmits<{ const emit = defineEmits<{
@@ -112,9 +112,9 @@ watch(
@apply after:left-0; @apply after:left-0;
@apply after:bottom-0; @apply after:bottom-0;
@apply after:bg-transparent; @apply after:bg-transparent;
@apply after:z-10; @apply after:z-2;
@apply after:w-0.5; @apply after:w-0.5;
@apply after:content-['']; @apply after:content-DEFAULT;
&.active { &.active {
@apply after:bg-accentLight; @apply after:bg-accentLight;

View File

@@ -8,7 +8,7 @@
{{ historyEntry.request.url }} {{ historyEntry.request.url }}
</span> </span>
<span <span
class="flex flex-shrink-0 truncate rounded-md border border-dividerDark px-1 text-tiny font-semibold" class="font-semibold truncate text-tiny flex flex-shrink-0 border border-dividerDark rounded-md px-1"
> >
{{ historyEntry.request.query.split("\n")[0] }} {{ historyEntry.request.query.split("\n")[0] }}
</span> </span>

View File

@@ -1,5 +1,5 @@
<template> <template>
<span class="flex flex-1 items-center space-x-2"> <span class="flex flex-1 space-x-2 items-center">
<template v-for="(folder, index) in pathFolders" :key="index"> <template v-for="(folder, index) in pathFolders" :key="index">
<span class="block" :class="{ truncate: index !== 0 }"> <span class="block" :class="{ truncate: index !== 0 }">
{{ folder.name }} {{ folder.name }}

View File

@@ -5,7 +5,7 @@
</span> </span>
<icon-lucide-chevron-right class="flex flex-shrink-0" /> <icon-lucide-chevron-right class="flex flex-shrink-0" />
<span <span
class="flex flex-shrink-0 truncate rounded-md border border-dividerDark px-1 text-tiny font-semibold" class="font-semibold truncate text-tiny flex flex-shrink-0 border border-dividerDark rounded-md px-1"
:class="entryStatus.className" :class="entryStatus.className"
> >
{{ historyEntry.request.method }} {{ historyEntry.request.method }}

View File

@@ -8,8 +8,8 @@
</template> </template>
<span <span
v-if="request" v-if="request"
class="flex flex-shrink-0 truncate rounded-md border border-dividerDark px-1 text-tiny font-semibold" class="font-semibold truncate text-tiny flex flex-shrink-0 border border-dividerDark rounded-md px-1"
:style="{ color: getMethodLabelColorClassOf(request) }" :class="getMethodLabelColorClassOf(request)"
> >
{{ request.method.toUpperCase() }} {{ request.method.toUpperCase() }}
</span> </span>

View File

@@ -6,7 +6,7 @@
@close="emit('hide-modal')" @close="emit('hide-modal')"
> >
<template #body> <template #body>
<div class="flex flex-col border-b border-divider transition"> <div class="flex flex-col border-b transition border-divider">
<div class="flex items-center"> <div class="flex items-center">
<input <input
id="command" id="command"
@@ -16,14 +16,14 @@
autocomplete="off" autocomplete="off"
name="command" name="command"
:placeholder="`${t('app.type_a_command_search')}`" :placeholder="`${t('app.type_a_command_search')}`"
class="flex flex-1 bg-transparent px-6 pt-5 pb-3 text-base text-secondaryDark" class="flex flex-1 text-base bg-transparent text-secondaryDark px-6 py-5"
/> />
<HoppSmartSpinner v-if="searchSession?.loading" class="mr-6" /> <HoppSmartSpinner v-if="searchSession?.loading" class="mr-6" />
</div> </div>
</div> </div>
<div <div
v-if="searchSession && search.length > 0" v-if="searchSession && search.length > 0"
class="flex flex-1 flex-col divide-y divide-dividerLight overflow-y-auto border-b border-divider" class="flex flex-col flex-1 overflow-y-auto border-b border-divider divide-y divide-dividerLight"
> >
<div <div
v-for="([sectionID, sectionResult], sectionIndex) in scoredResults" v-for="([sectionID, sectionResult], sectionIndex) in scoredResults"
@@ -31,7 +31,7 @@
class="flex flex-col" class="flex flex-col"
> >
<h5 <h5
class="sticky top-0 z-10 bg-primaryContrast px-6 py-2 text-secondaryLight" class="px-6 py-2 bg-primaryContrast z-10 text-secondaryLight sticky top-0"
> >
{{ sectionResult.title }} {{ sectionResult.title }}
</h5> </h5>
@@ -49,7 +49,7 @@
:text="`${t('state.nothing_found')} ‟${search}”`" :text="`${t('state.nothing_found')} ‟${search}”`"
> >
<template #icon> <template #icon>
<icon-lucide-search class="svg-icons pb-2 opacity-75" /> <icon-lucide-search class="pb-2 opacity-75 svg-icons" />
</template> </template>
<HoppButtonSecondary <HoppButtonSecondary
:label="t('action.clear')" :label="t('action.clear')"
@@ -59,7 +59,7 @@
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
</div> </div>
<div <div
class="flex flex-shrink-0 justify-between overflow-auto whitespace-nowrap p-4 text-tiny text-secondaryLight <sm:hidden" class="flex flex-shrink-0 text-tiny text-secondaryLight p-4 justify-between whitespace-nowrap overflow-auto <sm:hidden"
> >
<div class="flex items-center"> <div class="flex items-center">
<kbd class="shortcut-key"></kbd> <kbd class="shortcut-key"></kbd>

View File

@@ -12,16 +12,16 @@
@dragleave="ordering = false" @dragleave="ordering = false"
@dragend="resetDragState" @dragend="resetDragState"
></div> ></div>
<div class="relative flex flex-col"> <div class="flex flex-col relative">
<div <div
class="z-[1] pointer-events-none absolute inset-0 bg-accent opacity-0 transition" class="absolute bg-accent opacity-0 pointer-events-none inset-0 z-1 transition"
:class="{ :class="{
'opacity-25': 'opacity-25':
dragging && notSameDestination && notSameParentDestination, dragging && notSameDestination && notSameParentDestination,
}" }"
></div> ></div>
<div <div
class="z-[3] group pointer-events-auto relative flex cursor-pointer items-stretch" class="flex items-stretch group relative z-3 cursor-pointer pointer-events-auto"
:draggable="!hasNoTeamAccess" :draggable="!hasNoTeamAccess"
@dragstart="dragStart" @dragstart="dragStart"
@drop="handelDrop($event)" @drop="handelDrop($event)"
@@ -36,11 +36,11 @@
@contextmenu.prevent="options?.tippy.show()" @contextmenu.prevent="options?.tippy.show()"
> >
<div <div
class="flex min-w-0 flex-1 items-center justify-center" class="flex items-center justify-center flex-1 min-w-0"
@click="emit('toggle-children')" @click="emit('toggle-children')"
> >
<span <span
class="pointer-events-none flex items-center justify-center px-4" class="flex items-center justify-center px-4 pointer-events-none"
> >
<HoppSmartSpinner v-if="isCollLoading" /> <HoppSmartSpinner v-if="isCollLoading" />
<component <component
@@ -51,7 +51,7 @@
/> />
</span> </span>
<span <span
class="pointer-events-none flex min-w-0 flex-1 py-2 pr-2 transition group-hover:text-secondaryDark" class="flex flex-1 min-w-0 py-2 pr-2 transition pointer-events-none group-hover:text-secondaryDark"
> >
<span class="truncate" :class="{ 'text-accent': isSelected }"> <span class="truncate" :class="{ 'text-accent': isSelected }">
{{ collectionName }} {{ collectionName }}
@@ -290,13 +290,13 @@ const collectionIcon = computed(() => {
if (props.isSelected) return IconCheckCircle if (props.isSelected) return IconCheckCircle
else if (!props.isOpen) return IconFolder else if (!props.isOpen) return IconFolder
else if (props.isOpen) return IconFolderOpen else if (props.isOpen) return IconFolderOpen
return IconFolder else return IconFolder
}) })
const collectionName = computed(() => { const collectionName = computed(() => {
if ((props.data as HoppCollection<HoppRESTRequest>).name) if ((props.data as HoppCollection<HoppRESTRequest>).name)
return (props.data as HoppCollection<HoppRESTRequest>).name return (props.data as HoppCollection<HoppRESTRequest>).name
return (props.data as TeamCollection).title else return (props.data as TeamCollection).title
}) })
watch( watch(
@@ -424,8 +424,9 @@ const isCollLoading = computed(() => {
props.data.id props.data.id
) { ) {
return collectionMoveLoading.includes(props.data.id) return collectionMoveLoading.includes(props.data.id)
} else {
return false
} }
return false
}) })
const resetDragState = () => { const resetDragState = () => {

View File

@@ -1,568 +1,361 @@
<template> <template>
<ImportExportBase <HoppSmartModal
ref="collections-import-export" v-if="show"
modal-title="modal.collections" dialog
:importer-modules="importerModules" :title="t('modal.collections')"
:exporter-modules="exporterModules" styles="sm:max-w-md"
@hide-modal="emit('hide-modal')" @close="hideModal"
/> >
<template #actions>
<HoppButtonSecondary
v-if="importerType !== null"
v-tippy="{ theme: 'tooltip' }"
:title="t('action.go_back')"
:icon="IconArrowLeft"
@click="resetImport"
/>
</template>
<template #body>
<div v-if="importerType !== null" class="flex flex-col">
<div class="flex flex-col pb-4">
<div
v-for="(step, index) in importerSteps"
:key="`step-${index}`"
class="flex flex-col space-y-8"
>
<div v-if="step.name === 'FILE_IMPORT'" class="space-y-4">
<p class="flex items-center">
<span
class="inline-flex items-center justify-center flex-shrink-0 mr-4 border-4 rounded-full border-primary text-dividerDark"
:class="{
'!text-green-500': hasFile,
}"
>
<icon-lucide-check-circle class="svg-icons" />
</span>
<span>
{{ t(`${step.metadata.caption}`) }}
</span>
</p>
<p
class="flex flex-col ml-10 border border-dashed rounded border-dividerDark"
>
<input
id="inputChooseFileToImportFrom"
ref="inputChooseFileToImportFrom"
name="inputChooseFileToImportFrom"
type="file"
class="p-4 cursor-pointer transition file:transition file:cursor-pointer text-secondary hover:text-secondaryDark file:mr-2 file:py-2 file:px-4 file:rounded file:border-0 file:text-secondary hover:file:text-secondaryDark file:bg-primaryLight hover:file:bg-primaryDark"
:accept="step.metadata.acceptedFileTypes"
@change="onFileChange"
/>
</p>
</div>
<div v-else-if="step.name === 'URL_IMPORT'" class="space-y-4">
<p class="flex items-center">
<span
class="inline-flex items-center justify-center flex-shrink-0 mr-4 border-4 rounded-full border-primary text-dividerDark"
:class="{
'!text-green-500': hasGist,
}"
>
<icon-lucide-check-circle class="svg-icons" />
</span>
<span>
{{ t(`${step.metadata.caption}`) }}
</span>
</p>
<p class="flex flex-col ml-10">
<input
v-model="inputChooseGistToImportFrom"
type="url"
class="input"
:placeholder="`${t('import.gist_url')}`"
/>
</p>
</div>
<div
v-else-if="step.name === 'TARGET_MY_COLLECTION'"
class="flex flex-col"
>
<div class="select-wrapper">
<select
v-model="mySelectedCollectionID"
autocomplete="off"
class="select"
autofocus
>
<option :key="undefined" :value="undefined" disabled selected>
{{ t("collection.select") }}
</option>
<option
v-for="(collection, collectionIndex) in myCollections"
:key="`collection-${collectionIndex}`"
:value="collectionIndex"
class="bg-primary"
>
{{ collection.name }}
</option>
</select>
</div>
</div>
</div>
</div>
<HoppButtonPrimary
:label="t('import.title')"
:disabled="enableImportButton"
:loading="importingMyCollections"
@click="finishImport"
/>
</div>
<div v-else class="flex flex-col">
<HoppSmartExpand>
<template #body>
<HoppSmartItem
v-for="(importer, index) in importerModules"
:key="`importer-${index}`"
:icon="importer.icon"
:label="t(`${importer.name}`)"
@click="importerType = index"
/>
</template>
</HoppSmartExpand>
<hr />
<div class="flex flex-col space-y-2">
<HoppSmartItem
v-tippy="{ theme: 'tooltip' }"
:title="t('action.download_file')"
:icon="IconDownload"
:loading="exportingTeamCollections"
:label="t('export.as_json')"
@click="emit('export-json-collection')"
/>
<span
v-if="platform.platformFeatureFlags.exportAsGIST"
v-tippy="{ theme: 'tooltip' }"
:title="
!currentUser
? `${t('export.require_github')}`
: currentUser.provider !== 'github.com'
? `${t('export.require_github')}`
: undefined
"
class="flex"
>
<HoppSmartItem
:disabled="
!currentUser
? true
: currentUser.provider !== 'github.com'
? true
: false
"
:icon="IconGithub"
:loading="creatingGistCollection"
:label="t('export.create_secret_gist')"
@click="emit('create-collection-gist')"
/>
</span>
</div>
</div>
</template>
</HoppSmartModal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import * as E from "fp-ts/Either" import IconArrowLeft from "~icons/lucide/arrow-left"
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource" import IconDownload from "~icons/lucide/download"
import { UrlSource } from "~/helpers/import-export/import/import-sources/UrlSource"
import IconFile from "~icons/lucide/file"
import {
hoppRESTImporter,
hoppInsomniaImporter,
hoppPostmanImporter,
toTeamsImporter,
hoppOpenAPIImporter,
} from "~/helpers/import-export/import/importers"
import { defineStep } from "~/composables/step-components"
import { PropType, computed, ref } from "vue"
import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast"
import { HoppCollection } from "@hoppscotch/data"
import { HoppRESTRequest } from "@hoppscotch/data"
import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
import MyCollectionImport from "~/components/importExport/ImportExportSteps/MyCollectionImport.vue"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import IconFolderPlus from "~icons/lucide/folder-plus"
import IconOpenAPI from "~icons/lucide/file"
import IconPostman from "~icons/hopp/postman"
import IconInsomnia from "~icons/hopp/insomnia"
import IconGithub from "~icons/lucide/github" import IconGithub from "~icons/lucide/github"
import IconLink from "~icons/lucide/link" import { computed, PropType, ref, watch } from "vue"
import { pipe } from "fp-ts/function"
import IconUser from "~icons/lucide/user" import * as E from "fp-ts/Either"
import { useReadonlyStream } from "~/composables/stream" import { HoppRESTRequest, HoppCollection } from "@hoppscotch/data"
import { useI18n } from "@composables/i18n"
import { getTeamCollectionJSON } from "~/helpers/backend/helpers" import { useReadonlyStream } from "@composables/stream"
import { useToast } from "@composables/toast"
import { platform } from "~/platform" import { platform } from "~/platform"
import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
import { RESTCollectionImporters } from "~/helpers/import-export/import/importers"
import { StepReturnValue } from "~/helpers/import-export/steps"
import { initializeDownloadCollection } from "~/helpers/import-export/export"
import { collectionsGistExporter } from "~/helpers/import-export/export/gistExport"
import { myCollectionsExporter } from "~/helpers/import-export/export/myCollections"
import { teamCollectionsExporter } from "~/helpers/import-export/export/teamCollections"
import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
import { ImporterOrExporter } from "~/components/importExport/types"
const t = useI18n()
const toast = useToast() const toast = useToast()
const t = useI18n()
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined type CollectionType = "team-collections" | "my-collections"
type CollectionType =
| {
type: "team-collections"
selectedTeam: SelectedTeam
}
| { type: "my-collections" }
const props = defineProps({ const props = defineProps({
collectionsType: { show: {
type: Object as PropType<CollectionType>, type: Boolean,
default: () => ({ default: false,
type: "my-collections",
selectedTeam: undefined,
}),
required: true, required: true,
}, },
collectionsType: {
type: String as PropType<CollectionType>,
default: "my-collections",
required: true,
},
exportingTeamCollections: {
type: Boolean,
default: false,
required: false,
},
creatingGistCollection: {
type: Boolean,
default: false,
required: false,
},
importingMyCollections: {
type: Boolean,
default: false,
required: false,
},
}) })
const emit = defineEmits<{
(e: "hide-modal"): void
(e: "update-team-collections"): void
(e: "export-json-collection"): void
(e: "create-collection-gist"): void
(e: "import-to-teams", payload: HoppCollection<HoppRESTRequest>[]): void
}>()
const hasFile = ref(false)
const hasGist = ref(false)
const importerType = ref<number | null>(null)
const stepResults = ref<StepReturnValue[]>([])
const inputChooseFileToImportFrom = ref<HTMLInputElement | any>()
const mySelectedCollectionID = ref<number | undefined>(undefined)
const inputChooseGistToImportFrom = ref<string>("")
const importerModules = computed(() =>
RESTCollectionImporters.filter(
(i) => i.applicableTo?.includes(props.collectionsType) ?? true
)
)
const importerModule = computed(() => {
if (importerType.value === null) return null
return importerModules.value[importerType.value]
})
const importerSteps = computed(() => importerModule.value?.steps ?? null)
const enableImportButton = computed(
() => !(stepResults.value.length === importerSteps.value?.length)
)
watch(mySelectedCollectionID, (newValue) => {
if (newValue === undefined) return
stepResults.value = []
stepResults.value.push(newValue)
})
watch(inputChooseGistToImportFrom, (url) => {
stepResults.value = []
if (url === "") {
hasGist.value = false
} else {
hasGist.value = true
stepResults.value.push(inputChooseGistToImportFrom.value)
}
})
const myCollections = useReadonlyStream(restCollections$, [])
const currentUser = useReadonlyStream( const currentUser = useReadonlyStream(
platform.auth.getCurrentUserStream(), platform.auth.getCurrentUserStream(),
platform.auth.getCurrentUser() platform.auth.getCurrentUser()
) )
const showImportFailedError = () => { const importerAction = async (stepResults: StepReturnValue[]) => {
toast.error(t("import.failed")) if (!importerModule.value) return
}
const handleImportToStore = async ( pipe(
collections: HoppCollection<HoppRESTRequest>[] await importerModule.value.importer(stepResults as any)(),
) => { E.match(
const importResult = (err) => {
props.collectionsType.type === "my-collections" failedImport()
? await importToPersonalWorkspace(collections) console.error("error", err)
: await importToTeamsWorkspace(collections) },
(result) => {
if (props.collectionsType === "team-collections") {
emit("import-to-teams", result)
} else {
appendRESTCollections(result)
if (E.isRight(importResult)) { platform.analytics?.logEvent({
toast.success(t("state.file_imported")) type: "HOPP_IMPORT_COLLECTION",
emit("hide-modal") importer: importerModule.value!.name,
} else { platform: "rest",
toast.error(t("import.failed")) workspaceType: "personal",
} })
}
const importToPersonalWorkspace = ( fileImported()
collections: HoppCollection<HoppRESTRequest>[]
) => {
appendRESTCollections(collections)
return E.right({
success: true,
})
}
const importToTeamsWorkspace = async (
collections: HoppCollection<HoppRESTRequest>[]
) => {
if (!hasTeamWriteAccess.value || !selectedTeamID.value) {
return E.left({
success: false,
})
}
const res = await toTeamsImporter(
JSON.stringify(collections),
selectedTeamID.value
)()
return E.isRight(res)
? E.right({ success: true })
: E.left({
success: false,
})
}
const emit = defineEmits<{
(e: "hide-modal"): () => void
}>()
const isHoppMyCollectionExporterInProgress = ref(false)
const isHoppTeamCollectionExporterInProgress = ref(false)
const isHoppGistCollectionExporterInProgress = ref(false)
const isTeamWorkspace = computed(() => {
return props.collectionsType.type === "team-collections"
})
const HoppRESTImporter: ImporterOrExporter = {
metadata: {
id: "hopp_rest",
name: "import.from_json",
title: "import.from_json_description",
icon: IconFolderPlus,
disabled: false,
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
},
component: FileSource({
caption: "import.from_file",
acceptedFileTypes: ".json",
onImportFromFile: async (content) => {
const res = await hoppRESTImporter(content)()
if (E.isRight(res)) {
handleImportToStore(res.right)
platform.analytics?.logEvent({
type: "HOPP_IMPORT_COLLECTION",
importer: "import.from_json",
platform: "rest",
workspaceType: isTeamWorkspace.value ? "team" : "personal",
})
} else {
showImportFailedError()
}
},
}),
}
const HoppMyCollectionImporter: ImporterOrExporter = {
metadata: {
id: "hopp_my_collection",
name: "import.from_my_collections",
title: "import.from_my_collections_description",
icon: IconUser,
disabled: false,
applicableTo: ["team-workspace"],
},
component: defineStep("my_collection_import", MyCollectionImport, () => ({
async onImportFromMyCollection(content) {
handleImportToStore([content])
// our analytics consider this as an export event, so keeping compatibility with that
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "import_to_teams",
platform: "rest",
})
},
})),
}
const HoppOpenAPIImporter: ImporterOrExporter = {
metadata: {
id: "hopp_openapi",
name: "import.from_openapi",
title: "import.from_openapi_description",
icon: IconOpenAPI,
disabled: false,
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
},
supported_sources: [
{
id: "file_import",
name: "import.from_file",
icon: IconFile,
step: FileSource({
caption: "import.from_file",
acceptedFileTypes: ".json, .yaml, .yml",
onImportFromFile: async (content) => {
const res = await hoppOpenAPIImporter(content)()
if (E.isRight(res)) {
handleImportToStore(res.right)
platform.analytics?.logEvent({
platform: "rest",
type: "HOPP_IMPORT_COLLECTION",
importer: "import.from_openapi",
workspaceType: isTeamWorkspace.value ? "team" : "personal",
})
} else {
showImportFailedError()
}
},
}),
},
{
id: "url_import",
name: "import.from_url",
icon: IconLink,
step: UrlSource({
caption: "import.from_url",
onImportFromURL: async (content) => {
const res = await hoppOpenAPIImporter(content)()
if (E.isRight(res)) {
handleImportToStore(res.right)
platform.analytics?.logEvent({
platform: "rest",
type: "HOPP_IMPORT_COLLECTION",
importer: "import.from_openapi",
workspaceType: isTeamWorkspace.value ? "team" : "personal",
})
} else {
showImportFailedError()
}
},
}),
},
],
}
const HoppPostmanImporter: ImporterOrExporter = {
metadata: {
id: "hopp_postman",
name: "import.from_postman",
title: "import.from_postman_description",
icon: IconPostman,
disabled: false,
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
},
component: FileSource({
caption: "import.from_file",
acceptedFileTypes: ".json",
onImportFromFile: async (content) => {
const res = await hoppPostmanImporter(content)()
if (E.isRight(res)) {
handleImportToStore(res.right)
platform.analytics?.logEvent({
platform: "rest",
type: "HOPP_IMPORT_COLLECTION",
importer: "import.from_postman",
workspaceType: isTeamWorkspace.value ? "team" : "personal",
})
} else {
showImportFailedError()
}
},
}),
}
const HoppInsomniaImporter: ImporterOrExporter = {
metadata: {
id: "hopp_insomnia",
name: "import.from_insomnia",
title: "import.from_insomnia_description",
icon: IconInsomnia,
disabled: true,
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
},
component: FileSource({
caption: "import.from_file",
acceptedFileTypes: ".json",
onImportFromFile: async (content) => {
const res = await hoppInsomniaImporter(content)()
if (E.isRight(res)) {
handleImportToStore(res.right)
platform.analytics?.logEvent({
platform: "rest",
type: "HOPP_IMPORT_COLLECTION",
importer: "import.from_insomnia",
workspaceType: isTeamWorkspace.value ? "team" : "personal",
})
} else {
showImportFailedError()
}
},
}),
}
const HoppGistImporter: ImporterOrExporter = {
metadata: {
id: "hopp_gist",
name: "import.from_gist",
title: "import.from_gist_description",
icon: IconGithub,
disabled: true,
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
},
component: GistSource({
caption: "import.from_url",
onImportFromGist: async (content) => {
if (E.isLeft(content)) {
showImportFailedError()
return
}
const res = await hoppRESTImporter(content.right)()
if (E.isRight(res)) {
handleImportToStore(res.right)
platform.analytics?.logEvent({
platform: "rest",
type: "HOPP_IMPORT_COLLECTION",
importer: "import.from_gist",
workspaceType: isTeamWorkspace.value ? "team" : "personal",
})
} else {
showImportFailedError()
}
},
}),
}
const HoppMyCollectionsExporter: ImporterOrExporter = {
metadata: {
id: "hopp_my_collections",
name: "export.as_json",
title: "action.download_file",
icon: IconUser,
disabled: false,
applicableTo: ["personal-workspace"],
isLoading: isHoppMyCollectionExporterInProgress,
},
action: () => {
if (!myCollections.value.length) {
return toast.error(t("error.no_collections_to_export"))
}
isHoppMyCollectionExporterInProgress.value = true
const message = initializeDownloadCollection(
myCollectionsExporter(myCollections.value),
"Collections"
)
if (E.isRight(message)) {
toast.success(t(message.right))
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "json",
platform: "rest",
})
}
isHoppMyCollectionExporterInProgress.value = false
},
}
const HoppTeamCollectionsExporter: ImporterOrExporter = {
metadata: {
id: "hopp_team_collections",
name: "export.as_json",
title: "export.as_json_description",
icon: IconUser,
disabled: false,
applicableTo: ["team-workspace"],
isLoading: isHoppTeamCollectionExporterInProgress,
},
action: async () => {
isHoppTeamCollectionExporterInProgress.value = true
if (
props.collectionsType.type === "team-collections" &&
props.collectionsType.selectedTeam
) {
const res = await teamCollectionsExporter(
props.collectionsType.selectedTeam.id
)
if (E.isRight(res)) {
const { exportCollectionsToJSON } = res.right
if (!JSON.parse(exportCollectionsToJSON).length) {
isHoppTeamCollectionExporterInProgress.value = false
return toast.error(t("error.no_collections_to_export"))
} }
initializeDownloadCollection(
exportCollectionsToJSON,
"team-collections"
)
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "json",
platform: "rest",
})
} else {
toast.error(res.left.error.toString())
} }
} )
)
isHoppTeamCollectionExporterInProgress.value = false
},
} }
const HoppGistCollectionsExporter: ImporterOrExporter = { const finishImport = async () => {
metadata: { await importerAction(stepResults.value)
id: "create_secret_gist", }
name: "export.create_secret_gist",
icon: IconGithub,
disabled: !currentUser.value
? true
: currentUser.value.provider !== "github.com",
title: t("export.create_secret_gist"),
applicableTo: ["personal-workspace", "team-workspace"],
isLoading: isHoppGistCollectionExporterInProgress,
},
action: async () => {
isHoppGistCollectionExporterInProgress.value = true
const collectionJSON = await getCollectionJSON() const onFileChange = () => {
const accessToken = currentUser.value?.accessToken stepResults.value = []
if (!accessToken) { const inputFileToImport = inputChooseFileToImportFrom.value[0]
toast.error(t("error.something_went_wrong"))
isHoppGistCollectionExporterInProgress.value = false if (!inputFileToImport) {
hasFile.value = false
return
}
if (!inputFileToImport.files || inputFileToImport.files.length === 0) {
inputChooseFileToImportFrom.value[0].value = ""
hasFile.value = false
toast.show(t("action.choose_file").toString())
return
}
const reader = new FileReader()
reader.onload = ({ target }) => {
const content = target!.result as string | null
if (!content) {
hasFile.value = false
toast.show(t("action.choose_file").toString())
return return
} }
if (E.isRight(collectionJSON)) { stepResults.value.push(content)
collectionsGistExporter(collectionJSON.right, accessToken) hasFile.value = !!content?.length
}
platform.analytics?.logEvent({ reader.readAsText(inputFileToImport.files[0])
type: "HOPP_EXPORT_COLLECTION",
exporter: "gist",
platform: "rest",
})
}
isHoppGistCollectionExporterInProgress.value = false
},
} }
const importerModules = computed(() => { const fileImported = () => {
const enabledImporters = [ toast.success(t("state.file_imported").toString())
HoppRESTImporter, hideModal()
HoppMyCollectionImporter, }
HoppOpenAPIImporter, const failedImport = () => {
HoppPostmanImporter, toast.error(t("import.failed").toString())
HoppInsomniaImporter, }
HoppGistImporter, const hideModal = () => {
] resetImport()
emit("hide-modal")
}
const isTeams = props.collectionsType.type === "team-collections" const resetImport = () => {
importerType.value = null
return enabledImporters.filter((importer) => { hasFile.value = false
return isTeams hasGist.value = false
? importer.metadata.applicableTo.includes("team-workspace") stepResults.value = []
: importer.metadata.applicableTo.includes("personal-workspace") inputChooseFileToImportFrom.value = ""
}) inputChooseGistToImportFrom.value = ""
}) mySelectedCollectionID.value = undefined
const exporterModules = computed(() => {
const enabledExporters = [
HoppMyCollectionsExporter,
HoppTeamCollectionsExporter,
]
if (platform.platformFeatureFlags.exportAsGIST) {
enabledExporters.push(HoppGistCollectionsExporter)
}
return enabledExporters.filter((exporter) => {
return exporter.metadata.applicableTo.includes(
props.collectionsType.type === "my-collections"
? "personal-workspace"
: "team-workspace"
)
})
})
const hasTeamWriteAccess = computed(() => {
const { collectionsType } = props
const isTeamCollection = collectionsType.type === "team-collections"
if (!isTeamCollection || !collectionsType.selectedTeam) {
return false
}
return (
collectionsType.selectedTeam.myRole === "EDITOR" ||
collectionsType.selectedTeam.myRole === "OWNER"
)
})
const selectedTeamID = computed(() => {
const { collectionsType } = props
return collectionsType.type === "team-collections"
? collectionsType.selectedTeam?.id
: undefined
})
const myCollections = useReadonlyStream(restCollections$, [])
const getCollectionJSON = async () => {
if (
props.collectionsType.type === "team-collections" &&
props.collectionsType.selectedTeam?.id
) {
const res = await getTeamCollectionJSON(
props.collectionsType.selectedTeam?.id
)
return E.isRight(res)
? E.right(res.right.exportCollectionsToJSON)
: E.left(res.left)
}
if (props.collectionsType.type === "my-collections") {
return E.right(JSON.stringify(myCollections.value, null, 2))
}
return E.left("INVALID_SELECTED_TEAM_OR_INVALID_COLLECTION_TYPE")
} }
</script> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="flex flex-1 flex-col"> <div class="flex flex-col flex-1">
<div <div
class="sticky z-10 flex flex-1 justify-between border-b border-dividerLight bg-primary" class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
:style=" :style="
saveRequest saveRequest
? 'top: calc(var(--upper-primary-sticky-fold) - var(--line-height-body))' ? 'top: calc(var(--upper-primary-sticky-fold) - var(--line-height-body))'
@@ -31,7 +31,7 @@
/> />
</span> </span>
</div> </div>
<div class="flex flex-1 flex-col"> <div class="flex flex-col flex-1">
<HoppSmartTree :adapter="myAdapter"> <HoppSmartTree :adapter="myAdapter">
<template <template
#content="{ node, toggleChildren, isOpen, highlightChildren }" #content="{ node, toggleChildren, isOpen, highlightChildren }"
@@ -222,12 +222,6 @@
requestIndex: pathToIndex(node.id), requestIndex: pathToIndex(node.id),
}) })
" "
@share-request="
node.data.type === 'requests' &&
emit('share-request', {
request: node.data.data.data,
})
"
@drag-request=" @drag-request="
dragRequest($event, { dragRequest($event, {
folderPath: node.data.data.parentIndex, folderPath: node.data.data.parentIndex,
@@ -254,7 +248,7 @@
:text="`${t('state.nothing_found')}${filterText}`" :text="`${t('state.nothing_found')}${filterText}`"
> >
<template #icon> <template #icon>
<icon-lucide-search class="svg-icons pb-2 opacity-75" /> <icon-lucide-search class="pb-2 opacity-75 svg-icons" />
</template> </template>
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<HoppSmartPlaceholder <HoppSmartPlaceholder
@@ -264,10 +258,10 @@
:text="t('empty.collections')" :text="t('empty.collections')"
> >
<div class="flex flex-col items-center space-y-4"> <div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight"> <span class="text-secondaryLight text-center">
{{ t("collection.import_or_create") }} {{ t("collection.import_or_create") }}
</span> </span>
<div class="flex flex-col items-stretch gap-4"> <div class="flex gap-4 flex-col items-stretch">
<HoppButtonPrimary <HoppButtonPrimary
:icon="IconImport" :icon="IconImport"
:label="t('import.title')" :label="t('import.title')"
@@ -466,12 +460,6 @@ const emit = defineEmits<{
isActive: boolean isActive: boolean
} }
): void ): void
(
event: "share-request",
payload: {
request: HoppRESTRequest
}
): void
( (
event: "drop-request", event: "drop-request",
payload: { payload: {
@@ -538,12 +526,13 @@ const isSelected = ({
props.picked.folderPath === folderPath && props.picked.folderPath === folderPath &&
props.picked.requestIndex === requestIndex props.picked.requestIndex === requestIndex
) )
} else {
return (
props.picked &&
props.picked.pickedType === "my-folder" &&
props.picked.folderPath === folderPath
)
} }
return (
props.picked &&
props.picked.pickedType === "my-folder" &&
props.picked.folderPath === folderPath
)
} }
const tabs = useService(RESTTabService) const tabs = useService(RESTTabService)
@@ -740,10 +729,11 @@ class MyCollectionsAdapter implements SmartTreeAdapter<MyCollectionNode> {
status: "loaded", status: "loaded",
data: data, data: data,
} as ChildrenResult<Folder | Requests> } as ChildrenResult<Folder | Requests>
} } else {
return { return {
status: "loaded", status: "loaded",
data: [], data: [],
}
} }
}) })
} }

View File

@@ -13,7 +13,7 @@
@dragend="resetDragState" @dragend="resetDragState"
></div> ></div>
<div <div
class="group flex items-stretch" class="flex items-stretch group"
:draggable="!hasNoTeamAccess" :draggable="!hasNoTeamAccess"
@drop="handelDrop" @drop="handelDrop"
@dragstart="dragStart" @dragstart="dragStart"
@@ -23,12 +23,12 @@
@contextmenu.prevent="options?.tippy.show()" @contextmenu.prevent="options?.tippy.show()"
> >
<div <div
class="pointer-events-auto flex min-w-0 flex-1 cursor-pointer items-center justify-center" class="flex items-center justify-center flex-1 min-w-0 cursor-pointer pointer-events-auto"
@click="selectRequest()" @click="selectRequest()"
> >
<span <span
class="pointer-events-none flex w-16 items-center justify-center truncate px-2" class="flex items-center justify-center w-16 px-2 truncate pointer-events-none"
:style="{ color: getMethodLabelColorClassOf(request) }" :class="requestLabelColor"
> >
<component <component
:is="IconCheckCircle" :is="IconCheckCircle"
@@ -37,12 +37,12 @@
:class="{ 'text-accent': isSelected }" :class="{ 'text-accent': isSelected }"
/> />
<HoppSmartSpinner v-else-if="isRequestLoading" /> <HoppSmartSpinner v-else-if="isRequestLoading" />
<span v-else class="truncate text-tiny font-semibold"> <span v-else class="font-semibold truncate text-tiny">
{{ request.method }} {{ request.method }}
</span> </span>
</span> </span>
<span <span
class="pointer-events-none flex min-w-0 flex-1 items-center py-2 pr-2 transition group-hover:text-secondaryDark" class="flex items-center flex-1 min-w-0 py-2 pr-2 pointer-events-none transition group-hover:text-secondaryDark"
> >
<span class="truncate" :class="{ 'text-accent': isSelected }"> <span class="truncate" :class="{ 'text-accent': isSelected }">
{{ request.name }} {{ request.name }}
@@ -50,15 +50,15 @@
<span <span
v-if="isActive" v-if="isActive"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
class="relative mx-3 flex h-1.5 w-1.5 flex-shrink-0" class="relative h-1.5 w-1.5 flex flex-shrink-0 mx-3"
:title="`${t('collection.request_in_use')}`" :title="`${t('collection.request_in_use')}`"
> >
<span <span
class="absolute inline-flex h-full w-full flex-shrink-0 animate-ping rounded-full bg-green-500 opacity-75" class="absolute inline-flex flex-shrink-0 w-full h-full bg-green-500 rounded-full opacity-75 animate-ping"
> >
</span> </span>
<span <span
class="relative inline-flex h-1.5 w-1.5 flex-shrink-0 rounded-full bg-green-500" class="relative inline-flex flex-shrink-0 rounded-full h-1.5 w-1.5 bg-green-500"
></span> ></span>
</span> </span>
</span> </span>
@@ -93,7 +93,6 @@
@keyup.e="edit?.$el.click()" @keyup.e="edit?.$el.click()"
@keyup.d="duplicate?.$el.click()" @keyup.d="duplicate?.$el.click()"
@keyup.delete="deleteAction?.$el.click()" @keyup.delete="deleteAction?.$el.click()"
@keyup.s="shareAction?.$el.click()"
@keyup.escape="hide()" @keyup.escape="hide()"
> >
<HoppSmartItem <HoppSmartItem
@@ -133,18 +132,6 @@
} }
" "
/> />
<HoppSmartItem
ref="shareAction"
:icon="IconShare2"
:label="t('action.share')"
:shortcut="['S']"
@click="
() => {
emit('share-request')
hide()
}
"
/>
</div> </div>
</template> </template>
</tippy> </tippy>
@@ -174,7 +161,6 @@ import IconEdit from "~icons/lucide/edit"
import IconCopy from "~icons/lucide/copy" import IconCopy from "~icons/lucide/copy"
import IconTrash2 from "~icons/lucide/trash-2" import IconTrash2 from "~icons/lucide/trash-2"
import IconRotateCCW from "~icons/lucide/rotate-ccw" import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconShare2 from "~icons/lucide/share-2"
import { ref, PropType, watch, computed } from "vue" import { ref, PropType, watch, computed } from "vue"
import { HoppRESTRequest } from "@hoppscotch/data" import { HoppRESTRequest } from "@hoppscotch/data"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
@@ -253,7 +239,6 @@ const emit = defineEmits<{
(event: "duplicate-request"): void (event: "duplicate-request"): void
(event: "remove-request"): void (event: "remove-request"): void
(event: "select-request"): void (event: "select-request"): void
(event: "share-request"): void
(event: "drag-request", payload: DataTransfer): void (event: "drag-request", payload: DataTransfer): void
(event: "update-request-order", payload: DataTransfer): void (event: "update-request-order", payload: DataTransfer): void
(event: "update-last-request-order", payload: DataTransfer): void (event: "update-last-request-order", payload: DataTransfer): void
@@ -264,7 +249,6 @@ const edit = ref<HTMLButtonElement | null>(null)
const deleteAction = ref<HTMLButtonElement | null>(null) const deleteAction = ref<HTMLButtonElement | null>(null)
const options = ref<TippyComponent | null>(null) const options = ref<TippyComponent | null>(null)
const duplicate = ref<HTMLButtonElement | null>(null) const duplicate = ref<HTMLButtonElement | null>(null)
const shareAction = ref<HTMLButtonElement | null>(null)
const dragging = ref(false) const dragging = ref(false)
const ordering = ref(false) const ordering = ref(false)
@@ -276,6 +260,10 @@ const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
parentID: "", parentID: "",
}) })
const requestLabelColor = computed(() =>
getMethodLabelColorClassOf(props.request)
)
watch( watch(
() => props.duplicateLoading, () => props.duplicateLoading,
(val) => { (val) => {
@@ -374,8 +362,9 @@ const updateLastItemOrder = (e: DragEvent) => {
const isRequestLoading = computed(() => { const isRequestLoading = computed(() => {
if (props.requestMoveLoading.length > 0 && props.requestID) { if (props.requestMoveLoading.length > 0 && props.requestID) {
return props.requestMoveLoading.includes(props.requestID) return props.requestMoveLoading.includes(props.requestID)
} else {
return false
} }
return false
}) })
const resetDragState = () => { const resetDragState = () => {

View File

@@ -141,8 +141,9 @@ const reqName = computed(() => {
return props.request.name return props.request.name
} else if (props.mode === "rest") { } else if (props.mode === "rest") {
return restRequestName.value return restRequestName.value
} else {
return gqlRequestName.value
} }
return gqlRequestName.value
}) })
const requestName = ref(reqName.value) const requestName = ref(reqName.value)
@@ -479,20 +480,21 @@ const getErrorMessage = (err: GQLError<string>) => {
console.error(err) console.error(err)
if (err.type === "network_error") { if (err.type === "network_error") {
return t("error.network_error") return t("error.network_error")
} } else {
switch (err.error) { switch (err.error) {
case "team_coll/short_title": case "team_coll/short_title":
return t("collection.name_length_insufficient") return t("collection.name_length_insufficient")
case "team/invalid_coll_id": case "team/invalid_coll_id":
return t("team.invalid_id") return t("team.invalid_id")
case "team/not_required_role": case "team/not_required_role":
return t("profile.no_permission") return t("profile.no_permission")
case "team_req/not_required_role": case "team_req/not_required_role":
return t("profile.no_permission") return t("profile.no_permission")
case "Forbidden resource": case "Forbidden resource":
return t("profile.no_permission") return t("profile.no_permission")
default: default:
return t("error.something_went_wrong") return t("error.something_went_wrong")
}
} }
} }
</script> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="flex flex-1 flex-col"> <div class="flex flex-col flex-1">
<div <div
class="sticky z-10 flex flex-1 justify-between border-b border-dividerLight bg-primary" class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
:style=" :style="
saveRequest saveRequest
? 'top: calc(var(--upper-primary-sticky-fold) - var(--line-height-body))' ? 'top: calc(var(--upper-primary-sticky-fold) - var(--line-height-body))'
@@ -15,12 +15,12 @@
class="!rounded-none" class="!rounded-none"
:icon="IconPlus" :icon="IconPlus"
:title="t('team.no_access')" :title="t('team.no_access')"
:label="t('action.new')" :label="t('add.new')"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-else v-else
:icon="IconPlus" :icon="IconPlus"
:label="t('action.new')" :label="t('add.new')"
class="!rounded-none" class="!rounded-none"
@click="emit('display-modal-add')" @click="emit('display-modal-add')"
/> />
@@ -240,12 +240,6 @@
requestIndex: node.data.data.data.id, requestIndex: node.data.data.data.id,
}) })
" "
@share-request="
node.data.type === 'requests' &&
emit('share-request', {
request: node.data.data.data.request,
})
"
@drag-request=" @drag-request="
dragRequest($event, { dragRequest($event, {
folderPath: node.data.data.parentIndex, folderPath: node.data.data.parentIndex,
@@ -275,10 +269,10 @@
@drop.stop @drop.stop
> >
<div class="flex flex-col items-center space-y-4"> <div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight"> <span class="text-secondaryLight text-center">
{{ t("collection.import_or_create") }} {{ t("collection.import_or_create") }}
</span> </span>
<div class="flex flex-col items-stretch gap-4"> <div class="flex gap-4 flex-col items-stretch">
<HoppButtonPrimary <HoppButtonPrimary
:icon="IconImport" :icon="IconImport"
:label="t('import.title')" :label="t('import.title')"
@@ -479,12 +473,6 @@ const emit = defineEmits<{
folderPath?: string | undefined folderPath?: string | undefined
} }
): void ): void
(
event: "share-request",
payload: {
request: HoppRESTRequest
}
): void
( (
event: "drop-request", event: "drop-request",
payload: { payload: {
@@ -554,12 +542,13 @@ const isSelected = ({
props.picked.pickedType === "teams-request" && props.picked.pickedType === "teams-request" &&
props.picked.requestID === requestID props.picked.requestID === requestID
) )
} else {
return (
props.picked &&
props.picked.pickedType === "teams-folder" &&
props.picked.folderID === folderID
)
} }
return (
props.picked &&
props.picked.pickedType === "teams-folder" &&
props.picked.folderID === folderID
)
} }
const active = computed(() => tabs.currentActiveTab.value.document.saveContext) const active = computed(() => tabs.currentActiveTab.value.document.saveContext)
@@ -725,77 +714,81 @@ class TeamCollectionsAdapter implements SmartTreeAdapter<TeamCollectionNode> {
return { return {
status: "loading", status: "loading",
} }
} } else {
const data = this.data.value.map((item, index) => ({ const data = this.data.value.map((item, index) => ({
id: item.id, id: item.id,
data: {
isLastItem: index === this.data.value.length - 1,
type: "collections",
data: { data: {
parentIndex: null, isLastItem: index === this.data.value.length - 1,
data: item, type: "collections",
data: {
parentIndex: null,
data: item,
},
}, },
}, }))
})) return {
return { status: "loaded",
status: "loaded", data: cloneDeep(data),
data: cloneDeep(data), } as ChildrenResult<TeamCollections>
} as ChildrenResult<TeamCollections> }
} } else {
const parsedID = id.split("/")[id.split("/").length - 1] const parsedID = id.split("/")[id.split("/").length - 1]
!props.teamLoadingCollections.includes(parsedID) && !props.teamLoadingCollections.includes(parsedID) &&
emit("expand-team-collection", parsedID) emit("expand-team-collection", parsedID)
if (props.teamLoadingCollections.includes(parsedID)) { if (props.teamLoadingCollections.includes(parsedID)) {
return { return {
status: "loading", status: "loading",
}
} else {
const items = this.findCollInTree(this.data.value, parsedID)
if (items) {
const data = [
...(items.children
? items.children.map((item, index) => ({
id: `${id}/${item.id}`,
data: {
isLastItem:
items.children && items.children.length > 1
? index === items.children.length - 1
: false,
type: "folders",
data: {
parentIndex: parsedID,
data: item,
},
},
}))
: []),
...(items.requests
? items.requests.map((item, index) => ({
id: `${id}/${item.id}`,
data: {
isLastItem:
items.requests && items.requests.length > 1
? index === items.requests.length - 1
: false,
type: "requests",
data: {
parentIndex: parsedID,
data: item,
},
},
}))
: []),
]
return {
status: "loaded",
data: cloneDeep(data),
} as ChildrenResult<TeamFolder | TeamRequests>
} else {
return {
status: "loaded",
data: [],
}
}
} }
}
const items = this.findCollInTree(this.data.value, parsedID)
if (items) {
const data = [
...(items.children
? items.children.map((item, index) => ({
id: `${id}/${item.id}`,
data: {
isLastItem:
items.children && items.children.length > 1
? index === items.children.length - 1
: false,
type: "folders",
data: {
parentIndex: parsedID,
data: item,
},
},
}))
: []),
...(items.requests
? items.requests.map((item, index) => ({
id: `${id}/${item.id}`,
data: {
isLastItem:
items.requests && items.requests.length > 1
? index === items.requests.length - 1
: false,
type: "requests",
data: {
parentIndex: parsedID,
data: item,
},
},
}))
: []),
]
return {
status: "loaded",
data: cloneDeep(data),
} as ChildrenResult<TeamFolder | TeamRequests>
}
return {
status: "loaded",
data: [],
} }
}) })
} }

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]"> <div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
<div <div
class="group flex items-stretch" class="flex items-stretch group"
@dragover.prevent @dragover.prevent
@drop.prevent="dropEvent" @drop.prevent="dropEvent"
@dragover="dragging = true" @dragover="dragging = true"
@@ -11,7 +11,7 @@
@contextmenu.prevent="options.tippy.show()" @contextmenu.prevent="options.tippy.show()"
> >
<span <span
class="flex cursor-pointer items-center justify-center px-4" class="flex items-center justify-center px-4 cursor-pointer"
@click="toggleShowChildren()" @click="toggleShowChildren()"
> >
<component <component
@@ -21,7 +21,7 @@
/> />
</span> </span>
<span <span
class="flex min-w-0 flex-1 cursor-pointer py-2 pr-2 transition group-hover:text-secondaryDark" class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
@click="toggleShowChildren()" @click="toggleShowChildren()"
> >
<span class="truncate" :class="{ 'text-accent': isSelected }"> <span class="truncate" :class="{ 'text-accent': isSelected }">
@@ -136,10 +136,10 @@
</div> </div>
<div v-if="showChildren || isFiltered" class="flex"> <div v-if="showChildren || isFiltered" class="flex">
<div <div
class="ml-[1.375rem] flex w-0.5 transform cursor-nsResize bg-dividerLight transition hover:scale-x-125 hover:bg-dividerDark" class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-0.5 hover:bg-dividerDark hover:scale-x-125"
@click="toggleShowChildren()" @click="toggleShowChildren()"
></div> ></div>
<div class="flex flex-1 flex-col truncate"> <div class="flex flex-col flex-1 truncate">
<CollectionsGraphqlFolder <CollectionsGraphqlFolder
v-for="(folder, index) in collection.folders" v-for="(folder, index) in collection.folders"
:key="`folder-${String(index)}`" :key="`folder-${String(index)}`"
@@ -271,7 +271,7 @@ const collectionIcon = computed(() => {
if (isSelected.value) return IconCheckCircle if (isSelected.value) return IconCheckCircle
else if (!showChildren.value && !props.isFiltered) return IconFolder else if (!showChildren.value && !props.isFiltered) return IconFolder
else if (!showChildren.value || props.isFiltered) return IconFolderOpen else if (!showChildren.value || props.isFiltered) return IconFolderOpen
return IconFolder else return IconFolder
}) })
const pick = () => { const pick = () => {

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]"> <div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
<div <div
class="group flex items-stretch" class="flex items-stretch group"
@dragover.prevent @dragover.prevent
@drop.prevent="dropEvent" @drop.prevent="dropEvent"
@dragover="dragging = true" @dragover="dragging = true"
@@ -11,7 +11,7 @@
@contextmenu.prevent="options.tippy.show()" @contextmenu.prevent="options.tippy.show()"
> >
<span <span
class="flex cursor-pointer items-center justify-center px-4" class="flex items-center justify-center px-4 cursor-pointer"
@click="toggleShowChildren()" @click="toggleShowChildren()"
> >
<component <component
@@ -21,7 +21,7 @@
/> />
</span> </span>
<span <span
class="flex min-w-0 flex-1 cursor-pointer py-2 pr-2 transition group-hover:text-secondaryDark" class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
@click="toggleShowChildren()" @click="toggleShowChildren()"
> >
<span class="truncate" :class="{ 'text-accent': isSelected }"> <span class="truncate" :class="{ 'text-accent': isSelected }">
@@ -128,10 +128,10 @@
</div> </div>
<div v-if="showChildren || isFiltered" class="flex"> <div v-if="showChildren || isFiltered" class="flex">
<div <div
class="ml-[1.375rem] flex w-0.5 transform cursor-nsResize bg-dividerLight transition hover:scale-x-125 hover:bg-dividerDark" class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-0.5 hover:bg-dividerDark hover:scale-x-125"
@click="toggleShowChildren()" @click="toggleShowChildren()"
></div> ></div>
<div class="flex flex-1 flex-col truncate"> <div class="flex flex-col flex-1 truncate">
<!-- Referring to this component only (this is recursive) --> <!-- Referring to this component only (this is recursive) -->
<Folder <Folder
v-for="(subFolder, subFolderIndex) in folder.folders" v-for="(subFolder, subFolderIndex) in folder.folders"
@@ -253,7 +253,7 @@ const collectionIcon = computed(() => {
if (isSelected.value) return IconCheckCircle if (isSelected.value) return IconCheckCircle
else if (!showChildren.value && !props.isFiltered) return IconFolder else if (!showChildren.value && !props.isFiltered) return IconFolder
else if (showChildren.value || !props.isFiltered) return IconFolderOpen else if (showChildren.value || !props.isFiltered) return IconFolderOpen
return IconFolder else return IconFolder
}) })
const pick = () => { const pick = () => {

View File

@@ -1,227 +1,299 @@
<template> <template>
<ImportExportBase <HoppSmartModal
ref="collections-import-export" v-if="show"
modal-title="graphql_collections.title" dialog
:importer-modules="importerModules" :title="`${t('modal.collections')}`"
:exporter-modules="exporterModules" styles="sm:max-w-md"
@hide-modal="emit('hide-modal')" @close="hideModal"
/> >
<template #actions>
<span>
<tippy interactive trigger="click" theme="popover">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.more')"
:icon="IconMoreVertical"
:on-shown="() => tippyActions.focus()"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
:icon="IconGithub"
:label="t('import.from_gist')"
@click="
() => {
readCollectionGist()
hide()
}
"
/>
<span
v-tippy="{ theme: 'tooltip' }"
:title="
!currentUser
? `${t('export.require_github')}`
: currentUser.provider !== 'github.com'
? `${t('export.require_github')}`
: undefined
"
>
<HoppSmartItem
:disabled="
!currentUser
? true
: currentUser.provider !== 'github.com'
? true
: false
"
:icon="IconGithub"
:label="t('export.create_secret_gist')"
@click="
() => {
createCollectionGist()
hide()
}
"
/>
</span>
</div>
</template>
</tippy>
</span>
</template>
<template #body>
<div class="flex flex-col space-y-2">
<HoppSmartItem
:icon="IconFolderPlus"
:label="t('import.from_json')"
@click="openDialogChooseFileToImportFrom"
/>
<input
ref="inputChooseFileToImportFrom"
class="input"
type="file"
accept="application/json"
@change="importFromJSON"
/>
<hr />
<HoppSmartItem
v-tippy="{ theme: 'tooltip' }"
:title="t('action.download_file')"
:icon="IconDownload"
:label="t('export.as_json')"
@click="exportJSON"
/>
</div>
</template>
</HoppSmartModal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from "~/composables/i18n" import axios from "axios"
import { useToast } from "~/composables/toast" import IconMoreVertical from "~icons/lucide/more-vertical"
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data"
import { ImporterOrExporter } from "~/components/importExport/types"
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
import * as E from "fp-ts/Either"
import IconFolderPlus from "~icons/lucide/folder-plus" import IconFolderPlus from "~icons/lucide/folder-plus"
import IconUser from "~icons/lucide/user" import IconDownload from "~icons/lucide/download"
import { initializeDownloadCollection } from "~/helpers/import-export/export" import IconGithub from "~icons/lucide/github"
import { useReadonlyStream } from "~/composables/stream" import { computed, ref } from "vue"
import { platform } from "~/platform" import { platform } from "~/platform"
import { useI18n } from "@composables/i18n"
import { useReadonlyStream } from "@composables/stream"
import { useToast } from "@composables/toast"
import { import {
graphqlCollections$, graphqlCollections$,
setGraphqlCollections, setGraphqlCollections,
appendGraphqlCollections,
} from "~/newstore/collections" } from "~/newstore/collections"
import { hoppGqlCollectionsImporter } from "~/helpers/import-export/import/hoppGql"
import { gqlCollectionsExporter } from "~/helpers/import-export/export/gqlCollections"
import { gqlCollectionsGistExporter } from "~/helpers/import-export/export/gqlCollectionsGistExporter"
import { computed } from "vue"
const t = useI18n() defineProps<{
show: boolean
}>()
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const toast = useToast() const toast = useToast()
const t = useI18n()
const collections = useReadonlyStream(graphqlCollections$, [])
const currentUser = useReadonlyStream( const currentUser = useReadonlyStream(
platform.auth.getCurrentUserStream(), platform.auth.getCurrentUserStream(),
platform.auth.getCurrentUser() platform.auth.getCurrentUser()
) )
const GqlCollectionsHoppImporter: ImporterOrExporter = { // Template refs
metadata: { const tippyActions = ref<any | null>(null)
id: "import.from_json", const inputChooseFileToImportFrom = ref<HTMLInputElement>()
name: "import.from_json",
icon: IconFolderPlus,
title: "import.from_json",
applicableTo: ["personal-workspace"],
disabled: false,
},
component: FileSource({
acceptedFileTypes: "application/json",
caption: "import.from_json_description",
onImportFromFile: async (gqlCollections) => {
const res = await hoppGqlCollectionsImporter(gqlCollections)
if (E.isLeft(res)) { const collectionJson = computed(() => {
showImportFailedError() return JSON.stringify(collections.value, null, 2)
return
}
handleImportToStore(res.right)
platform.analytics?.logEvent({
type: "HOPP_IMPORT_COLLECTION",
platform: "gql",
workspaceType: "personal",
importer: "json",
})
emit("hide-modal")
},
}),
}
const GqlCollectionsGistImporter: ImporterOrExporter = {
metadata: {
id: "import.from_gist",
name: "import.from_gist",
icon: IconFolderPlus,
title: "import.from_gist",
applicableTo: ["personal-workspace", "team-workspace"],
disabled: false,
},
component: GistSource({
caption: "import.gql_collections_from_gist_description",
onImportFromGist: async (gqlCollections) => {
if (E.isLeft(gqlCollections)) {
showImportFailedError()
return
}
const res = await hoppGqlCollectionsImporter(gqlCollections.right)
if (E.isLeft(res)) {
showImportFailedError()
return
}
handleImportToStore(res.right)
platform.analytics?.logEvent({
type: "HOPP_IMPORT_COLLECTION",
platform: "gql",
workspaceType: "personal",
importer: "gist",
})
emit("hide-modal")
},
}),
}
const gqlCollections = useReadonlyStream(graphqlCollections$, [])
const GqlCollectionsHoppExporter: ImporterOrExporter = {
metadata: {
id: "export.as_json",
name: "export.as_json",
title: "action.download_file",
icon: IconUser,
disabled: false,
applicableTo: ["personal-workspace", "team-workspace"],
},
action: () => {
if (!gqlCollections.value.length) {
return toast.error(t("error.no_collections_to_export"))
}
const message = initializeDownloadCollection(
gqlCollectionsExporter(gqlCollections.value),
"GQLCollections"
)
if (E.isLeft(message)) {
toast.error(t("export.failed"))
return
}
toast.success(message.right)
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
platform: "gql",
exporter: "json",
})
},
}
const GqlCollectionsGistExporter: ImporterOrExporter = {
metadata: {
id: "export.as_gist",
name: "export.create_secret_gist",
title: !currentUser
? "export.require_github"
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
currentUser.provider !== "github.com"
? `export.require_github`
: "export.create_secret_gist",
icon: IconUser,
disabled: !currentUser.value
? true
: currentUser.value.provider !== "github.com",
applicableTo: ["personal-workspace"],
},
action: async () => {
if (!currentUser.value) {
toast.error(t("profile.no_permission"))
return
}
const accessToken = currentUser.value?.accessToken
if (accessToken) {
const res = await gqlCollectionsGistExporter(
JSON.stringify(gqlCollections.value),
accessToken
)
if (E.isLeft(res)) {
toast.error(t("export.failed"))
return
}
toast.success(t("export.success"))
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
platform: "gql",
exporter: "gist",
})
window.open(res.right, "_blank")
}
},
}
const importerModules = [GqlCollectionsHoppImporter, GqlCollectionsGistImporter]
const exporterModules = computed(() => {
const modules = [GqlCollectionsHoppExporter]
if (platform.platformFeatureFlags.exportAsGIST) {
modules.push(GqlCollectionsGistExporter)
}
return modules
}) })
const showImportFailedError = () => { const createCollectionGist = async () => {
toast.error(t("import.failed")) if (!currentUser.value) {
toast.error(t("profile.no_permission").toString())
return
}
try {
const res = await axios.post(
"https://api.github.com/gists",
{
files: {
"hoppscotch-collections.json": {
content: collectionJson.value,
},
},
},
{
headers: {
Authorization: `token ${currentUser.value.accessToken}`,
Accept: "application/vnd.github.v3+json",
},
}
)
toast.success(t("export.gist_created").toString())
window.open(res.data.html_url)
} catch (e) {
toast.error(t("error.something_went_wrong").toString())
console.error(e)
}
} }
const handleImportToStore = async ( const fileImported = () => {
gqlCollections: HoppCollection<HoppGQLRequest>[] toast.success(t("state.file_imported").toString())
) => {
setGraphqlCollections(gqlCollections)
toast.success(t("import.success"))
} }
const emit = defineEmits<{ const failedImport = () => {
(e: "hide-modal"): () => void toast.error(t("import.failed").toString())
}>() }
const readCollectionGist = async () => {
const gist = prompt(t("import.gist_url").toString())
if (!gist) return
try {
const { files } = (await axios.get(
`https://api.github.com/gists/${gist.split("/").pop()}`,
{
headers: {
Accept: "application/vnd.github.v3+json",
},
}
)) as {
files: {
[fileName: string]: {
content: any
}
}
}
const collections = JSON.parse(Object.values(files)[0].content)
setGraphqlCollections(collections)
fileImported()
} catch (e) {
failedImport()
console.error(e)
}
}
const hideModal = () => {
emit("hide-modal")
}
const openDialogChooseFileToImportFrom = () => {
if (inputChooseFileToImportFrom.value)
inputChooseFileToImportFrom.value.click()
}
const importFromJSON = () => {
if (!inputChooseFileToImportFrom.value) return
if (
!inputChooseFileToImportFrom.value.files ||
inputChooseFileToImportFrom.value.files.length === 0
) {
toast.show(t("action.choose_file").toString())
return
}
const reader = new FileReader()
reader.onload = ({ target }) => {
const content = target!.result as string | null
if (!content) {
toast.show(t("action.choose_file").toString())
return
}
const collections = JSON.parse(content)
if (collections[0]) {
const [name, folders, requests] = Object.keys(collections[0])
if (name === "name" && folders === "folders" && requests === "requests") {
// Do nothing
}
} else {
failedImport()
return
}
appendGraphqlCollections(collections)
platform.analytics?.logEvent({
type: "HOPP_IMPORT_COLLECTION",
importer: "json",
workspaceType: "personal",
platform: "gql",
})
fileImported()
}
reader.readAsText(inputChooseFileToImportFrom.value.files[0])
inputChooseFileToImportFrom.value.value = ""
}
const exportJSON = async () => {
const dataToWrite = collectionJson.value
const parsedCollections = JSON.parse(dataToWrite)
if (!parsedCollections.length) {
return toast.error(t("error.no_collections_to_export"))
}
const file = new Blob([dataToWrite], { type: "application/json" })
const url = URL.createObjectURL(file)
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
URL.revokeObjectURL(url)
const result = await platform.io.saveFileWithDialog({
data: dataToWrite,
contentType: "application/json",
suggestedFilename: filename,
filters: [
{
name: "Hoppscotch Collection JSON file",
extensions: ["json"],
},
],
})
if (result.type === "unknown" || result.type === "saved") {
platform?.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "json",
platform: "gql",
})
toast.success(t("state.download_started").toString())
}
}
</script> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]"> <div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
<div <div
class="group flex items-stretch" class="flex items-stretch group"
draggable="true" draggable="true"
@dragstart="dragStart" @dragstart="dragStart"
@dragover.stop @dragover.stop
@@ -10,7 +10,7 @@
@contextmenu.prevent="options.tippy.show()" @contextmenu.prevent="options.tippy.show()"
> >
<span <span
class="flex w-16 cursor-pointer items-center justify-center truncate px-2" class="flex items-center justify-center w-16 px-2 truncate cursor-pointer"
@click="selectRequest()" @click="selectRequest()"
> >
<component <component
@@ -20,7 +20,7 @@
/> />
</span> </span>
<span <span
class="flex min-w-0 flex-1 cursor-pointer items-center py-2 pr-2 transition group-hover:text-secondaryDark" class="flex items-center flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
@click="selectRequest()" @click="selectRequest()"
> >
<span class="truncate" :class="{ 'text-accent': isSelected }"> <span class="truncate" :class="{ 'text-accent': isSelected }">
@@ -29,15 +29,15 @@
<span <span
v-if="isActive" v-if="isActive"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
class="relative mx-3 flex h-1.5 w-1.5 flex-shrink-0" class="relative h-1.5 w-1.5 flex flex-shrink-0 mx-3"
:title="`${t('collection.request_in_use')}`" :title="`${t('collection.request_in_use')}`"
> >
<span <span
class="absolute inline-flex h-full w-full flex-shrink-0 animate-ping rounded-full bg-green-500 opacity-75" class="absolute inline-flex flex-shrink-0 w-full h-full bg-green-500 rounded-full opacity-75 animate-ping"
> >
</span> </span>
<span <span
class="relative inline-flex h-1.5 w-1.5 flex-shrink-0 rounded-full bg-green-500" class="relative inline-flex flex-shrink-0 rounded-full h-1.5 w-1.5 bg-green-500"
></span> ></span>
</span> </span>
</span> </span>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div :class="{ 'rounded border border-divider': saveRequest }"> <div :class="{ 'rounded border border-divider': saveRequest }">
<div <div
class="sticky z-10 flex flex-shrink-0 flex-col overflow-x-auto rounded-t bg-primary" class="sticky z-10 flex flex-col flex-shrink-0 overflow-x-auto rounded-t bg-primary"
:style=" :style="
saveRequest ? 'top: calc(-1 * var(--line-height-body))' : 'top: 0' saveRequest ? 'top: calc(-1 * var(--line-height-body))' : 'top: 0'
" "
@@ -11,10 +11,10 @@
type="search" type="search"
autocomplete="off" autocomplete="off"
:placeholder="t('action.search')" :placeholder="t('action.search')"
class="!border-0 bg-transparent py-2 pl-4 pr-2" class="py-2 pl-4 pr-2 bg-transparent !border-0"
/> />
<div <div
class="flex flex-1 flex-shrink-0 justify-between border-y border-dividerLight bg-primary" class="flex justify-between flex-1 flex-shrink-0 border-y bg-primary border-dividerLight"
> >
<HoppButtonSecondary <HoppButtonSecondary
:icon="IconPlus" :icon="IconPlus"
@@ -67,10 +67,10 @@
:text="t('empty.collections')" :text="t('empty.collections')"
> >
<div class="flex flex-col items-center space-y-4"> <div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight"> <span class="text-secondaryLight text-center">
{{ t("collection.import_or_create") }} {{ t("collection.import_or_create") }}
</span> </span>
<div class="flex flex-col items-stretch gap-4"> <div class="flex gap-4 flex-col items-stretch">
<HoppButtonPrimary <HoppButtonPrimary
:icon="IconImport" :icon="IconImport"
:label="t('import.title')" :label="t('import.title')"
@@ -93,7 +93,7 @@
:text="`${t('state.nothing_found')}${filterText}`" :text="`${t('state.nothing_found')}${filterText}`"
> >
<template #icon> <template #icon>
<icon-lucide-search class="svg-icons pb-2 opacity-75" /> <icon-lucide-search class="pb-2 opacity-75 svg-icons" />
</template> </template>
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<CollectionsGraphqlAdd <CollectionsGraphqlAdd
@@ -137,7 +137,7 @@
@hide-modal="displayModalEditRequest(false)" @hide-modal="displayModalEditRequest(false)"
/> />
<CollectionsGraphqlImportExport <CollectionsGraphqlImportExport
v-if="showModalImportExport" :show="showModalImportExport"
@hide-modal="displayModalImportExport(false)" @hide-modal="displayModalImportExport(false)"
/> />
</div> </div>

View File

@@ -11,7 +11,7 @@
@dragend="draggingToRoot = false" @dragend="draggingToRoot = false"
> >
<div <div
class="sticky z-10 flex flex-shrink-0 flex-col overflow-x-auto bg-primary border-b border-dividerLight" class="sticky z-10 flex flex-col flex-shrink-0 overflow-x-auto border-b bg-primary border-dividerLight"
:class="{ 'rounded-t': saveRequest }" :class="{ 'rounded-t': saveRequest }"
:style=" :style="
saveRequest ? 'top: calc(-1 * var(--line-height-body))' : 'top: 0' saveRequest ? 'top: calc(-1 * var(--line-height-body))' : 'top: 0'
@@ -22,7 +22,7 @@
v-model="filterTexts" v-model="filterTexts"
type="search" type="search"
autocomplete="off" autocomplete="off"
class="flex w-full bg-transparent px-4 py-2" class="flex w-full p-4 py-2 bg-transparent h-8"
:placeholder="t('action.search')" :placeholder="t('action.search')"
:disabled="collectionsType.type === 'team-collections'" :disabled="collectionsType.type === 'team-collections'"
/> />
@@ -41,7 +41,6 @@
@export-data="exportData" @export-data="exportData"
@remove-collection="removeCollection" @remove-collection="removeCollection"
@remove-folder="removeFolder" @remove-folder="removeFolder"
@share-request="shareRequest"
@drop-collection="dropCollection" @drop-collection="dropCollection"
@update-request-order="updateRequestOrder" @update-request-order="updateRequestOrder"
@update-collection-order="updateCollectionOrder" @update-collection-order="updateCollectionOrder"
@@ -72,7 +71,6 @@
@export-data="exportData" @export-data="exportData"
@remove-collection="removeCollection" @remove-collection="removeCollection"
@remove-folder="removeFolder" @remove-folder="removeFolder"
@share-request="shareRequest"
@edit-request="editRequest" @edit-request="editRequest"
@duplicate-request="duplicateRequest" @duplicate-request="duplicateRequest"
@remove-request="removeRequest" @remove-request="removeRequest"
@@ -87,12 +85,12 @@
@display-modal-import-export="displayModalImportExport(true)" @display-modal-import-export="displayModalImportExport(true)"
/> />
<div <div
class="py-15 hidden flex-1 flex-col items-center justify-center bg-primaryDark px-4 text-secondaryLight" class="hidden bg-primaryDark flex-col flex-1 items-center py-15 justify-center px-4 text-secondaryLight"
:class="{ :class="{
'!flex': draggingToRoot && currentReorderingStatus.type !== 'request', '!flex': draggingToRoot && currentReorderingStatus.type !== 'request',
}" }"
> >
<icon-lucide-list-end class="svg-icons !h-8 !w-8" /> <icon-lucide-list-end class="svg-icons !w-8 !h-8" />
</div> </div>
<CollectionsAdd <CollectionsAdd
:show="showModalAdd" :show="showModalAdd"
@@ -140,13 +138,17 @@
@hide-modal="showConfirmModal = false" @hide-modal="showConfirmModal = false"
@resolve="resolveConfirmModal" @resolve="resolveConfirmModal"
/> />
<CollectionsImportExport <CollectionsImportExport
v-if="showModalImportExport" :show="showModalImportExport"
:collections-type="collectionsType" :collections-type="collectionsType.type"
:exporting-team-collections="exportingTeamCollections"
:creating-gist-collection="creatingGistCollection"
:importing-my-collections="importingMyCollections"
@export-json-collection="exportJSONCollection"
@create-collection-gist="createCollectionGist"
@import-to-teams="importToTeams"
@hide-modal="displayModalImportExport(false)" @hide-modal="displayModalImportExport(false)"
/> />
<TeamsAdd <TeamsAdd
:show="showTeamModalAdd" :show="showTeamModalAdd"
@hide-modal="displayTeamModalAdd(false)" @hide-modal="displayTeamModalAdd(false)"
@@ -195,6 +197,7 @@ import {
createChildCollection, createChildCollection,
renameCollection, renameCollection,
deleteCollection, deleteCollection,
importJSONToTeam,
moveRESTTeamCollection, moveRESTTeamCollection,
updateOrderRESTTeamCollection, updateOrderRESTTeamCollection,
} from "~/helpers/backend/mutations/TeamCollection" } from "~/helpers/backend/mutations/TeamCollection"
@@ -209,9 +212,12 @@ import { TeamCollection } from "~/helpers/teams/TeamCollection"
import { Collection as NodeCollection } from "./MyCollections.vue" import { Collection as NodeCollection } from "./MyCollections.vue"
import { import {
getCompleteCollectionTree, getCompleteCollectionTree,
getTeamCollectionJSON,
teamCollToHoppRESTColl, teamCollToHoppRESTColl,
} from "~/helpers/backend/helpers" } from "~/helpers/backend/helpers"
import * as E from "fp-ts/Either"
import { platform } from "~/platform" import { platform } from "~/platform"
import { createCollectionGists } from "~/helpers/gist"
import { import {
getRequestsByPath, getRequestsByPath,
resolveSaveContextOnRequestReorder, resolveSaveContextOnRequestReorder,
@@ -223,7 +229,7 @@ import {
resetTeamRequestsContext, resetTeamRequestsContext,
} from "~/helpers/collection/collection" } from "~/helpers/collection/collection"
import { currentReorderingStatus$ } from "~/newstore/reordering" import { currentReorderingStatus$ } from "~/newstore/reordering"
import { defineActionHandler, invokeAction } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { WorkspaceService } from "~/services/workspace.service" import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest" import { RESTTabService } from "~/services/tab/rest"
@@ -297,6 +303,12 @@ const draggingToRoot = ref(false)
const collectionMoveLoading = ref<string[]>([]) const collectionMoveLoading = ref<string[]>([])
const requestMoveLoading = ref<string[]>([]) const requestMoveLoading = ref<string[]>([])
// Export - Import refs
const collectionJSON = ref("")
const exportingTeamCollections = ref(false)
const creatingGistCollection = ref(false)
const importingMyCollections = ref(false)
// TeamList-Adapter // TeamList-Adapter
const workspaceService = useService(WorkspaceService) const workspaceService = useService(WorkspaceService)
const teamListAdapter = workspaceService.acquireTeamListAdapter(null) const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
@@ -400,12 +412,14 @@ const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
}) })
const hasTeamWriteAccess = computed(() => { const hasTeamWriteAccess = computed(() => {
if (collectionsType.value.type !== "team-collections") { if (!collectionsType.value.selectedTeam) return false
return false
}
const role = collectionsType.value.selectedTeam?.myRole if (
return role === "OWNER" || role === "EDITOR" collectionsType.value.type === "team-collections" &&
collectionsType.value.selectedTeam.myRole !== "VIEWER"
)
return true
else return false
}) })
const filteredCollections = computed(() => { const filteredCollections = computed(() => {
@@ -1055,7 +1069,7 @@ const onRemoveCollection = () => {
const collectionIndex = editingCollectionIndex.value const collectionIndex = editingCollectionIndex.value
const collectionToRemove = const collectionToRemove =
collectionIndex || collectionIndex === 0 collectionIndex || collectionIndex == 0
? navigateToFolderWithIndexPath(restCollectionStore.value.state, [ ? navigateToFolderWithIndexPath(restCollectionStore.value.state, [
collectionIndex, collectionIndex,
]) ])
@@ -1454,8 +1468,9 @@ const checkIfCollectionIsAParentOfTheChildren = (
) )
if (isEqual(slicedDestinationCollectionPath, collectionDraggedPath)) { if (isEqual(slicedDestinationCollectionPath, collectionDraggedPath)) {
return true return true
} else {
return false
} }
return false
} }
return false return false
@@ -1476,8 +1491,9 @@ const isMoveToSameLocation = (
if (isEqual(draggedItemParentPathArr, destinationPathArr)) { if (isEqual(draggedItemParentPathArr, destinationPathArr)) {
return true return true
} else {
return false
} }
return false
} }
} }
@@ -1657,22 +1673,25 @@ const isSameSameParent = (
const dragedItemParent = draggedItemIndex.slice(0, -1) const dragedItemParent = draggedItemIndex.slice(0, -1)
return dragedItemParent.join("/") === destinationCollectionIndex return dragedItemParent.join("/") === destinationCollectionIndex
} } else {
if (destinationItemPath === null) return false if (destinationItemPath === null) return false
const destinationItemIndex = pathToIndex(destinationItemPath) const destinationItemIndex = pathToIndex(destinationItemPath)
// length of 1 means the request is in the root // length of 1 means the request is in the root
if (draggedItemIndex.length === 1 && destinationItemIndex.length === 1) { if (draggedItemIndex.length === 1 && destinationItemIndex.length === 1) {
return true
} else if (draggedItemIndex.length === destinationItemIndex.length) {
const dragedItemParent = draggedItemIndex.slice(0, -1)
const destinationItemParent = destinationItemIndex.slice(0, -1)
if (isEqual(dragedItemParent, destinationItemParent)) {
return true return true
} else if (draggedItemIndex.length === destinationItemIndex.length) {
const dragedItemParent = draggedItemIndex.slice(0, -1)
const destinationItemParent = destinationItemIndex.slice(0, -1)
if (isEqual(dragedItemParent, destinationItemParent)) {
return true
} else {
return false
}
} else {
return false
} }
return false
} }
return false
} }
/** /**
@@ -1814,6 +1833,33 @@ const updateCollectionOrder = (payload: {
} }
} }
// Import - Export Collection functions // Import - Export Collection functions
/**
* Export the whole my collection or specific team collection to JSON
*/
const getJSONCollection = async () => {
if (collectionsType.value.type === "my-collections") {
collectionJSON.value = JSON.stringify(myCollections.value, null, 2)
} else {
if (!collectionsType.value.selectedTeam) return
exportingTeamCollections.value = true
pipe(
await getTeamCollectionJSON(collectionsType.value.selectedTeam.id),
E.match(
(err) => {
toast.error(`${getErrorMessage(err)}`)
exportingTeamCollections.value = false
},
(result) => {
const { exportCollectionsToJSON } = result
collectionJSON.value = exportCollectionsToJSON
exportingTeamCollections.value = false
}
)
)
}
return collectionJSON.value
}
/** /**
* Create a downloadable file from a collection and prompts the user to download it. * Create a downloadable file from a collection and prompts the user to download it.
@@ -1882,15 +1928,88 @@ const exportData = async (
} }
} }
const shareRequest = ({ request }: { request: HoppRESTRequest }) => { const exportJSONCollection = async () => {
if (currentUser.value) { platform.analytics?.logEvent({
// opens the share request modal type: "HOPP_EXPORT_COLLECTION",
invokeAction("share.request", { exporter: "json",
request, platform: "rest",
}) })
} else {
invokeAction("modals.login.toggle") await getJSONCollection()
const parsedCollections = JSON.parse(collectionJSON.value)
if (!parsedCollections.length) {
return toast.error(t("error.no_collections_to_export"))
} }
initializeDownloadCollection(collectionJSON.value, null)
}
const createCollectionGist = async () => {
if (!currentUser.value || !currentUser.value.accessToken) {
toast.error(t("profile.no_permission").toString())
return
}
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "gist",
platform: "rest",
})
creatingGistCollection.value = true
await getJSONCollection()
pipe(
createCollectionGists(collectionJSON.value, currentUser.value.accessToken),
TE.match(
(err) => {
toast.error(t("error.something_went_wrong").toString())
console.error(err)
creatingGistCollection.value = false
},
(result) => {
toast.success(t("export.gist_created").toString())
creatingGistCollection.value = false
window.open(result.data.html_url)
}
)
)()
}
const importToTeams = async (collection: HoppCollection<HoppRESTRequest>[]) => {
if (!hasTeamWriteAccess.value) {
toast.error(t("team.no_access").toString())
return
}
if (!collectionsType.value.selectedTeam) return
importingMyCollections.value = true
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "import-to-teams",
platform: "rest",
})
pipe(
importJSONToTeam(
JSON.stringify(collection),
collectionsType.value.selectedTeam.id
),
TE.match(
(err: GQLError<string>) => {
toast.error(`${getErrorMessage(err)}`)
importingMyCollections.value = false
},
() => {
importingMyCollections.value = false
displayModalImportExport(false)
}
)
)()
} }
const resolveConfirmModal = (title: string | null) => { const resolveConfirmModal = (title: string | null) => {
@@ -1922,36 +2041,37 @@ const getErrorMessage = (err: GQLError<string>) => {
console.error(err) console.error(err)
if (err.type === "network_error") { if (err.type === "network_error") {
return t("error.network_error") return t("error.network_error")
} } else {
switch (err.error) { switch (err.error) {
case "team_coll/short_title": case "team_coll/short_title":
return t("collection.name_length_insufficient") return t("collection.name_length_insufficient")
case "team/invalid_coll_id": case "team/invalid_coll_id":
case "bug/team_coll/no_coll_id": case "bug/team_coll/no_coll_id":
case "team_req/invalid_target_id": case "team_req/invalid_target_id":
return t("team.invalid_coll_id") return t("team.invalid_coll_id")
case "team/not_required_role": case "team/not_required_role":
return t("profile.no_permission") return t("profile.no_permission")
case "team_req/not_required_role": case "team_req/not_required_role":
return t("profile.no_permission") return t("profile.no_permission")
case "Forbidden resource": case "Forbidden resource":
return t("profile.no_permission") return t("profile.no_permission")
case "team_req/not_found": case "team_req/not_found":
return t("team.no_request_found") return t("team.no_request_found")
case "bug/team_req/no_req_id": case "bug/team_req/no_req_id":
return t("team.no_request_found") return t("team.no_request_found")
case "team/collection_is_parent_coll": case "team/collection_is_parent_coll":
return t("team.parent_coll_move") return t("team.parent_coll_move")
case "team/target_and_destination_collection_are_same": case "team/target_and_destination_collection_are_same":
return t("team.same_target_destination") return t("team.same_target_destination")
case "team/target_collection_is_already_root_collection": case "team/target_collection_is_already_root_collection":
return t("collection.invalid_root_move") return t("collection.invalid_root_move")
case "team_req/requests_not_from_same_collection": case "team_req/requests_not_from_same_collection":
return t("request.different_collection") return t("request.different_collection")
case "team/team_collections_have_different_parents": case "team/team_collections_have_different_parents":
return t("collection.different_parent") return t("collection.different_parent")
default: default:
return t("error.something_went_wrong") return t("error.something_went_wrong")
}
} }
} }

View File

@@ -11,11 +11,11 @@
v-if="!currentInterceptorSupportsCookies" v-if="!currentInterceptorSupportsCookies"
:text="t('cookies.modal.interceptor_no_support')" :text="t('cookies.modal.interceptor_no_support')"
> >
<AppInterceptor class="rounded border border-dividerLight p-2" /> <AppInterceptor class="p-2 border rounded border-dividerLight" />
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<div v-else class="flex flex-col"> <div v-else class="flex flex-col">
<div <div
class="sticky -mx-4 -mt-4 flex space-x-2 border-b border-dividerLight bg-primary px-4 py-4" class="flex bg-primary space-x-2 border-b sticky border-dividerLight -mx-4 px-4 py-4 -mt-4"
style="top: calc(-1 * var(--line-height-body))" style="top: calc(-1 * var(--line-height-body))"
> >
<HoppSmartInput <HoppSmartInput
@@ -46,7 +46,7 @@
:key="domain" :key="domain"
class="flex flex-col" class="flex flex-col"
> >
<div class="flex flex-1 items-center justify-between"> <div class="flex items-center justify-between flex-1">
<label for="cookiesList" class="p-4"> <label for="cookiesList" class="p-4">
{{ domain }} {{ domain }}
</label> </label>
@@ -65,11 +65,11 @@
/> />
</div> </div>
</div> </div>
<div class="rounded border border-divider"> <div class="border rounded border-divider">
<div class="divide-y divide-dividerLight"> <div class="divide-y divide-dividerLight">
<div <div
v-if="entries.length === 0" v-if="entries.length === 0"
class="flex flex-col items-center gap-2 p-4" class="flex flex-col gap-2 p-4 items-center"
> >
{{ t("cookies.modal.no_cookies_in_domain") }} {{ t("cookies.modal.no_cookies_in_domain") }}
</div> </div>
@@ -80,7 +80,7 @@
class="flex divide-x divide-dividerLight" class="flex divide-x divide-dividerLight"
> >
<input <input
class="flex flex-1 bg-transparent px-4 py-2" class="flex flex-1 px-4 py-2 bg-transparent"
:value="entry" :value="entry"
readonly readonly
/> />

View File

@@ -6,10 +6,10 @@
@close="hideModal" @close="hideModal"
> >
<template #body> <template #body>
<div class="rounded border border-dividerLight"> <div class="border rounded border-dividerLight">
<div class="flex flex-col"> <div class="flex flex-col">
<div class="flex items-center justify-between pl-4"> <div class="flex items-center justify-between pl-4">
<label class="truncate font-semibold text-secondaryLight"> <label class="font-semibold truncate text-secondaryLight">
{{ t("cookies.modal.cookie_string") }} {{ t("cookies.modal.cookie_string") }}
</label> </label>
<div class="flex items-center"> <div class="flex items-center">
@@ -43,7 +43,7 @@
<div class="h-46"> <div class="h-46">
<div <div
ref="cookieEditor" ref="cookieEditor"
class="h-full rounded-b border-t border-dividerLight" class="h-full border-t rounded-b border-dividerLight"
></div> ></div>
</div> </div>
</div> </div>

View File

@@ -1,212 +0,0 @@
<template>
<div class="flex flex-1 flex-col">
<header
class="flex flex-1 flex-shrink-0 items-center justify-between space-x-2 overflow-x-auto overflow-y-hidden px-2 py-2"
>
<div class="flex flex-1 items-center justify-between space-x-2">
<HoppButtonSecondary
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
:label="t('app.name')"
to="https://hoppscotch.io/"
blank
/>
<div class="flex">
<HoppSmartItem
:label="t('app.open_in_hoppscotch')"
:to="sharedRequestURL"
blank
/>
</div>
</div>
</header>
<div class="flex-1">
<div
class="flex-none flex-shrink-0 bg-primary p-4 sm:flex sm:flex-shrink-0 sm:space-x-2"
>
<div
class="min-w-52 flex flex-1 whitespace-nowrap rounded border border-divider"
>
<div class="relative flex">
<span
class="flex justify-center items-center w-26 cursor-pointer rounded-l bg-primaryLight px-4 py-2 font-semibold text-secondaryDark transition"
>
{{ tab.document.request.method }}
</span>
</div>
<div
class="flex flex-1 whitespace-nowrap rounded-r border-l border-divider bg-primaryLight transition"
>
<input
name="method"
:value="tab.document.request.endpoint"
class="flex-1 px-4 bg-primary"
disabled
/>
</div>
</div>
<div class="mt-2 flex sm:mt-0">
<HoppButtonPrimary
id="send"
:title="`${t(
'action.send'
)} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
:label="`${!loading ? t('action.send') : t('action.cancel')}`"
class="min-w-20 flex-1"
@click="!loading ? newSendRequest() : cancelRequest()"
/>
<HoppButtonSecondary
:title="`${t(
'request.save'
)} <kbd>${getSpecialKey()}</kbd><kbd>S</kbd>`"
:label="t('request.save')"
filled
:icon="IconSave"
class="flex-1 rounded rounded-r-none"
blank
:to="sharedRequestURL"
/>
</div>
</div>
</div>
<HttpRequestOptions
v-model="tab.document.request"
v-model:option-tab="selectedOptionTab"
:properties="properties"
/>
<HttpResponse :document="tab.document" :is-embed="true" />
</div>
</template>
<script lang="ts" setup>
import { Ref } from "vue"
import { computed, useModel } from "vue"
import { ref } from "vue"
import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import * as E from "fp-ts/Either"
import { useStreamSubscriber } from "~/composables/stream"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
import { runRESTRequest$ } from "~/helpers/RequestRunner"
import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
import IconSave from "~icons/lucide/save"
const t = useI18n()
const toast = useToast()
const props = defineProps<{
modelTab: HoppTab<HoppRESTDocument>
properties: string[]
sharedRequestID: string
}>()
const tab = useModel(props, "modelTab")
const selectedOptionTab = ref(props.properties[0])
const requestCancelFunc: Ref<(() => void) | null> = ref(null)
const loading = ref(false)
const baseURL = import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
const sharedRequestURL = computed(() => {
return `${baseURL}/r/${props.sharedRequestID}`
})
const { subscribeToStream } = useStreamSubscriber()
const newSendRequest = async () => {
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
toast.error(`${t("empty.endpoint")}`)
return
}
ensureMethodInEndpoint()
loading.value = true
const [cancel, streamPromise] = runRESTRequest$(tab)
const streamResult = await streamPromise
requestCancelFunc.value = cancel
if (E.isRight(streamResult)) {
subscribeToStream(
streamResult.right,
(responseState) => {
if (loading.value) {
// Check exists because, loading can be set to false
// when cancelled
updateRESTResponse(responseState)
}
},
() => {
loading.value = false
},
() => {
// TODO: Change this any to a proper type
const result = (streamResult.right as any).value
if (
result.type === "network_fail" &&
result.error?.error === "NO_PW_EXT_HOOK"
) {
const errorResponse: HoppRESTResponse = {
type: "extension_error",
error: result.error.humanMessage.heading,
component: result.error.component,
req: result.req,
}
updateRESTResponse(errorResponse)
}
loading.value = false
}
)
} else {
loading.value = false
toast.error(`${t("error.script_fail")}`)
let error: Error
if (typeof streamResult.left === "string") {
error = { name: "RequestFailure", message: streamResult.left }
} else {
error = streamResult.left
}
updateRESTResponse({
type: "script_fail",
error,
})
}
}
const updateRESTResponse = (response: HoppRESTResponse | null) => {
tab.value.document.response = response
}
const newEndpoint = computed(() => {
return tab.value.document.request.endpoint
})
const ensureMethodInEndpoint = () => {
if (
!/^http[s]?:\/\//.test(newEndpoint.value) &&
!newEndpoint.value.startsWith("<<")
) {
const domain = newEndpoint.value.split(/[/:#?]+/)[0]
if (domain === "localhost" || /([0-9]+\.)*[0-9]/.test(domain)) {
tab.value.document.request.endpoint =
"http://" + tab.value.document.request.endpoint
} else {
tab.value.document.request.endpoint =
"https://" + tab.value.document.request.endpoint
}
}
}
const cancelRequest = () => {
loading.value = false
requestCancelFunc.value?.()
updateRESTResponse(null)
}
</script>

View File

@@ -5,9 +5,9 @@
@close="hideModal" @close="hideModal"
> >
<template #body> <template #body>
<div class="flex flex-1 flex-col space-y-4"> <div class="flex space-y-4 flex-1 flex-col">
<div class="ml-2 flex items-center space-x-8"> <div class="flex items-center space-x-8 ml-2">
<label for="name" class="min-w-[2.5rem] font-semibold">{{ <label for="name" class="font-semibold min-w-10">{{
t("environment.name") t("environment.name")
}}</label> }}</label>
<input <input
@@ -17,8 +17,8 @@
class="input" class="input"
/> />
</div> </div>
<div class="ml-2 flex items-center space-x-8"> <div class="flex items-center space-x-8 ml-2">
<label for="value" class="min-w-[2.5rem] font-semibold">{{ <label for="value" class="font-semibold min-w-10">{{
t("environment.value") t("environment.value")
}}</label> }}</label>
<input <input
@@ -28,21 +28,21 @@
:placeholder="t('environment.value')" :placeholder="t('environment.value')"
/> />
</div> </div>
<div class="ml-2 flex items-center space-x-8"> <div class="flex items-center space-x-8 ml-2">
<label for="scope" class="min-w-[2.5rem] font-semibold"> <label for="scope" class="font-semibold min-w-10">
{{ t("environment.scope") }} {{ t("environment.scope") }}
</label> </label>
<div <div
class="relative flex flex-1 flex-col rounded border border-divider focus-visible:border-dividerDark" class="relative flex flex-1 flex-col border border-divider rounded focus-visible:border-dividerDark"
> >
<EnvironmentsSelector v-model="scope" :is-scope-selector="true" /> <EnvironmentsSelector v-model="scope" :is-scope-selector="true" />
</div> </div>
</div> </div>
<div v-if="replaceWithVariable" class="mt-3 flex space-x-2"> <div v-if="replaceWithVariable" class="flex space-x-2 mt-3">
<div class="min-w-[4rem]" /> <div class="min-w-18" />
<HoppSmartCheckbox <HoppSmartCheckbox
:on="replaceWithVariable" :on="replaceWithVariable"
:title="t('environment.replace_with_variable')" title="t('environment.replace_with_variable'))"
@change="replaceWithVariable = !replaceWithVariable" @change="replaceWithVariable = !replaceWithVariable"
/> />
<label for="replaceWithVariable"> <label for="replaceWithVariable">
@@ -205,14 +205,15 @@ const addEnvironment = async () => {
const getErrorMessage = (err: GQLError<string>) => { const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") { if (err.type === "network_error") {
return t("error.network_error") return t("error.network_error")
} } else {
switch (err.error) { switch (err.error) {
case "team_environment/not_found": case "team_environment/not_found":
return t("team_environment.not_found") return t("team_environment.not_found")
case "Forbidden resource": case "Forbidden resource":
return t("profile.no_permission") return t("profile.no_permission")
default: default:
return t("error.something_went_wrong") return t("error.something_went_wrong")
}
} }
} }
</script> </script>

View File

@@ -1,60 +1,154 @@
<template> <template>
<ImportExportBase <HoppSmartModal
ref="collections-import-export" v-if="show"
modal-title="environment.title" dialog
:importer-modules="importerModules" :title="`${t('environment.title')}`"
:exporter-modules="exporterModules" styles="sm:max-w-md"
@hide-modal="emit('hide-modal')" @close="hideModal"
/> >
<template #actions>
<span>
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => tippyActions!.focus()"
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.more')"
:icon="IconMoreVertical"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
:icon="IconGithub"
:label="t('import.from_gist')"
@click="
() => {
readEnvironmentGist()
hide()
}
"
/>
<span
v-tippy="{ theme: 'tooltip' }"
:title="
!currentUser
? `${t('export.require_github')}`
: currentUser.provider !== 'github.com'
? `${t('export.require_github')}`
: undefined
"
>
<HoppSmartItem
:disabled="
!currentUser
? true
: currentUser.provider !== 'github.com'
? true
: false
"
:icon="IconGithub"
:label="t('export.create_secret_gist')"
@click="
() => {
createEnvironmentGist()
hide()
}
"
/>
</span>
</div>
</template>
</tippy>
</span>
</template>
<template #body>
<div v-if="loading" class="flex flex-col items-center justify-center p-4">
<HoppSmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<div v-else class="flex flex-col space-y-2">
<HoppSmartItem
:icon="IconFolderPlus"
:label="t('import.from_json')"
@click="openDialogChooseFileToImportFrom"
/>
<input
ref="inputChooseFileToImportFrom"
class="input"
type="file"
accept="application/json"
@change="importFromJSON"
/>
<hr />
<HoppSmartItem
v-tippy="{ theme: 'tooltip' }"
:title="t('action.download_file')"
:icon="IconDownload"
:label="t('export.as_json')"
@click="exportJSON"
/>
</div>
</template>
</HoppSmartModal>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from "~/composables/i18n" import IconMoreVertical from "~icons/lucide/more-vertical"
import { useToast } from "~/composables/toast"
import { Environment } from "@hoppscotch/data"
import { ImporterOrExporter } from "~/components/importExport/types"
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
import { hoppEnvImporter } from "~/helpers/import-export/import/hoppEnv"
import * as E from "fp-ts/Either"
import { appendEnvironments, environments$ } from "~/newstore/environments"
import { createTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { GQLError } from "~/helpers/backend/GQLClient"
import { CreateTeamEnvironmentMutation } from "~/helpers/backend/graphql"
import { postmanEnvImporter } from "~/helpers/import-export/import/postmanEnv"
import IconFolderPlus from "~icons/lucide/folder-plus" import IconFolderPlus from "~icons/lucide/folder-plus"
import IconPostman from "~icons/hopp/postman" import IconDownload from "~icons/lucide/download"
import IconUser from "~icons/lucide/user" import IconGithub from "~icons/lucide/github"
import { initializeDownloadCollection } from "~/helpers/import-export/export" import { computed, ref } from "vue"
import { computed } from "vue" import { Environment } from "@hoppscotch/data"
import { useReadonlyStream } from "~/composables/stream"
import { environmentsExporter } from "~/helpers/import-export/export/environments"
import { environmentsGistExporter } from "~/helpers/import-export/export/environmentsGistExport"
import { platform } from "~/platform" import { platform } from "~/platform"
import axios from "axios"
const t = useI18n() import { useI18n } from "@composables/i18n"
const toast = useToast() import { useReadonlyStream } from "@composables/stream"
import { useToast } from "@composables/toast"
import {
environments$,
replaceEnvironments,
appendEnvironments,
} from "~/newstore/environments"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { createTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
import { GQLError } from "~/helpers/backend/GQLClient"
import { TippyComponent } from "vue-tippy"
const props = defineProps<{ const props = defineProps<{
show: boolean
teamEnvironments?: TeamEnvironment[] teamEnvironments?: TeamEnvironment[]
teamId?: string | undefined teamId?: string | undefined
environmentType: "MY_ENV" | "TEAM_ENV" environmentType: "MY_ENV" | "TEAM_ENV"
}>() }>()
const myEnvironments = useReadonlyStream(environments$, []) const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const toast = useToast()
const t = useI18n()
const loading = ref(false)
const myEnvironments = useReadonlyStream(environments$, [])
const currentUser = useReadonlyStream( const currentUser = useReadonlyStream(
platform.auth.getCurrentUserStream(), platform.auth.getCurrentUserStream(),
platform.auth.getCurrentUser() platform.auth.getCurrentUser()
) )
const isTeamEnvironment = computed(() => { // Template refs
return props.environmentType === "TEAM_ENV" const tippyActions = ref<TippyComponent | null>(null)
}) const inputChooseFileToImportFrom = ref<HTMLInputElement>()
const environmentJson = computed(() => { const environmentJson = computed(() => {
if ( if (
@@ -64,249 +158,266 @@ const environmentJson = computed(() => {
const teamEnvironments = props.teamEnvironments.map( const teamEnvironments = props.teamEnvironments.map(
(x) => x.environment as Environment (x) => x.environment as Environment
) )
return teamEnvironments return JSON.stringify(teamEnvironments, null, 2)
} else {
return JSON.stringify(myEnvironments.value, null, 2)
} }
return myEnvironments.value
}) })
const HoppEnvironmentsImport: ImporterOrExporter = { const createEnvironmentGist = async () => {
metadata: { if (!currentUser.value) {
id: "import.from_json", toast.error(t("profile.no_permission").toString())
name: "import.from_json",
icon: IconFolderPlus,
title: "import.from_json",
applicableTo: ["personal-workspace", "team-workspace"],
disabled: false,
},
component: FileSource({
acceptedFileTypes: "application/json",
caption: "import.hoppscotch_environment_description",
onImportFromFile: async (environments) => {
const res = await hoppEnvImporter(environments)()
if (E.isLeft(res)) { return
showImportFailedError() }
return
try {
const res = await axios.post(
"https://api.github.com/gists",
{
files: {
"hoppscotch-environments.json": {
content: environmentJson.value,
},
},
},
{
headers: {
Authorization: `token ${currentUser.value.accessToken}`,
Accept: "application/vnd.github.v3+json",
},
} }
handleImportToStore(res.right)
platform.analytics?.logEvent({
type: "HOPP_IMPORT_ENVIRONMENT",
platform: "rest",
workspaceType: isTeamEnvironment.value ? "team" : "personal",
})
emit("hide-modal")
},
}),
}
const PostmanEnvironmentsImport: ImporterOrExporter = {
metadata: {
id: "import.from_postman",
name: "import.from_postman",
icon: IconPostman,
title: "import.from_json",
applicableTo: ["personal-workspace", "team-workspace"],
disabled: false,
},
component: FileSource({
acceptedFileTypes: "application/json",
caption: "import.postman_environment_description",
onImportFromFile: async (environments) => {
const res = await postmanEnvImporter(environments)()
if (E.isLeft(res)) {
showImportFailedError()
return
}
handleImportToStore([res.right])
platform.analytics?.logEvent({
type: "HOPP_IMPORT_ENVIRONMENT",
platform: "rest",
workspaceType: isTeamEnvironment.value ? "team" : "personal",
})
emit("hide-modal")
},
}),
}
const EnvironmentsImportFromGIST: ImporterOrExporter = {
metadata: {
id: "import.environments_from_gist",
name: "import.environments_from_gist",
icon: IconFolderPlus,
title: "import.environments_from_gist",
applicableTo: ["personal-workspace", "team-workspace"],
disabled: false,
},
component: GistSource({
caption: "import.environments_from_gist_description",
onImportFromGist: async (environments) => {
if (E.isLeft(environments)) {
showImportFailedError()
return
}
const res = await hoppEnvImporter(environments.right)()
if (E.isLeft(res)) {
showImportFailedError()
return
}
handleImportToStore(res.right)
platform.analytics?.logEvent({
type: "HOPP_IMPORT_ENVIRONMENT",
platform: "rest",
workspaceType: isTeamEnvironment.value ? "team" : "personal",
})
emit("hide-modal")
},
}),
}
const HoppEnvironmentsExport: ImporterOrExporter = {
metadata: {
id: "export.as_json",
name: "export.as_json",
title: "action.download_file",
icon: IconUser,
disabled: false,
applicableTo: ["personal-workspace", "team-workspace"],
},
action: () => {
if (!environmentJson.value.length) {
return toast.error(t("error.no_environments_to_export"))
}
const message = initializeDownloadCollection(
environmentsExporter(environmentJson.value),
"Environments"
) )
if (E.isLeft(message)) { toast.success(t("export.gist_created").toString())
toast.error(t(message.left))
return
}
toast.success(t(message.right))
platform.analytics?.logEvent({ platform.analytics?.logEvent({
type: "HOPP_EXPORT_ENVIRONMENT", type: "HOPP_EXPORT_ENVIRONMENT",
platform: "rest", platform: "rest",
}) })
},
}
const HoppEnvironmentsGistExporter: ImporterOrExporter = { window.open(res.data.html_url)
metadata: { } catch (e) {
id: "export.as_gist", toast.error(t("error.something_went_wrong").toString())
name: "export.create_secret_gist", console.error(e)
title:
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
currentUser?.provider === "github.com"
? "export.create_secret_gist"
: "export.require_github",
icon: IconUser,
disabled: !currentUser.value
? true
: currentUser.value.provider !== "github.com",
applicableTo: ["personal-workspace", "team-workspace"],
},
action: async () => {
if (!currentUser.value) {
toast.error(t("profile.no_permission"))
return
}
const accessToken = currentUser.value?.accessToken
if (accessToken) {
const res = await environmentsGistExporter(
JSON.stringify(environmentJson.value),
accessToken
)
if (E.isLeft(res)) {
toast.error(t("export.failed"))
return
}
toast.success(t("export.success"))
platform.analytics?.logEvent({
type: "HOPP_EXPORT_ENVIRONMENT",
platform: "rest",
})
window.open(res.right, "_blank")
}
},
}
const importerModules = [
HoppEnvironmentsImport,
EnvironmentsImportFromGIST,
PostmanEnvironmentsImport,
]
const exporterModules = computed(() => {
const enabledExporters = [HoppEnvironmentsExport]
if (platform.platformFeatureFlags.exportAsGIST) {
enabledExporters.push(HoppEnvironmentsGistExporter)
} }
}
return enabledExporters const fileImported = () => {
}) toast.success(t("state.file_imported").toString())
}
const showImportFailedError = () => { const failedImport = () => {
toast.error(t("import.failed").toString()) toast.error(t("import.failed").toString())
} }
const handleImportToStore = async (environments: Environment[]) => { const readEnvironmentGist = async () => {
if (props.environmentType === "MY_ENV") { const gist = prompt(t("import.gist_url").toString())
appendEnvironments(environments) if (!gist) return
toast.success(t("state.file_imported"))
} else { try {
await importToTeams(environments) const { files } = (await axios.get(
`https://api.github.com/gists/${gist.split("/").pop()}`,
{
headers: {
Accept: "application/vnd.github.v3+json",
},
}
)) as {
files: {
[fileName: string]: {
content: any
}
}
}
const environments = JSON.parse(Object.values(files)[0].content)
if (props.environmentType === "MY_ENV") {
replaceEnvironments(environments)
fileImported()
} else {
importToTeams(environments)
}
} catch (e) {
failedImport()
console.error(e)
} }
} }
const hideModal = () => {
emit("hide-modal")
}
const openDialogChooseFileToImportFrom = () => {
if (inputChooseFileToImportFrom.value)
inputChooseFileToImportFrom.value.click()
}
const importToTeams = async (content: Environment[]) => { const importToTeams = async (content: Environment[]) => {
const envImportPromises: Promise< loading.value = true
E.Either<GQLError<"">, CreateTeamEnvironmentMutation>
>[] = []
for (const [, env] of content.entries()) { platform.analytics?.logEvent({
const res = createTeamEnvironment( type: "HOPP_IMPORT_ENVIRONMENT",
JSON.stringify(env.variables), platform: "rest",
props.teamId as string, workspaceType: "team",
env.name })
)()
envImportPromises.push(res) for (const [i, env] of content.entries()) {
} if (i === content.length - 1) {
await pipe(
const res = await Promise.all(envImportPromises) createTeamEnvironment(
JSON.stringify(env.variables),
const failedImports = res.some((r) => E.isLeft(r)) props.teamId as string,
env.name
if (failedImports) { ),
toast.error(t("import.failed")) TE.match(
} else { (err: GQLError<string>) => {
toast.success(t("import.success")) console.error(err)
toast.error(`${getErrorMessage(err)}`)
},
() => {
loading.value = false
hideModal()
fileImported()
}
)
)()
} else {
await pipe(
createTeamEnvironment(
JSON.stringify(env.variables),
props.teamId as string,
env.name
),
TE.match(
(err: GQLError<string>) => {
console.error(err)
toast.error(`${getErrorMessage(err)}`)
},
() => {
// wait for all the environments to be created then fire the toast
}
)
)()
}
} }
} }
const emit = defineEmits<{ const importFromJSON = () => {
(e: "hide-modal"): () => void if (!inputChooseFileToImportFrom.value) return
}>()
if (
!inputChooseFileToImportFrom.value.files ||
inputChooseFileToImportFrom.value.files.length === 0
) {
toast.show(t("action.choose_file").toString())
return
}
platform.analytics?.logEvent({
type: "HOPP_IMPORT_ENVIRONMENT",
platform: "rest",
workspaceType: "personal",
})
const reader = new FileReader()
reader.onload = ({ target }) => {
const content = target!.result as string | null
if (!content) {
toast.show(t("action.choose_file").toString())
return
}
const environments = JSON.parse(content)
if (
environments._postman_variable_scope === "environment" ||
environments._postman_variable_scope === "globals"
) {
importFromPostman(environments)
} else if (environments[0]) {
const [name, variables] = Object.keys(environments[0])
if (name === "name" && variables === "variables") {
// Do nothing
}
importFromHoppscotch(environments)
} else {
failedImport()
}
}
reader.readAsText(inputChooseFileToImportFrom.value.files[0])
inputChooseFileToImportFrom.value.value = ""
}
const importFromHoppscotch = (environments: Environment[]) => {
if (props.environmentType === "MY_ENV") {
appendEnvironments(environments)
fileImported()
} else {
importToTeams(environments)
}
}
const importFromPostman = ({
name,
values,
}: {
name: string
values: { key: string; value: string }[]
}) => {
const environment: Environment = { name, variables: [] }
values.forEach(({ key, value }) => environment.variables.push({ key, value }))
const environments = [environment]
importFromHoppscotch(environments)
}
const exportJSON = async () => {
const dataToWrite = environmentJson.value
const parsedCollections = JSON.parse(dataToWrite)
if (!parsedCollections.length) {
return toast.error(t("error.no_environments_to_export"))
}
const file = new Blob([dataToWrite], { type: "application/json" })
const url = URL.createObjectURL(file)
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
URL.revokeObjectURL(url)
const result = await platform.io.saveFileWithDialog({
data: dataToWrite,
contentType: "application/json",
suggestedFilename: filename,
filters: [
{
name: "JSON file",
extensions: ["json"],
},
],
})
if (result.type === "unknown" || result.type === "saved") {
toast.success(t("state.download_started").toString())
}
}
const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") {
return t("error.network_error")
} else {
switch (err.error) {
case "team_environment/not_found":
return t("team_environment.not_found")
default:
return t("error.something_went_wrong")
}
}
}
</script> </script>

View File

@@ -6,9 +6,10 @@
theme="popover" theme="popover"
:on-shown="() => envSelectorActions!.focus()" :on-shown="() => envSelectorActions!.focus()"
> >
<HoppSmartSelectWrapper <span
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="`${t('environment.select')}`" :title="`${t('environment.select')}`"
class="select-wrapper"
> >
<HoppButtonSecondary <HoppButtonSecondary
:icon="IconLayers" :icon="IconLayers"
@@ -19,9 +20,9 @@
: `${t('environment.select')}` : `${t('environment.select')}`
: '' : ''
" "
class="flex-1 !justify-start rounded-none pr-8" class="flex-1 !justify-start pr-8 rounded-none"
/> />
</HoppSmartSelectWrapper> </span>
<template #content="{ hide }"> <template #content="{ hide }">
<div <div
ref="envSelectorActions" ref="envSelectorActions"
@@ -100,7 +101,7 @@
<img <img
:src="`/images/states/${colorMode.value}/blockchain.svg`" :src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy" loading="lazy"
class="mb-2 inline-flex h-16 w-16 flex-col object-contain object-center" class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
:alt="`${t('empty.environments')}`" :alt="`${t('empty.environments')}`"
/> />
<span class="pb-2 text-center"> <span class="pb-2 text-center">
@@ -147,7 +148,7 @@
<img <img
:src="`/images/states/${colorMode.value}/blockchain.svg`" :src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy" loading="lazy"
class="mb-2 inline-flex h-16 w-16 flex-col object-contain object-center" class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
:alt="`${t('empty.environments')}`" :alt="`${t('empty.environments')}`"
/> />
<span class="pb-2 text-center"> <span class="pb-2 text-center">
@@ -159,7 +160,7 @@
v-if="!teamListLoading && teamAdapterError" v-if="!teamListLoading && teamAdapterError"
class="flex flex-col items-center py-4" class="flex flex-col items-center py-4"
> >
<icon-lucide-help-circle class="svg-icons mb-4" /> <icon-lucide-help-circle class="mb-4 svg-icons" />
{{ getErrorMessage(teamAdapterError) }} {{ getErrorMessage(teamAdapterError) }}
</div> </div>
</HoppSmartTab> </HoppSmartTab>
@@ -189,7 +190,7 @@
@keyup.escape="hide()" @keyup.escape="hide()"
> >
<div <div
class="sticky top-0 flex items-center justify-between truncate rounded border border-divider bg-primary pl-4 font-semibold text-secondaryDark" class="sticky top-0 font-semibold truncate flex items-center justify-between text-secondaryDark bg-primary border border-divider rounded pl-4"
> >
{{ t("environment.global_variables") }} {{ t("environment.global_variables") }}
<HoppButtonSecondary <HoppButtonSecondary
@@ -204,16 +205,12 @@
" "
/> />
</div> </div>
<div class="my-2 flex flex-1 flex-col space-y-2 pl-4 pr-2"> <div class="my-2 flex flex-col flex-1 space-y-2 pl-4 pr-2">
<div class="flex flex-1 space-x-4"> <div class="flex flex-1 space-x-4">
<span <span class="w-1/4 min-w-32 truncate text-tiny font-semibold">
class="min-w-[9rem] w-1/4 truncate text-tiny font-semibold"
>
{{ t("environment.name") }} {{ t("environment.name") }}
</span> </span>
<span <span class="w-full min-w-32 truncate text-tiny font-semibold">
class="min-w-[9rem] w-full truncate text-tiny font-semibold"
>
{{ t("environment.value") }} {{ t("environment.value") }}
</span> </span>
</div> </div>
@@ -222,10 +219,10 @@
:key="index" :key="index"
class="flex flex-1 space-x-4" class="flex flex-1 space-x-4"
> >
<span class="min-w-[9rem] w-1/4 truncate text-secondaryLight"> <span class="text-secondaryLight w-1/4 min-w-32 truncate">
{{ variable.key }} {{ variable.key }}
</span> </span>
<span class="min-w-[9rem] w-full truncate text-secondaryLight"> <span class="text-secondaryLight w-full min-w-32 truncate">
{{ variable.value }} {{ variable.value }}
</span> </span>
</div> </div>
@@ -234,7 +231,7 @@
</div> </div>
</div> </div>
<div <div
class="sticky top-0 mt-2 flex items-center justify-between truncate rounded border border-divider bg-primary pl-4 font-semibold text-secondaryDark" class="sticky top-0 mt-2 font-semibold truncate flex items-center justify-between text-secondaryDark bg-primary border border-divider rounded pl-4"
:class="{ :class="{
'bg-primaryLight': !selectedEnv.variables, 'bg-primaryLight': !selectedEnv.variables,
}" }"
@@ -255,20 +252,16 @@
</div> </div>
<div <div
v-if="selectedEnv.type === 'NO_ENV_SELECTED'" v-if="selectedEnv.type === 'NO_ENV_SELECTED'"
class="my-2 flex flex-1 flex-col pl-4 text-secondaryLight" class="text-secondaryLight my-2 flex flex-col flex-1 pl-4"
> >
{{ t("environment.no_active_environment") }} {{ t("environment.no_active_environment") }}
</div> </div>
<div v-else class="my-2 flex flex-1 flex-col space-y-2 pl-4 pr-2"> <div v-else class="my-2 flex flex-col flex-1 space-y-2 pl-4 pr-2">
<div class="flex flex-1 space-x-4"> <div class="flex flex-1 space-x-4">
<span <span class="w-1/4 min-w-32 truncate text-tiny font-semibold">
class="min-w-[9rem] w-1/4 truncate text-tiny font-semibold"
>
{{ t("environment.name") }} {{ t("environment.name") }}
</span> </span>
<span <span class="w-full min-w-32 truncate text-tiny font-semibold">
class="min-w-[9rem] w-full truncate text-tiny font-semibold"
>
{{ t("environment.value") }} {{ t("environment.value") }}
</span> </span>
</div> </div>
@@ -277,10 +270,10 @@
:key="index" :key="index"
class="flex flex-1 space-x-4" class="flex flex-1 space-x-4"
> >
<span class="min-w-[9rem] w-1/4 truncate text-secondaryLight"> <span class="text-secondaryLight w-1/4 min-w-32 truncate">
{{ variable.key }} {{ variable.key }}
</span> </span>
<span class="min-w-[9rem] w-full truncate text-secondaryLight"> <span class="text-secondaryLight w-full min-w-32 truncate">
{{ variable.value }} {{ variable.value }}
</span> </span>
</div> </div>
@@ -453,11 +446,12 @@ const isEnvActive = (id: string | number) => {
} else { } else {
if (selectedEnvironmentIndex.value.type === "MY_ENV") { if (selectedEnvironmentIndex.value.type === "MY_ENV") {
return selectedEnv.value.index === id return selectedEnv.value.index === id
} else {
return (
selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
selectedEnv.value.teamEnvID === id
)
} }
return (
selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
selectedEnv.value.teamEnvID === id
)
} }
} }
@@ -502,36 +496,40 @@ const selectedEnv = computed(() => {
name: props.modelValue.environment.environment.name, name: props.modelValue.environment.environment.name,
teamEnvID: props.modelValue.environment.id, teamEnvID: props.modelValue.environment.id,
} }
} else {
return { type: "global", name: "Global" }
} }
return { type: "global", name: "Global" } } else {
} if (selectedEnvironmentIndex.value.type === "MY_ENV") {
if (selectedEnvironmentIndex.value.type === "MY_ENV") { const environment =
const environment = myEnvironments.value[selectedEnvironmentIndex.value.index]
myEnvironments.value[selectedEnvironmentIndex.value.index]
return {
type: "MY_ENV",
index: selectedEnvironmentIndex.value.index,
name: environment.name,
variables: environment.variables,
}
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
const teamEnv = teamEnvironmentList.value.find(
(env) =>
env.id ===
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
selectedEnvironmentIndex.value.teamEnvID)
)
if (teamEnv) {
return { return {
type: "TEAM_ENV", type: "MY_ENV",
name: teamEnv.environment.name, index: selectedEnvironmentIndex.value.index,
teamEnvID: selectedEnvironmentIndex.value.teamEnvID, name: environment.name,
variables: teamEnv.environment.variables, variables: environment.variables,
} }
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
const teamEnv = teamEnvironmentList.value.find(
(env) =>
env.id ===
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
selectedEnvironmentIndex.value.teamEnvID)
)
if (teamEnv) {
return {
type: "TEAM_ENV",
name: teamEnv.environment.name,
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
variables: teamEnv.environment.variables,
}
} else {
return { type: "NO_ENV_SELECTED" }
}
} else {
return { type: "NO_ENV_SELECTED" }
} }
return { type: "NO_ENV_SELECTED" }
} }
return { type: "NO_ENV_SELECTED" }
}) })
// Set the selected environment as initial scope value // Set the selected environment as initial scope value
@@ -579,12 +577,13 @@ const envQuickPeekActions = ref<TippyComponent | null>(null)
const getErrorMessage = (err: GQLError<string>) => { const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") { if (err.type === "network_error") {
return t("error.network_error") return t("error.network_error")
} } else {
switch (err.error) { switch (err.error) {
case "team_environment/not_found": case "team_environment/not_found":
return t("team_environment.not_found") return t("team_environment.not_found")
default: default:
return t("error.something_went_wrong") return t("error.something_went_wrong")
}
} }
} }
@@ -593,8 +592,9 @@ const globalEnvs = useReadonlyStream(globalEnv$, [])
const environmentVariables = computed(() => { const environmentVariables = computed(() => {
if (selectedEnv.value.variables) { if (selectedEnv.value.variables) {
return selectedEnv.value.variables return selectedEnv.value.variables
} else {
return []
} }
return []
}) })
const editGlobalEnv = () => { const editGlobalEnv = () => {

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div <div
class="sticky top-0 z-10 flex flex-shrink-0 flex-col overflow-x-auto bg-primary" class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary"
> >
<WorkspaceCurrent :section="t('tab.environments')" /> <WorkspaceCurrent :section="t('tab.environments')" />
<EnvironmentsMyEnvironment <EnvironmentsMyEnvironment

View File

@@ -16,7 +16,7 @@
@submit="saveEnvironment" @submit="saveEnvironment"
/> />
<div class="flex flex-1 items-center justify-between"> <div class="flex items-center justify-between flex-1">
<label for="variableList" class="p-4"> <label for="variableList" class="p-4">
{{ t("environment.variable_list") }} {{ t("environment.variable_list") }}
</label> </label>
@@ -37,11 +37,11 @@
</div> </div>
<div <div
v-if="evnExpandError" v-if="evnExpandError"
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400" class="w-full px-4 py-2 mb-2 overflow-auto font-mono text-red-400 whitespace-normal rounded bg-primaryLight"
> >
{{ t("environment.nested_overflow") }} {{ t("environment.nested_overflow") }}
</div> </div>
<div class="divide-y divide-dividerLight rounded border border-divider"> <div class="border rounded divide-y divide-dividerLight border-divider">
<div <div
v-for="({ id, env }, index) in vars" v-for="({ id, env }, index) in vars"
:key="`variable-${id}-${index}`" :key="`variable-${id}-${index}`"
@@ -50,7 +50,7 @@
<input <input
v-model="env.key" v-model="env.key"
v-focus v-focus
class="flex flex-1 bg-transparent px-4 py-2" class="flex flex-1 px-4 py-2 bg-transparent"
:placeholder="`${t('count.variable', { count: index + 1 })}`" :placeholder="`${t('count.variable', { count: index + 1 })}`"
:name="'param' + index" :name="'param' + index"
/> />
@@ -198,8 +198,9 @@ const workingEnv = computed(() => {
type: "MY_ENV", type: "MY_ENV",
index: props.editingEnvironmentIndex, index: props.editingEnvironmentIndex,
}) })
} else {
return null
} }
return null
}) })
const envList = useReadonlyStream(environments$, []) || props.envVars() const envList = useReadonlyStream(environments$, []) || props.envVars()
@@ -225,11 +226,12 @@ const liveEnvs = computed(() => {
return [ return [
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })), ...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
] ]
} else {
return [
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
...globalVars.value.map((x) => ({ ...x, source: "Global" })),
]
} }
return [
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
...globalVars.value.map((x) => ({ ...x, source: "Global" })),
]
}) })
watch( watch(

View File

@@ -1,24 +1,24 @@
<template> <template>
<div <div
class="group flex items-stretch" class="flex items-stretch group"
@contextmenu.prevent="options!.tippy.show()" @contextmenu.prevent="options!.tippy.show()"
> >
<span <span
v-if="environmentIndex === 'Global'" v-if="environmentIndex === 'Global'"
class="flex cursor-pointer items-center justify-center px-4" class="flex items-center justify-center px-4 cursor-pointer"
@click="emit('edit-environment')" @click="emit('edit-environment')"
> >
<icon-lucide-globe class="svg-icons" /> <icon-lucide-globe class="svg-icons" />
</span> </span>
<span <span
v-else v-else
class="flex cursor-pointer items-center justify-center px-4" class="flex items-center justify-center px-4 cursor-pointer"
@click="emit('edit-environment')" @click="emit('edit-environment')"
> >
<icon-lucide-layers class="svg-icons" /> <icon-lucide-layers class="svg-icons" />
</span> </span>
<span <span
class="flex min-w-0 flex-1 cursor-pointer py-2 pr-2 transition group-hover:text-secondaryDark" class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
@click="emit('edit-environment')" @click="emit('edit-environment')"
> >
<span class="truncate"> <span class="truncate">

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div <div
class="sticky top-upperPrimaryStickyFold z-10 flex flex-1 flex-shrink-0 justify-between overflow-x-auto border-b border-dividerLight bg-primary" class="sticky z-10 flex justify-between flex-1 flex-shrink-0 overflow-x-auto border-b top-upperPrimaryStickyFold border-dividerLight bg-primary"
> >
<HoppButtonSecondary <HoppButtonSecondary
:icon="IconPlus" :icon="IconPlus"
@@ -39,10 +39,10 @@
:text="t('empty.environments')" :text="t('empty.environments')"
> >
<div class="flex flex-col items-center space-y-4"> <div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight"> <span class="text-secondaryLight text-center">
{{ t("environment.import_or_create") }} {{ t("environment.import_or_create") }}
</span> </span>
<div class="flex flex-col items-stretch gap-4"> <div class="flex gap-4 flex-col items-stretch">
<HoppButtonPrimary <HoppButtonPrimary
:icon="IconImport" :icon="IconImport"
:label="t('import.title')" :label="t('import.title')"
@@ -68,7 +68,7 @@
@hide-modal="displayModalEdit(false)" @hide-modal="displayModalEdit(false)"
/> />
<EnvironmentsImportExport <EnvironmentsImportExport
v-if="showModalImportExport" :show="showModalImportExport"
environment-type="MY_ENV" environment-type="MY_ENV"
@hide-modal="displayModalImportExport(false)" @hide-modal="displayModalImportExport(false)"
/> />

View File

@@ -16,7 +16,7 @@
@submit="saveEnvironment" @submit="saveEnvironment"
/> />
<div class="flex flex-1 items-center justify-between"> <div class="flex items-center justify-between flex-1">
<label for="variableList" class="p-4"> <label for="variableList" class="p-4">
{{ t("environment.variable_list") }} {{ t("environment.variable_list") }}
</label> </label>
@@ -37,11 +37,11 @@
</div> </div>
<div <div
v-if="evnExpandError" v-if="evnExpandError"
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400" class="w-full px-4 py-2 mb-2 overflow-auto font-mono text-red-400 whitespace-normal rounded bg-primaryLight"
> >
{{ t("environment.nested_overflow") }} {{ t("environment.nested_overflow") }}
</div> </div>
<div class="divide-y divide-dividerLight rounded border border-divider"> <div class="border rounded divide-y divide-dividerLight border-divider">
<div <div
v-for="({ id, env }, index) in vars" v-for="({ id, env }, index) in vars"
:key="`variable-${id}-${index}`" :key="`variable-${id}-${index}`"
@@ -50,7 +50,7 @@
<input <input
v-model="env.key" v-model="env.key"
v-focus v-focus
class="flex flex-1 bg-transparent px-4 py-2" class="flex flex-1 px-4 py-2 bg-transparent"
:class="isViewer && 'opacity-25'" :class="isViewer && 'opacity-25'"
:placeholder="`${t('count.variable', { count: index + 1 })}`" :placeholder="`${t('count.variable', { count: index + 1 })}`"
:name="'param' + index" :name="'param' + index"
@@ -205,8 +205,11 @@ const evnExpandError = computed(() => {
const liveEnvs = computed(() => { const liveEnvs = computed(() => {
if (evnExpandError.value) { if (evnExpandError.value) {
return [] return []
} else {
return [
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
]
} }
return [...vars.value.map((x) => ({ ...x.env, source: editingName.value! }))]
}) })
watch( watch(
@@ -335,12 +338,13 @@ const hideModal = () => {
const getErrorMessage = (err: GQLError<string>) => { const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") { if (err.type === "network_error") {
return t("error.network_error") return t("error.network_error")
} } else {
switch (err.error) { switch (err.error) {
case "team_environment/not_found": case "team_environment/not_found":
return t("team_environment.not_found") return t("team_environment.not_found")
default: default:
return t("error.something_went_wrong") return t("error.something_went_wrong")
}
} }
} }
</script> </script>

View File

@@ -1,16 +1,16 @@
<template> <template>
<div <div
class="group flex items-stretch" class="flex items-stretch group"
@contextmenu.prevent="options!.tippy.show()" @contextmenu.prevent="options!.tippy.show()"
> >
<span <span
class="flex cursor-pointer items-center justify-center px-4" class="flex items-center justify-center px-4 cursor-pointer"
@click="emit('edit-environment')" @click="emit('edit-environment')"
> >
<icon-lucide-layers class="svg-icons" /> <icon-lucide-layers class="svg-icons" />
</span> </span>
<span <span
class="flex min-w-0 flex-1 cursor-pointer py-2 pr-2 transition group-hover:text-secondaryDark" class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
@click="emit('edit-environment')" @click="emit('edit-environment')"
> >
<span class="truncate"> <span class="truncate">
@@ -184,12 +184,13 @@ const duplicateEnvironments = () => {
const getErrorMessage = (err: GQLError<string>) => { const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") { if (err.type === "network_error") {
return t("error.network_error") return t("error.network_error")
} } else {
switch (err.error) { switch (err.error) {
case "team_environment/not_found": case "team_environment/not_found":
return t("team_environment.not_found") return t("team_environment.not_found")
default: default:
return t("error.something_went_wrong") return t("error.something_went_wrong")
}
} }
} }
</script> </script>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div <div
class="sticky top-upperPrimaryStickyFold z-10 flex flex-1 flex-shrink-0 justify-between overflow-x-auto border-b border-dividerLight bg-primary" class="sticky z-10 flex justify-between flex-1 flex-shrink-0 overflow-x-auto border-b top-upperPrimaryStickyFold border-dividerLight bg-primary"
> >
<HoppButtonSecondary <HoppButtonSecondary
v-if="team === undefined || team.myRole === 'VIEWER'" v-if="team === undefined || team.myRole === 'VIEWER'"
@@ -50,10 +50,10 @@
:text="t('empty.environments')" :text="t('empty.environments')"
> >
<div class="flex flex-col items-center space-y-4"> <div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight"> <span class="text-secondaryLight text-center">
{{ t("environment.import_or_create") }} {{ t("environment.import_or_create") }}
</span> </span>
<div class="flex flex-col items-stretch gap-4"> <div class="flex gap-4 flex-col items-stretch">
<HoppButtonPrimary <HoppButtonPrimary
:icon="IconImport" :icon="IconImport"
:label="t('import.title')" :label="t('import.title')"
@@ -94,7 +94,7 @@
v-if="!loading && adapterError" v-if="!loading && adapterError"
class="flex flex-col items-center py-4" class="flex flex-col items-center py-4"
> >
<icon-lucide-help-circle class="svg-icons mb-4" /> <icon-lucide-help-circle class="mb-4 svg-icons" />
{{ getErrorMessage(adapterError) }} {{ getErrorMessage(adapterError) }}
</div> </div>
<EnvironmentsTeamsDetails <EnvironmentsTeamsDetails
@@ -107,7 +107,7 @@
@hide-modal="displayModalEdit(false)" @hide-modal="displayModalEdit(false)"
/> />
<EnvironmentsImportExport <EnvironmentsImportExport
v-if="showModalImportExport" :show="showModalImportExport"
:team-environments="teamEnvironments" :team-environments="teamEnvironments"
:team-id="team?.id" :team-id="team?.id"
environment-type="TEAM_ENV" environment-type="TEAM_ENV"
@@ -174,12 +174,13 @@ const resetSelectedData = () => {
const getErrorMessage = (err: GQLError<string>) => { const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") { if (err.type === "network_error") {
return t("error.network_error") return t("error.network_error")
} } else {
switch (err.error) { switch (err.error) {
case "team_environment/not_found": case "team_environment/not_found":
return t("team_environment.not_found") return t("team_environment.not_found")
default: default:
return t("error.something_went_wrong") return t("error.something_went_wrong")
}
} }
} }

View File

@@ -47,9 +47,9 @@
/> />
</form> </form>
<div v-if="mode === 'email-sent'" class="flex flex-col px-4"> <div v-if="mode === 'email-sent'" class="flex flex-col px-4">
<div class="flex max-w-md flex-col items-center justify-center"> <div class="flex flex-col items-center justify-center max-w-md">
<icon-lucide-inbox class="h-6 w-6 text-accent" /> <icon-lucide-inbox class="w-6 h-6 text-accent" />
<h3 class="my-2 text-center text-lg"> <h3 class="my-2 text-lg text-center">
{{ t("auth.we_sent_magic_link") }} {{ t("auth.we_sent_magic_link") }}
</h3> </h3>
<p class="text-center"> <p class="text-center">
@@ -63,7 +63,7 @@
<template #footer> <template #footer>
<div <div
v-if="mode === 'sign-in' && tosLink && privacyPolicyLink" v-if="mode === 'sign-in' && tosLink && privacyPolicyLink"
class="text-tiny text-secondaryLight" class="text-secondaryLight text-tiny"
> >
By signing in, you are agreeing to our By signing in, you are agreeing to our
<HoppSmartAnchor <HoppSmartAnchor
@@ -90,7 +90,7 @@
</div> </div>
<div <div
v-if="mode === 'email-sent'" v-if="mode === 'email-sent'"
class="flex flex-1 justify-between text-secondaryLight" class="flex justify-between flex-1 text-secondaryLight"
> >
<HoppSmartAnchor <HoppSmartAnchor
class="link" class="link"
@@ -111,21 +111,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { Ref, computed, onMounted, ref } from "vue" import { Ref, computed, onMounted, ref } from "vue"
import { useI18n } from "@composables/i18n"
import { useStreamSubscriber } from "@composables/stream" import { useStreamSubscriber } from "@composables/stream"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n"
import { platform } from "~/platform" import { platform } from "~/platform"
import { setLocalConfig } from "~/newstore/localpersistence"
import IconEmail from "~icons/auth/email"
import IconGithub from "~icons/auth/github" import IconGithub from "~icons/auth/github"
import IconGoogle from "~icons/auth/google" import IconGoogle from "~icons/auth/google"
import IconEmail from "~icons/auth/email"
import IconMicrosoft from "~icons/auth/microsoft" import IconMicrosoft from "~icons/auth/microsoft"
import IconArrowLeft from "~icons/lucide/arrow-left" import IconArrowLeft from "~icons/lucide/arrow-left"
import { useService } from "dioc/vue"
import { LoginItemDef } from "~/platform/auth" import { LoginItemDef } from "~/platform/auth"
import { PersistenceService } from "~/services/persistence"
defineProps<{ defineProps<{
show: boolean show: boolean
@@ -139,8 +138,6 @@ const { subscribeToStream } = useStreamSubscriber()
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const persistenceService = useService(PersistenceService)
const form = { const form = {
email: "", email: "",
} }
@@ -263,7 +260,7 @@ const signInWithEmail = async () => {
.signInWithEmail(form.email) .signInWithEmail(form.email)
.then(() => { .then(() => {
mode.value = "email-sent" mode.value = "email-sent"
persistenceService.setLocalConfig("emailForSignIn", form.email) setLocalConfig("emailForSignIn", form.email)
}) })
.catch((e) => { .catch((e) => {
console.error(e) console.error(e)

Some files were not shown because too many files have changed in this diff Show More