Compare commits

..

15 Commits

Author SHA1 Message Date
Balu Babu
a134e20cdf chore: reverted old services to old ports 2023-11-14 04:49:45 +05:30
Balu Babu
a501282f2b chore: made changes to hoppscotch-aio for subpath access 2023-11-13 22:42:08 +05:30
Balu Babu
eb248fa0df chore: added subpath support to hoppscotch-backend service 2023-11-13 21:35:22 +05:30
Balu Babu
f1e4ac7fc4 chore: hoppscotch-sh-admin supports std http ports 2023-11-13 02:14:43 +05:30
Balu Babu
28059ddc60 chore: hoppscotch-app supports std http ports 2023-11-13 01:42:23 +05:30
jamesgeorge007
64517a53af feat: subpath access for individual containers 2023-11-10 12:51:35 +05:30
jamesgeorge007
79a9285f93 chore: dedicated Caddyfiles based on AIO container launch strategies
Determine the Caddy config file based on the `ENABLE_SUBPATH_BASED_ACCESS` environment variable.
2023-11-10 12:51:35 +05:30
jamesgeorge007
aebcbac979 chore: generate multiple builds for sh-admin
Enables seamless transition to/from subpath based access
2023-11-10 12:51:35 +05:30
jamesgeorge007
d19d96ba9c chore: add TODO comment for pending Caddy configurations and port updates 2023-11-10 12:51:35 +05:30
jamesgeorge007
312940009e refactor: infer base URL from the BASE_URL env var across the app 2023-11-10 12:51:35 +05:30
jamesgeorge007
8703a0dcfd build: generate build with admin as the base for subpath based access 2023-11-10 12:51:35 +05:30
jamesgeorge007
03e21e0b0c fix: ensure any non-existent file in the server is routed to the corresponding SPA
try_files avoids 404s, redirecting to SPA root; catch-all handles unknown paths for selfhost-web.
2023-11-10 12:51:35 +05:30
jamesgeorge007
d6e4b6497f chore: convey the base route as admin where sh-admin gets served 2023-11-10 12:51:35 +05:30
jamesgeorge007
2f1fca2917 chore: update aio Caddyfile 2023-11-10 12:51:35 +05:30
jamesgeorge007
04092d8597 refactor: make service worker ignore important routes 2023-11-10 12:51:35 +05:30
497 changed files with 8141 additions and 26803 deletions

View File

@@ -12,8 +12,8 @@ SESSION_SECRET='add some secret here'
# Hoppscotch App Domain Config
REDIRECT_URL="http://localhost:3000"
WHITELISTED_ORIGINS="http://localhost:3170,http://localhost:3000,http://localhost:3100"
VITE_ALLOWED_AUTH_PROVIDERS=GOOGLE,GITHUB,MICROSOFT,EMAIL
WHITELISTED_ORIGINS = "http://localhost:3170,http://localhost:3000,http://localhost:3100"
VITE_ALLOWED_AUTH_PROVIDERS = GOOGLE,GITHUB,MICROSOFT,EMAIL
# Google Auth Config
GOOGLE_CLIENT_ID="************************************************"

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"
]
}

11
Caddyfile Normal file
View File

@@ -0,0 +1,11 @@
:3500 {
handle_path /admin* {
reverse_proxy localhost:3100
}
handle_path /backend* {
reverse_proxy localhost:3170
}
reverse_proxy localhost:3000 # Proxy other requests to your server running on port 3001
}

View File

@@ -6,14 +6,10 @@
:3100 {
try_files {path} /
root * /site/sh-admin-multiport-setup
root * /site/sh-admin
file_server
}
:3170 {
reverse_proxy localhost:8080
}
:80 {
respond 404
}

View File

@@ -6,10 +6,6 @@
respond 404
}
:3170 {
reverse_proxy localhost:8080
}
:80 {
# Serve the `selfhost-web` SPA by default
root * /site/selfhost-web

View File

@@ -78,7 +78,7 @@ services:
ports:
- "3000:3000"
- "3100:3100"
- "3170:3170"
- "3170:8080"
- "3080:80"
# The preset DB service, you can delete/comment the below lines if

View File

@@ -32,9 +32,6 @@
"lint-staged": "12.4.0"
},
"pnpm": {
"overrides": {
"vue": "3.3.9"
},
"packageExtensions": {
"httpsnippet@^3.0.1": {
"peerDependencies": {

View File

@@ -17,7 +17,7 @@
"types": "dist/index.d.ts",
"sideEffects": false,
"dependencies": {
"@codemirror/language": "6.9.0",
"@codemirror/language": "^6.9.2",
"@lezer/highlight": "1.1.4",
"@lezer/lr": "^1.3.13"
},

View File

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

View File

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

View File

@@ -0,0 +1,7 @@
:80 :3170 {
handle_path /backend* {
reverse_proxy localhost:8080
}
}

View File

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

View File

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

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 {
id String @id @default(cuid())
parentID String?
data Json?
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
children TeamCollection[] @relation("TeamCollectionChildParent")
requests TeamRequest[]
@@ -75,8 +74,7 @@ model Shortcode {
creatorUid String?
User User? @relation(fields: [creatorUid], references: [uid])
createdOn DateTime @default(now())
updatedOn DateTime @default(now()) @updatedAt
updatedOn DateTime @updatedAt @default(now())
@@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique")
}
@@ -197,7 +195,6 @@ model UserCollection {
userUid String
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
title String
data Json?
orderIndex Int
type ReqType
createdOn DateTime @default(now()) @db.Timestamp(3)

View File

@@ -1,7 +1,8 @@
#!/usr/local/bin/node
// @ts-check
import { spawn } from 'child_process';
import { execSync, spawn } from 'child_process';
import fs from 'fs';
import process from 'process';
function runChildProcessWithPrefix(command, args, prefix) {
@@ -26,16 +27,20 @@ function runChildProcessWithPrefix(command, args, prefix) {
});
childProcess.on('error', (stuff) => {
console.error('error');
console.error(stuff);
console.log('error');
console.log(stuff);
});
return childProcess;
}
const caddyFileName =
process.env.ENABLE_SUBPATH_BASED_ACCESS === 'true'
? 'backend-subpath.Caddyfile'
: 'backend-multiport.Caddyfile';
const caddyProcess = runChildProcessWithPrefix(
'caddy',
['run', '--config', '/etc/caddy/backend.Caddyfile', '--adapter', 'caddyfile'],
['run', '--config', `/etc/caddy/${caddyFileName}`, '--adapter', 'caddyfile'],
'App/Admin Dashboard Caddy',
);
const backendProcess = runChildProcessWithPrefix(

View File

@@ -11,7 +11,6 @@ import { TeamEnvironmentsModule } from '../team-environments/team-environments.m
import { TeamCollectionModule } from '../team-collection/team-collection.module';
import { TeamRequestModule } from '../team-request/team-request.module';
import { InfraResolver } from './infra.resolver';
import { ShortcodeModule } from 'src/shortcode/shortcode.module';
@Module({
imports: [
@@ -24,7 +23,6 @@ import { ShortcodeModule } from 'src/shortcode/shortcode.module';
TeamEnvironmentsModule,
TeamCollectionModule,
TeamRequestModule,
ShortcodeModule,
],
providers: [InfraResolver, AdminResolver, AdminService],
exports: [AdminService],

View File

@@ -443,23 +443,6 @@ export class AdminResolver {
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 */
@Subscription(() => InvitedUser, {

View File

@@ -15,7 +15,6 @@ import {
INVALID_EMAIL,
USER_ALREADY_INVITED,
} from '../errors';
import { ShortcodeService } from 'src/shortcode/shortcode.service';
const mockPrisma = mockDeep<PrismaService>();
const mockPubSub = mockDeep<PubSubService>();
@@ -26,7 +25,6 @@ const mockTeamRequestService = mockDeep<TeamRequestService>();
const mockTeamInvitationService = mockDeep<TeamInvitationService>();
const mockTeamCollectionService = mockDeep<TeamCollectionService>();
const mockMailerService = mockDeep<MailerService>();
const mockShortcodeService = mockDeep<ShortcodeService>();
const adminService = new AdminService(
mockUserService,
@@ -38,7 +36,6 @@ const adminService = new AdminService(
mockPubSub as any,
mockPrisma as any,
mockMailerService,
mockShortcodeService,
);
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 { TeamInvitationService } from '../team-invitation/team-invitation.service';
import { TeamMemberRole } from '../team/team.model';
import { ShortcodeService } from 'src/shortcode/shortcode.service';
@Injectable()
export class AdminService {
@@ -38,7 +37,6 @@ export class AdminService {
private readonly pubsub: PubSubService,
private readonly prisma: PrismaService,
private readonly mailerService: MailerService,
private readonly shortcodeService: ShortcodeService,
) {}
/**
@@ -434,35 +432,4 @@ export class AdminService {
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

@@ -15,7 +15,6 @@ 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)
@@ -203,23 +202,4 @@ export class InfraResolver {
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;
/**
* 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
* (GqlRequestTeamMemberGuard)
@@ -592,13 +585,6 @@ export const USER_COLL_REORDERING_FAILED =
export const USER_COLL_SAME_NEXT_COLL =
'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
* (UserCollectionService)

View File

@@ -21,8 +21,8 @@ import {
} from 'src/team-request/team-request.model';
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
import { InvitedUser } from '../admin/invited-user.model';
import { UserCollection } from '@prisma/client';
import {
UserCollection,
UserCollectionRemovedData,
UserCollectionReorderData,
} from 'src/user-collection/user-collections.model';

View File

@@ -1,5 +1,4 @@
import { Field, ID, ObjectType } from '@nestjs/graphql';
import { User } from 'src/user/user.model';
@ObjectType()
export class Shortcode {
@@ -24,46 +23,3 @@ export class Shortcode {
})
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

@@ -8,7 +8,7 @@ import {
} from '@nestjs/graphql';
import * as E from 'fp-ts/Either';
import { UseGuards } from '@nestjs/common';
import { Shortcode, ShortcodeWithUserEmail } from './shortcode.model';
import { Shortcode } from './shortcode.model';
import { ShortcodeService } from './shortcode.service';
import { throwErr } from 'src/utils';
import { GqlUser } from 'src/decorators/gql-user.decorator';
@@ -19,7 +19,6 @@ import { AuthUser } from '../types/AuthUser';
import { PaginationArgs } from 'src/types/input-types.args';
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
import { SkipThrottle } from '@nestjs/throttler';
import { GqlAdminGuard } from 'src/admin/guards/gql-admin.guard';
@UseGuards(GqlThrottlerGuard)
@Resolver(() => Shortcode)
@@ -122,7 +121,7 @@ export class ShortcodeResolver {
@Args({
name: 'code',
type: () => ID,
description: 'The shortcode to remove',
description: 'The shortcode to resolve',
})
code: string,
) {

View File

@@ -1,13 +1,12 @@
import { mockDeep, mockReset } from 'jest-mock-extended';
import { PrismaService } from '../prisma/prisma.service';
import {
INVALID_EMAIL,
SHORTCODE_INVALID_PROPERTIES_JSON,
SHORTCODE_INVALID_REQUEST_JSON,
SHORTCODE_NOT_FOUND,
SHORTCODE_PROPERTIES_NOT_FOUND,
} from 'src/errors';
import { Shortcode, ShortcodeWithUserEmail } from './shortcode.model';
import { Shortcode } from './shortcode.model';
import { ShortcodeService } from './shortcode.service';
import { UserService } from 'src/user/user.service';
import { AuthUser } from 'src/types/AuthUser';
@@ -98,35 +97,6 @@ const shortcodes = [
},
];
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('getShortCode', () => {
test('should return a valid Shortcode with valid Shortcode ID', async () => {
@@ -471,99 +441,4 @@ describe('ShortcodeService', () => {
);
});
});
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

@@ -10,7 +10,7 @@ import {
SHORTCODE_PROPERTIES_NOT_FOUND,
} from 'src/errors';
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 { PubSubService } from 'src/pubsub/pubsub.service';
import { UserService } from 'src/user/user.service';
@@ -180,7 +180,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
}
/**
* Delete a ShortCode created by User of uid
* Delete a ShortCode
*
* @param shortcode ShortCode
* @param uid User Uid
@@ -223,26 +223,6 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
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
@@ -283,57 +263,4 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
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' })
title: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
}
@ArgsType()
@@ -33,13 +26,6 @@ export class CreateChildTeamCollectionArgs {
@Field({ name: 'childTitle', description: 'Title of the new collection' })
childTitle: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
}
@ArgsType()
@@ -47,14 +33,12 @@ export class RenameTeamCollectionArgs {
@Field(() => ID, {
name: 'collectionID',
description: 'ID of the collection',
deprecationReason: 'Switch to updateTeamCollection mutation instead',
})
collectionID: string;
@Field({
name: 'newTitle',
description: 'The updated title of the collection',
deprecationReason: 'Switch to updateTeamCollection mutation instead',
})
newTitle: string;
}
@@ -114,26 +98,3 @@ export class ReplaceTeamCollectionArgs {
})
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;
@Field({
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
@Field(() => ID, {
description: 'ID of the collection',
nullable: true,
})
parentID: string;
teamID: string;
}
@ObjectType()

View File

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

View File

@@ -1,7 +1,6 @@
import { Team, TeamCollection as DBTeamCollection } from '@prisma/client';
import { mockDeep, mockReset } from 'jest-mock-extended';
import {
TEAM_COLL_DATA_INVALID,
TEAM_COLL_DEST_SAME,
TEAM_COLL_INVALID_JSON,
TEAM_COLL_IS_PARENT_COLL,
@@ -18,7 +17,6 @@ import { PrismaService } from 'src/prisma/prisma.service';
import { PubSubService } from 'src/pubsub/pubsub.service';
import { AuthUser } from 'src/types/AuthUser';
import { TeamCollectionService } from './team-collection.service';
import { TeamCollection } from './team-collection.model';
const mockPrisma = mockDeep<PrismaService>();
const mockPubSub = mockDeep<PubSubService>();
@@ -53,60 +51,35 @@ const rootTeamCollection: DBTeamCollection = {
id: '123',
orderIndex: 1,
parentID: null,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
};
const rootTeamCollectionsCasted: TeamCollection = {
id: rootTeamCollection.id,
title: rootTeamCollection.title,
parentID: rootTeamCollection.parentID,
data: JSON.stringify(rootTeamCollection.data),
};
const rootTeamCollection_2: DBTeamCollection = {
id: 'erv',
orderIndex: 2,
parentID: null,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
};
const rootTeamCollection_2Casted: TeamCollection = {
id: 'erv',
parentID: null,
data: JSON.stringify(rootTeamCollection_2.data),
title: 'Root Collection 1',
};
const childTeamCollection: DBTeamCollection = {
id: 'rfe',
orderIndex: 1,
parentID: rootTeamCollection.id,
data: {},
title: 'Child Collection 1',
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
};
const childTeamCollectionCasted: TeamCollection = {
id: 'rfe',
parentID: rootTeamCollection.id,
data: JSON.stringify(childTeamCollection.data),
title: 'Child Collection 1',
};
const childTeamCollection_2: DBTeamCollection = {
id: 'bgdz',
orderIndex: 1,
data: {},
parentID: rootTeamCollection_2.id,
title: 'Child Collection 1',
teamID: team.id,
@@ -114,20 +87,11 @@ const childTeamCollection_2: DBTeamCollection = {
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[] = [
{
id: 'fdv',
orderIndex: 1,
parentID: null,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -138,8 +102,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 2,
parentID: null,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -149,8 +111,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 3,
parentID: null,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -159,8 +119,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
id: 'bre3',
orderIndex: 4,
parentID: null,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -171,8 +129,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 5,
parentID: null,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -183,8 +139,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
parentID: null,
title: 'Root Collection 1',
teamID: team.id,
data: {},
createdOn: currentTime,
updatedOn: currentTime,
},
@@ -194,8 +148,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
parentID: null,
title: 'Root Collection 1',
teamID: team.id,
data: {},
createdOn: currentTime,
updatedOn: currentTime,
},
@@ -204,7 +156,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 8,
parentID: null,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -214,7 +165,6 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 9,
parentID: null,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -225,83 +175,17 @@ const rootTeamCollectionList: DBTeamCollection[] = [
parentID: null,
title: 'Root Collection 1',
teamID: team.id,
data: {},
createdOn: 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[] = [
{
id: '123',
orderIndex: 1,
parentID: rootTeamCollection.id,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -311,8 +195,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
orderIndex: 2,
parentID: rootTeamCollection.id,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -322,8 +204,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
orderIndex: 3,
parentID: rootTeamCollection.id,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -332,8 +212,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '567',
orderIndex: 4,
parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -343,8 +221,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '123',
orderIndex: 5,
parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -354,8 +230,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '678',
orderIndex: 6,
parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -365,8 +239,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '789',
orderIndex: 7,
parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -376,8 +248,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '890',
orderIndex: 8,
parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -387,7 +257,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '012',
orderIndex: 9,
parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -397,8 +266,6 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '0bhu',
orderIndex: 10,
parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1',
teamID: team.id,
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(() => {
mockReset(mockPrisma);
mockPubSub.publish.mockClear();
@@ -513,7 +311,7 @@ describe('getParentOfCollection', () => {
const result = await teamCollectionService.getParentOfCollection(
childTeamCollection.id,
);
expect(result).toEqual(rootTeamCollectionsCasted);
expect(result).toEqual(rootTeamCollection);
});
test('should return null successfully for a root collection with valid collectionID', async () => {
@@ -549,7 +347,7 @@ describe('getChildrenOfCollection', () => {
null,
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 () => {
@@ -565,9 +363,9 @@ describe('getChildrenOfCollection', () => {
10,
);
expect(result).toEqual([
{ ...childTeamCollectionListCasted[7] },
{ ...childTeamCollectionListCasted[8] },
{ ...childTeamCollectionListCasted[9] },
{ ...childTeamCollectionList[7] },
{ ...childTeamCollectionList[8] },
{ ...childTeamCollectionList[9] },
]);
});
@@ -594,7 +392,7 @@ describe('getTeamRootCollections', () => {
null,
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 () => {
@@ -610,9 +408,9 @@ describe('getTeamRootCollections', () => {
10,
);
expect(result).toEqual([
{ ...rootTeamCollectionListCasted[7] },
{ ...rootTeamCollectionListCasted[8] },
{ ...rootTeamCollectionListCasted[9] },
{ ...rootTeamCollectionList[7] },
{ ...rootTeamCollectionList[8] },
{ ...rootTeamCollectionList[9] },
]);
});
@@ -666,7 +464,6 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID,
'ab',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id,
);
expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE);
@@ -681,27 +478,11 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID,
'abcd',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id,
);
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 () => {
// isOwnerCheck
mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce(
@@ -715,10 +496,9 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID,
'abcdefg',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id,
);
expect(result).toEqualRight(rootTeamCollectionsCasted);
expect(result).toEqualRight(rootTeamCollection);
});
test('should successfully create a new child TeamCollection with valid inputs', async () => {
@@ -734,10 +514,9 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection(
childTeamCollection.teamID,
childTeamCollection.title,
JSON.stringify(rootTeamCollection.data),
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 () => {
@@ -753,13 +532,11 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection(
childTeamCollection.teamID,
childTeamCollection.title,
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id,
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollection.teamID}/coll_added`,
childTeamCollectionCasted,
childTeamCollection,
);
});
@@ -776,13 +553,11 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID,
'abcdefg',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id,
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_added`,
rootTeamCollectionsCasted,
rootTeamCollection,
);
});
});
@@ -812,7 +587,7 @@ describe('renameCollection', () => {
'NewTitle',
);
expect(result).toEqualRight({
...rootTeamCollectionsCasted,
...rootTeamCollection,
title: 'NewTitle',
});
});
@@ -850,7 +625,7 @@ describe('renameCollection', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_updated`,
{
...rootTeamCollectionsCasted,
...rootTeamCollection,
title: 'NewTitle',
},
);
@@ -1057,8 +832,9 @@ describe('moveCollection', () => {
null,
);
expect(result).toEqualRight({
...childTeamCollectionCasted,
...childTeamCollection,
parentID: null,
orderIndex: 2,
});
});
@@ -1114,8 +890,9 @@ describe('moveCollection', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollection.teamID}/coll_moved`,
{
...childTeamCollectionCasted,
...childTeamCollection,
parentID: null,
orderIndex: 2,
},
);
});
@@ -1154,8 +931,9 @@ describe('moveCollection', () => {
childTeamCollection_2.id,
);
expect(result).toEqualRight({
...rootTeamCollectionsCasted,
parentID: childTeamCollection_2Casted.id,
...rootTeamCollection,
parentID: childTeamCollection_2.id,
orderIndex: 1,
});
});
@@ -1195,8 +973,9 @@ describe('moveCollection', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollection_2.teamID}/coll_moved`,
{
...rootTeamCollectionsCasted,
parentID: childTeamCollection_2Casted.id,
...rootTeamCollection,
parentID: childTeamCollection_2.id,
orderIndex: 1,
},
);
});
@@ -1235,8 +1014,9 @@ describe('moveCollection', () => {
childTeamCollection_2.id,
);
expect(result).toEqualRight({
...childTeamCollectionCasted,
parentID: childTeamCollection_2Casted.id,
...childTeamCollection,
parentID: childTeamCollection_2.id,
orderIndex: 1,
});
});
@@ -1276,8 +1056,9 @@ describe('moveCollection', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollection.teamID}/coll_moved`,
{
...childTeamCollectionCasted,
parentID: childTeamCollection_2Casted.id,
...childTeamCollection,
parentID: childTeamCollection_2.id,
orderIndex: 1,
},
);
});
@@ -1373,7 +1154,7 @@ describe('updateCollectionOrder', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollectionList[4].teamID}/coll_order_updated`,
{
collection: rootTeamCollectionListCasted[4],
collection: rootTeamCollectionList[4],
nextCollection: null,
},
);
@@ -1454,8 +1235,8 @@ describe('updateCollectionOrder', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollectionList[2].teamID}/coll_order_updated`,
{
collection: childTeamCollectionListCasted[4],
nextCollection: childTeamCollectionListCasted[2],
collection: childTeamCollectionList[4],
nextCollection: childTeamCollectionList[2],
},
);
});
@@ -1521,7 +1302,7 @@ describe('importCollectionsFromJSON', () => {
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_added`,
rootTeamCollectionsCasted,
rootTeamCollection,
);
});
});
@@ -1640,7 +1421,7 @@ describe('replaceCollectionsWithJSON', () => {
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`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

View File

@@ -13,7 +13,6 @@ import {
TEAM_COLL_IS_PARENT_COLL,
TEAM_COL_SAME_NEXT_COLL,
TEAM_COL_REORDERING_FAILED,
TEAM_COLL_DATA_INVALID,
} from '../errors';
import { PubSubService } from '../pubsub/pubsub.service';
import { isValidLength } from 'src/utils';
@@ -70,7 +69,6 @@ export class TeamCollectionService {
this.generatePrismaQueryObjForFBCollFolder(f, teamID, index + 1),
),
},
data: folder.data ?? undefined,
};
}
@@ -120,7 +118,6 @@ export class TeamCollectionService {
name: collection.right.title,
folders: childrenCollectionObjects,
requests: requests.map((x) => x.request),
data: JSON.stringify(collection.right.data),
};
return E.right(result);
@@ -201,11 +198,8 @@ export class TeamCollectionService {
),
);
teamCollections.forEach((collection) =>
this.pubsub.publish(
`team_coll/${destTeamID}/coll_added`,
this.cast(collection),
),
teamCollections.forEach((x) =>
this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x),
);
return E.right(true);
@@ -274,11 +268,8 @@ export class TeamCollectionService {
),
);
teamCollections.forEach((collections) =>
this.pubsub.publish(
`team_coll/${destTeamID}/coll_added`,
this.cast(collections),
),
teamCollections.forEach((x) =>
this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x),
);
return E.right(true);
@@ -286,17 +277,11 @@ export class TeamCollectionService {
/**
* Typecast a database TeamCollection to a TeamCollection model
*
* @param teamCollection database TeamCollection
* @returns TeamCollection model
*/
private cast(teamCollection: DBTeamCollection): TeamCollection {
return <TeamCollection>{
id: teamCollection.id,
title: teamCollection.title,
parentID: teamCollection.parentID,
data: !teamCollection.data ? null : JSON.stringify(teamCollection.data),
};
return <TeamCollection>{ ...teamCollection };
}
/**
@@ -339,7 +324,7 @@ export class TeamCollectionService {
});
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
* @returns A list of child collections
*/
async getChildrenOfCollection(
getChildrenOfCollection(
collectionID: string,
cursor: string | null,
take: number,
) {
const res = await this.prisma.teamCollection.findMany({
return this.prisma.teamCollection.findMany({
where: {
parentID: collectionID,
},
@@ -366,12 +351,6 @@ export class TeamCollectionService {
skip: cursor ? 1 : 0,
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,
take: number,
) {
const res = await this.prisma.teamCollection.findMany({
return this.prisma.teamCollection.findMany({
where: {
teamID,
parentID: null,
@@ -399,12 +378,6 @@ export class TeamCollectionService {
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
});
const teamCollections = res.map((teamCollection) =>
this.cast(teamCollection),
);
return teamCollections;
}
/**
@@ -497,7 +470,6 @@ export class TeamCollectionService {
async createCollection(
teamID: string,
title: string,
data: string | null = null,
parentTeamCollectionID: string | null,
) {
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 (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
? {
connect: {
@@ -533,23 +498,18 @@ export class TeamCollectionService {
},
},
parent: isParent,
data: data ?? undefined,
orderIndex: !parentTeamCollectionID
? (await this.getRootCollectionsCount(teamID)) + 1
: (await this.getChildCollectionsCount(parentTeamCollectionID)) + 1,
},
});
this.pubsub.publish(
`team_coll/${teamID}/coll_added`,
this.cast(teamCollection),
);
this.pubsub.publish(`team_coll/${teamID}/coll_added`, teamCollection);
return E.right(this.cast(teamCollection));
}
/**
* @deprecated Use updateTeamCollection method instead
* Update the title of a TeamCollection
*
* @param collectionID The Collection ID
@@ -572,10 +532,10 @@ export class TeamCollectionService {
this.pubsub.publish(
`team_coll/${updatedTeamCollection.teamID}/coll_updated`,
this.cast(updatedTeamCollection),
updatedTeamCollection,
);
return E.right(this.cast(updatedTeamCollection));
return E.right(updatedTeamCollection);
} catch (error) {
return E.left(TEAM_COLL_NOT_FOUND);
}
@@ -734,8 +694,8 @@ export class TeamCollectionService {
* @returns An Option of boolean, is parent or not
*/
private async isParent(
collection: DBTeamCollection,
destCollection: DBTeamCollection,
collection: TeamCollection,
destCollection: TeamCollection,
): 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
//* Valid condition, isParent returns false
@@ -1011,49 +971,4 @@ export class TeamCollectionService {
const teamCollectionsCount = this.prisma.teamCollection.count();
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',
parentID: null,
teamID: team.id,
data: {},
title: 'Team Collection 1',
orderIndex: 1,
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 {
id?: string;
folders: CollectionFolder[];
requests: any[];
name: string;
data?: string;
}

View File

@@ -6,13 +6,6 @@ import { PaginationArgs } from 'src/types/input-types.args';
export class CreateRootUserCollectionArgs {
@Field({ name: 'title', description: 'Title of the new user collection' })
title: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
}
@ArgsType()
export class CreateChildUserCollectionArgs {
@@ -24,13 +17,6 @@ export class CreateChildUserCollectionArgs {
description: 'ID of the parent to the new user collection',
})
parentUserCollectionID: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
}
@ArgsType()
@@ -109,26 +95,3 @@ export class ImportUserCollectionsFromJSONArgs {
})
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,
RenameUserCollectionsArgs,
UpdateUserCollectionArgs,
UpdateUserCollectionsArgs,
} from './input-type.args';
import { ReqType } from 'src/types/RequestTypes';
import * as E from 'fp-ts/Either';
@@ -143,13 +142,7 @@ export class UserCollectionResolver {
);
if (E.isLeft(userCollection)) throwErr(userCollection.left);
return <UserCollection>{
...userCollection.right,
userID: userCollection.right.userUid,
data: !userCollection.right.data
? null
: JSON.stringify(userCollection.right.data),
};
return userCollection.right;
}
@Query(() => UserCollectionExportJSONData, {
@@ -198,7 +191,6 @@ export class UserCollectionResolver {
await this.userCollectionService.createUserCollection(
user,
args.title,
args.data,
null,
ReqType.REST,
);
@@ -220,7 +212,6 @@ export class UserCollectionResolver {
await this.userCollectionService.createUserCollection(
user,
args.title,
args.data,
null,
ReqType.GQL,
);
@@ -241,7 +232,6 @@ export class UserCollectionResolver {
await this.userCollectionService.createUserCollection(
user,
args.title,
args.data,
args.parentUserCollectionID,
ReqType.GQL,
);
@@ -262,7 +252,6 @@ export class UserCollectionResolver {
await this.userCollectionService.createUserCollection(
user,
args.title,
args.data,
args.parentUserCollectionID,
ReqType.REST,
);
@@ -370,26 +359,6 @@ export class UserCollectionResolver {
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
@Subscription(() => UserCollection, {
description: 'Listen for User Collection Creation',

View File

@@ -12,7 +12,6 @@ import {
USER_NOT_FOUND,
USER_NOT_OWNER,
USER_COLL_INVALID_JSON,
USER_COLL_DATA_INVALID,
} from 'src/errors';
import { PrismaService } from 'src/prisma/prisma.service';
import { AuthUser } from 'src/types/AuthUser';
@@ -44,12 +43,8 @@ export class UserCollectionService {
*/
private cast(collection: UserCollection) {
return <UserCollectionModel>{
id: collection.id,
title: collection.title,
type: collection.type,
parentID: collection.parentID,
...collection,
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,
type: ReqType,
) {
const res = await this.prisma.userCollection.findMany({
return this.prisma.userCollection.findMany({
where: {
parentID: collectionID,
type: type,
@@ -181,12 +176,6 @@ export class UserCollectionService {
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
});
const childCollections = res.map((childCollection) =>
this.cast(childCollection),
);
return childCollections;
}
/**
@@ -222,20 +211,12 @@ export class UserCollectionService {
async createUserCollection(
user: AuthUser,
title: string,
data: string | null = null,
parentUserCollectionID: string | null,
type: ReqType,
) {
const isTitleValid = isValidLength(title, this.TITLE_LENGTH);
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 (parentUserCollectionID !== null) {
const parentCollection = await this.getUserCollection(
@@ -270,19 +251,15 @@ export class UserCollectionService {
},
},
parent: isParent,
data: data ?? undefined,
orderIndex: !parentUserCollectionID
? (await this.getRootCollectionsCount(user.uid)) + 1
: (await this.getChildCollectionsCount(parentUserCollectionID)) + 1,
},
});
await this.pubsub.publish(
`user_coll/${user.uid}/created`,
this.cast(userCollection),
);
await this.pubsub.publish(`user_coll/${user.uid}/created`, userCollection);
return E.right(this.cast(userCollection));
return E.right(userCollection);
}
/**
@@ -299,7 +276,7 @@ export class UserCollectionService {
take: number,
type: ReqType,
) {
const res = await this.prisma.userCollection.findMany({
return this.prisma.userCollection.findMany({
where: {
userUid: user.uid,
parentID: null,
@@ -312,12 +289,6 @@ export class UserCollectionService {
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
});
const userCollections = res.map((childCollection) =>
this.cast(childCollection),
);
return userCollections;
}
/**
@@ -336,7 +307,7 @@ export class UserCollectionService {
take: number,
type: ReqType,
) {
const res = await this.prisma.userCollection.findMany({
return this.prisma.userCollection.findMany({
where: {
userUid: user.uid,
parentID: userCollectionID,
@@ -346,16 +317,9 @@ export class UserCollectionService {
skip: cursor ? 1 : 0,
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
*
* @param newTitle The new title of collection
@@ -387,10 +351,10 @@ export class UserCollectionService {
this.pubsub.publish(
`user_coll/${updatedUserCollection.userUid}/updated`,
this.cast(updatedUserCollection),
updatedUserCollection,
);
return E.right(this.cast(updatedUserCollection));
return E.right(updatedUserCollection);
} catch (error) {
return E.left(USER_COLL_NOT_FOUND);
}
@@ -627,10 +591,10 @@ export class UserCollectionService {
this.pubsub.publish(
`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
@@ -678,10 +642,10 @@ export class UserCollectionService {
this.pubsub.publish(
`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
};
}),
data: JSON.stringify(collection.right.data),
};
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
};
}),
data: JSON.stringify(parentCollection.right.data),
}),
collectionType: parentCollection.right.type,
});
@@ -1009,7 +971,6 @@ export class UserCollectionService {
this.generatePrismaQueryObj(f, userID, index + 1, reqType),
),
},
data: folder.data ?? undefined,
};
}
@@ -1079,63 +1040,10 @@ export class UserCollectionService {
),
);
userCollections.forEach((collection) =>
this.pubsub.publish(`user_coll/${userID}/created`, this.cast(collection)),
userCollections.forEach((x) =>
this.pubsub.publish(`user_coll/${userID}/created`, x),
);
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;
@Field({
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
@Field(() => ReqType, {
description: 'Type of the user collection',
})

View File

@@ -147,7 +147,7 @@ module.exports = {
// The glob patterns Jest uses to detect test files
testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
"**/src/__tests__/commands/**/*.*.ts",
"**/src/__tests__/**/*.*.ts",
],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped

View File

@@ -19,9 +19,8 @@
"debugger": "node debugger.js 9999",
"prepublish": "pnpm exec tsup",
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",
"test": "pnpm run build && jest && rm -rf dist",
"do-typecheck": "pnpm exec tsc --noEmit",
"do-test": "pnpm test"
"test": "pnpm run build && jest && rm -rf dist"
},
"keywords": [
"cli",

View File

@@ -28,7 +28,7 @@ describe("Test 'hopp test <file>' command:", () => {
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
});
test("Malformed collection file.", async () => {
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
"malformed-collection2.json"
@@ -106,7 +106,7 @@ describe("Test 'hopp test <file> --env <file>' command:", () => {
const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`;
const { error, stdout } = await execAsync(cmd);
const { error } = await execAsync(cmd);
expect(error).toBeNull();
});
@@ -129,6 +129,7 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
console.log("invalid value thing", out)
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});

View File

@@ -1,6 +1,7 @@
{
"URL": "https://echo.hoppscotch.io",
"HOST": "echo.hoppscotch.io",
"X-COUNTRY": "IN",
"BODY_VALUE": "body_value",
"BODY_KEY": "body_key"
}

View File

@@ -12,7 +12,7 @@
"method": "POST",
"auth": { "authType": "none", "authActive": true },
"preRequestScript": "",
"testScript": "const HOST = pw.env.get(\"HOST\");\nconst UNSET_ENV = pw.env.get(\"UNSET_ENV\");\nconst EXPECTED_URL = \"https://echo.hoppscotch.io\";\nconst URL = pw.env.get(\"URL\");\nconst BODY_VALUE = pw.env.get(\"BODY_VALUE\");\n\n// Check JSON response property\npw.test(\"Check headers properties.\", ()=> {\n pw.expect(pw.response.body.headers.host).toBe(HOST);\n});\n\npw.test(\"Check data properties.\", () => {\n\tconst DATA = pw.response.body.data;\n \n pw.expect(DATA).toBeType(\"string\");\n pw.expect(JSON.parse(DATA).body_key).toBe(BODY_VALUE);\n});\n\npw.test(\"Check request URL.\", () => {\n pw.expect(URL).toBe(EXPECTED_URL);\n})\n\npw.test(\"Check unset ENV.\", () => {\n pw.expect(UNSET_ENV).toBeType(\"undefined\");\n})",
"testScript": "const HOST = pw.env.get(\"HOST\");\nconst UNSET_ENV = pw.env.get(\"UNSET_ENV\");\nconst EXPECTED_URL = \"https://echo.hoppscotch.io\";\nconst URL = pw.env.get(\"URL\");\nconst X_COUNTRY = pw.env.get(\"X-COUNTRY\");\nconst BODY_VALUE = pw.env.get(\"BODY_VALUE\");\n\n// Check JSON response property\npw.test(\"Check headers properties.\", ()=> {\n pw.expect(pw.response.body.headers.host).toBe(HOST);\n\t pw.expect(pw.response.body.headers[\"x-country\"]).toBe(X_COUNTRY); \n});\n\npw.test(\"Check data properties.\", () => {\n\tconst DATA = pw.response.body.data;\n \n pw.expect(DATA).toBeType(\"string\");\n pw.expect(JSON.parse(DATA).body_key).toBe(BODY_VALUE);\n});\n\npw.test(\"Check request URL.\", () => {\n pw.expect(URL).toBe(EXPECTED_URL);\n})\n\npw.test(\"Check unset ENV.\", () => {\n pw.expect(UNSET_ENV).toBeType(\"undefined\");\n})",
"body": {
"contentType": "application/json",
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"

View File

@@ -6,24 +6,23 @@ import {
parseTemplateString,
parseTemplateStringE,
} from "@hoppscotch/data";
import { runPreRequestScript } from "@hoppscotch/js-sandbox/node";
import * as A from "fp-ts/Array";
import * as E from "fp-ts/Either";
import * as O from "fp-ts/Option";
import * as RA from "fp-ts/ReadonlyArray";
import * as TE from "fp-ts/TaskEither";
import { runPreRequestScript } from "@hoppscotch/js-sandbox";
import { flow, pipe } from "fp-ts/function";
import * as TE from "fp-ts/TaskEither";
import * as E from "fp-ts/Either";
import * as RA from "fp-ts/ReadonlyArray";
import * as A from "fp-ts/Array";
import * as O from "fp-ts/Option";
import * as S from "fp-ts/string";
import qs from "qs";
import { EffectiveHoppRESTRequest } from "../interfaces/request";
import { HoppCLIError, error } from "../types/errors";
import { error, HoppCLIError } from "../types/errors";
import { HoppEnvs } from "../types/request";
import { PreRequestMetrics } from "../types/response";
import { isHoppCLIError } from "./checks";
import { arrayFlatMap, arraySort, tupleToRecord } from "./functions/array";
import { getEffectiveFinalMetaData } from "./getters";
import { tupleToRecord, arraySort, arrayFlatMap } from "./functions/array";
import { toFormData } from "./mutators";
import { getEffectiveFinalMetaData } from "./getters";
import { PreRequestMetrics } from "../types/response";
/**
* Runs pre-request-script runner over given request which extracts set ENVs and

View File

@@ -1,19 +1,17 @@
import { HoppRESTRequest } from "@hoppscotch/data";
import { TestDescriptor } from "@hoppscotch/js-sandbox";
import { runTestScript } from "@hoppscotch/js-sandbox/node";
import * as A from "fp-ts/Array";
import * as RA from "fp-ts/ReadonlyArray";
import * as T from "fp-ts/Task";
import * as TE from "fp-ts/TaskEither";
import { flow, pipe } from "fp-ts/function";
import { execTestScript, TestDescriptor } from "@hoppscotch/js-sandbox";
import { hrtime } from "process";
import { flow, pipe } from "fp-ts/function";
import * as RA from "fp-ts/ReadonlyArray";
import * as A from "fp-ts/Array";
import * as TE from "fp-ts/TaskEither";
import * as T from "fp-ts/Task";
import {
RequestRunnerResponse,
TestReport,
TestScriptParams,
} from "../interfaces/response";
import { HoppCLIError, error } from "../types/errors";
import { error, HoppCLIError } from "../types/errors";
import { HoppEnvs } from "../types/request";
import { ExpectResult, TestMetrics, TestRunnerRes } from "../types/response";
import { getDurationInSeconds } from "./getters";
@@ -38,7 +36,7 @@ export const testRunner = (
pipe(
TE.of(testScriptData),
TE.chain(({ testScript, response, envs }) =>
runTestScript(testScript, envs, response)
execTestScript(testScript, envs, response)
)
)
),

View File

@@ -57,7 +57,7 @@ module.exports = {
{
name: "localStorage",
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
@@ -66,10 +66,8 @@ module.exports = {
{
selector: "CallExpression[callee.object.property.name='localStorage']",
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

@@ -5,4 +5,5 @@ module.exports = {
printWidth: 80,
useTabs: false,
tabWidth: 2,
plugins: ["prettier-plugin-tailwindcss"],
}

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width=".88em" height="1em" viewBox="0 0 21 24"><path fill="currentColor" d="M12.731 2.751 17.666 5.6a2.138 2.138 0 1 1 2.07 3.548l-.015.003v5.7a2.14 2.14 0 1 1-2.098 3.502l-.002-.002-4.905 2.832a2.14 2.14 0 1 1-4.079.054l-.004.015-4.941-2.844a2.14 2.14 0 1 1-2.067-3.556l.015-.003V9.15a2.14 2.14 0 1 1 1.58-3.926l-.01-.005c.184.106.342.231.479.376l.001.001 4.938-2.85a2.14 2.14 0 1 1 4.096.021l.004-.015zm-.515.877a.766.766 0 0 1-.057.057l-.001.001 6.461 11.19c.026-.009.056-.016.082-.023V9.146a2.14 2.14 0 0 1-1.555-2.603l-.003.015.019-.072zm-3.015.059-.06-.06-4.946 2.852A2.137 2.137 0 0 1 2.749 9.12l-.015.004-.076.021v5.708l.084.023 6.461-11.19zm2.076.507a2.164 2.164 0 0 1-1.207-.004l.015.004-6.46 11.189c.286.276.496.629.597 1.026l.003.015h12.911c.102-.413.313-.768.599-1.043l.001-.001L11.28 4.194zm.986 16.227 4.917-2.838a1.748 1.748 0 0 1-.038-.142H4.222l-.021.083 4.939 2.852c.39-.403.936-.653 1.54-.653.626 0 1.189.268 1.581.696l.001.002z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width=".88em" height="1em" viewBox="0 0 21 24" class="iconify iconify--fontisto"><path fill="currentColor" d="M12.731 2.751 17.666 5.6a2.138 2.138 0 1 1 2.07 3.548l-.015.003v5.7a2.14 2.14 0 1 1-2.098 3.502l-.002-.002-4.905 2.832a2.14 2.14 0 1 1-4.079.054l-.004.015-4.941-2.844a2.14 2.14 0 1 1-2.067-3.556l.015-.003V9.15a2.14 2.14 0 1 1 1.58-3.926l-.01-.005c.184.106.342.231.479.376l.001.001 4.938-2.85a2.14 2.14 0 1 1 4.096.021l.004-.015zm-.515.877a.766.766 0 0 1-.057.057l-.001.001 6.461 11.19c.026-.009.056-.016.082-.023V9.146a2.14 2.14 0 0 1-1.555-2.603l-.003.015.019-.072zm-3.015.059-.06-.06-4.946 2.852A2.137 2.137 0 0 1 2.749 9.12l-.015.004-.076.021v5.708l.084.023 6.461-11.19zm2.076.507a2.164 2.164 0 0 1-1.207-.004l.015.004-6.46 11.189c.286.276.496.629.597 1.026l.003.015h12.911c.102-.413.313-.768.599-1.043l.001-.001L11.28 4.194zm.986 16.227 4.917-2.838a1.748 1.748 0 0 1-.038-.142H4.222l-.021.083 4.939 2.852c.39-.403.936-.653 1.54-.653.626 0 1.189.268 1.581.696l.001.002z"/></svg>

Before

Width:  |  Height:  |  Size: 1017 B

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M10.133 1h4.409a.5.5 0 0 1 .5.5v4.422c0 .026-.035.033-.045.01l-.048-.112a9.095 9.095 0 0 0-4.825-4.776c-.023-.01-.016-.044.01-.044Zm-8.588.275h-.5v1h.5c7.027 0 12.229 5.199 12.229 12.226v.5h1v-.5c0-7.58-5.65-13.226-13.229-13.226Zm.034 4.22h-.5v1h.5c2.361 0 4.348.837 5.744 2.238 1.395 1.401 2.227 3.395 2.227 5.758v.5h1v-.5c0-2.604-.921-4.859-2.52-6.463-1.596-1.605-3.845-2.532-6.45-2.532Zm-.528 8.996v-4.423c0-.041.033-.074.074-.074a4.923 4.923 0 0 1 4.923 4.922.074.074 0 0 1-.074.074H1.551a.5.5 0 0 1-.5-.5Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 684 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M9.277 2.084a.5.5 0 0 1 .185.607l-2.269 5.5a.5.5 0 0 1-.462.309H3.5a.5.5 0 0 1-.354-.854l5.5-5.5a.5.5 0 0 1 .631-.062ZM4.707 7.5h1.69l1.186-2.875L4.707 7.5Zm2.016 6.416a.5.5 0 0 1-.185-.607l2.269-5.5a.5.5 0 0 1 .462-.309H12.5a.5.5 0 0 1 .354.854l-5.5 5.5a.5.5 0 0 1-.631.062Zm4.57-5.416h-1.69l-1.186 2.875L11.293 8.5Z" clip-rule="evenodd"/><path fill="currentColor" fill-rule="evenodd" d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1 0A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" clip-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 633 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" d="M1 2h4.257a2.5 2.5 0 0 1 1.768.732L9.293 5 5 9.293 3.732 8.025A2.5 2.5 0 0 1 3 6.257V4H2v2.257a3.5 3.5 0 0 0 1.025 2.475L5 10.707l1.25-1.25 2.396 2.397.708-.708L6.957 8.75 8.75 6.957l2.396 2.397.708-.708L9.457 6.25 10.707 5 7.732 2.025A3.5 3.5 0 0 0 5.257 1H1v1ZM10.646 2.354l2.622 2.62A2.5 2.5 0 0 1 14 6.744V12h1V6.743a3.5 3.5 0 0 0-1.025-2.475l-2.621-2.622-.707.708ZM4.268 13.975l-2.622-2.621.708-.708 2.62 2.622A2.5 2.5 0 0 0 6.744 14H15v1H6.743a3.5 3.5 0 0 1-2.475-1.025Z"/></svg>

Before

Width:  |  Height:  |  Size: 610 B

View File

@@ -29,6 +29,14 @@
@apply antialiased;
accent-color: var(--accent-color);
font-variant-ligatures: common-ligatures;
// Colors
--info-color: #ec4899;
--success-color: #10b981;
--blue-color: #3b82f6;
--warning-color: #f59e0b;
--cl-error-color: #ef4444;
--sv-error-color: #dc2626;
}
::-webkit-scrollbar-track {
@@ -57,7 +65,7 @@ input::placeholder,
textarea::placeholder,
.cm-placeholder {
@apply text-secondary;
@apply opacity-50 #{!important};
@apply opacity-50;
}
input,
@@ -76,7 +84,7 @@ body {
@apply font-medium;
@apply select-none;
@apply overflow-x-hidden;
@apply leading-body #{!important};
@apply leading-body;
animation: fade 300ms forwards;
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
@@ -174,7 +182,7 @@ a {
@apply font-semibold;
@apply px-2 py-1;
@apply truncate;
@apply leading-body;
@apply leading-normal;
@apply items-center;
kbd {
@@ -221,7 +229,7 @@ a {
@apply overflow-y-auto;
@apply text-body text-secondary;
@apply p-2;
@apply leading-body;
@apply leading-normal;
@apply focus:outline-none;
scroll-behavior: smooth;
@@ -253,7 +261,7 @@ a {
hr {
@apply border-b border-dividerLight;
@apply my-2 #{!important};
@apply my-2;
}
.heading {
@@ -342,28 +350,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 {
color: var(--status-info-color);
color: var(--info-color);
}
.success-response {
color: var(--status-success-color);
color: var(--success-color);
}
.redirect-response {
color: var(--status-redirect-color);
.redir-response {
color: var(--warning-color);
}
.critical-error-response {
color: var(--status-critical-error-color);
.cl-error-response {
color: var(--cl-error-color);
}
.server-error-response {
color: var(--status-server-error-color);
.sv-error-response {
color: var(--sv-error-color);
}
.missing-data-response {
color: var(--status-missing-data-color);
@apply text-secondaryLight;
}
.toasted-container {
@@ -513,12 +537,12 @@ pre.ace_editor {
@apply inline-flex;
@apply font-sans;
@apply text-tiny;
@apply bg-dividerLight;
@apply bg-divider;
@apply rounded;
@apply ml-2;
@apply px-1;
@apply min-w-[1.25rem];
@apply min-h-[1.25rem];
@apply min-w-5;
@apply min-h-5;
@apply items-center;
@apply justify-center;
@apply border border-dividerDark;

View File

@@ -1,89 +1,89 @@
@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");
--accent-color: #10b981;
--accent-light-color: #34d399;
--accent-dark-color: #059669;
--accent-contrast-color: #fff;
--gradient-from-color: #a7f3d0;
--gradient-via-color: #34d399;
--gradient-to-color: #059669;
}
@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");
--accent-color: #14b8a6;
--accent-light-color: #2dd4bf;
--accent-dark-color: #0d9488;
--accent-contrast-color: #fff;
--gradient-from-color: #99f6e4;
--gradient-via-color: #2dd4bf;
--gradient-to-color: #0d9488;
}
@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");
--accent-color: #3b82f6;
--accent-light-color: #60a5fa;
--accent-dark-color: #2563eb;
--accent-contrast-color: #fff;
--gradient-from-color: #bfdbfe;
--gradient-via-color: #60a5fa;
--gradient-to-color: #2563eb;
}
@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");
--accent-color: #6366f1;
--accent-light-color: #818cf8;
--accent-dark-color: #4f46e5;
--accent-contrast-color: #fff;
--gradient-from-color: #c7d2fe;
--gradient-via-color: #818cf8;
--gradient-to-color: #4f46e5;
}
@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");
--accent-color: #8b5cf6;
--accent-light-color: #a78bfa;
--accent-dark-color: #7c3aed;
--accent-contrast-color: #fff;
--gradient-from-color: #ddd6fe;
--gradient-via-color: #a78bfa;
--gradient-to-color: #7c3aed;
}
@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");
--accent-color: #f59e0b;
--accent-light-color: #fbbf24;
--accent-dark-color: #d97706;
--accent-contrast-color: #fff;
--gradient-from-color: #fde68a;
--gradient-via-color: #fbbf24;
--gradient-to-color: #d97706;
}
@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");
--accent-color: #f97316;
--accent-light-color: #fb923c;
--accent-dark-color: #ea580c;
--accent-contrast-color: #fff;
--gradient-from-color: #fed7aa;
--gradient-via-color: #fb923c;
--gradient-to-color: #ea580c;
}
@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");
--accent-color: #ef4444;
--accent-light-color: #f87171;
--accent-dark-color: #dc2626;
--accent-contrast-color: #fff;
--gradient-from-color: #fecaca;
--gradient-via-color: #f87171;
--gradient-to-color: #dc2626;
}
@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");
--accent-color: #ec4899;
--accent-light-color: #f472b6;
--accent-dark-color: #db2777;
--accent-contrast-color: #fff;
--gradient-from-color: #fbcfe8;
--gradient-via-color: #f472b6;
--gradient-to-color: #db2777;
}

View File

@@ -1,16 +1,17 @@
@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.625rem;
--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.75rem;
--upper-mobile-secondary-sticky-fold: 8.813rem;
--upper-mobile-sticky-fold: 10.875rem;
--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;
@@ -19,122 +20,62 @@
--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");
--primary-dark-color: #262626;
--primary-contrast-color: #171717;
--secondary-color: theme("colors.neutral.400");
--secondary-light-color: theme("colors.neutral.500");
--secondary-dark-color: theme("colors.zinc.50");
--secondary-color: #a3a3a3;
--secondary-light-color: #737373;
--secondary-dark-color: #fafafa;
--divider-color: #1f1f1f;
--divider-color: #262626;
--divider-light-color: #1f1f1f;
--divider-dark-color: theme("colors.zinc.800");
--divider-dark-color: #2d2d2d;
--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");
--error-color: #292524;
--tooltip-color: #f5f5f5;
--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 light-theme {
--primary-color: #ffffff;
--primary-light-color: #f9fafb;
--primary-dark-color: #f3f4f6;
--primary-contrast-color: #fdfdfd;
--secondary-color: #6b7280;
--secondary-light-color: #9ca3af;
--secondary-dark-color: #111827;
--divider-color: #f3f4f6;
--divider-light-color: #f3f4f6;
--divider-dark-color: #d1d5db;
--error-color: #fef3c7;
--tooltip-color: #262626;
--popover-color: #ffffff;
--editor-theme: "textmate";
}
@mixin black-theme {
--primary-color: #0f0f0f;
--primary-light-color: theme("colors.neutral.900");
--primary-light-color: #171717;
--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");
--secondary-color: #a3a3a3;
--secondary-light-color: #737373;
--secondary-dark-color: #f5f5f5;
--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");
--divider-color: #1c1c1e;
--divider-light-color: #181818;
--divider-dark-color: #323232;
--error-color: #1c1917;
--tooltip-color: #f5f5f5;
--popover-color: #0f0f0f;
--editor-theme: "twilight";
}

View File

@@ -1,41 +1,41 @@
@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: #a78bfa;
--editor-name-color: #60a5fa;
--editor-operator-color: #818cf8;
--editor-invalid-color: #f87171;
--editor-separator-color: #9ca3af;
--editor-meta-color: #9ca3af;
--editor-variable-color: #34d399;
--editor-link-color: #22d3ee;
--editor-process-color: #e879f9;
--editor-constant-color: #a78bfa;
--editor-keyword-color: #f472b6;
}
@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 light-editor-theme {
--editor-type-color: #7c3aed;
--editor-name-color: #dc2626;
--editor-operator-color: #4f46e5;
--editor-invalid-color: #dc2626;
--editor-separator-color: #4b5563;
--editor-meta-color: #4b5563;
--editor-variable-color: #059669;
--editor-link-color: #0891b2;
--editor-process-color: #2563eb;
--editor-constant-color: #c026d3;
--editor-keyword-color: #db2777;
}
@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");
--editor-type-color: #a78bfa;
--editor-name-color: #e879f9;
--editor-operator-color: #818cf8;
--editor-invalid-color: #f87171;
--editor-separator-color: #9ca3af;
--editor-meta-color: #9ca3af;
--editor-variable-color: #34d399;
--editor-link-color: #22d3ee;
--editor-process-color: #a78bfa;
--editor-constant-color: #60a5fa;
--editor-keyword-color: #f472b6;
}

View File

@@ -1,6 +1,5 @@
{
"action": {
"add": "Add",
"autoscroll": "Autoscroll",
"cancel": "Cancel",
"choose_file": "Choose a file",
@@ -11,7 +10,6 @@
"connect": "Connect",
"connecting": "Connecting",
"copy": "Copy",
"create": "Create",
"delete": "Delete",
"disconnect": "Disconnect",
"dismiss": "Dismiss",
@@ -41,7 +39,6 @@
"scroll_to_top": "Scroll to top",
"search": "Search",
"send": "Send",
"share": "Share",
"start": "Start",
"starting": "Starting",
"stop": "Stop",
@@ -57,30 +54,10 @@
"new": "Add new",
"star": "Add star"
},
"cookies": {
"modal": {
"new_domain_name": "New domain name",
"set": "Set a cookie",
"cookie_string": "Cookie string",
"enter_cookie_string": "Enter cookie string",
"cookie_name": "Name",
"cookie_value": "Value",
"cookie_path": "Path",
"cookie_expires": "Expires",
"managed_tab": "Managed",
"raw_tab": "Raw",
"interceptor_no_support": "Your currently selected interceptor does not support cookies. Select a different Interceptor and try again.",
"empty_domains": "Domain list is empty",
"empty_domain": "Domain is empty",
"no_cookies_in_domain": "No cookies set for this domain"
}
},
"app": {
"chat_with_us": "Chat with us",
"contact_us": "Contact us",
"cookies": "Cookies",
"copy": "Copy",
"copy_interface_type": "Copy interface type",
"copy_user_id": "Copy User Auth Token",
"developer_option": "Developer options",
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
@@ -96,7 +73,6 @@
"keyboard_shortcuts": "Keyboard shortcuts",
"name": "Hoppscotch",
"new_version_found": "New version found. Refresh to update.",
"open_in_hoppscotch": "Open in Hoppscotch",
"options": "Options",
"proxy_privacy_policy": "Proxy privacy policy",
"reload": "Reload",
@@ -143,21 +119,7 @@
"password": "Password",
"token": "Token",
"type": "Authorization Type",
"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"
}
"username": "Username"
},
"collection": {
"created": "Collection created",
@@ -191,7 +153,6 @@
"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_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_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.",
@@ -233,8 +194,7 @@
"profile": "Login to view your profile",
"protocols": "Protocols are empty",
"schema": "Connect to a GraphQL endpoint to view schema",
"shared_requests_logout": "Login to view your shared requests or create a new one",
"shared_requests": "Shared requests are empty",
"shortcodes": "Shortcodes are empty",
"subscription": "Subscriptions are empty",
"team_name": "Team name empty",
"teams": "You don't belong to any teams",
@@ -274,13 +234,9 @@
"variable": "Variable",
"variable_list": "Variable List"
},
"graphql_collections": {
"title": "GraphQL Collections"
},
"error": {
"browser_support_sse": "This browser doesn't seems to have Server Sent Events support.",
"check_console_details": "Check console log for details.",
"check_how_to_add_origin": "Check how you can add an origin",
"curl_invalid_format": "cURL is not formatted properly",
"danger_zone": "Danger zone",
"delete_account": "Your account is currently an owner in these teams:",
@@ -301,7 +257,6 @@
"no_environments_to_export": "No environments to export. Please create an environment to get started.",
"no_results_found": "No matches found",
"page_not_found": "This page could not be found",
"please_install_extension": "Please install the extension and add origin to the extension.",
"proxy_error": "Proxy error",
"script_fail": "Could not execute pre-request script",
"something_went_wrong": "Something went wrong",
@@ -312,8 +267,7 @@
"create_secret_gist": "Create secret Gist",
"gist_created": "Gist created",
"require_github": "Login with GitHub to create secret gist",
"title": "Export",
"failed": "Something went wrong while exporting"
"title": "Export"
},
"filter": {
"all": "All",
@@ -350,8 +304,8 @@
"authorization": "The authorization header will be automatically generated when you send the request.",
"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.",
"offline": "You're using Hoppscotch offline. Updates will sync when you're online, based on workspace settings.",
"offline_short": "You're using Hoppscotch offline.",
"offline": "You seem to be offline. Data in this workspace might not be up to date.",
"offline_short": "You seem to be offline.",
"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.",
"script_fail": "It seems there is a glitch in the pre-request script. Check the error below and fix the script accordingly.",
@@ -380,7 +334,6 @@
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
"from_postman": "Import from Postman",
"from_postman_description": "Import from Postman collection",
"from_file": "Import from File",
"from_url": "Import from URL",
"gist_url": "Enter Gist URL",
"import_from_url_invalid_fetch": "Couldn't get data from the url",
@@ -388,14 +341,7 @@
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
"import_from_url_success": "Collections Imported",
"json_description": "Import collections from a Hoppscotch Collections JSON file",
"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"
"title": "Import"
},
"inspections": {
"description": "Inspect possible errors",
@@ -432,9 +378,7 @@
"close_unsaved_tab": "You have unsaved changes",
"collections": "Collections",
"confirm": "Confirm",
"customize_request": "Customize Request",
"edit_request": "Edit Request",
"share_request": "Share Request",
"import_export": "Import / Export"
},
"mqtt": {
@@ -510,14 +454,14 @@
"structured": "Structured",
"text": "Text"
},
"copy_link": "Copy link",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Duration",
"enter_curl": "Enter cURL command",
"generate_code": "Generate code",
"generated_code": "Generated code",
"go_to_authorization_tab": "Go to Authorization tab",
"go_to_body_tab": "Go to Body tab",
"go_to_authorization_tab": "Go to Authorization",
"header_list": "Header List",
"invalid_name": "Please provide a name for the request",
"method": "Method",
@@ -542,7 +486,6 @@
"saved": "Request saved",
"share": "Share",
"share_description": "Share Hoppscotch with your friends",
"share_request": "Share Request",
"stop": "Stop",
"title": "Request",
"type": "Request type",
@@ -620,34 +563,16 @@
"use_experimental_url_bar": "Use experimental URL bar with environment highlighting",
"user": "User",
"verified_email": "Verified email",
"additional": "Additional Settings",
"verify_email": "Verify email"
},
"shared_requests": {
"button": "Button",
"button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.",
"customize": "Customize",
"creating_widget": "Creating widget",
"copy_html": "Copy HTML",
"copy_link": "Copy Link",
"copy_markdown": "Copy Markdown",
"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"
}
"shortcodes": {
"actions": "Actions",
"created_on": "Created on",
"deleted": "Shortcode deleted",
"method": "Method",
"not_found": "Shortcode not found",
"short_code": "Short code",
"url": "URL"
},
"shortcut": {
"general": {
@@ -677,6 +602,7 @@
"title": "Others"
},
"request": {
"copy_request_link": "Copy Request Link",
"delete_method": "Select DELETE method",
"get_method": "Select GET method",
"head_method": "Select HEAD method",
@@ -692,7 +618,6 @@
"save_to_collections": "Save to Collections",
"send_request": "Send Request",
"show_code": "Generate code snippet",
"share_request": "Share Request",
"title": "Request"
},
"response": {
@@ -817,7 +742,6 @@
"connection_failed": "Connection failed",
"connection_lost": "Connection lost",
"copied_to_clipboard": "Copied to clipboard",
"copied_interface_to_clipboard": "Copied {language} interface type to clipboard",
"deleted": "Deleted",
"deprecated": "DEPRECATED",
"disabled": "Disabled",
@@ -840,7 +764,7 @@
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
"published_message": "Published message: {message} to topic: {topic}",
"reconnection_error": "Failed to reconnect",
"show": "Show",
"show":"Show",
"subscribed_failed": "Failed to subscribe to topic: {topic}",
"subscribed_success": "Successfully subscribed to topic: {topic}",
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
@@ -876,7 +800,6 @@
"queries": "Queries",
"query": "Query",
"schema": "Schema",
"shared_requests": "Shared Requests",
"socketio": "Socket.IO",
"sse": "SSE",
"tests": "Tests",

View File

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

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="156" height="32" fill="none"><rect width="156" height="32" fill="#000" rx="4"/><text xmlns="http://www.w3.org/2000/svg" x="50%" y="50%" fill="#fff" dominant-baseline="central" font-family="Helvetica,sans-serif" font-size="12" font-weight="bold" text-anchor="middle" text-rendering="geometricPrecision">▶ Run in Hoppscotch</text></svg>

Before

Width:  |  Height:  |  Size: 386 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="156" height="32" fill="none"><rect width="156" height="32" fill="#fff" rx="4"/><text xmlns="http://www.w3.org/2000/svg" x="50%" y="50%" fill="#000" dominant-baseline="central" font-family="Helvetica,sans-serif" font-size="12" font-weight="bold" text-anchor="middle" text-rendering="geometricPrecision">▶ Run in Hoppscotch</text></svg>

Before

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

After

Width:  |  Height:  |  Size: 926 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 624 B

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 871 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 360 KiB

After

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 385 KiB

After

Width:  |  Height:  |  Size: 535 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

@@ -1,50 +1 @@
<svg width="824" height="824" viewBox="0 0 824 824" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="824" height="824" rx="184" fill="#08110F"/>
<rect width="824" height="824" rx="184" fill="url(#paint0_radial_0_21)" fill-opacity="0.5"/>
<path d="M435.425 463.217C429.441 476.657 411.033 481.515 394.309 474.07C377.585 466.624 368.879 449.693 374.863 436.253C380.846 422.813 399.254 417.954 415.978 425.4C432.702 432.846 441.409 449.777 435.425 463.217Z" fill="url(#paint1_linear_0_21)"/>
<path d="M435.425 463.217C429.441 476.657 411.033 481.515 394.309 474.07C377.585 466.624 368.879 449.693 374.863 436.253C380.846 422.813 399.254 417.954 415.978 425.4C432.702 432.846 441.409 449.777 435.425 463.217Z" fill="url(#paint2_radial_0_21)" style="mix-blend-mode:soft-light"/>
<path d="M535.563 521.172C553.071 526.191 570.536 518.856 574.571 504.789C578.606 490.722 567.684 475.251 550.175 470.232C532.666 465.213 515.201 472.548 511.166 486.615C507.131 500.682 518.054 516.153 535.563 521.172Z" fill="url(#paint3_linear_0_21)"/>
<path d="M535.563 521.172C553.071 526.191 570.536 518.856 574.571 504.789C578.606 490.722 567.684 475.251 550.175 470.232C532.666 465.213 515.201 472.548 511.166 486.615C507.131 500.682 518.054 516.153 535.563 521.172Z" fill="url(#paint4_radial_0_21)" style="mix-blend-mode:soft-light"/>
<path d="M292.782 355.633C308.227 365.286 314.462 383.173 306.709 395.584C298.955 407.995 280.149 410.231 264.704 400.578C249.258 390.924 243.023 373.037 250.777 360.626C258.53 348.215 277.337 345.98 292.782 355.633Z" fill="url(#paint5_linear_0_21)"/>
<path d="M292.782 355.633C308.227 365.286 314.462 383.173 306.709 395.584C298.955 407.995 280.149 410.231 264.704 400.578C249.258 390.924 243.023 373.037 250.777 360.626C258.53 348.215 277.337 345.98 292.782 355.633Z" fill="url(#paint6_radial_0_21)" style="mix-blend-mode:soft-light"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M502.355 231.325C581.373 266.506 632.095 343.263 634.119 429.03C680.633 465.639 726.858 516.883 705.36 565.168C681.25 619.319 595.382 617.091 497.781 589.689C450.767 615.718 392.444 620.168 339.689 596.68C286.934 573.192 251.229 526.908 239.1 474.517C153.428 420.321 94.3151 357.999 118.425 303.847C139.923 255.562 208.935 255.626 267.265 265.697C332.356 209.81 423.338 196.144 502.355 231.325ZM159.38 322.082C147.667 348.389 210.578 423.052 382.845 499.751C555.111 576.449 652.693 573.241 664.405 546.934C674.099 525.16 634.213 483.308 588.537 450.878C553.009 425.484 504.344 397.494 440.864 369.231C423.586 361.538 416.839 341.008 424.104 324.691C431.369 308.374 447.329 297.463 480.93 295.91C496.747 295.862 498.823 291.476 499.546 287.716C500.442 281.915 492.401 276.002 484.108 272.31C418.17 242.953 337.453 255.265 281.503 314.178C226.84 301.933 169.074 300.309 159.38 322.082Z" fill="url(#paint7_linear_0_21)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M502.355 231.325C581.373 266.506 632.095 343.263 634.119 429.03C680.633 465.639 726.858 516.883 705.36 565.168C681.25 619.319 595.382 617.091 497.781 589.689C450.767 615.718 392.444 620.168 339.689 596.68C286.934 573.192 251.229 526.908 239.1 474.517C153.428 420.321 94.3151 357.999 118.425 303.847C139.923 255.562 208.935 255.626 267.265 265.697C332.356 209.81 423.338 196.144 502.355 231.325ZM159.38 322.082C147.667 348.389 210.578 423.052 382.845 499.751C555.111 576.449 652.693 573.241 664.405 546.934C674.099 525.16 634.213 483.308 588.537 450.878C553.009 425.484 504.344 397.494 440.864 369.231C423.586 361.538 416.839 341.008 424.104 324.691C431.369 308.374 447.329 297.463 480.93 295.91C496.747 295.862 498.823 291.476 499.546 287.716C500.442 281.915 492.401 276.002 484.108 272.31C418.17 242.953 337.453 255.265 281.503 314.178C226.84 301.933 169.074 300.309 159.38 322.082Z" fill="url(#paint8_radial_0_21)" style="mix-blend-mode:soft-light"/>
<defs>
<radialGradient id="paint0_radial_0_21" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(814.524 12.36) rotate(125.613) scale(1089.59 1210.34)">
<stop stop-color="#00D196" stop-opacity="0.5"/>
<stop offset="0.996771" stop-color="#00D196" stop-opacity="0"/>
</radialGradient>
<linearGradient id="paint1_linear_0_21" x1="411.893" y1="212" x2="411.893" y2="612" gradientUnits="userSpaceOnUse">
<stop stop-color="#00D196"/>
<stop offset="1" stop-color="#00B381"/>
</linearGradient>
<radialGradient id="paint2_radial_0_21" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(644.721 344.481) rotate(159.984) scale(631.37 385.135)">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</radialGradient>
<linearGradient id="paint3_linear_0_21" x1="411.893" y1="212" x2="411.893" y2="612" gradientUnits="userSpaceOnUse">
<stop stop-color="#00D196"/>
<stop offset="1" stop-color="#00B381"/>
</linearGradient>
<radialGradient id="paint4_radial_0_21" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(644.721 344.481) rotate(159.984) scale(631.37 385.135)">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</radialGradient>
<linearGradient id="paint5_linear_0_21" x1="411.893" y1="212" x2="411.893" y2="612" gradientUnits="userSpaceOnUse">
<stop stop-color="#00D196"/>
<stop offset="1" stop-color="#00B381"/>
</linearGradient>
<radialGradient id="paint6_radial_0_21" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(644.721 344.481) rotate(159.984) scale(631.37 385.135)">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</radialGradient>
<linearGradient id="paint7_linear_0_21" x1="411.893" y1="212" x2="411.893" y2="612" gradientUnits="userSpaceOnUse">
<stop stop-color="#00D196"/>
<stop offset="1" stop-color="#00B381"/>
</linearGradient>
<radialGradient id="paint8_radial_0_21" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(644.721 344.481) rotate(159.984) scale(631.37 385.135)">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</radialGradient>
</defs>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill="none"><path fill="#10B981" d="M0 0h512v512H0z"/><circle cx="197.76" cy="157.84" r="10" fill="#fff" fill-opacity=".75"/><circle cx="259.76" cy="161.84" r="12" fill="#fff" fill-opacity=".75"/><circle cx="319.76" cy="177.84" r="10" fill="#fff" fill-opacity=".75"/><path d="M344.963 235.676c2.075-12.698-38.872-29.804-90.967-38.094-52.09-8.296-96.404-4.665-98.48 8.033-.257 1.035 0 1.812.263 2.853-1.298-.521-76.714 211.212-76.714 211.212H364.14s-17.621-181.414-20.211-181.414c.515-.772 1.035-1.549 1.035-2.59Z" fill="url(#a)"/><path d="M314.902 227.386c-1.298 8.033-30.839 9.845-66.343 4.402-35.247-5.7-62.982-16.843-61.684-24.618.521-2.59 3.888-4.665 9.331-5.7-18.141.777-30.062 4.145-31.096 9.845-1.555 10.628 34.726 25.139 81.373 32.657 46.647 7.512 85.782 4.665 87.594-5.7 1.041-6.226-9.33-12.961-26.431-19.439 4.923 2.847 7.513 5.957 7.256 8.553Z" fill="#A7F3D0" fill-opacity=".5"/><path d="M333.557 157.413c-3.104-32.137-27.729-59.351-60.9-64.53-33.172-5.186-64.531 12.954-77.749 42.238 21.251 1.298 44.057 3.631 67.904 7.518 25.396 3.888 49.237 9.074 70.745 14.774Z" fill="url(#b)"/><path d="M74.142 158.002c-2.59 15.808 30.319 35.247 81.894 51.055-.257-1.04-.257-1.818-.257-2.853 2.07-12.698 46.127-16.328 98.48-8.032 52.347 8.29 93.037 25.396 90.961 38.094-.257 1.04-.514 1.818-1.035 2.589 53.645.778 90.968-7.512 93.557-23.32 3.625-24.104-74.638-56.498-174.93-72.306-100.555-15.808-185.045-9.331-188.67 14.773Zm115.586-1.298c.778-4.145 4.665-7.255 8.81-6.477 4.145.777 7.256 4.665 6.478 8.81-.52 4.145-4.665 6.998-8.81 6.478-4.145-.778-7.255-4.666-6.478-8.811Zm59.866 4.145c.777-5.7 6.22-9.587 11.92-8.547 5.7.778 9.588 6.215 8.553 11.921-1.041 5.442-6.478 9.33-11.92 8.553-5.706-.778-9.594-6.221-8.553-11.927Zm62.975 15.294c.778-4.145 4.665-7.255 8.81-6.478 4.145.778 7.255 4.666 6.478 8.811-.515 4.145-4.665 7.255-8.81 6.477-4.145-.777-7.256-4.665-6.478-8.81Z" fill="url(#c)"/><defs><radialGradient id="b" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0 32.7063 -69.3245 0 264.232 124.706)"><stop stop-color="#047857"/><stop offset="1" stop-color="#064E3B"/></radialGradient><radialGradient id="c" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(255.837 186.754) scale(1389.61)"><stop stop-color="#047857"/><stop offset=".115" stop-color="#064E3B"/></radialGradient><linearGradient id="a" x1="224.998" y1="157.606" x2="224.998" y2="403.696" gradientUnits="userSpaceOnUse"><stop stop-color="#86EFAC" stop-opacity=".75"/><stop offset=".635" stop-color="#fff" stop-opacity=".2"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient></defs></svg>

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -5,7 +5,7 @@
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
declare module "vue" {
export interface GlobalComponents {
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
@@ -59,9 +59,6 @@ declare module 'vue' {
CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
CookiesAllModal: typeof import('./components/cookies/AllModal.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']
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
@@ -109,7 +106,6 @@ declare module 'vue' {
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
HoppSmartSelectWrapper: typeof import('@hoppscotch/ui')['HoppSmartSelectWrapper']
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
@@ -163,14 +159,6 @@ declare module 'vue' {
IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideUsers: typeof import('~icons/lucide/users')['default']
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
IconLucideX: typeof import('~icons/lucide/x')['default']
ImportExportBase: typeof import('./components/importExport/Base.vue')['default']
ImportExportImportExportList: typeof import('./components/importExport/ImportExportList.vue')['default']
ImportExportImportExportSourcesList: typeof import('./components/importExport/ImportExportSourcesList.vue')['default']
ImportExportImportExportStepsFileImport: typeof import('./components/importExport/ImportExportSteps/FileImport.vue')['default']
ImportExportImportExportStepsMyCollectionImport: typeof import('./components/importExport/ImportExportSteps/MyCollectionImport.vue')['default']
ImportExportImportExportStepsUrlImport: typeof import('./components/importExport/ImportExportSteps/UrlImport.vue')['default']
InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default']
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
@@ -193,16 +181,6 @@ declare module 'vue' {
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
SettingsExtension: typeof import('./components/settings/Extension.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']
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
@@ -223,11 +201,9 @@ declare module 'vue' {
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.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']
SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.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']
SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
SmartTree: typeof import('./../../hoppscotch-ui/src/components/smart/Tree.vue')['default']

View File

@@ -1,61 +1,45 @@
<template>
<div
:role="bannerRole"
class="flex items-center justify-between px-4 py-2 text-tiny text-secondaryDark"
class="flex items-center px-4 py-2 text-tiny"
:class="bannerColor"
>
<div class="flex items-center">
<component :is="bannerIcon" class="mr-2" />
<span :class="{ 'hidden sm:inline-flex': banner.alternateText }">
{{ banner.text(t) }}
<component :is="bannerIcon" class="mr-2 text-white" />
<span class="text-white">
<span v-if="banner.alternateText" class="md:hidden">
{{ banner.alternateText }}
</span>
<span v-if="banner.alternateText" class="inline-flex sm:hidden">
{{ banner.alternateText(t) }}
<span class="<md:hidden">
{{ banner.text }}
</span>
</div>
<icon-lucide-x
v-if="dismissible"
class="opacity-50 hover:cursor-pointer hover:opacity-100"
@click="emit('dismiss')"
/>
</span>
</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 props = defineProps<{
banner: BannerContent
}>()
const ariaRoles: Record<BannerType, string> = {
info: "status",
warning: "status",
error: "alert",
warning: "status",
info: "status",
}
const bgColors: Record<BannerType, string> = {
info: "bg-bannerInfo",
warning: "bg-bannerWarning",
error: "bg-bannerError",
error: "bg-red-700",
warning: "bg-yellow-700",
info: "bg-stone-800",
}
const icons = {

View File

@@ -20,12 +20,6 @@
<AppInterceptor />
</template>
</tippy>
<HoppButtonSecondary
v-if="platform.platformFeatureFlags.cookiesEnabled ?? false"
:label="t('app.cookies')"
:icon="IconCookie"
@click="showCookiesModal = true"
/>
</div>
<div class="flex">
<tippy
@@ -201,17 +195,12 @@
:show="showDeveloperOptions"
@hide-modal="showDeveloperOptions = false"
/>
<CookiesAllModal
:show="showCookiesModal"
@hide-modal="showCookiesModal = false"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue"
import { version } from "~/../package.json"
import IconCookie from "~icons/lucide/cookie"
import IconSidebar from "~icons/lucide/sidebar"
import IconZap from "~icons/lucide/zap"
import IconShare2 from "~icons/lucide/share-2"
@@ -234,9 +223,7 @@ import { invokeAction } from "@helpers/actions"
import { HoppSmartItem } from "@hoppscotch/ui"
const t = useI18n()
const showDeveloperOptions = ref(false)
const showCookiesModal = ref(false)
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
const SIDEBAR = useSetting("SIDEBAR")

View File

@@ -1,28 +1,24 @@
<template>
<div>
<header
ref="headerRef"
class="grid grid-cols-5 grid-rows-1 gap-2 overflow-x-auto overflow-y-hidden p-2"
@mousedown.prevent="platform.ui?.appHeader?.onHeaderAreaClick?.()"
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="col-span-2 flex items-center justify-between space-x-2"
class="inline-flex flex-1 items-center justify-start space-x-2"
:style="{
paddingTop: platform.ui?.appHeader?.paddingTop?.value,
paddingLeft: platform.ui?.appHeader?.paddingLeft?.value,
}"
>
<div class="flex">
<HoppButtonSecondary
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
:label="t('app.name')"
to="/"
/>
</div>
<HoppButtonSecondary
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
:label="t('app.name')"
to="/"
/>
</div>
<div class="col-span-1 flex items-center justify-between space-x-2">
<div class="inline-flex flex-1 items-center justify-center space-x-2">
<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 max-w-[15rem] flex-1 cursor-text items-center justify-between self-stretch rounded border border-dividerDark bg-primaryDark px-2 py-1 text-secondaryLight transition 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')"
>
<span class="inline-flex flex-1 items-center">
@@ -34,189 +30,192 @@
<kbd class="shortcut-key">K</kbd>
</span>
</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 class="col-span-2 flex items-center justify-between space-x-2">
<div class="flex">
<div class="inline-flex flex-1 items-center justify-end space-x-2">
<div
v-if="currentUser === null"
class="inline-flex items-center space-x-2"
>
<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()"
:icon="IconUploadCloud"
:label="t('header.save_workspace')"
class="py-1.75 !focus-visible:text-green-600 !hover:text-green-600 hidden border border-green-600/25 bg-green-500/[.15] !text-green-500 hover:border-green-800/50 hover:bg-green-400/10 focus-visible:border-green-800/50 focus-visible:bg-green-400/10 md:flex"
@click="invokeAction('modals.login.toggle')"
/>
<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')"
<HoppButtonPrimary
:label="t('header.login')"
@click="invokeAction('modals.login.toggle')"
/>
</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
v-if="currentUser === null"
class="inline-flex items-center space-x-2"
class="flex divide-x divide-green-600/25 rounded border border-green-600/25 bg-green-500/[.15] focus-within:divide-green-800/50 focus-within:border-green-800/50 focus-within:bg-green-400/10 hover:divide-green-800/50 hover:border-green-800/50 hover:bg-green-400/10"
>
<HoppButtonSecondary
:icon="IconUploadCloud"
:label="t('header.save_workspace')"
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"
@click="invokeAction('modals.login.toggle')"
v-tippy="{ theme: 'tooltip' }"
:title="t('team.invite_tooltip')"
:icon="IconUserPlus"
class="py-1.75 !focus-visible:text-green-600 !hover:text-green-600 !text-green-500"
@click="handleInvite()"
/>
<HoppButtonPrimary
: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
<HoppButtonSecondary
v-if="
workspace.type === 'team' &&
selectedTeam &&
selectedTeam.teamMembers.length > 1
selectedTeam?.myRole === 'OWNER'
"
:team-members="selectedTeam.teamMembers"
show-count
class="mx-2"
@handle-click="handleTeamEdit()"
v-tippy="{ theme: 'tooltip' }"
:title="t('team.edit')"
:icon="IconSettings"
class="py-1.75 !focus-visible:text-green-600 !hover:text-green-600 !text-green-500"
@click="handleTeamEdit()"
/>
<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"
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('team.invite_tooltip')"
:icon="IconUserPlus"
class="!focus-visible:text-emerald-600 !hover:text-emerald-600 !text-emerald-500"
@click="handleInvite()"
/>
<HoppButtonSecondary
v-if="
workspace.type === 'team' &&
selectedTeam &&
selectedTeam?.myRole === 'OWNER'
"
v-tippy="{ theme: 'tooltip' }"
:title="t('team.edit')"
:icon="IconSettings"
class="!focus-visible:text-emerald-600 !hover:text-emerald-600 !text-emerald-500"
@click="handleTeamEdit()"
/>
</div>
</div>
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => accountActions.focus()"
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('workspace.change')"
:label="mdAndLarger ? workspaceName : ``"
:icon="workspace.type === 'personal' ? IconUser : IconUsers"
class="select-wrapper !focus-visible:text-blue-600 !hover:text-blue-600 rounded border border-blue-600/25 bg-blue-500/[.15] py-[0.4375rem] pr-8 !text-blue-500 hover:border-blue-800/50 hover:bg-blue-400/10 focus-visible:border-blue-800/50 focus-visible:bg-blue-400/10"
/>
<template #content="{ hide }">
<div
ref="accountActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
@click="hide()"
>
<WorkspaceSelector />
</div>
</template>
</tippy>
<span class="px-2">
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => accountActions.focus()"
:on-shown="() => tippyActions.focus()"
>
<HoppSmartSelectWrapper
class="!text-blue-500 !focus-visible:text-blue-600 !hover:text-blue-600"
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('workspace.change')"
:label="mdAndLarger ? workspaceName : ``"
:icon="workspace.type === 'personal' ? IconUser : IconUsers"
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"
/>
</HoppSmartSelectWrapper>
<HoppSmartPicture
v-if="currentUser.photoURL"
v-tippy="{
theme: 'tooltip',
}"
:url="currentUser.photoURL"
:alt="
currentUser.displayName ||
t('profile.default_hopp_displayname')
"
: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 }">
<div
ref="accountActions"
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()"
@click="hide()"
>
<WorkspaceSelector />
<div class="flex flex-col px-2 text-tiny">
<span class="inline-flex truncate font-semibold">
{{
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>
</template>
</tippy>
<span class="px-2">
<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>
</span>
</div>
</div>
</header>
<AppBanner
v-if="bannerContent"
:banner="bannerContent"
:dismissible="true"
@dismiss="dismissOfflineBanner"
/>
<AppBanner v-if="banner" :banner="banner" />
<TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" />
<TeamsInvite
v-if="workspace.type === 'team' && workspace.teamID"
@@ -232,6 +231,7 @@
@invite-team="inviteTeam(editingTeamName, editingTeamID)"
@refetch-teams="refetchTeams"
/>
<HoppSmartConfirmModal
:show="confirmRemove"
:title="t('confirm.remove_team')"
@@ -264,11 +264,7 @@ import IconUsers from "~icons/lucide/users"
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
import {
BannerService,
BannerContent,
BANNER_PRIORITY_HIGH,
} from "~/services/banner.service"
import { BannerService } from "~/services/banner.service"
const t = useI18n()
const toast = useToast()
@@ -286,33 +282,21 @@ const showTeamsModal = ref(false)
const breakpoints = useBreakpoints(breakpointsTailwind)
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 { content: banner } = useService(BannerService)
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)
watch(network, () => {
if (network.isOnline) {
banner.value = null
return
}
if (banner.content && bannerID) {
banner.removeBanner(bannerID)
banner.value = {
type: "info",
text: t("helpers.offline"),
alternateText: t("helpers.offline_short"),
}
})
const dismissOfflineBanner = () => banner.removeBanner(bannerID!)
const currentUser = useReadonlyStream(
platform.auth.getProbableUserStream(),
platform.auth.getProbableUser()

View File

@@ -92,8 +92,9 @@ const getHighestSeverity = computed(() => {
},
{ severity: 0 }
)
} else {
return { severity: 0 }
}
return { severity: 0 }
})
const severityColor = (severity: number) => {

View File

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

View File

@@ -9,8 +9,8 @@
>
<component
:is="entry.icon"
class="svg-icons opacity-80"
:class="{ 'opacity-25': active }"
class="svg-icons opacity-50"
:class="{ 'opacity-100': active }"
/>
<template
v-if="entry.text.type === 'text' && typeof entry.text.text === 'string'"
@@ -82,9 +82,9 @@ const props = defineProps<{
const formattedShortcutKeys = computed(
() =>
props.entry.meta?.keyboardShortcut?.map(
(key) => SPECIAL_KEY_CHARS[key] ?? capitalize(key)
)
props.entry.meta?.keyboardShortcut?.map((key) => {
return SPECIAL_KEY_CHARS[key] ?? capitalize(key)
})
)
const emit = defineEmits<{

View File

@@ -16,7 +16,7 @@
autocomplete="off"
name="command"
: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 bg-transparent px-6 py-5 text-base text-secondaryDark"
/>
<HoppSmartSpinner v-if="searchSession?.loading" class="mr-6" />
</div>
@@ -112,7 +112,6 @@ import {
WorkspaceSpotlightSearcherService,
} from "~/services/spotlight/searchers/workspace.searcher"
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
import { platform } from "~/platform"
const t = useI18n()
@@ -142,10 +141,6 @@ useService(WorkspaceSpotlightSearcherService)
useService(SwitchWorkspaceSpotlightSearcherService)
useService(InterceptorSpotlightSearcherService)
platform.spotlight?.additionalSearchers?.forEach((searcher) =>
useService(searcher)
)
const search = ref("")
const searchSession = ref<SpotlightSearchState>()

View File

@@ -14,14 +14,14 @@
></div>
<div class="relative flex flex-col">
<div
class="z-[1] pointer-events-none absolute inset-0 bg-accent opacity-0 transition"
class="z-1 pointer-events-none absolute inset-0 bg-accent opacity-0 transition"
:class="{
'opacity-25':
dragging && notSameDestination && notSameParentDestination,
}"
></div>
<div
class="z-[3] group pointer-events-auto relative flex cursor-pointer items-stretch"
class="z-3 group pointer-events-auto relative flex cursor-pointer items-stretch"
:draggable="!hasNoTeamAccess"
@dragstart="dragStart"
@drop="handelDrop($event)"
@@ -290,13 +290,13 @@ const collectionIcon = computed(() => {
if (props.isSelected) return IconCheckCircle
else if (!props.isOpen) return IconFolder
else if (props.isOpen) return IconFolderOpen
return IconFolder
else return IconFolder
})
const collectionName = computed(() => {
if ((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(
@@ -424,8 +424,9 @@ const isCollLoading = computed(() => {
props.data.id
) {
return collectionMoveLoading.includes(props.data.id)
} else {
return false
}
return false
})
const resetDragState = () => {

View File

@@ -1,568 +1,361 @@
<template>
<ImportExportBase
ref="collections-import-export"
modal-title="modal.collections"
:importer-modules="importerModules"
:exporter-modules="exporterModules"
@hide-modal="emit('hide-modal')"
/>
<HoppSmartModal
v-if="show"
dialog
:title="t('modal.collections')"
styles="sm:max-w-md"
@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="mr-4 inline-flex flex-shrink-0 items-center justify-center rounded-full border-4 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="ml-10 flex flex-col rounded border border-dashed border-dividerDark"
>
<input
id="inputChooseFileToImportFrom"
ref="inputChooseFileToImportFrom"
name="inputChooseFileToImportFrom"
type="file"
class="cursor-pointer p-4 text-secondary transition file:mr-2 file:cursor-pointer file:rounded file:border-0 file:bg-primaryLight file:px-4 file:py-2 file:text-secondary file:transition hover:text-secondaryDark hover:file:bg-primaryDark hover:file:text-secondaryDark"
: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="mr-4 inline-flex flex-shrink-0 items-center justify-center rounded-full border-4 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="ml-10 flex flex-col">
<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>
<script setup lang="ts">
import * as E from "fp-ts/Either"
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
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 IconArrowLeft from "~icons/lucide/arrow-left"
import IconDownload from "~icons/lucide/download"
import IconGithub from "~icons/lucide/github"
import IconLink from "~icons/lucide/link"
import IconUser from "~icons/lucide/user"
import { useReadonlyStream } from "~/composables/stream"
import { getTeamCollectionJSON } from "~/helpers/backend/helpers"
import { computed, PropType, ref, watch } from "vue"
import { pipe } from "fp-ts/function"
import * as E from "fp-ts/Either"
import { HoppRESTRequest, HoppCollection } from "@hoppscotch/data"
import { useI18n } from "@composables/i18n"
import { useReadonlyStream } from "@composables/stream"
import { useToast } from "@composables/toast"
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 t = useI18n()
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
type CollectionType =
| {
type: "team-collections"
selectedTeam: SelectedTeam
}
| { type: "my-collections" }
type CollectionType = "team-collections" | "my-collections"
const props = defineProps({
collectionsType: {
type: Object as PropType<CollectionType>,
default: () => ({
type: "my-collections",
selectedTeam: undefined,
}),
show: {
type: Boolean,
default: false,
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(
platform.auth.getCurrentUserStream(),
platform.auth.getCurrentUser()
)
const showImportFailedError = () => {
toast.error(t("import.failed"))
}
const importerAction = async (stepResults: StepReturnValue[]) => {
if (!importerModule.value) return
const handleImportToStore = async (
collections: HoppCollection<HoppRESTRequest>[]
) => {
const importResult =
props.collectionsType.type === "my-collections"
? await importToPersonalWorkspace(collections)
: await importToTeamsWorkspace(collections)
pipe(
await importerModule.value.importer(stepResults as any)(),
E.match(
(err) => {
failedImport()
console.error("error", err)
},
(result) => {
if (props.collectionsType === "team-collections") {
emit("import-to-teams", result)
} else {
appendRESTCollections(result)
if (E.isRight(importResult)) {
toast.success(t("state.file_imported"))
emit("hide-modal")
} else {
toast.error(t("import.failed"))
}
}
platform.analytics?.logEvent({
type: "HOPP_IMPORT_COLLECTION",
importer: importerModule.value!.name,
platform: "rest",
workspaceType: "personal",
})
const importToPersonalWorkspace = (
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"))
fileImported()
}
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 = {
metadata: {
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 finishImport = async () => {
await importerAction(stepResults.value)
}
const collectionJSON = await getCollectionJSON()
const accessToken = currentUser.value?.accessToken
const onFileChange = () => {
stepResults.value = []
if (!accessToken) {
toast.error(t("error.something_went_wrong"))
isHoppGistCollectionExporterInProgress.value = false
const inputFileToImport = inputChooseFileToImportFrom.value[0]
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
}
if (E.isRight(collectionJSON)) {
collectionsGistExporter(collectionJSON.right, accessToken)
stepResults.value.push(content)
hasFile.value = !!content?.length
}
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "gist",
platform: "rest",
})
}
isHoppGistCollectionExporterInProgress.value = false
},
reader.readAsText(inputFileToImport.files[0])
}
const importerModules = computed(() => {
const enabledImporters = [
HoppRESTImporter,
HoppMyCollectionImporter,
HoppOpenAPIImporter,
HoppPostmanImporter,
HoppInsomniaImporter,
HoppGistImporter,
]
const fileImported = () => {
toast.success(t("state.file_imported").toString())
hideModal()
}
const failedImport = () => {
toast.error(t("import.failed").toString())
}
const hideModal = () => {
resetImport()
emit("hide-modal")
}
const isTeams = props.collectionsType.type === "team-collections"
return enabledImporters.filter((importer) => {
return isTeams
? importer.metadata.applicableTo.includes("team-workspace")
: importer.metadata.applicableTo.includes("personal-workspace")
})
})
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")
const resetImport = () => {
importerType.value = null
hasFile.value = false
hasGist.value = false
stepResults.value = []
inputChooseFileToImportFrom.value = ""
inputChooseGistToImportFrom.value = ""
mySelectedCollectionID.value = undefined
}
</script>

View File

@@ -222,12 +222,6 @@
requestIndex: pathToIndex(node.id),
})
"
@share-request="
node.data.type === 'requests' &&
emit('share-request', {
request: node.data.data.data,
})
"
@drag-request="
dragRequest($event, {
folderPath: node.data.data.parentIndex,
@@ -466,12 +460,6 @@ const emit = defineEmits<{
isActive: boolean
}
): void
(
event: "share-request",
payload: {
request: HoppRESTRequest
}
): void
(
event: "drop-request",
payload: {
@@ -538,12 +526,13 @@ const isSelected = ({
props.picked.folderPath === folderPath &&
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)
@@ -740,10 +729,11 @@ class MyCollectionsAdapter implements SmartTreeAdapter<MyCollectionNode> {
status: "loaded",
data: data,
} as ChildrenResult<Folder | Requests>
}
return {
status: "loaded",
data: [],
} else {
return {
status: "loaded",
data: [],
}
}
})
}

View File

@@ -28,7 +28,8 @@
>
<span
class="pointer-events-none flex w-16 items-center justify-center truncate px-2"
:style="{ color: getMethodLabelColorClassOf(request) }"
:class="requestLabelColor"
:style="{ color: requestLabelColor }"
>
<component
:is="IconCheckCircle"
@@ -93,7 +94,6 @@
@keyup.e="edit?.$el.click()"
@keyup.d="duplicate?.$el.click()"
@keyup.delete="deleteAction?.$el.click()"
@keyup.s="shareAction?.$el.click()"
@keyup.escape="hide()"
>
<HoppSmartItem
@@ -133,18 +133,6 @@
}
"
/>
<HoppSmartItem
ref="shareAction"
:icon="IconShare2"
:label="t('action.share')"
:shortcut="['S']"
@click="
() => {
emit('share-request')
hide()
}
"
/>
</div>
</template>
</tippy>
@@ -174,7 +162,6 @@ import IconEdit from "~icons/lucide/edit"
import IconCopy from "~icons/lucide/copy"
import IconTrash2 from "~icons/lucide/trash-2"
import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconShare2 from "~icons/lucide/share-2"
import { ref, PropType, watch, computed } from "vue"
import { HoppRESTRequest } from "@hoppscotch/data"
import { useI18n } from "@composables/i18n"
@@ -253,7 +240,6 @@ const emit = defineEmits<{
(event: "duplicate-request"): void
(event: "remove-request"): void
(event: "select-request"): void
(event: "share-request"): void
(event: "drag-request", payload: DataTransfer): void
(event: "update-request-order", payload: DataTransfer): void
(event: "update-last-request-order", payload: DataTransfer): void
@@ -264,7 +250,6 @@ const edit = ref<HTMLButtonElement | null>(null)
const deleteAction = ref<HTMLButtonElement | null>(null)
const options = ref<TippyComponent | null>(null)
const duplicate = ref<HTMLButtonElement | null>(null)
const shareAction = ref<HTMLButtonElement | null>(null)
const dragging = ref(false)
const ordering = ref(false)
@@ -276,6 +261,10 @@ const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
parentID: "",
})
const requestLabelColor = computed(() =>
getMethodLabelColorClassOf(props.request)
)
watch(
() => props.duplicateLoading,
(val) => {
@@ -374,8 +363,9 @@ const updateLastItemOrder = (e: DragEvent) => {
const isRequestLoading = computed(() => {
if (props.requestMoveLoading.length > 0 && props.requestID) {
return props.requestMoveLoading.includes(props.requestID)
} else {
return false
}
return false
})
const resetDragState = () => {

View File

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

View File

@@ -15,12 +15,12 @@
class="!rounded-none"
:icon="IconPlus"
:title="t('team.no_access')"
:label="t('action.new')"
:label="t('add.new')"
/>
<HoppButtonSecondary
v-else
:icon="IconPlus"
:label="t('action.new')"
:label="t('add.new')"
class="!rounded-none"
@click="emit('display-modal-add')"
/>
@@ -240,12 +240,6 @@
requestIndex: node.data.data.data.id,
})
"
@share-request="
node.data.type === 'requests' &&
emit('share-request', {
request: node.data.data.data.request,
})
"
@drag-request="
dragRequest($event, {
folderPath: node.data.data.parentIndex,
@@ -479,12 +473,6 @@ const emit = defineEmits<{
folderPath?: string | undefined
}
): void
(
event: "share-request",
payload: {
request: HoppRESTRequest
}
): void
(
event: "drop-request",
payload: {
@@ -554,12 +542,13 @@ const isSelected = ({
props.picked.pickedType === "teams-request" &&
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)
@@ -725,77 +714,81 @@ class TeamCollectionsAdapter implements SmartTreeAdapter<TeamCollectionNode> {
return {
status: "loading",
}
}
const data = this.data.value.map((item, index) => ({
id: item.id,
data: {
isLastItem: index === this.data.value.length - 1,
type: "collections",
} else {
const data = this.data.value.map((item, index) => ({
id: item.id,
data: {
parentIndex: null,
data: item,
isLastItem: index === this.data.value.length - 1,
type: "collections",
data: {
parentIndex: null,
data: item,
},
},
},
}))
return {
status: "loaded",
data: cloneDeep(data),
} as ChildrenResult<TeamCollections>
}
const parsedID = id.split("/")[id.split("/").length - 1]
!props.teamLoadingCollections.includes(parsedID) &&
emit("expand-team-collection", parsedID)
if (props.teamLoadingCollections.includes(parsedID)) {
return {
status: "loading",
}))
return {
status: "loaded",
data: cloneDeep(data),
} as ChildrenResult<TeamCollections>
}
} else {
const parsedID = id.split("/")[id.split("/").length - 1]
!props.teamLoadingCollections.includes(parsedID) &&
emit("expand-team-collection", parsedID)
if (props.teamLoadingCollections.includes(parsedID)) {
return {
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

@@ -271,7 +271,7 @@ const collectionIcon = computed(() => {
if (isSelected.value) return IconCheckCircle
else if (!showChildren.value && !props.isFiltered) return IconFolder
else if (!showChildren.value || props.isFiltered) return IconFolderOpen
return IconFolder
else return IconFolder
})
const pick = () => {

View File

@@ -253,7 +253,7 @@ const collectionIcon = computed(() => {
if (isSelected.value) return IconCheckCircle
else if (!showChildren.value && !props.isFiltered) return IconFolder
else if (showChildren.value || !props.isFiltered) return IconFolderOpen
return IconFolder
else return IconFolder
})
const pick = () => {

View File

@@ -1,227 +1,291 @@
<template>
<ImportExportBase
ref="collections-import-export"
modal-title="graphql_collections.title"
:importer-modules="importerModules"
:exporter-modules="exporterModules"
@hide-modal="emit('hide-modal')"
/>
<HoppSmartModal
v-if="show"
dialog
:title="`${t('modal.collections')}`"
styles="sm:max-w-md"
@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>
<script setup lang="ts">
import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast"
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 axios from "axios"
import IconMoreVertical from "~icons/lucide/more-vertical"
import IconFolderPlus from "~icons/lucide/folder-plus"
import IconUser from "~icons/lucide/user"
import { initializeDownloadCollection } from "~/helpers/import-export/export"
import { useReadonlyStream } from "~/composables/stream"
import IconDownload from "~icons/lucide/download"
import IconGithub from "~icons/lucide/github"
import { computed, ref } from "vue"
import { platform } from "~/platform"
import { useI18n } from "@composables/i18n"
import { useReadonlyStream } from "@composables/stream"
import { useToast } from "@composables/toast"
import {
graphqlCollections$,
setGraphqlCollections,
appendGraphqlCollections,
} 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 t = useI18n()
const collections = useReadonlyStream(graphqlCollections$, [])
const currentUser = useReadonlyStream(
platform.auth.getCurrentUserStream(),
platform.auth.getCurrentUser()
)
const GqlCollectionsHoppImporter: ImporterOrExporter = {
metadata: {
id: "import.from_json",
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)
// Template refs
const tippyActions = ref<any | null>(null)
const inputChooseFileToImportFrom = ref<HTMLInputElement>()
if (E.isLeft(res)) {
showImportFailedError()
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 collectionJson = computed(() => {
return JSON.stringify(collections.value, null, 2)
})
const showImportFailedError = () => {
toast.error(t("import.failed"))
const createCollectionGist = async () => {
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 (
gqlCollections: HoppCollection<HoppGQLRequest>[]
) => {
setGraphqlCollections(gqlCollections)
toast.success(t("import.success"))
const fileImported = () => {
toast.success(t("state.file_imported").toString())
}
const emit = defineEmits<{
(e: "hide-modal"): () => void
}>()
const failedImport = () => {
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 = () => {
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 a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
platform?.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "json",
platform: "gql",
})
// TODO: get uri from meta
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
document.body.appendChild(a)
a.click()
toast.success(t("state.download_started").toString())
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
}
</script>

View File

@@ -137,7 +137,7 @@
@hide-modal="displayModalEditRequest(false)"
/>
<CollectionsGraphqlImportExport
v-if="showModalImportExport"
:show="showModalImportExport"
@hide-modal="displayModalImportExport(false)"
/>
</div>

View File

@@ -11,7 +11,7 @@
@dragend="draggingToRoot = false"
>
<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-shrink-0 flex-col overflow-x-auto border-b border-dividerLight bg-primary"
:class="{ 'rounded-t': saveRequest }"
:style="
saveRequest ? 'top: calc(-1 * var(--line-height-body))' : 'top: 0'
@@ -22,7 +22,7 @@
v-model="filterTexts"
type="search"
autocomplete="off"
class="flex w-full bg-transparent px-4 py-2"
class="flex h-8 w-full bg-transparent p-4 py-2"
:placeholder="t('action.search')"
:disabled="collectionsType.type === 'team-collections'"
/>
@@ -41,7 +41,6 @@
@export-data="exportData"
@remove-collection="removeCollection"
@remove-folder="removeFolder"
@share-request="shareRequest"
@drop-collection="dropCollection"
@update-request-order="updateRequestOrder"
@update-collection-order="updateCollectionOrder"
@@ -72,7 +71,6 @@
@export-data="exportData"
@remove-collection="removeCollection"
@remove-folder="removeFolder"
@share-request="shareRequest"
@edit-request="editRequest"
@duplicate-request="duplicateRequest"
@remove-request="removeRequest"
@@ -140,13 +138,17 @@
@hide-modal="showConfirmModal = false"
@resolve="resolveConfirmModal"
/>
<CollectionsImportExport
v-if="showModalImportExport"
:collections-type="collectionsType"
:show="showModalImportExport"
: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)"
/>
<TeamsAdd
:show="showTeamModalAdd"
@hide-modal="displayTeamModalAdd(false)"
@@ -195,6 +197,7 @@ import {
createChildCollection,
renameCollection,
deleteCollection,
importJSONToTeam,
moveRESTTeamCollection,
updateOrderRESTTeamCollection,
} from "~/helpers/backend/mutations/TeamCollection"
@@ -209,9 +212,12 @@ import { TeamCollection } from "~/helpers/teams/TeamCollection"
import { Collection as NodeCollection } from "./MyCollections.vue"
import {
getCompleteCollectionTree,
getTeamCollectionJSON,
teamCollToHoppRESTColl,
} from "~/helpers/backend/helpers"
import * as E from "fp-ts/Either"
import { platform } from "~/platform"
import { createCollectionGists } from "~/helpers/gist"
import {
getRequestsByPath,
resolveSaveContextOnRequestReorder,
@@ -223,7 +229,7 @@ import {
resetTeamRequestsContext,
} from "~/helpers/collection/collection"
import { currentReorderingStatus$ } from "~/newstore/reordering"
import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { defineActionHandler } from "~/helpers/actions"
import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
@@ -297,6 +303,12 @@ const draggingToRoot = ref(false)
const collectionMoveLoading = 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
const workspaceService = useService(WorkspaceService)
const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
@@ -400,12 +412,14 @@ const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
})
const hasTeamWriteAccess = computed(() => {
if (collectionsType.value.type !== "team-collections") {
return false
}
if (!collectionsType.value.selectedTeam) return false
const role = collectionsType.value.selectedTeam?.myRole
return role === "OWNER" || role === "EDITOR"
if (
collectionsType.value.type === "team-collections" &&
collectionsType.value.selectedTeam.myRole !== "VIEWER"
)
return true
else return false
})
const filteredCollections = computed(() => {
@@ -1055,7 +1069,7 @@ const onRemoveCollection = () => {
const collectionIndex = editingCollectionIndex.value
const collectionToRemove =
collectionIndex || collectionIndex === 0
collectionIndex || collectionIndex == 0
? navigateToFolderWithIndexPath(restCollectionStore.value.state, [
collectionIndex,
])
@@ -1454,8 +1468,9 @@ const checkIfCollectionIsAParentOfTheChildren = (
)
if (isEqual(slicedDestinationCollectionPath, collectionDraggedPath)) {
return true
} else {
return false
}
return false
}
return false
@@ -1476,8 +1491,9 @@ const isMoveToSameLocation = (
if (isEqual(draggedItemParentPathArr, destinationPathArr)) {
return true
} else {
return false
}
return false
}
}
@@ -1657,22 +1673,25 @@ const isSameSameParent = (
const dragedItemParent = draggedItemIndex.slice(0, -1)
return dragedItemParent.join("/") === destinationCollectionIndex
}
if (destinationItemPath === null) return false
const destinationItemIndex = pathToIndex(destinationItemPath)
} else {
if (destinationItemPath === null) return false
const destinationItemIndex = pathToIndex(destinationItemPath)
// length of 1 means the request is in the root
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)) {
// length of 1 means the request is in the root
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
} else {
return false
}
} else {
return false
}
return false
}
return false
}
/**
@@ -1814,31 +1833,61 @@ const updateCollectionOrder = (payload: {
}
}
// 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.
* @param collectionJSON - JSON string of the collection
* @param name - Name of the collection set as the file name
*/
const initializeDownloadCollection = async (
const initializeDownloadCollection = (
collectionJSON: string,
name: string | null
) => {
const result = await platform.io.saveFileWithDialog({
data: collectionJSON,
contentType: "application/json",
suggestedFilename: `${name ?? "collection"}.json`,
filters: [
{
name: "Hoppscotch Collection JSON file",
extensions: ["json"],
},
],
})
const file = new Blob([collectionJSON], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
if (result.type === "unknown" || result.type === "saved") {
toast.success(t("state.download_started").toString())
if (name) {
a.download = `${name}.json`
} else {
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
}
document.body.appendChild(a)
a.click()
toast.success(t("state.download_started").toString())
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
}
/**
@@ -1867,14 +1916,11 @@ const exportData = async (
exportLoading.value = false
return
},
async (coll) => {
(coll) => {
const hoppColl = teamCollToHoppRESTColl(coll)
const collectionJSONString = JSON.stringify(hoppColl)
await initializeDownloadCollection(
collectionJSONString,
hoppColl.name
)
initializeDownloadCollection(collectionJSONString, hoppColl.name)
exportLoading.value = false
}
)
@@ -1882,15 +1928,88 @@ const exportData = async (
}
}
const shareRequest = ({ request }: { request: HoppRESTRequest }) => {
if (currentUser.value) {
// opens the share request modal
invokeAction("share.request", {
request,
})
} else {
invokeAction("modals.login.toggle")
const exportJSONCollection = async () => {
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "json",
platform: "rest",
})
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) => {
@@ -1922,36 +2041,37 @@ const getErrorMessage = (err: GQLError<string>) => {
console.error(err)
if (err.type === "network_error") {
return t("error.network_error")
}
switch (err.error) {
case "team_coll/short_title":
return t("collection.name_length_insufficient")
case "team/invalid_coll_id":
case "bug/team_coll/no_coll_id":
case "team_req/invalid_target_id":
return t("team.invalid_coll_id")
case "team/not_required_role":
return t("profile.no_permission")
case "team_req/not_required_role":
return t("profile.no_permission")
case "Forbidden resource":
return t("profile.no_permission")
case "team_req/not_found":
return t("team.no_request_found")
case "bug/team_req/no_req_id":
return t("team.no_request_found")
case "team/collection_is_parent_coll":
return t("team.parent_coll_move")
case "team/target_and_destination_collection_are_same":
return t("team.same_target_destination")
case "team/target_collection_is_already_root_collection":
return t("collection.invalid_root_move")
case "team_req/requests_not_from_same_collection":
return t("request.different_collection")
case "team/team_collections_have_different_parents":
return t("collection.different_parent")
default:
return t("error.something_went_wrong")
} else {
switch (err.error) {
case "team_coll/short_title":
return t("collection.name_length_insufficient")
case "team/invalid_coll_id":
case "bug/team_coll/no_coll_id":
case "team_req/invalid_target_id":
return t("team.invalid_coll_id")
case "team/not_required_role":
return t("profile.no_permission")
case "team_req/not_required_role":
return t("profile.no_permission")
case "Forbidden resource":
return t("profile.no_permission")
case "team_req/not_found":
return t("team.no_request_found")
case "bug/team_req/no_req_id":
return t("team.no_request_found")
case "team/collection_is_parent_coll":
return t("team.parent_coll_move")
case "team/target_and_destination_collection_are_same":
return t("team.same_target_destination")
case "team/target_collection_is_already_root_collection":
return t("collection.invalid_root_move")
case "team_req/requests_not_from_same_collection":
return t("request.different_collection")
case "team/team_collections_have_different_parents":
return t("collection.different_parent")
default:
return t("error.something_went_wrong")
}
}
}

View File

@@ -1,269 +0,0 @@
<template>
<HoppSmartModal
v-if="show"
dialog
:title="t('app.cookies')"
aria-modal="true"
@close="hideModal"
>
<template #body>
<HoppSmartPlaceholder
v-if="!currentInterceptorSupportsCookies"
:text="t('cookies.modal.interceptor_no_support')"
>
<AppInterceptor class="rounded border border-dividerLight p-2" />
</HoppSmartPlaceholder>
<div v-else class="flex flex-col">
<div
class="sticky -mx-4 -mt-4 flex space-x-2 border-b border-dividerLight bg-primary px-4 py-4"
style="top: calc(-1 * var(--line-height-body))"
>
<HoppSmartInput
v-model="newDomainText"
class="flex-1"
:placeholder="t('cookies.modal.new_domain_name')"
@keyup.enter="addNewDomain"
/>
<HoppButtonSecondary
outline
filled
:label="t('action.add')"
@click="addNewDomain"
/>
</div>
<div class="flex flex-col space-y-4">
<HoppSmartPlaceholder
v-if="workingCookieJar.size === 0"
:src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('cookies.modal.empty_domains')}`"
:text="t('cookies.modal.empty_domains')"
class="mt-6"
>
</HoppSmartPlaceholder>
<div
v-for="[domain, entries] in workingCookieJar.entries()"
v-else
:key="domain"
class="flex flex-col"
>
<div class="flex flex-1 items-center justify-between">
<label for="cookiesList" class="p-4">
{{ domain }}
</label>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.delete')"
:icon="IconTrash2"
@click="deleteDomain(domain)"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('add.new')"
:icon="IconPlus"
@click="addCookieToDomain(domain)"
/>
</div>
</div>
<div class="rounded border border-divider">
<div class="divide-y divide-dividerLight">
<div
v-if="entries.length === 0"
class="flex flex-col items-center gap-2 p-4"
>
{{ t("cookies.modal.no_cookies_in_domain") }}
</div>
<template v-else>
<div
v-for="(entry, entryIndex) in entries"
:key="`${entry}-${entryIndex}`"
class="flex divide-x divide-dividerLight"
>
<input
class="flex flex-1 bg-transparent px-4 py-2"
:value="entry"
readonly
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.edit')"
:icon="IconEdit"
@click="editCookie(domain, entryIndex, entry)"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
:icon="IconTrash"
color="red"
@click="deleteCookie(domain, entryIndex)"
/>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
</template>
<template v-if="currentInterceptorSupportsCookies" #footer>
<span class="flex space-x-2">
<HoppButtonPrimary
v-focus
:label="t('action.save')"
outline
@click="saveCookieChanges"
/>
<HoppButtonSecondary
:label="t('action.cancel')"
outline
filled
@click="cancelCookieChanges"
/>
</span>
<HoppButtonSecondary
:label="t('action.clear_all')"
outline
filled
@click="clearAllDomains"
/>
</template>
</HoppSmartModal>
<CookiesEditCookie
:show="!!showEditModalFor"
:entry="showEditModalFor"
@save-cookie="saveCookie"
@hide-modal="showEditModalFor = null"
/>
</template>
<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { useService } from "dioc/vue"
import { CookieJarService } from "~/services/cookie-jar.service"
import IconTrash from "~icons/lucide/trash"
import IconEdit from "~icons/lucide/edit"
import IconTrash2 from "~icons/lucide/trash-2"
import IconPlus from "~icons/lucide/plus"
import { cloneDeep } from "lodash-es"
import { ref, watch, computed } from "vue"
import { InterceptorService } from "~/services/interceptor.service"
import { EditCookieConfig } from "./EditCookie.vue"
import { useColorMode } from "@composables/theming"
import { useToast } from "@composables/toast"
const props = defineProps<{
show: boolean
}>()
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const t = useI18n()
const colorMode = useColorMode()
const toast = useToast()
const newDomainText = ref("")
const interceptorService = useService(InterceptorService)
const cookieJarService = useService(CookieJarService)
const workingCookieJar = ref(cloneDeep(cookieJarService.cookieJar.value))
const currentInterceptorSupportsCookies = computed(() => {
const currentInterceptor = interceptorService.currentInterceptor.value
if (!currentInterceptor) return true
return currentInterceptor.supportsCookies ?? false
})
function addNewDomain() {
if (newDomainText.value === "" || /^\s+$/.test(newDomainText.value)) {
toast.error(`${t("cookies.modal.empty_domain")}`)
return
}
workingCookieJar.value.set(newDomainText.value, [])
newDomainText.value = ""
}
function deleteDomain(domain: string) {
workingCookieJar.value.delete(domain)
}
function addCookieToDomain(domain: string) {
showEditModalFor.value = { type: "create", domain }
}
function clearAllDomains() {
workingCookieJar.value = new Map()
toast.success(`${t("state.cleared")}`)
}
watch(
() => props.show,
(show) => {
if (show) {
workingCookieJar.value = cloneDeep(cookieJarService.cookieJar.value)
}
}
)
const showEditModalFor = ref<EditCookieConfig | null>(null)
function saveCookieChanges() {
cookieJarService.cookieJar.value = workingCookieJar.value
hideModal()
}
function cancelCookieChanges() {
hideModal()
}
function editCookie(domain: string, entryIndex: number, cookieEntry: string) {
showEditModalFor.value = {
type: "edit",
domain,
entryIndex,
currentCookieEntry: cookieEntry,
}
}
function deleteCookie(domain: string, entryIndex: number) {
const entry = workingCookieJar.value.get(domain)
if (entry) {
entry.splice(entryIndex, 1)
}
}
function saveCookie(cookie: string) {
if (showEditModalFor.value?.type === "create") {
const { domain } = showEditModalFor.value
const entry = workingCookieJar.value.get(domain)!
entry.push(cookie)
showEditModalFor.value = null
return
}
if (showEditModalFor.value?.type !== "edit") return
const { domain, entryIndex } = showEditModalFor.value!
const entry = workingCookieJar.value.get(domain)
if (entry) {
entry[entryIndex] = cookie
}
showEditModalFor.value = null
}
const hideModal = () => {
emit("hide-modal")
}
</script>

View File

@@ -1,195 +0,0 @@
<template>
<HoppSmartModal
v-if="show"
dialog
:title="t('cookies.modal.set')"
@close="hideModal"
>
<template #body>
<div class="rounded border border-dividerLight">
<div class="flex flex-col">
<div class="flex items-center justify-between pl-4">
<label class="truncate font-semibold text-secondaryLight">
{{ t("cookies.modal.cookie_string") }}
</label>
<div class="flex items-center">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:icon="IconTrash2"
@click="clearContent()"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
:icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="t('action.download_file')"
:icon="downloadIcon"
@click="downloadResponse"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="t('action.copy')"
:icon="copyIcon"
@click="copyResponse"
/>
</div>
</div>
<div class="h-46">
<div
ref="cookieEditor"
class="h-full rounded-b border-t border-dividerLight"
></div>
</div>
</div>
</div>
</template>
<template #footer>
<div class="flex space-x-2">
<HoppButtonPrimary
v-focus
:label="t('action.save')"
outline
@click="saveCookieChange"
/>
<HoppButtonSecondary
:label="t('action.cancel')"
outline
filled
@click="cancelCookieChange"
/>
</div>
<span class="flex">
<HoppButtonSecondary
:icon="pasteIcon"
:label="`${t('action.paste')}`"
filled
outline
@click="handlePaste"
/>
</span>
</template>
</HoppSmartModal>
</template>
<script lang="ts">
export type EditCookieConfig =
| { type: "create"; domain: string }
| {
type: "edit"
domain: string
entryIndex: number
currentCookieEntry: string
}
</script>
<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { useCodemirror } from "~/composables/codemirror"
import { watch, ref, reactive } from "vue"
import { refAutoReset } from "@vueuse/core"
import IconWrapText from "~icons/lucide/wrap-text"
import IconClipboard from "~icons/lucide/clipboard"
import IconCheck from "~icons/lucide/check"
import IconTrash2 from "~icons/lucide/trash-2"
import { useToast } from "~/composables/toast"
import {
useCopyResponse,
useDownloadResponse,
} from "~/composables/lens-actions"
// TODO: Build Managed Mode!
const props = defineProps<{
show: boolean
entry: EditCookieConfig | null
}>()
const emit = defineEmits<{
(e: "save-cookie", cookie: string): void
(e: "hide-modal"): void
}>()
const t = useI18n()
const toast = useToast()
const cookieEditor = ref<HTMLElement>()
const rawCookieString = ref("")
const linewrapEnabled = ref(true)
useCodemirror(
cookieEditor,
rawCookieString,
reactive({
extendedEditorConfig: {
mode: "text/plain",
placeholder: `${t("cookies.modal.enter_cookie_string")}`,
lineWrapping: linewrapEnabled,
},
linter: null,
completer: null,
environmentHighlights: false,
})
)
const pasteIcon = refAutoReset<typeof IconClipboard | typeof IconCheck>(
IconClipboard,
1000
)
watch(
() => props.entry,
() => {
if (!props.entry) return
if (props.entry.type === "create") {
rawCookieString.value = ""
return
}
rawCookieString.value = props.entry.currentCookieEntry
}
)
function hideModal() {
emit("hide-modal")
}
function cancelCookieChange() {
hideModal()
}
async function handlePaste() {
try {
const text = await navigator.clipboard.readText()
if (text) {
rawCookieString.value = text
pasteIcon.value = IconCheck
}
} catch (e) {
console.error("Failed to copy: ", e)
toast.error(t("profile.no_permission").toString())
}
}
function saveCookieChange() {
emit("save-cookie", rawCookieString.value)
}
const { copyIcon, copyResponse } = useCopyResponse(rawCookieString)
const { downloadIcon, downloadResponse } = useDownloadResponse(
"",
rawCookieString
)
function clearContent() {
rawCookieString.value = ""
}
</script>

View File

@@ -1,206 +0,0 @@
<template>
<div class="flex flex-col flex-1">
<header
class="flex items-center justify-between flex-1 flex-shrink-0 px-2 py-2 space-x-2 overflow-x-auto overflow-y-hidden"
>
<div class="flex items-center justify-between flex-1 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">
<HoppButtonSecondary
:label="t('app.open_in_hoppscotch')"
:to="sharedRequestURL"
blank
/>
</div>
</div>
</header>
<div class="sticky top-0 z-10 flex-1">
<div
class="flex-none flex-shrink-0 p-4 bg-primary sm:flex sm:flex-shrink-0 sm:space-x-2"
>
<div
class="flex flex-1 overflow-hidden border divide-x rounded text-secondaryDark divide-divider min-w-[12rem] overflow-x-auto border-divider"
>
<span
class="flex items-center justify-center px-4 py-2 font-semibold transition rounded-l"
>
{{ tab.document.request.method }}
</span>
<div
class="flex items-center flex-1 flex-shrink-0 min-w-0 px-4 py-2 truncate rounded-r"
>
{{ tab.document.request.endpoint }}
</div>
</div>
<div class="flex mt-2 space-x-2 sm:mt-0">
<HoppButtonPrimary
id="send"
:title="`${t(
'action.send'
)} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
:label="`${!loading ? t('action.send') : t('action.cancel')}`"
class="flex-1 min-w-20"
outline
@click="!loading ? newSendRequest() : cancelRequest()"
/>
<div class="flex">
<HoppButtonSecondary
:title="`${t(
'request.save'
)} <kbd>${getSpecialKey()}</kbd><kbd>S</kbd>`"
:label="t('request.save')"
filled
:icon="IconSave"
class="flex-1 rounded"
blank
outline
:to="sharedRequestURL"
/>
</div>
</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

@@ -7,7 +7,7 @@
<template #body>
<div class="flex flex-1 flex-col space-y-4">
<div class="ml-2 flex items-center space-x-8">
<label for="name" class="min-w-[2.5rem] font-semibold">{{
<label for="name" class="min-w-10 font-semibold">{{
t("environment.name")
}}</label>
<input
@@ -18,7 +18,7 @@
/>
</div>
<div class="ml-2 flex items-center space-x-8">
<label for="value" class="min-w-[2.5rem] font-semibold">{{
<label for="value" class="min-w-10 font-semibold">{{
t("environment.value")
}}</label>
<input
@@ -29,7 +29,7 @@
/>
</div>
<div class="ml-2 flex items-center space-x-8">
<label for="scope" class="min-w-[2.5rem] font-semibold">
<label for="scope" class="min-w-10 font-semibold">
{{ t("environment.scope") }}
</label>
<div
@@ -39,10 +39,10 @@
</div>
</div>
<div v-if="replaceWithVariable" class="mt-3 flex space-x-2">
<div class="min-w-[4rem]" />
<div class="min-w-18" />
<HoppSmartCheckbox
:on="replaceWithVariable"
:title="t('environment.replace_with_variable')"
title="t('environment.replace_with_variable'))"
@change="replaceWithVariable = !replaceWithVariable"
/>
<label for="replaceWithVariable">
@@ -205,14 +205,15 @@ const addEnvironment = async () => {
const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") {
return t("error.network_error")
}
switch (err.error) {
case "team_environment/not_found":
return t("team_environment.not_found")
case "Forbidden resource":
return t("profile.no_permission")
default:
return t("error.something_went_wrong")
} else {
switch (err.error) {
case "team_environment/not_found":
return t("team_environment.not_found")
case "Forbidden resource":
return t("profile.no_permission")
default:
return t("error.something_went_wrong")
}
}
}
</script>

View File

@@ -1,60 +1,154 @@
<template>
<ImportExportBase
ref="collections-import-export"
modal-title="environment.title"
:importer-modules="importerModules"
:exporter-modules="exporterModules"
@hide-modal="emit('hide-modal')"
/>
<HoppSmartModal
v-if="show"
dialog
:title="`${t('environment.title')}`"
styles="sm:max-w-md"
@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>
<script setup lang="ts">
import { useI18n } from "~/composables/i18n"
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 IconMoreVertical from "~icons/lucide/more-vertical"
import IconFolderPlus from "~icons/lucide/folder-plus"
import IconPostman from "~icons/hopp/postman"
import IconUser from "~icons/lucide/user"
import { initializeDownloadCollection } from "~/helpers/import-export/export"
import { computed } from "vue"
import { useReadonlyStream } from "~/composables/stream"
import { environmentsExporter } from "~/helpers/import-export/export/environments"
import { environmentsGistExporter } from "~/helpers/import-export/export/environmentsGistExport"
import IconDownload from "~icons/lucide/download"
import IconGithub from "~icons/lucide/github"
import { computed, ref } from "vue"
import { Environment } from "@hoppscotch/data"
import { platform } from "~/platform"
const t = useI18n()
const toast = useToast()
import axios from "axios"
import { useI18n } from "@composables/i18n"
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<{
show: boolean
teamEnvironments?: TeamEnvironment[]
teamId?: string | undefined
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(
platform.auth.getCurrentUserStream(),
platform.auth.getCurrentUser()
)
const isTeamEnvironment = computed(() => {
return props.environmentType === "TEAM_ENV"
})
// Template refs
const tippyActions = ref<TippyComponent | null>(null)
const inputChooseFileToImportFrom = ref<HTMLInputElement>()
const environmentJson = computed(() => {
if (
@@ -64,249 +158,258 @@ const environmentJson = computed(() => {
const teamEnvironments = props.teamEnvironments.map(
(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 = {
metadata: {
id: "import.from_json",
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)()
const createEnvironmentGist = async () => {
if (!currentUser.value) {
toast.error(t("profile.no_permission").toString())
if (E.isLeft(res)) {
showImportFailedError()
return
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.error(t(message.left))
return
}
toast.success(t(message.right))
toast.success(t("export.gist_created").toString())
platform.analytics?.logEvent({
type: "HOPP_EXPORT_ENVIRONMENT",
platform: "rest",
})
},
}
const HoppEnvironmentsGistExporter: ImporterOrExporter = {
metadata: {
id: "export.as_gist",
name: "export.create_secret_gist",
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)
window.open(res.data.html_url)
} catch (e) {
toast.error(t("error.something_went_wrong").toString())
console.error(e)
}
}
return enabledExporters
})
const fileImported = () => {
toast.success(t("state.file_imported").toString())
}
const showImportFailedError = () => {
const failedImport = () => {
toast.error(t("import.failed").toString())
}
const handleImportToStore = async (environments: Environment[]) => {
if (props.environmentType === "MY_ENV") {
appendEnvironments(environments)
toast.success(t("state.file_imported"))
} else {
await importToTeams(environments)
const readEnvironmentGist = 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 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 envImportPromises: Promise<
E.Either<GQLError<"">, CreateTeamEnvironmentMutation>
>[] = []
loading.value = true
for (const [, env] of content.entries()) {
const res = createTeamEnvironment(
JSON.stringify(env.variables),
props.teamId as string,
env.name
)()
platform.analytics?.logEvent({
type: "HOPP_IMPORT_ENVIRONMENT",
platform: "rest",
workspaceType: "team",
})
envImportPromises.push(res)
}
const res = await Promise.all(envImportPromises)
const failedImports = res.some((r) => E.isLeft(r))
if (failedImports) {
toast.error(t("import.failed"))
} else {
toast.success(t("import.success"))
for (const [i, env] of content.entries()) {
if (i === content.length - 1) {
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)}`)
},
() => {
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<{
(e: "hide-modal"): () => void
}>()
const importFromJSON = () => {
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 = () => {
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 a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO: get uri from meta
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
document.body.appendChild(a)
a.click()
toast.success(t("state.download_started").toString())
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
}
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>

View File

@@ -6,9 +6,10 @@
theme="popover"
:on-shown="() => envSelectorActions!.focus()"
>
<HoppSmartSelectWrapper
<span
v-tippy="{ theme: 'tooltip' }"
:title="`${t('environment.select')}`"
class="select-wrapper"
>
<HoppButtonSecondary
:icon="IconLayers"
@@ -21,7 +22,7 @@
"
class="flex-1 !justify-start rounded-none pr-8"
/>
</HoppSmartSelectWrapper>
</span>
<template #content="{ hide }">
<div
ref="envSelectorActions"
@@ -206,14 +207,10 @@
</div>
<div class="my-2 flex flex-1 flex-col space-y-2 pl-4 pr-2">
<div class="flex flex-1 space-x-4">
<span
class="min-w-[9rem] w-1/4 truncate text-tiny font-semibold"
>
<span class="min-w-32 w-1/4 truncate text-tiny font-semibold">
{{ t("environment.name") }}
</span>
<span
class="min-w-[9rem] w-full truncate text-tiny font-semibold"
>
<span class="min-w-32 w-full truncate text-tiny font-semibold">
{{ t("environment.value") }}
</span>
</div>
@@ -222,10 +219,10 @@
:key="index"
class="flex flex-1 space-x-4"
>
<span class="min-w-[9rem] w-1/4 truncate text-secondaryLight">
<span class="min-w-32 w-1/4 truncate text-secondaryLight">
{{ variable.key }}
</span>
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
<span class="min-w-32 w-full truncate text-secondaryLight">
{{ variable.value }}
</span>
</div>
@@ -261,14 +258,10 @@
</div>
<div v-else class="my-2 flex flex-1 flex-col space-y-2 pl-4 pr-2">
<div class="flex flex-1 space-x-4">
<span
class="min-w-[9rem] w-1/4 truncate text-tiny font-semibold"
>
<span class="min-w-32 w-1/4 truncate text-tiny font-semibold">
{{ t("environment.name") }}
</span>
<span
class="min-w-[9rem] w-full truncate text-tiny font-semibold"
>
<span class="min-w-32 w-full truncate text-tiny font-semibold">
{{ t("environment.value") }}
</span>
</div>
@@ -277,10 +270,10 @@
:key="index"
class="flex flex-1 space-x-4"
>
<span class="min-w-[9rem] w-1/4 truncate text-secondaryLight">
<span class="min-w-32 w-1/4 truncate text-secondaryLight">
{{ variable.key }}
</span>
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
<span class="min-w-32 w-full truncate text-secondaryLight">
{{ variable.value }}
</span>
</div>
@@ -453,11 +446,12 @@ const isEnvActive = (id: string | number) => {
} else {
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
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,
teamEnvID: props.modelValue.environment.id,
}
} else {
return { type: "global", name: "Global" }
}
return { type: "global", name: "Global" }
}
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
const environment =
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) {
} else {
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
const environment =
myEnvironments.value[selectedEnvironmentIndex.value.index]
return {
type: "TEAM_ENV",
name: teamEnv.environment.name,
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
variables: teamEnv.environment.variables,
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 {
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
@@ -579,12 +577,13 @@ const envQuickPeekActions = ref<TippyComponent | null>(null)
const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") {
return t("error.network_error")
}
switch (err.error) {
case "team_environment/not_found":
return t("team_environment.not_found")
default:
return t("error.something_went_wrong")
} else {
switch (err.error) {
case "team_environment/not_found":
return t("team_environment.not_found")
default:
return t("error.something_went_wrong")
}
}
}
@@ -593,8 +592,9 @@ const globalEnvs = useReadonlyStream(globalEnv$, [])
const environmentVariables = computed(() => {
if (selectedEnv.value.variables) {
return selectedEnv.value.variables
} else {
return []
}
return []
})
const editGlobalEnv = () => {

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