Compare commits
23 Commits
feat/colle
...
feat/share
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
502da61b8b | ||
|
|
f2777a9a75 | ||
|
|
4bd3e89f89 | ||
|
|
09e9601940 | ||
|
|
fd4a5c626f | ||
|
|
67cfef82af | ||
|
|
aa18249791 | ||
|
|
9d8fdb4d04 | ||
|
|
fbca9b06c3 | ||
|
|
bb0bf35164 | ||
|
|
5a35c098ec | ||
|
|
ab7c29d228 | ||
|
|
d9c75ed79e | ||
|
|
6fa722df7b | ||
|
|
18864bfecf | ||
|
|
95754cb2b4 | ||
|
|
ed2a461dc5 | ||
|
|
8d5a456dbd | ||
|
|
2528bbb92f | ||
|
|
259cd48dbb | ||
|
|
b43531f200 | ||
|
|
26da3e18a9 | ||
|
|
bb4b640e58 |
@@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "TeamCollection" ADD COLUMN "data" JSONB;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "UserCollection" ADD COLUMN "data" JSONB;
|
||||
@@ -43,6 +43,7 @@ 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[]
|
||||
@@ -74,7 +75,8 @@ model Shortcode {
|
||||
creatorUid String?
|
||||
User User? @relation(fields: [creatorUid], references: [uid])
|
||||
createdOn DateTime @default(now())
|
||||
updatedOn DateTime @updatedAt @default(now())
|
||||
updatedOn DateTime @default(now()) @updatedAt
|
||||
|
||||
@@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique")
|
||||
}
|
||||
|
||||
@@ -195,6 +197,7 @@ 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)
|
||||
|
||||
@@ -254,6 +254,13 @@ 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)
|
||||
@@ -585,6 +592,13 @@ 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)
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -14,6 +14,13 @@ 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()
|
||||
@@ -26,6 +33,13 @@ 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()
|
||||
@@ -33,12 +47,14 @@ 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;
|
||||
}
|
||||
@@ -98,3 +114,26 @@ 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;
|
||||
}
|
||||
|
||||
@@ -12,12 +12,17 @@ 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()
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
MoveTeamCollectionArgs,
|
||||
RenameTeamCollectionArgs,
|
||||
ReplaceTeamCollectionArgs,
|
||||
UpdateTeamCollectionArgs,
|
||||
UpdateTeamCollectionOrderArgs,
|
||||
} from './input-type.args';
|
||||
import * as E from 'fp-ts/Either';
|
||||
@@ -141,7 +142,14 @@ export class TeamCollectionResolver {
|
||||
);
|
||||
|
||||
if (E.isLeft(teamCollections)) throwErr(teamCollections.left);
|
||||
return teamCollections.right;
|
||||
return <TeamCollection>{
|
||||
id: teamCollections.right.id,
|
||||
title: teamCollections.right.title,
|
||||
parentID: teamCollections.right.parentID,
|
||||
data: !teamCollections.right.data
|
||||
? null
|
||||
: JSON.stringify(teamCollections.right.data),
|
||||
};
|
||||
}
|
||||
|
||||
// Mutations
|
||||
@@ -155,6 +163,7 @@ export class TeamCollectionResolver {
|
||||
const teamCollection = await this.teamCollectionService.createCollection(
|
||||
args.teamID,
|
||||
args.title,
|
||||
args.data,
|
||||
null,
|
||||
);
|
||||
|
||||
@@ -230,6 +239,7 @@ export class TeamCollectionResolver {
|
||||
const teamCollection = await this.teamCollectionService.createCollection(
|
||||
team.right.id,
|
||||
args.childTitle,
|
||||
args.data,
|
||||
args.collectionID,
|
||||
);
|
||||
|
||||
@@ -239,6 +249,7 @@ export class TeamCollectionResolver {
|
||||
|
||||
@Mutation(() => TeamCollection, {
|
||||
description: 'Rename a collection',
|
||||
deprecationReason: 'Switch to updateTeamCollection mutation instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||
@@ -303,6 +314,23 @@ 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, {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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,
|
||||
@@ -17,6 +18,7 @@ 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>();
|
||||
@@ -51,35 +53,60 @@ 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,
|
||||
@@ -87,11 +114,20 @@ 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,
|
||||
@@ -102,6 +138,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
||||
orderIndex: 2,
|
||||
parentID: null,
|
||||
title: 'Root Collection 1',
|
||||
data: {},
|
||||
|
||||
teamID: team.id,
|
||||
createdOn: currentTime,
|
||||
updatedOn: currentTime,
|
||||
@@ -111,6 +149,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
||||
orderIndex: 3,
|
||||
parentID: null,
|
||||
title: 'Root Collection 1',
|
||||
data: {},
|
||||
|
||||
teamID: team.id,
|
||||
createdOn: currentTime,
|
||||
updatedOn: currentTime,
|
||||
@@ -119,6 +159,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
||||
id: 'bre3',
|
||||
orderIndex: 4,
|
||||
parentID: null,
|
||||
data: {},
|
||||
|
||||
title: 'Root Collection 1',
|
||||
teamID: team.id,
|
||||
createdOn: currentTime,
|
||||
@@ -129,6 +171,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
||||
orderIndex: 5,
|
||||
parentID: null,
|
||||
title: 'Root Collection 1',
|
||||
data: {},
|
||||
|
||||
teamID: team.id,
|
||||
createdOn: currentTime,
|
||||
updatedOn: currentTime,
|
||||
@@ -139,6 +183,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
||||
parentID: null,
|
||||
title: 'Root Collection 1',
|
||||
teamID: team.id,
|
||||
data: {},
|
||||
|
||||
createdOn: currentTime,
|
||||
updatedOn: currentTime,
|
||||
},
|
||||
@@ -148,6 +194,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
||||
parentID: null,
|
||||
title: 'Root Collection 1',
|
||||
teamID: team.id,
|
||||
data: {},
|
||||
|
||||
createdOn: currentTime,
|
||||
updatedOn: currentTime,
|
||||
},
|
||||
@@ -156,6 +204,7 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
||||
orderIndex: 8,
|
||||
parentID: null,
|
||||
title: 'Root Collection 1',
|
||||
data: {},
|
||||
teamID: team.id,
|
||||
createdOn: currentTime,
|
||||
updatedOn: currentTime,
|
||||
@@ -165,6 +214,7 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
||||
orderIndex: 9,
|
||||
parentID: null,
|
||||
title: 'Root Collection 1',
|
||||
data: {},
|
||||
teamID: team.id,
|
||||
createdOn: currentTime,
|
||||
updatedOn: currentTime,
|
||||
@@ -175,17 +225,83 @@ 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,
|
||||
@@ -195,6 +311,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
||||
orderIndex: 2,
|
||||
parentID: rootTeamCollection.id,
|
||||
title: 'Root Collection 1',
|
||||
data: {},
|
||||
|
||||
teamID: team.id,
|
||||
createdOn: currentTime,
|
||||
updatedOn: currentTime,
|
||||
@@ -204,6 +322,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
||||
orderIndex: 3,
|
||||
parentID: rootTeamCollection.id,
|
||||
title: 'Root Collection 1',
|
||||
data: {},
|
||||
|
||||
teamID: team.id,
|
||||
createdOn: currentTime,
|
||||
updatedOn: currentTime,
|
||||
@@ -212,6 +332,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
||||
id: '567',
|
||||
orderIndex: 4,
|
||||
parentID: rootTeamCollection.id,
|
||||
data: {},
|
||||
|
||||
title: 'Root Collection 1',
|
||||
teamID: team.id,
|
||||
createdOn: currentTime,
|
||||
@@ -221,6 +343,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
||||
id: '123',
|
||||
orderIndex: 5,
|
||||
parentID: rootTeamCollection.id,
|
||||
data: {},
|
||||
|
||||
title: 'Root Collection 1',
|
||||
teamID: team.id,
|
||||
createdOn: currentTime,
|
||||
@@ -230,6 +354,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
||||
id: '678',
|
||||
orderIndex: 6,
|
||||
parentID: rootTeamCollection.id,
|
||||
data: {},
|
||||
|
||||
title: 'Root Collection 1',
|
||||
teamID: team.id,
|
||||
createdOn: currentTime,
|
||||
@@ -239,6 +365,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
||||
id: '789',
|
||||
orderIndex: 7,
|
||||
parentID: rootTeamCollection.id,
|
||||
data: {},
|
||||
|
||||
title: 'Root Collection 1',
|
||||
teamID: team.id,
|
||||
createdOn: currentTime,
|
||||
@@ -248,6 +376,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
||||
id: '890',
|
||||
orderIndex: 8,
|
||||
parentID: rootTeamCollection.id,
|
||||
data: {},
|
||||
|
||||
title: 'Root Collection 1',
|
||||
teamID: team.id,
|
||||
createdOn: currentTime,
|
||||
@@ -257,6 +387,7 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
||||
id: '012',
|
||||
orderIndex: 9,
|
||||
parentID: rootTeamCollection.id,
|
||||
data: {},
|
||||
title: 'Root Collection 1',
|
||||
teamID: team.id,
|
||||
createdOn: currentTime,
|
||||
@@ -266,6 +397,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
||||
id: '0bhu',
|
||||
orderIndex: 10,
|
||||
parentID: rootTeamCollection.id,
|
||||
data: {},
|
||||
|
||||
title: 'Root Collection 1',
|
||||
teamID: team.id,
|
||||
createdOn: currentTime,
|
||||
@@ -273,6 +406,75 @@ 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();
|
||||
@@ -311,7 +513,7 @@ describe('getParentOfCollection', () => {
|
||||
const result = await teamCollectionService.getParentOfCollection(
|
||||
childTeamCollection.id,
|
||||
);
|
||||
expect(result).toEqual(rootTeamCollection);
|
||||
expect(result).toEqual(rootTeamCollectionsCasted);
|
||||
});
|
||||
|
||||
test('should return null successfully for a root collection with valid collectionID', async () => {
|
||||
@@ -347,7 +549,7 @@ describe('getChildrenOfCollection', () => {
|
||||
null,
|
||||
10,
|
||||
);
|
||||
expect(result).toEqual(childTeamCollectionList);
|
||||
expect(result).toEqual(childTeamCollectionListCasted);
|
||||
});
|
||||
|
||||
test('should return a list of 3 child collections successfully with cursor being equal to the 7th item in the list', async () => {
|
||||
@@ -363,9 +565,9 @@ describe('getChildrenOfCollection', () => {
|
||||
10,
|
||||
);
|
||||
expect(result).toEqual([
|
||||
{ ...childTeamCollectionList[7] },
|
||||
{ ...childTeamCollectionList[8] },
|
||||
{ ...childTeamCollectionList[9] },
|
||||
{ ...childTeamCollectionListCasted[7] },
|
||||
{ ...childTeamCollectionListCasted[8] },
|
||||
{ ...childTeamCollectionListCasted[9] },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -392,7 +594,7 @@ describe('getTeamRootCollections', () => {
|
||||
null,
|
||||
10,
|
||||
);
|
||||
expect(result).toEqual(rootTeamCollectionList);
|
||||
expect(result).toEqual(rootTeamCollectionListCasted);
|
||||
});
|
||||
|
||||
test('should return a list of 3 root collections successfully with cursor being equal to the 7th item in the list', async () => {
|
||||
@@ -408,9 +610,9 @@ describe('getTeamRootCollections', () => {
|
||||
10,
|
||||
);
|
||||
expect(result).toEqual([
|
||||
{ ...rootTeamCollectionList[7] },
|
||||
{ ...rootTeamCollectionList[8] },
|
||||
{ ...rootTeamCollectionList[9] },
|
||||
{ ...rootTeamCollectionListCasted[7] },
|
||||
{ ...rootTeamCollectionListCasted[8] },
|
||||
{ ...rootTeamCollectionListCasted[9] },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -464,6 +666,7 @@ describe('createCollection', () => {
|
||||
const result = await teamCollectionService.createCollection(
|
||||
rootTeamCollection.teamID,
|
||||
'ab',
|
||||
JSON.stringify(rootTeamCollection.data),
|
||||
rootTeamCollection.id,
|
||||
);
|
||||
expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE);
|
||||
@@ -478,11 +681,27 @@ 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(
|
||||
@@ -496,9 +715,10 @@ describe('createCollection', () => {
|
||||
const result = await teamCollectionService.createCollection(
|
||||
rootTeamCollection.teamID,
|
||||
'abcdefg',
|
||||
JSON.stringify(rootTeamCollection.data),
|
||||
rootTeamCollection.id,
|
||||
);
|
||||
expect(result).toEqualRight(rootTeamCollection);
|
||||
expect(result).toEqualRight(rootTeamCollectionsCasted);
|
||||
});
|
||||
|
||||
test('should successfully create a new child TeamCollection with valid inputs', async () => {
|
||||
@@ -514,9 +734,10 @@ describe('createCollection', () => {
|
||||
const result = await teamCollectionService.createCollection(
|
||||
childTeamCollection.teamID,
|
||||
childTeamCollection.title,
|
||||
JSON.stringify(rootTeamCollection.data),
|
||||
rootTeamCollection.id,
|
||||
);
|
||||
expect(result).toEqualRight(childTeamCollection);
|
||||
expect(result).toEqualRight(childTeamCollectionCasted);
|
||||
});
|
||||
|
||||
test('should send pubsub message to "team_coll/<teamID>/coll_added" if child TeamCollection is created successfully', async () => {
|
||||
@@ -532,11 +753,13 @@ 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`,
|
||||
childTeamCollection,
|
||||
childTeamCollectionCasted,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -553,11 +776,13 @@ 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`,
|
||||
rootTeamCollection,
|
||||
rootTeamCollectionsCasted,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -587,7 +812,7 @@ describe('renameCollection', () => {
|
||||
'NewTitle',
|
||||
);
|
||||
expect(result).toEqualRight({
|
||||
...rootTeamCollection,
|
||||
...rootTeamCollectionsCasted,
|
||||
title: 'NewTitle',
|
||||
});
|
||||
});
|
||||
@@ -625,7 +850,7 @@ describe('renameCollection', () => {
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_coll/${rootTeamCollection.teamID}/coll_updated`,
|
||||
{
|
||||
...rootTeamCollection,
|
||||
...rootTeamCollectionsCasted,
|
||||
title: 'NewTitle',
|
||||
},
|
||||
);
|
||||
@@ -832,9 +1057,8 @@ describe('moveCollection', () => {
|
||||
null,
|
||||
);
|
||||
expect(result).toEqualRight({
|
||||
...childTeamCollection,
|
||||
...childTeamCollectionCasted,
|
||||
parentID: null,
|
||||
orderIndex: 2,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -890,9 +1114,8 @@ describe('moveCollection', () => {
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_coll/${childTeamCollection.teamID}/coll_moved`,
|
||||
{
|
||||
...childTeamCollection,
|
||||
...childTeamCollectionCasted,
|
||||
parentID: null,
|
||||
orderIndex: 2,
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -931,9 +1154,8 @@ describe('moveCollection', () => {
|
||||
childTeamCollection_2.id,
|
||||
);
|
||||
expect(result).toEqualRight({
|
||||
...rootTeamCollection,
|
||||
parentID: childTeamCollection_2.id,
|
||||
orderIndex: 1,
|
||||
...rootTeamCollectionsCasted,
|
||||
parentID: childTeamCollection_2Casted.id,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -973,9 +1195,8 @@ describe('moveCollection', () => {
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_coll/${childTeamCollection_2.teamID}/coll_moved`,
|
||||
{
|
||||
...rootTeamCollection,
|
||||
parentID: childTeamCollection_2.id,
|
||||
orderIndex: 1,
|
||||
...rootTeamCollectionsCasted,
|
||||
parentID: childTeamCollection_2Casted.id,
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -1014,9 +1235,8 @@ describe('moveCollection', () => {
|
||||
childTeamCollection_2.id,
|
||||
);
|
||||
expect(result).toEqualRight({
|
||||
...childTeamCollection,
|
||||
parentID: childTeamCollection_2.id,
|
||||
orderIndex: 1,
|
||||
...childTeamCollectionCasted,
|
||||
parentID: childTeamCollection_2Casted.id,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1056,9 +1276,8 @@ describe('moveCollection', () => {
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_coll/${childTeamCollection.teamID}/coll_moved`,
|
||||
{
|
||||
...childTeamCollection,
|
||||
parentID: childTeamCollection_2.id,
|
||||
orderIndex: 1,
|
||||
...childTeamCollectionCasted,
|
||||
parentID: childTeamCollection_2Casted.id,
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -1154,7 +1373,7 @@ describe('updateCollectionOrder', () => {
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_coll/${childTeamCollectionList[4].teamID}/coll_order_updated`,
|
||||
{
|
||||
collection: rootTeamCollectionList[4],
|
||||
collection: rootTeamCollectionListCasted[4],
|
||||
nextCollection: null,
|
||||
},
|
||||
);
|
||||
@@ -1235,8 +1454,8 @@ describe('updateCollectionOrder', () => {
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_coll/${childTeamCollectionList[2].teamID}/coll_order_updated`,
|
||||
{
|
||||
collection: childTeamCollectionList[4],
|
||||
nextCollection: childTeamCollectionList[2],
|
||||
collection: childTeamCollectionListCasted[4],
|
||||
nextCollection: childTeamCollectionListCasted[2],
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -1302,7 +1521,7 @@ describe('importCollectionsFromJSON', () => {
|
||||
);
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_coll/${rootTeamCollection.teamID}/coll_added`,
|
||||
rootTeamCollection,
|
||||
rootTeamCollectionsCasted,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1421,7 +1640,7 @@ describe('replaceCollectionsWithJSON', () => {
|
||||
);
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_coll/${rootTeamCollection.teamID}/coll_added`,
|
||||
rootTeamCollection,
|
||||
rootTeamCollectionsCasted,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1458,4 +1677,64 @@ 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
|
||||
|
||||
@@ -13,6 +13,7 @@ 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';
|
||||
@@ -69,6 +70,7 @@ export class TeamCollectionService {
|
||||
this.generatePrismaQueryObjForFBCollFolder(f, teamID, index + 1),
|
||||
),
|
||||
},
|
||||
data: folder.data ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,6 +120,7 @@ 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);
|
||||
@@ -198,8 +201,11 @@ export class TeamCollectionService {
|
||||
),
|
||||
);
|
||||
|
||||
teamCollections.forEach((x) =>
|
||||
this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x),
|
||||
teamCollections.forEach((collection) =>
|
||||
this.pubsub.publish(
|
||||
`team_coll/${destTeamID}/coll_added`,
|
||||
this.cast(collection),
|
||||
),
|
||||
);
|
||||
|
||||
return E.right(true);
|
||||
@@ -268,8 +274,11 @@ export class TeamCollectionService {
|
||||
),
|
||||
);
|
||||
|
||||
teamCollections.forEach((x) =>
|
||||
this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x),
|
||||
teamCollections.forEach((collections) =>
|
||||
this.pubsub.publish(
|
||||
`team_coll/${destTeamID}/coll_added`,
|
||||
this.cast(collections),
|
||||
),
|
||||
);
|
||||
|
||||
return E.right(true);
|
||||
@@ -277,11 +286,17 @@ 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>{ ...teamCollection };
|
||||
return <TeamCollection>{
|
||||
id: teamCollection.id,
|
||||
title: teamCollection.title,
|
||||
parentID: teamCollection.parentID,
|
||||
data: !teamCollection.data ? null : JSON.stringify(teamCollection.data),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -324,7 +339,7 @@ export class TeamCollectionService {
|
||||
});
|
||||
if (!teamCollection) return null;
|
||||
|
||||
return teamCollection.parent;
|
||||
return !teamCollection.parent ? null : this.cast(teamCollection.parent);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -335,12 +350,12 @@ export class TeamCollectionService {
|
||||
* @param take Number of items we want returned
|
||||
* @returns A list of child collections
|
||||
*/
|
||||
getChildrenOfCollection(
|
||||
async getChildrenOfCollection(
|
||||
collectionID: string,
|
||||
cursor: string | null,
|
||||
take: number,
|
||||
) {
|
||||
return this.prisma.teamCollection.findMany({
|
||||
const res = await this.prisma.teamCollection.findMany({
|
||||
where: {
|
||||
parentID: collectionID,
|
||||
},
|
||||
@@ -351,6 +366,12 @@ export class TeamCollectionService {
|
||||
skip: cursor ? 1 : 0,
|
||||
cursor: cursor ? { id: cursor } : undefined,
|
||||
});
|
||||
|
||||
const childCollections = res.map((teamCollection) =>
|
||||
this.cast(teamCollection),
|
||||
);
|
||||
|
||||
return childCollections;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -366,7 +387,7 @@ export class TeamCollectionService {
|
||||
cursor: string | null,
|
||||
take: number,
|
||||
) {
|
||||
return this.prisma.teamCollection.findMany({
|
||||
const res = await this.prisma.teamCollection.findMany({
|
||||
where: {
|
||||
teamID,
|
||||
parentID: null,
|
||||
@@ -378,6 +399,12 @@ export class TeamCollectionService {
|
||||
skip: cursor ? 1 : 0,
|
||||
cursor: cursor ? { id: cursor } : undefined,
|
||||
});
|
||||
|
||||
const teamCollections = res.map((teamCollection) =>
|
||||
this.cast(teamCollection),
|
||||
);
|
||||
|
||||
return teamCollections;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -470,6 +497,7 @@ export class TeamCollectionService {
|
||||
async createCollection(
|
||||
teamID: string,
|
||||
title: string,
|
||||
data: string | null = null,
|
||||
parentTeamCollectionID: string | null,
|
||||
) {
|
||||
const isTitleValid = isValidLength(title, this.TITLE_LENGTH);
|
||||
@@ -481,6 +509,13 @@ 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: {
|
||||
@@ -498,18 +533,23 @@ 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`, teamCollection);
|
||||
this.pubsub.publish(
|
||||
`team_coll/${teamID}/coll_added`,
|
||||
this.cast(teamCollection),
|
||||
);
|
||||
|
||||
return E.right(this.cast(teamCollection));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use updateTeamCollection method instead
|
||||
* Update the title of a TeamCollection
|
||||
*
|
||||
* @param collectionID The Collection ID
|
||||
@@ -532,10 +572,10 @@ export class TeamCollectionService {
|
||||
|
||||
this.pubsub.publish(
|
||||
`team_coll/${updatedTeamCollection.teamID}/coll_updated`,
|
||||
updatedTeamCollection,
|
||||
this.cast(updatedTeamCollection),
|
||||
);
|
||||
|
||||
return E.right(updatedTeamCollection);
|
||||
return E.right(this.cast(updatedTeamCollection));
|
||||
} catch (error) {
|
||||
return E.left(TEAM_COLL_NOT_FOUND);
|
||||
}
|
||||
@@ -694,8 +734,8 @@ export class TeamCollectionService {
|
||||
* @returns An Option of boolean, is parent or not
|
||||
*/
|
||||
private async isParent(
|
||||
collection: TeamCollection,
|
||||
destCollection: TeamCollection,
|
||||
collection: DBTeamCollection,
|
||||
destCollection: DBTeamCollection,
|
||||
): 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
|
||||
@@ -971,4 +1011,49 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ const teamCollection: DbTeamCollection = {
|
||||
id: 'team-coll-1',
|
||||
parentID: null,
|
||||
teamID: team.id,
|
||||
data: {},
|
||||
title: 'Team Collection 1',
|
||||
orderIndex: 1,
|
||||
createdOn: new Date(),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,13 @@ 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 {
|
||||
@@ -17,6 +24,13 @@ 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()
|
||||
@@ -95,3 +109,26 @@ 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;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
MoveUserCollectionArgs,
|
||||
RenameUserCollectionsArgs,
|
||||
UpdateUserCollectionArgs,
|
||||
UpdateUserCollectionsArgs,
|
||||
} from './input-type.args';
|
||||
import { ReqType } from 'src/types/RequestTypes';
|
||||
import * as E from 'fp-ts/Either';
|
||||
@@ -142,7 +143,13 @@ export class UserCollectionResolver {
|
||||
);
|
||||
|
||||
if (E.isLeft(userCollection)) throwErr(userCollection.left);
|
||||
return userCollection.right;
|
||||
return <UserCollection>{
|
||||
...userCollection.right,
|
||||
userID: userCollection.right.userUid,
|
||||
data: !userCollection.right.data
|
||||
? null
|
||||
: JSON.stringify(userCollection.right.data),
|
||||
};
|
||||
}
|
||||
|
||||
@Query(() => UserCollectionExportJSONData, {
|
||||
@@ -191,6 +198,7 @@ export class UserCollectionResolver {
|
||||
await this.userCollectionService.createUserCollection(
|
||||
user,
|
||||
args.title,
|
||||
args.data,
|
||||
null,
|
||||
ReqType.REST,
|
||||
);
|
||||
@@ -212,6 +220,7 @@ export class UserCollectionResolver {
|
||||
await this.userCollectionService.createUserCollection(
|
||||
user,
|
||||
args.title,
|
||||
args.data,
|
||||
null,
|
||||
ReqType.GQL,
|
||||
);
|
||||
@@ -232,6 +241,7 @@ export class UserCollectionResolver {
|
||||
await this.userCollectionService.createUserCollection(
|
||||
user,
|
||||
args.title,
|
||||
args.data,
|
||||
args.parentUserCollectionID,
|
||||
ReqType.GQL,
|
||||
);
|
||||
@@ -252,6 +262,7 @@ export class UserCollectionResolver {
|
||||
await this.userCollectionService.createUserCollection(
|
||||
user,
|
||||
args.title,
|
||||
args.data,
|
||||
args.parentUserCollectionID,
|
||||
ReqType.REST,
|
||||
);
|
||||
@@ -359,6 +370,26 @@ 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',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ 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';
|
||||
@@ -43,8 +44,12 @@ export class UserCollectionService {
|
||||
*/
|
||||
private cast(collection: UserCollection) {
|
||||
return <UserCollectionModel>{
|
||||
...collection,
|
||||
id: collection.id,
|
||||
title: collection.title,
|
||||
type: collection.type,
|
||||
parentID: collection.parentID,
|
||||
userID: collection.userUid,
|
||||
data: !collection.data ? null : JSON.stringify(collection.data),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -146,7 +151,7 @@ export class UserCollectionService {
|
||||
},
|
||||
});
|
||||
|
||||
return parent;
|
||||
return !parent ? null : this.cast(parent);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -164,7 +169,7 @@ export class UserCollectionService {
|
||||
take: number,
|
||||
type: ReqType,
|
||||
) {
|
||||
return this.prisma.userCollection.findMany({
|
||||
const res = await this.prisma.userCollection.findMany({
|
||||
where: {
|
||||
parentID: collectionID,
|
||||
type: type,
|
||||
@@ -176,6 +181,12 @@ export class UserCollectionService {
|
||||
skip: cursor ? 1 : 0,
|
||||
cursor: cursor ? { id: cursor } : undefined,
|
||||
});
|
||||
|
||||
const childCollections = res.map((childCollection) =>
|
||||
this.cast(childCollection),
|
||||
);
|
||||
|
||||
return childCollections;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,12 +222,20 @@ 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(
|
||||
@@ -251,15 +270,19 @@ 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`, userCollection);
|
||||
await this.pubsub.publish(
|
||||
`user_coll/${user.uid}/created`,
|
||||
this.cast(userCollection),
|
||||
);
|
||||
|
||||
return E.right(userCollection);
|
||||
return E.right(this.cast(userCollection));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -276,7 +299,7 @@ export class UserCollectionService {
|
||||
take: number,
|
||||
type: ReqType,
|
||||
) {
|
||||
return this.prisma.userCollection.findMany({
|
||||
const res = await this.prisma.userCollection.findMany({
|
||||
where: {
|
||||
userUid: user.uid,
|
||||
parentID: null,
|
||||
@@ -289,6 +312,12 @@ export class UserCollectionService {
|
||||
skip: cursor ? 1 : 0,
|
||||
cursor: cursor ? { id: cursor } : undefined,
|
||||
});
|
||||
|
||||
const userCollections = res.map((childCollection) =>
|
||||
this.cast(childCollection),
|
||||
);
|
||||
|
||||
return userCollections;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -307,7 +336,7 @@ export class UserCollectionService {
|
||||
take: number,
|
||||
type: ReqType,
|
||||
) {
|
||||
return this.prisma.userCollection.findMany({
|
||||
const res = await this.prisma.userCollection.findMany({
|
||||
where: {
|
||||
userUid: user.uid,
|
||||
parentID: userCollectionID,
|
||||
@@ -317,9 +346,16 @@ 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
|
||||
@@ -351,10 +387,10 @@ export class UserCollectionService {
|
||||
|
||||
this.pubsub.publish(
|
||||
`user_coll/${updatedUserCollection.userUid}/updated`,
|
||||
updatedUserCollection,
|
||||
this.cast(updatedUserCollection),
|
||||
);
|
||||
|
||||
return E.right(updatedUserCollection);
|
||||
return E.right(this.cast(updatedUserCollection));
|
||||
} catch (error) {
|
||||
return E.left(USER_COLL_NOT_FOUND);
|
||||
}
|
||||
@@ -591,10 +627,10 @@ export class UserCollectionService {
|
||||
|
||||
this.pubsub.publish(
|
||||
`user_coll/${collection.right.userUid}/moved`,
|
||||
updatedCollection.right,
|
||||
this.cast(updatedCollection.right),
|
||||
);
|
||||
|
||||
return E.right(updatedCollection.right);
|
||||
return E.right(this.cast(updatedCollection.right));
|
||||
}
|
||||
|
||||
// destCollectionID != null i.e move into another collection
|
||||
@@ -642,10 +678,10 @@ export class UserCollectionService {
|
||||
|
||||
this.pubsub.publish(
|
||||
`user_coll/${collection.right.userUid}/moved`,
|
||||
updatedCollection.right,
|
||||
this.cast(updatedCollection.right),
|
||||
);
|
||||
|
||||
return E.right(updatedCollection.right);
|
||||
return E.right(this.cast(updatedCollection.right));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -846,6 +882,7 @@ 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);
|
||||
@@ -918,6 +955,7 @@ 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,
|
||||
});
|
||||
@@ -971,6 +1009,7 @@ export class UserCollectionService {
|
||||
this.generatePrismaQueryObj(f, userID, index + 1, reqType),
|
||||
),
|
||||
},
|
||||
data: folder.data ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1040,10 +1079,63 @@ export class UserCollectionService {
|
||||
),
|
||||
);
|
||||
|
||||
userCollections.forEach((x) =>
|
||||
this.pubsub.publish(`user_coll/${userID}/created`, x),
|
||||
userCollections.forEach((collection) =>
|
||||
this.pubsub.publish(`user_coll/${userID}/created`, this.cast(collection)),
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,12 @@ 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',
|
||||
})
|
||||
|
||||
@@ -69,5 +69,7 @@ module.exports = {
|
||||
"Do not use 'localStorage' directly. Please use the PersistenceService",
|
||||
},
|
||||
],
|
||||
eqeqeq: 1,
|
||||
"no-else-return": 1,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -5,5 +5,4 @@ module.exports = {
|
||||
printWidth: 80,
|
||||
useTabs: false,
|
||||
tabWidth: 2,
|
||||
plugins: ["prettier-plugin-tailwindcss"],
|
||||
}
|
||||
|
||||
@@ -29,14 +29,6 @@
|
||||
@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 {
|
||||
@@ -65,7 +57,7 @@ input::placeholder,
|
||||
textarea::placeholder,
|
||||
.cm-placeholder {
|
||||
@apply text-secondary;
|
||||
@apply opacity-50;
|
||||
@apply opacity-50 #{!important};
|
||||
}
|
||||
|
||||
input,
|
||||
@@ -84,7 +76,7 @@ body {
|
||||
@apply font-medium;
|
||||
@apply select-none;
|
||||
@apply overflow-x-hidden;
|
||||
@apply leading-body;
|
||||
@apply leading-body #{!important};
|
||||
animation: fade 300ms forwards;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-touch-callout: none;
|
||||
@@ -182,7 +174,7 @@ a {
|
||||
@apply font-semibold;
|
||||
@apply px-2 py-1;
|
||||
@apply truncate;
|
||||
@apply leading-normal;
|
||||
@apply leading-body;
|
||||
@apply items-center;
|
||||
|
||||
kbd {
|
||||
@@ -229,7 +221,7 @@ a {
|
||||
@apply overflow-y-auto;
|
||||
@apply text-body text-secondary;
|
||||
@apply p-2;
|
||||
@apply leading-normal;
|
||||
@apply leading-body;
|
||||
@apply focus:outline-none;
|
||||
scroll-behavior: smooth;
|
||||
|
||||
@@ -261,7 +253,7 @@ a {
|
||||
|
||||
hr {
|
||||
@apply border-b border-dividerLight;
|
||||
@apply my-2;
|
||||
@apply my-2 #{!important};
|
||||
}
|
||||
|
||||
.heading {
|
||||
@@ -350,44 +342,28 @@ 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(--info-color);
|
||||
color: var(--status-info-color);
|
||||
}
|
||||
|
||||
.success-response {
|
||||
color: var(--success-color);
|
||||
color: var(--status-success-color);
|
||||
}
|
||||
|
||||
.redir-response {
|
||||
color: var(--warning-color);
|
||||
.redirect-response {
|
||||
color: var(--status-redirect-color);
|
||||
}
|
||||
|
||||
.cl-error-response {
|
||||
color: var(--cl-error-color);
|
||||
.critical-error-response {
|
||||
color: var(--status-critical-error-color);
|
||||
}
|
||||
|
||||
.sv-error-response {
|
||||
color: var(--sv-error-color);
|
||||
.server-error-response {
|
||||
color: var(--status-server-error-color);
|
||||
}
|
||||
|
||||
.missing-data-response {
|
||||
@apply text-secondaryLight;
|
||||
color: var(--status-missing-data-color);
|
||||
}
|
||||
|
||||
.toasted-container {
|
||||
@@ -537,12 +513,12 @@ pre.ace_editor {
|
||||
@apply inline-flex;
|
||||
@apply font-sans;
|
||||
@apply text-tiny;
|
||||
@apply bg-divider;
|
||||
@apply bg-dividerLight;
|
||||
@apply rounded;
|
||||
@apply ml-2;
|
||||
@apply px-1;
|
||||
@apply min-w-5;
|
||||
@apply min-h-5;
|
||||
@apply min-w-[1.25rem];
|
||||
@apply min-h-[1.25rem];
|
||||
@apply items-center;
|
||||
@apply justify-center;
|
||||
@apply border border-dividerDark;
|
||||
|
||||
@@ -1,89 +1,89 @@
|
||||
@mixin green-theme {
|
||||
--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;
|
||||
--accent-color: theme("colors.emerald.500");
|
||||
--accent-light-color: theme("colors.emerald.400");
|
||||
--accent-dark-color: theme("colors.emerald.600");
|
||||
--accent-contrast-color: theme("colors.white");
|
||||
--gradient-from-color: theme("colors.emerald.400");
|
||||
--gradient-via-color: theme("colors.emerald.500");
|
||||
--gradient-to-color: theme("colors.emerald.600");
|
||||
}
|
||||
|
||||
@mixin teal-theme {
|
||||
--accent-color: #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;
|
||||
--accent-color: theme("colors.teal.500");
|
||||
--accent-light-color: theme("colors.teal.400");
|
||||
--accent-dark-color: theme("colors.teal.600");
|
||||
--accent-contrast-color: theme("colors.white");
|
||||
--gradient-from-color: theme("colors.teal.400");
|
||||
--gradient-via-color: theme("colors.teal.500");
|
||||
--gradient-to-color: theme("colors.teal.600");
|
||||
}
|
||||
|
||||
@mixin blue-theme {
|
||||
--accent-color: #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;
|
||||
--accent-color: theme("colors.blue.500");
|
||||
--accent-light-color: theme("colors.blue.400");
|
||||
--accent-dark-color: theme("colors.blue.600");
|
||||
--accent-contrast-color: theme("colors.white");
|
||||
--gradient-from-color: theme("colors.blue.400");
|
||||
--gradient-via-color: theme("colors.blue.500");
|
||||
--gradient-to-color: theme("colors.blue.600");
|
||||
}
|
||||
|
||||
@mixin indigo-theme {
|
||||
--accent-color: #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;
|
||||
--accent-color: theme("colors.indigo.500");
|
||||
--accent-light-color: theme("colors.indigo.400");
|
||||
--accent-dark-color: theme("colors.indigo.600");
|
||||
--accent-contrast-color: theme("colors.white");
|
||||
--gradient-from-color: theme("colors.indigo.400");
|
||||
--gradient-via-color: theme("colors.indigo.500");
|
||||
--gradient-to-color: theme("colors.indigo.600");
|
||||
}
|
||||
|
||||
@mixin purple-theme {
|
||||
--accent-color: #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;
|
||||
--accent-color: theme("colors.purple.500");
|
||||
--accent-light-color: theme("colors.purple.400");
|
||||
--accent-dark-color: theme("colors.purple.600");
|
||||
--accent-contrast-color: theme("colors.white");
|
||||
--gradient-from-color: theme("colors.purple.400");
|
||||
--gradient-via-color: theme("colors.purple.500");
|
||||
--gradient-to-color: theme("colors.purple.600");
|
||||
}
|
||||
|
||||
@mixin yellow-theme {
|
||||
--accent-color: #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;
|
||||
--accent-color: theme("colors.amber.500");
|
||||
--accent-light-color: theme("colors.amber.400");
|
||||
--accent-dark-color: theme("colors.amber.600");
|
||||
--accent-contrast-color: theme("colors.white");
|
||||
--gradient-from-color: theme("colors.amber.400");
|
||||
--gradient-via-color: theme("colors.amber.500");
|
||||
--gradient-to-color: theme("colors.amber.600");
|
||||
}
|
||||
|
||||
@mixin orange-theme {
|
||||
--accent-color: #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;
|
||||
--accent-color: theme("colors.orange.500");
|
||||
--accent-light-color: theme("colors.orange.400");
|
||||
--accent-dark-color: theme("colors.orange.600");
|
||||
--accent-contrast-color: theme("colors.white");
|
||||
--gradient-from-color: theme("colors.orange.400");
|
||||
--gradient-via-color: theme("colors.orange.500");
|
||||
--gradient-to-color: theme("colors.orange.600");
|
||||
}
|
||||
|
||||
@mixin red-theme {
|
||||
--accent-color: #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;
|
||||
--accent-color: theme("colors.red.500");
|
||||
--accent-light-color: theme("colors.red.400");
|
||||
--accent-dark-color: theme("colors.red.600");
|
||||
--accent-contrast-color: theme("colors.white");
|
||||
--gradient-from-color: theme("colors.red.400");
|
||||
--gradient-via-color: theme("colors.red.500");
|
||||
--gradient-to-color: theme("colors.red.600");
|
||||
}
|
||||
|
||||
@mixin pink-theme {
|
||||
--accent-color: #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;
|
||||
--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");
|
||||
}
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
@mixin base-theme {
|
||||
--font-sans: "Inter Variable", sans-serif;
|
||||
--font-icon: "Material Symbols Rounded Variable";
|
||||
--font-mono: "Roboto Mono Variable", monospace;
|
||||
--font-size-body: 0.75rem;
|
||||
--font-size-tiny: 0.688rem;
|
||||
--font-size-tiny: 0.625rem;
|
||||
--line-height-body: 1rem;
|
||||
--upper-primary-sticky-fold: 4.125rem;
|
||||
--upper-secondary-sticky-fold: 6.188rem;
|
||||
--upper-tertiary-sticky-fold: 8.25rem;
|
||||
--upper-fourth-sticky-fold: 10.2rem;
|
||||
--upper-mobile-primary-sticky-fold: 6.625rem;
|
||||
--upper-mobile-secondary-sticky-fold: 8.688rem;
|
||||
--upper-mobile-sticky-fold: 10.75rem;
|
||||
--upper-mobile-primary-sticky-fold: 6.75rem;
|
||||
--upper-mobile-secondary-sticky-fold: 8.813rem;
|
||||
--upper-mobile-sticky-fold: 10.875rem;
|
||||
--upper-mobile-tertiary-sticky-fold: 8.25rem;
|
||||
--lower-primary-sticky-fold: 3rem;
|
||||
--lower-secondary-sticky-fold: 5.063rem;
|
||||
@@ -20,62 +19,122 @@
|
||||
--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: #262626;
|
||||
--primary-contrast-color: #171717;
|
||||
--primary-dark-color: theme("colors.neutral.800");
|
||||
--primary-contrast-color: theme("colors.neutral.900");
|
||||
|
||||
--secondary-color: #a3a3a3;
|
||||
--secondary-light-color: #737373;
|
||||
--secondary-dark-color: #fafafa;
|
||||
--secondary-color: theme("colors.neutral.400");
|
||||
--secondary-light-color: theme("colors.neutral.500");
|
||||
--secondary-dark-color: theme("colors.zinc.50");
|
||||
|
||||
--divider-color: #262626;
|
||||
--divider-color: #1f1f1f;
|
||||
--divider-light-color: #1f1f1f;
|
||||
--divider-dark-color: #2d2d2d;
|
||||
--divider-dark-color: theme("colors.zinc.800");
|
||||
|
||||
--error-color: #292524;
|
||||
--tooltip-color: #f5f5f5;
|
||||
--banner-info-color: theme("colors.stone.800");
|
||||
--banner-warning-color: theme("colors.yellow.800");
|
||||
--banner-error-color: theme("colors.red.800");
|
||||
|
||||
--tooltip-color: theme("colors.neutral.100");
|
||||
--popover-color: #1b1b1b;
|
||||
|
||||
--method-get-color: theme("colors.emerald.500");
|
||||
--method-post-color: theme("colors.yellow.500");
|
||||
--method-put-color: theme("colors.sky.500");
|
||||
--method-patch-color: theme("colors.violet.500");
|
||||
--method-delete-color: theme("colors.rose.500");
|
||||
--method-head-color: theme("colors.teal.500");
|
||||
--method-options-color: theme("colors.indigo.500");
|
||||
--method-default-color: theme("colors.neutral.500");
|
||||
|
||||
--status-info-color: theme("colors.blue.500");
|
||||
--status-success-color: theme("colors.green.500");
|
||||
--status-redirect-color: theme("colors.amber.500");
|
||||
--status-critical-error-color: theme("colors.red.500");
|
||||
--status-server-error-color: theme("colors.rose.500");
|
||||
--status-missing-data-color: theme("colors.slate.500");
|
||||
|
||||
--editor-theme: "merbivore_soft";
|
||||
}
|
||||
|
||||
@mixin 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: #171717;
|
||||
--primary-light-color: theme("colors.neutral.900");
|
||||
--primary-dark-color: #181818;
|
||||
--primary-contrast-color: #0f0f0f;
|
||||
|
||||
--secondary-color: #a3a3a3;
|
||||
--secondary-light-color: #737373;
|
||||
--secondary-dark-color: #f5f5f5;
|
||||
--secondary-color: theme("colors.neutral.400");
|
||||
--secondary-light-color: theme("colors.neutral.500");
|
||||
--secondary-dark-color: theme("colors.neutral.50");
|
||||
|
||||
--divider-color: #1c1c1e;
|
||||
--divider-light-color: #181818;
|
||||
--divider-dark-color: #323232;
|
||||
--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");
|
||||
|
||||
--error-color: #1c1917;
|
||||
--tooltip-color: #f5f5f5;
|
||||
--popover-color: #0f0f0f;
|
||||
--editor-theme: "twilight";
|
||||
}
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
@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 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 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 dark-editor-theme {
|
||||
--editor-type-color: theme("colors.violet.400");
|
||||
--editor-name-color: theme("colors.blue.400");
|
||||
--editor-operator-color: theme("colors.indigo.400");
|
||||
--editor-invalid-color: theme("colors.red.400");
|
||||
--editor-separator-color: theme("colors.gray.400");
|
||||
--editor-meta-color: theme("colors.gray.400");
|
||||
--editor-variable-color: theme("colors.emerald.400");
|
||||
--editor-link-color: theme("colors.cyan.400");
|
||||
--editor-process-color: theme("colors.fuchsia.400");
|
||||
--editor-constant-color: theme("colors.violet.400");
|
||||
--editor-keyword-color: theme("colors.pink.400");
|
||||
}
|
||||
|
||||
@mixin black-editor-theme {
|
||||
--editor-type-color: #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;
|
||||
--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");
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"connect": "Connect",
|
||||
"connecting": "Connecting",
|
||||
"copy": "Copy",
|
||||
"create": "Create",
|
||||
"delete": "Delete",
|
||||
"disconnect": "Disconnect",
|
||||
"dismiss": "Dismiss",
|
||||
@@ -40,6 +41,7 @@
|
||||
"scroll_to_top": "Scroll to top",
|
||||
"search": "Search",
|
||||
"send": "Send",
|
||||
"share": "Share",
|
||||
"start": "Start",
|
||||
"starting": "Starting",
|
||||
"stop": "Stop",
|
||||
@@ -78,6 +80,7 @@
|
||||
"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.",
|
||||
@@ -93,6 +96,7 @@
|
||||
"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",
|
||||
@@ -187,6 +191,7 @@
|
||||
"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.",
|
||||
@@ -228,7 +233,8 @@
|
||||
"profile": "Login to view your profile",
|
||||
"protocols": "Protocols are empty",
|
||||
"schema": "Connect to a GraphQL endpoint to view schema",
|
||||
"shortcodes": "Shortcodes are empty",
|
||||
"shared_requests_logout": "Login to view your shared requests or create a new one",
|
||||
"shared_requests": "Shared requests are empty",
|
||||
"subscription": "Subscriptions are empty",
|
||||
"team_name": "Team name empty",
|
||||
"teams": "You don't belong to any teams",
|
||||
@@ -268,6 +274,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.",
|
||||
@@ -303,7 +312,8 @@
|
||||
"create_secret_gist": "Create secret Gist",
|
||||
"gist_created": "Gist created",
|
||||
"require_github": "Login with GitHub to create secret gist",
|
||||
"title": "Export"
|
||||
"title": "Export",
|
||||
"failed": "Something went wrong while exporting"
|
||||
},
|
||||
"filter": {
|
||||
"all": "All",
|
||||
@@ -340,8 +350,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 seem to be offline. Data in this workspace might not be up to date.",
|
||||
"offline_short": "You seem to be offline.",
|
||||
"offline": "You're using Hoppscotch offline. Updates will sync when you're online, based on workspace settings.",
|
||||
"offline_short": "You're using Hoppscotch 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.",
|
||||
@@ -370,6 +380,7 @@
|
||||
"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",
|
||||
@@ -377,7 +388,14 @@
|
||||
"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"
|
||||
"title": "Import",
|
||||
"hoppscotch_environment": "Hoppscotch Environment",
|
||||
"hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
|
||||
"postman_environment": "Postman Environment",
|
||||
"postman_environment_description": "Import Postman Environment JSON file",
|
||||
"environments_from_gist": "Import From Gist",
|
||||
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
|
||||
"gql_collections_from_gist_description": "Import GraphQL Collections From Gist"
|
||||
},
|
||||
"inspections": {
|
||||
"description": "Inspect possible errors",
|
||||
@@ -414,7 +432,9 @@
|
||||
"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": {
|
||||
@@ -490,7 +510,6 @@
|
||||
"structured": "Structured",
|
||||
"text": "Text"
|
||||
},
|
||||
"copy_link": "Copy link",
|
||||
"different_collection": "Cannot reorder requests from different collections",
|
||||
"duplicated": "Request duplicated",
|
||||
"duration": "Duration",
|
||||
@@ -523,6 +542,7 @@
|
||||
"saved": "Request saved",
|
||||
"share": "Share",
|
||||
"share_description": "Share Hoppscotch with your friends",
|
||||
"share_request": "Share Request",
|
||||
"stop": "Stop",
|
||||
"title": "Request",
|
||||
"type": "Request type",
|
||||
@@ -603,14 +623,31 @@
|
||||
"additional": "Additional Settings",
|
||||
"verify_email": "Verify email"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "Actions",
|
||||
"created_on": "Created on",
|
||||
"deleted": "Shortcode deleted",
|
||||
"method": "Method",
|
||||
"not_found": "Shortcode not found",
|
||||
"short_code": "Short code",
|
||||
"url": "URL"
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"shortcut": {
|
||||
"general": {
|
||||
@@ -640,7 +677,6 @@
|
||||
"title": "Others"
|
||||
},
|
||||
"request": {
|
||||
"copy_request_link": "Copy Request Link",
|
||||
"delete_method": "Select DELETE method",
|
||||
"get_method": "Select GET method",
|
||||
"head_method": "Select HEAD method",
|
||||
@@ -656,6 +692,7 @@
|
||||
"save_to_collections": "Save to Collections",
|
||||
"send_request": "Send Request",
|
||||
"show_code": "Generate code snippet",
|
||||
"share_request": "Share Request",
|
||||
"title": "Request"
|
||||
},
|
||||
"response": {
|
||||
@@ -780,6 +817,7 @@
|
||||
"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",
|
||||
@@ -838,6 +876,7 @@
|
||||
"queries": "Queries",
|
||||
"query": "Query",
|
||||
"schema": "Schema",
|
||||
"shared_requests": "Shared Requests",
|
||||
"socketio": "Socket.IO",
|
||||
"sse": "SSE",
|
||||
"tests": "Tests",
|
||||
|
||||
@@ -22,45 +22,41 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.1.0",
|
||||
"@codemirror/autocomplete": "^6.10.2",
|
||||
"@codemirror/autocomplete": "^6.11.0",
|
||||
"@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.0",
|
||||
"@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.1.4",
|
||||
"@urql/core": "^4.1.1",
|
||||
"@lezer/highlight": "1.2.0",
|
||||
"@unhead/vue": "^1.8.8",
|
||||
"@urql/core": "^4.2.0",
|
||||
"@urql/devtools": "^2.0.3",
|
||||
"@urql/exchange-auth": "^2.1.6",
|
||||
"@urql/exchange-graphcache": "^6.3.2",
|
||||
"@urql/exchange-graphcache": "^6.3.3",
|
||||
"@vitejs/plugin-legacy": "^4.1.1",
|
||||
"@vueuse/core": "^10.3.0",
|
||||
"@vueuse/head": "^1.3.1",
|
||||
"acorn-walk": "^8.2.0",
|
||||
"axios": "^1.4.0",
|
||||
"@vueuse/core": "^10.6.1",
|
||||
"acorn-walk": "^8.3.0",
|
||||
"axios": "^1.6.2",
|
||||
"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.0",
|
||||
"graphql-language-service-interface": "^2.9.1",
|
||||
"graphql": "^16.8.1",
|
||||
"graphql-language-service-interface": "^2.10.2",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"httpsnippet": "^3.0.1",
|
||||
"insomnia-importers": "^3.6.0",
|
||||
@@ -68,14 +64,15 @@
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonpath-plus": "^7.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lossless-json": "^2.0.11",
|
||||
"minisearch": "^6.1.0",
|
||||
"lossless-json": "^3.0.2",
|
||||
"minisearch": "^6.3.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"paho-mqtt": "^1.1.0",
|
||||
"path": "^0.12.7",
|
||||
"postman-collection": "^4.2.0",
|
||||
"postman-collection": "^4.3.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",
|
||||
@@ -89,19 +86,19 @@
|
||||
"tern": "^0.24.3",
|
||||
"timers": "^0.1.1",
|
||||
"tippy.js": "^6.3.7",
|
||||
"url": "^0.11.1",
|
||||
"url": "^0.11.3",
|
||||
"util": "^0.12.5",
|
||||
"uuid": "^9.0.0",
|
||||
"verzod": "^0.2.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-pdf-embed": "^1.1.6",
|
||||
"vue-router": "^4.2.4",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "^3.3.8",
|
||||
"vue-i18n": "^9.7.1",
|
||||
"vue-pdf-embed": "^1.2.1",
|
||||
"vue-router": "^4.2.5",
|
||||
"vue-tippy": "6.3.1",
|
||||
"vuedraggable-es": "^4.1.1",
|
||||
"wonka": "^6.3.4",
|
||||
"workbox-window": "^7.0.0",
|
||||
"xml-formatter": "^3.5.0",
|
||||
"xml-formatter": "^3.6.0",
|
||||
"yargs-parser": "^21.1.1",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
@@ -113,57 +110,58 @@
|
||||
"@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": "^2.4.5",
|
||||
"@graphql-codegen/urql-introspection": "^2.2.1",
|
||||
"@graphql-codegen/typescript-urql-graphcache": "^3.0.0",
|
||||
"@graphql-codegen/urql-introspection": "^3.0.0",
|
||||
"@graphql-typed-document-node/core": "^3.2.0",
|
||||
"@iconify-json/lucide": "^1.1.119",
|
||||
"@iconify-json/lucide": "^1.1.141",
|
||||
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
||||
"@relmify/jest-fp-ts": "^2.1.1",
|
||||
"@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",
|
||||
"@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",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.47.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-vue": "^9.18.1",
|
||||
"glob": "^10.3.10",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"openapi-types": "^12.1.3",
|
||||
"postcss": "^8.4.23",
|
||||
"prettier-plugin-tailwindcss": "^0.5.6",
|
||||
"rollup-plugin-polyfill-node": "^0.12.0",
|
||||
"sass": "^1.66.0",
|
||||
"prettier": "^3.1.0",
|
||||
"prettier-plugin-tailwindcss": "^0.5.7",
|
||||
"rollup-plugin-polyfill-node": "^0.13.0",
|
||||
"sass": "^1.69.5",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"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-fonts": "^0.6.0",
|
||||
"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-html-config": "^1.0.11",
|
||||
"vite-plugin-inspect": "^0.7.38",
|
||||
"vite-plugin-inspect": "^0.7.42",
|
||||
"vite-plugin-pages": "^0.31.0",
|
||||
"vite-plugin-pages-sitemap": "^1.6.1",
|
||||
"vite-plugin-pwa": "^0.16.4",
|
||||
"vite-plugin-pwa": "^0.17.0",
|
||||
"vite-plugin-vue-layouts": "^0.8.0",
|
||||
"vitest": "^0.34.2",
|
||||
"vue-tsc": "^1.8.8"
|
||||
"vitest": "^0.34.6",
|
||||
"vue-tsc": "^1.8.22"
|
||||
}
|
||||
}
|
||||
}
|
||||
15
packages/hoppscotch-common/src/components.d.ts
vendored
15
packages/hoppscotch-common/src/components.d.ts
vendored
@@ -61,6 +61,7 @@ declare module 'vue' {
|
||||
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']
|
||||
@@ -108,6 +109,7 @@ 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']
|
||||
@@ -160,6 +162,8 @@ declare module 'vue' {
|
||||
IconLucideRss: typeof import('~icons/lucide/rss')['default']
|
||||
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']
|
||||
InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default']
|
||||
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
|
||||
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
||||
@@ -183,6 +187,16 @@ 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']
|
||||
@@ -203,6 +217,7 @@ 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']
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
<template>
|
||||
<div
|
||||
:role="bannerRole"
|
||||
class="flex items-center px-4 py-2 text-tiny"
|
||||
class="flex items-center justify-between px-4 py-2 text-tiny text-secondaryDark"
|
||||
:class="bannerColor"
|
||||
>
|
||||
<component :is="bannerIcon" class="mr-2 text-white" />
|
||||
|
||||
<span class="text-white">
|
||||
<span v-if="banner.alternateText" class="md:hidden">
|
||||
{{ banner.alternateText(t) }}
|
||||
</span>
|
||||
<span :class="banner.alternateText ? '<md:hidden' : ''">
|
||||
<div class="flex items-center">
|
||||
<component :is="bannerIcon" class="mr-2" />
|
||||
<span :class="{ 'hidden sm:inline-flex': banner.alternateText }">
|
||||
{{ banner.text(t) }}
|
||||
</span>
|
||||
</span>
|
||||
<span v-if="banner.alternateText" class="inline-flex sm:hidden">
|
||||
{{ banner.alternateText(t) }}
|
||||
</span>
|
||||
</div>
|
||||
<icon-lucide-x
|
||||
v-if="dismissible"
|
||||
class="opacity-50 hover:cursor-pointer hover:opacity-100"
|
||||
@click="emit('dismiss')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -26,22 +30,32 @@ import IconAlertCircle from "~icons/lucide/alert-circle"
|
||||
import IconAlertTriangle from "~icons/lucide/alert-triangle"
|
||||
import IconInfo from "~icons/lucide/info"
|
||||
|
||||
const props = defineProps<{
|
||||
banner: BannerContent
|
||||
}>()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
banner: BannerContent
|
||||
dismissible?: boolean
|
||||
}>(),
|
||||
{
|
||||
dismissible: false,
|
||||
}
|
||||
)
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "dismiss"): void
|
||||
}>()
|
||||
|
||||
const ariaRoles: Record<BannerType, string> = {
|
||||
error: "alert",
|
||||
warning: "status",
|
||||
info: "status",
|
||||
warning: "status",
|
||||
error: "alert",
|
||||
}
|
||||
|
||||
const bgColors: Record<BannerType, string> = {
|
||||
error: "bg-red-700",
|
||||
warning: "bg-yellow-700",
|
||||
info: "bg-stone-800",
|
||||
info: "bg-bannerInfo",
|
||||
warning: "bg-bannerWarning",
|
||||
error: "bg-bannerError",
|
||||
}
|
||||
|
||||
const icons = {
|
||||
|
||||
@@ -2,25 +2,27 @@
|
||||
<div>
|
||||
<header
|
||||
ref="headerRef"
|
||||
class="flex flex-1 flex-shrink-0 items-center justify-between space-x-2 overflow-x-auto overflow-y-hidden px-2 py-2"
|
||||
class="grid grid-cols-5 grid-rows-1 gap-2 overflow-x-auto overflow-y-hidden p-2"
|
||||
@mousedown.prevent="platform.ui?.appHeader?.onHeaderAreaClick?.()"
|
||||
>
|
||||
<div
|
||||
class="inline-flex flex-1 items-center justify-start space-x-2"
|
||||
class="col-span-2 flex items-center justify-between space-x-2"
|
||||
:style="{
|
||||
paddingTop: platform.ui?.appHeader?.paddingTop?.value,
|
||||
paddingLeft: platform.ui?.appHeader?.paddingLeft?.value,
|
||||
}"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||
:label="t('app.name')"
|
||||
to="/"
|
||||
/>
|
||||
<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>
|
||||
</div>
|
||||
<div class="inline-flex flex-1 items-center justify-center space-x-2">
|
||||
<div class="col-span-1 flex items-center justify-between space-x-2">
|
||||
<button
|
||||
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"
|
||||
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"
|
||||
@click="invokeAction('modals.search.toggle')"
|
||||
>
|
||||
<span class="inline-flex flex-1 items-center">
|
||||
@@ -32,192 +34,189 @@
|
||||
<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="inline-flex flex-1 items-center justify-end space-x-2">
|
||||
<div
|
||||
v-if="currentUser === null"
|
||||
class="inline-flex items-center space-x-2"
|
||||
>
|
||||
<div class="col-span-2 flex items-center justify-between space-x-2">
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
: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')"
|
||||
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()"
|
||||
/>
|
||||
<HoppButtonPrimary
|
||||
:label="t('header.login')"
|
||||
@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')"
|
||||
/>
|
||||
</div>
|
||||
<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 class="flex">
|
||||
<div
|
||||
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"
|
||||
v-if="currentUser === null"
|
||||
class="inline-flex items-center space-x-2"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
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()"
|
||||
: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')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
<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
|
||||
v-if="
|
||||
workspace.type === 'team' &&
|
||||
selectedTeam &&
|
||||
selectedTeam?.myRole === 'OWNER'
|
||||
selectedTeam.teamMembers.length > 1
|
||||
"
|
||||
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()"
|
||||
:team-members="selectedTeam.teamMembers"
|
||||
show-count
|
||||
class="mx-2"
|
||||
@handle-click="handleTeamEdit()"
|
||||
/>
|
||||
</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">
|
||||
<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>
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
:on-shown="() => accountActions.focus()"
|
||||
>
|
||||
<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'
|
||||
"
|
||||
/>
|
||||
<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>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
ref="accountActions"
|
||||
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()"
|
||||
>
|
||||
<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()"
|
||||
/>
|
||||
<WorkspaceSelector />
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<AppBanner v-if="bannerContent" :banner="bannerContent" />
|
||||
<AppBanner
|
||||
v-if="bannerContent"
|
||||
:banner="bannerContent"
|
||||
:dismissible="true"
|
||||
@dismiss="dismissOfflineBanner"
|
||||
/>
|
||||
<TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" />
|
||||
<TeamsInvite
|
||||
v-if="workspace.type === 'team' && workspace.teamID"
|
||||
@@ -233,7 +232,6 @@
|
||||
@invite-team="inviteTeam(editingTeamName, editingTeamID)"
|
||||
@refetch-teams="refetchTeams"
|
||||
/>
|
||||
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmRemove"
|
||||
:title="t('confirm.remove_team')"
|
||||
@@ -293,7 +291,7 @@ const bannerContent = computed(() => banner.content.value?.content)
|
||||
let bannerID: number | null = null
|
||||
|
||||
const offlineBanner: BannerContent = {
|
||||
type: "info",
|
||||
type: "warning",
|
||||
text: (t) => t("helpers.offline"),
|
||||
alternateText: (t) => t("helpers.offline_short"),
|
||||
score: BANNER_PRIORITY_HIGH,
|
||||
@@ -314,6 +312,8 @@ watch(isOnline, () => {
|
||||
}
|
||||
})
|
||||
|
||||
const dismissOfflineBanner = () => banner.removeBanner(bannerID!)
|
||||
|
||||
const currentUser = useReadonlyStream(
|
||||
platform.auth.getProbableUserStream(),
|
||||
platform.auth.getProbableUser()
|
||||
|
||||
@@ -92,9 +92,8 @@ const getHighestSeverity = computed(() => {
|
||||
},
|
||||
{ severity: 0 }
|
||||
)
|
||||
} else {
|
||||
return { severity: 0 }
|
||||
}
|
||||
return { severity: 0 }
|
||||
})
|
||||
|
||||
const severityColor = (severity: number) => {
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
>
|
||||
<component
|
||||
:is="entry.icon"
|
||||
class="svg-icons opacity-50"
|
||||
:class="{ 'opacity-100': active }"
|
||||
class="svg-icons opacity-80"
|
||||
:class="{ 'opacity-25': 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) => {
|
||||
return SPECIAL_KEY_CHARS[key] ?? capitalize(key)
|
||||
})
|
||||
props.entry.meta?.keyboardShortcut?.map(
|
||||
(key) => SPECIAL_KEY_CHARS[key] ?? capitalize(key)
|
||||
)
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
autocomplete="off"
|
||||
name="command"
|
||||
:placeholder="`${t('app.type_a_command_search')}`"
|
||||
class="flex flex-1 bg-transparent px-6 py-5 text-base text-secondaryDark"
|
||||
class="flex flex-1 bg-transparent px-6 pt-5 pb-3 text-base text-secondaryDark"
|
||||
/>
|
||||
<HoppSmartSpinner v-if="searchSession?.loading" class="mr-6" />
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
else return IconFolder
|
||||
return IconFolder
|
||||
})
|
||||
|
||||
const collectionName = computed(() => {
|
||||
if ((props.data as HoppCollection<HoppRESTRequest>).name)
|
||||
return (props.data as HoppCollection<HoppRESTRequest>).name
|
||||
else return (props.data as TeamCollection).title
|
||||
return (props.data as TeamCollection).title
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -424,9 +424,8 @@ const isCollLoading = computed(() => {
|
||||
props.data.id
|
||||
) {
|
||||
return collectionMoveLoading.includes(props.data.id)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const resetDragState = () => {
|
||||
|
||||
@@ -1,361 +1,568 @@
|
||||
<template>
|
||||
<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>
|
||||
<ImportExportBase
|
||||
ref="collections-import-export"
|
||||
modal-title="modal.collections"
|
||||
:importer-modules="importerModules"
|
||||
:exporter-modules="exporterModules"
|
||||
@hide-modal="emit('hide-modal')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconArrowLeft from "~icons/lucide/arrow-left"
|
||||
import IconDownload from "~icons/lucide/download"
|
||||
import IconGithub from "~icons/lucide/github"
|
||||
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 { 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 { RESTCollectionImporters } from "~/helpers/import-export/import/importers"
|
||||
import { StepReturnValue } from "~/helpers/import-export/steps"
|
||||
import MyCollectionImport from "~/components/importExport/ImportExportSteps/MyCollectionImport.vue"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
|
||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||
import IconOpenAPI from "~icons/lucide/file"
|
||||
import IconPostman from "~icons/hopp/postman"
|
||||
import IconInsomnia from "~icons/hopp/insomnia"
|
||||
import IconGithub from "~icons/lucide/github"
|
||||
import IconLink from "~icons/lucide/link"
|
||||
|
||||
import IconUser from "~icons/lucide/user"
|
||||
import { useReadonlyStream } from "~/composables/stream"
|
||||
|
||||
import { getTeamCollectionJSON } from "~/helpers/backend/helpers"
|
||||
|
||||
import { platform } from "~/platform"
|
||||
|
||||
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 toast = useToast()
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
type CollectionType = "team-collections" | "my-collections"
|
||||
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
|
||||
|
||||
type CollectionType =
|
||||
| {
|
||||
type: "team-collections"
|
||||
selectedTeam: SelectedTeam
|
||||
}
|
||||
| { type: "my-collections" }
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: true,
|
||||
},
|
||||
collectionsType: {
|
||||
type: String as PropType<CollectionType>,
|
||||
default: "my-collections",
|
||||
type: Object as PropType<CollectionType>,
|
||||
default: () => ({
|
||||
type: "my-collections",
|
||||
selectedTeam: undefined,
|
||||
}),
|
||||
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 importerAction = async (stepResults: StepReturnValue[]) => {
|
||||
if (!importerModule.value) return
|
||||
const showImportFailedError = () => {
|
||||
toast.error(t("import.failed"))
|
||||
}
|
||||
|
||||
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)
|
||||
const handleImportToStore = async (
|
||||
collections: HoppCollection<HoppRESTRequest>[]
|
||||
) => {
|
||||
const importResult =
|
||||
props.collectionsType.type === "my-collections"
|
||||
? await importToPersonalWorkspace(collections)
|
||||
: await importToTeamsWorkspace(collections)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_COLLECTION",
|
||||
importer: importerModule.value!.name,
|
||||
platform: "rest",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
if (E.isRight(importResult)) {
|
||||
toast.success(t("state.file_imported"))
|
||||
emit("hide-modal")
|
||||
} else {
|
||||
toast.error(t("import.failed"))
|
||||
}
|
||||
}
|
||||
|
||||
fileImported()
|
||||
}
|
||||
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 finishImport = async () => {
|
||||
await importerAction(stepResults.value)
|
||||
const HoppTeamCollectionsExporter: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "hopp_team_collections",
|
||||
name: "export.as_json",
|
||||
title: "export.as_json_description",
|
||||
icon: IconUser,
|
||||
disabled: false,
|
||||
applicableTo: ["team-workspace"],
|
||||
isLoading: isHoppTeamCollectionExporterInProgress,
|
||||
},
|
||||
action: async () => {
|
||||
isHoppTeamCollectionExporterInProgress.value = true
|
||||
|
||||
if (
|
||||
props.collectionsType.type === "team-collections" &&
|
||||
props.collectionsType.selectedTeam
|
||||
) {
|
||||
const res = await teamCollectionsExporter(
|
||||
props.collectionsType.selectedTeam.id
|
||||
)
|
||||
|
||||
if (E.isRight(res)) {
|
||||
const { exportCollectionsToJSON } = res.right
|
||||
|
||||
if (!JSON.parse(exportCollectionsToJSON).length) {
|
||||
isHoppTeamCollectionExporterInProgress.value = false
|
||||
|
||||
return toast.error(t("error.no_collections_to_export"))
|
||||
}
|
||||
|
||||
initializeDownloadCollection(
|
||||
exportCollectionsToJSON,
|
||||
"team-collections"
|
||||
)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "json",
|
||||
platform: "rest",
|
||||
})
|
||||
} else {
|
||||
toast.error(res.left.error.toString())
|
||||
}
|
||||
}
|
||||
|
||||
isHoppTeamCollectionExporterInProgress.value = false
|
||||
},
|
||||
}
|
||||
|
||||
const onFileChange = () => {
|
||||
stepResults.value = []
|
||||
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 inputFileToImport = inputChooseFileToImportFrom.value[0]
|
||||
const collectionJSON = await getCollectionJSON()
|
||||
const accessToken = currentUser.value?.accessToken
|
||||
|
||||
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())
|
||||
if (!accessToken) {
|
||||
toast.error(t("error.something_went_wrong"))
|
||||
isHoppGistCollectionExporterInProgress.value = false
|
||||
return
|
||||
}
|
||||
|
||||
stepResults.value.push(content)
|
||||
hasFile.value = !!content?.length
|
||||
if (E.isRight(collectionJSON)) {
|
||||
collectionsGistExporter(collectionJSON.right, accessToken)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "gist",
|
||||
platform: "rest",
|
||||
})
|
||||
}
|
||||
|
||||
isHoppGistCollectionExporterInProgress.value = false
|
||||
},
|
||||
}
|
||||
|
||||
const importerModules = computed(() => {
|
||||
const enabledImporters = [
|
||||
HoppRESTImporter,
|
||||
HoppMyCollectionImporter,
|
||||
HoppOpenAPIImporter,
|
||||
HoppPostmanImporter,
|
||||
HoppInsomniaImporter,
|
||||
HoppGistImporter,
|
||||
]
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
reader.readAsText(inputFileToImport.files[0])
|
||||
}
|
||||
return enabledExporters.filter((exporter) => {
|
||||
return exporter.metadata.applicableTo.includes(
|
||||
props.collectionsType.type === "my-collections"
|
||||
? "personal-workspace"
|
||||
: "team-workspace"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
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 hasTeamWriteAccess = computed(() => {
|
||||
const { collectionsType } = props
|
||||
|
||||
const resetImport = () => {
|
||||
importerType.value = null
|
||||
hasFile.value = false
|
||||
hasGist.value = false
|
||||
stepResults.value = []
|
||||
inputChooseFileToImportFrom.value = ""
|
||||
inputChooseGistToImportFrom.value = ""
|
||||
mySelectedCollectionID.value = undefined
|
||||
const isTeamCollection = collectionsType.type === "team-collections"
|
||||
|
||||
if (!isTeamCollection || !collectionsType.selectedTeam) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
collectionsType.selectedTeam.myRole === "EDITOR" ||
|
||||
collectionsType.selectedTeam.myRole === "OWNER"
|
||||
)
|
||||
})
|
||||
|
||||
const selectedTeamID = computed(() => {
|
||||
const { collectionsType } = props
|
||||
|
||||
return collectionsType.type === "team-collections"
|
||||
? collectionsType.selectedTeam?.id
|
||||
: undefined
|
||||
})
|
||||
|
||||
const myCollections = useReadonlyStream(restCollections$, [])
|
||||
|
||||
const getCollectionJSON = async () => {
|
||||
if (
|
||||
props.collectionsType.type === "team-collections" &&
|
||||
props.collectionsType.selectedTeam?.id
|
||||
) {
|
||||
const res = await getTeamCollectionJSON(
|
||||
props.collectionsType.selectedTeam?.id
|
||||
)
|
||||
|
||||
return E.isRight(res)
|
||||
? E.right(res.right.exportCollectionsToJSON)
|
||||
: E.left(res.left)
|
||||
}
|
||||
|
||||
if (props.collectionsType.type === "my-collections") {
|
||||
return E.right(JSON.stringify(myCollections.value, null, 2))
|
||||
}
|
||||
|
||||
return E.left("INVALID_SELECTED_TEAM_OR_INVALID_COLLECTION_TYPE")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -222,6 +222,12 @@
|
||||
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,
|
||||
@@ -460,6 +466,12 @@ const emit = defineEmits<{
|
||||
isActive: boolean
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "share-request",
|
||||
payload: {
|
||||
request: HoppRESTRequest
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "drop-request",
|
||||
payload: {
|
||||
@@ -526,13 +538,12 @@ 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)
|
||||
@@ -729,11 +740,10 @@ class MyCollectionsAdapter implements SmartTreeAdapter<MyCollectionNode> {
|
||||
status: "loaded",
|
||||
data: data,
|
||||
} as ChildrenResult<Folder | Requests>
|
||||
} else {
|
||||
return {
|
||||
status: "loaded",
|
||||
data: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: "loaded",
|
||||
data: [],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,8 +28,7 @@
|
||||
>
|
||||
<span
|
||||
class="pointer-events-none flex w-16 items-center justify-center truncate px-2"
|
||||
:class="requestLabelColor"
|
||||
:style="{ color: requestLabelColor }"
|
||||
:style="{ color: getMethodLabelColorClassOf(request) }"
|
||||
>
|
||||
<component
|
||||
:is="IconCheckCircle"
|
||||
@@ -94,6 +93,7 @@
|
||||
@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,6 +133,18 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="shareAction"
|
||||
:icon="IconShare2"
|
||||
:label="t('action.share')"
|
||||
:shortcut="['S']"
|
||||
@click="
|
||||
() => {
|
||||
emit('share-request')
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
@@ -162,6 +174,7 @@ 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"
|
||||
@@ -240,6 +253,7 @@ 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
|
||||
@@ -250,6 +264,7 @@ 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)
|
||||
@@ -261,10 +276,6 @@ const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
|
||||
parentID: "",
|
||||
})
|
||||
|
||||
const requestLabelColor = computed(() =>
|
||||
getMethodLabelColorClassOf(props.request)
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.duplicateLoading,
|
||||
(val) => {
|
||||
@@ -363,9 +374,8 @@ 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 = () => {
|
||||
|
||||
@@ -141,9 +141,8 @@ 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)
|
||||
@@ -480,21 +479,20 @@ const getErrorMessage = (err: GQLError<string>) => {
|
||||
console.error(err)
|
||||
if (err.type === "network_error") {
|
||||
return t("error.network_error")
|
||||
} 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")
|
||||
}
|
||||
}
|
||||
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>
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
class="!rounded-none"
|
||||
:icon="IconPlus"
|
||||
:title="t('team.no_access')"
|
||||
:label="t('add.new')"
|
||||
:label="t('action.new')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:icon="IconPlus"
|
||||
:label="t('add.new')"
|
||||
:label="t('action.new')"
|
||||
class="!rounded-none"
|
||||
@click="emit('display-modal-add')"
|
||||
/>
|
||||
@@ -240,6 +240,12 @@
|
||||
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,
|
||||
@@ -473,6 +479,12 @@ const emit = defineEmits<{
|
||||
folderPath?: string | undefined
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "share-request",
|
||||
payload: {
|
||||
request: HoppRESTRequest
|
||||
}
|
||||
): void
|
||||
(
|
||||
event: "drop-request",
|
||||
payload: {
|
||||
@@ -542,13 +554,12 @@ 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)
|
||||
@@ -714,82 +725,78 @@ class TeamCollectionsAdapter implements SmartTreeAdapter<TeamCollectionNode> {
|
||||
return {
|
||||
status: "loading",
|
||||
}
|
||||
} else {
|
||||
const data = this.data.value.map((item, index) => ({
|
||||
id: item.id,
|
||||
}
|
||||
const data = this.data.value.map((item, index) => ({
|
||||
id: item.id,
|
||||
data: {
|
||||
isLastItem: index === this.data.value.length - 1,
|
||||
type: "collections",
|
||||
data: {
|
||||
isLastItem: index === this.data.value.length - 1,
|
||||
type: "collections",
|
||||
data: {
|
||||
parentIndex: null,
|
||||
data: item,
|
||||
},
|
||||
parentIndex: null,
|
||||
data: item,
|
||||
},
|
||||
}))
|
||||
return {
|
||||
status: "loaded",
|
||||
data: cloneDeep(data),
|
||||
} as ChildrenResult<TeamCollections>
|
||||
}
|
||||
} else {
|
||||
const parsedID = id.split("/")[id.split("/").length - 1]
|
||||
},
|
||||
}))
|
||||
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)
|
||||
!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: [],
|
||||
}
|
||||
}
|
||||
if (props.teamLoadingCollections.includes(parsedID)) {
|
||||
return {
|
||||
status: "loading",
|
||||
}
|
||||
}
|
||||
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: [],
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
else return IconFolder
|
||||
return IconFolder
|
||||
})
|
||||
|
||||
const pick = () => {
|
||||
|
||||
@@ -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
|
||||
else return IconFolder
|
||||
return IconFolder
|
||||
})
|
||||
|
||||
const pick = () => {
|
||||
|
||||
@@ -1,299 +1,227 @@
|
||||
<template>
|
||||
<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>
|
||||
<ImportExportBase
|
||||
ref="collections-import-export"
|
||||
modal-title="graphql_collections.title"
|
||||
:importer-modules="importerModules"
|
||||
:exporter-modules="exporterModules"
|
||||
@hide-modal="emit('hide-modal')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import axios from "axios"
|
||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||
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 IconFolderPlus from "~icons/lucide/folder-plus"
|
||||
import IconDownload from "~icons/lucide/download"
|
||||
import IconGithub from "~icons/lucide/github"
|
||||
import { computed, ref } from "vue"
|
||||
import IconUser from "~icons/lucide/user"
|
||||
import { initializeDownloadCollection } from "~/helpers/import-export/export"
|
||||
import { useReadonlyStream } from "~/composables/stream"
|
||||
|
||||
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"
|
||||
|
||||
defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
const collections = useReadonlyStream(graphqlCollections$, [])
|
||||
const toast = useToast()
|
||||
|
||||
const currentUser = useReadonlyStream(
|
||||
platform.auth.getCurrentUserStream(),
|
||||
platform.auth.getCurrentUser()
|
||||
)
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
const inputChooseFileToImportFrom = ref<HTMLInputElement>()
|
||||
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)
|
||||
|
||||
const collectionJson = computed(() => {
|
||||
return JSON.stringify(collections.value, null, 2)
|
||||
})
|
||||
|
||||
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",
|
||||
},
|
||||
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"
|
||||
)
|
||||
|
||||
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 fileImported = () => {
|
||||
toast.success(t("state.file_imported").toString())
|
||||
}
|
||||
|
||||
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())
|
||||
if (E.isLeft(message)) {
|
||||
toast.error(t("export.failed"))
|
||||
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)
|
||||
toast.success(message.right)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_COLLECTION",
|
||||
importer: "json",
|
||||
workspaceType: "personal",
|
||||
platform: "gql",
|
||||
})
|
||||
|
||||
fileImported()
|
||||
}
|
||||
reader.readAsText(inputChooseFileToImportFrom.value.files[0])
|
||||
inputChooseFileToImportFrom.value.value = ""
|
||||
}
|
||||
|
||||
const exportJSON = async () => {
|
||||
const dataToWrite = collectionJson.value
|
||||
|
||||
const parsedCollections = JSON.parse(dataToWrite)
|
||||
|
||||
if (!parsedCollections.length) {
|
||||
return toast.error(t("error.no_collections_to_export"))
|
||||
}
|
||||
|
||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
||||
const url = URL.createObjectURL(file)
|
||||
|
||||
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
const result = await platform.io.saveFileWithDialog({
|
||||
data: dataToWrite,
|
||||
contentType: "application/json",
|
||||
suggestedFilename: filename,
|
||||
filters: [
|
||||
{
|
||||
name: "Hoppscotch Collection JSON file",
|
||||
extensions: ["json"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (result.type === "unknown" || result.type === "saved") {
|
||||
platform?.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "json",
|
||||
platform: "gql",
|
||||
exporter: "json",
|
||||
})
|
||||
|
||||
toast.success(t("state.download_started").toString())
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const GqlCollectionsGistExporter: ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: "export.as_gist",
|
||||
name: "export.create_secret_gist",
|
||||
title: !currentUser
|
||||
? "export.require_github"
|
||||
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
currentUser.provider !== "github.com"
|
||||
? `export.require_github`
|
||||
: "export.create_secret_gist",
|
||||
icon: IconUser,
|
||||
disabled: !currentUser.value
|
||||
? true
|
||||
: currentUser.value.provider !== "github.com",
|
||||
applicableTo: ["personal-workspace"],
|
||||
},
|
||||
action: async () => {
|
||||
if (!currentUser.value) {
|
||||
toast.error(t("profile.no_permission"))
|
||||
return
|
||||
}
|
||||
|
||||
const accessToken = currentUser.value?.accessToken
|
||||
|
||||
if (accessToken) {
|
||||
const res = await gqlCollectionsGistExporter(
|
||||
JSON.stringify(gqlCollections.value),
|
||||
accessToken
|
||||
)
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
toast.error(t("export.failed"))
|
||||
return
|
||||
}
|
||||
|
||||
toast.success(t("export.success"))
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
platform: "gql",
|
||||
exporter: "gist",
|
||||
})
|
||||
|
||||
window.open(res.right, "_blank")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const importerModules = [GqlCollectionsHoppImporter, GqlCollectionsGistImporter]
|
||||
|
||||
const exporterModules = computed(() => {
|
||||
const modules = [GqlCollectionsHoppExporter]
|
||||
|
||||
if (platform.platformFeatureFlags.exportAsGIST) {
|
||||
modules.push(GqlCollectionsGistExporter)
|
||||
}
|
||||
|
||||
return modules
|
||||
})
|
||||
|
||||
const showImportFailedError = () => {
|
||||
toast.error(t("import.failed"))
|
||||
}
|
||||
|
||||
const handleImportToStore = async (
|
||||
gqlCollections: HoppCollection<HoppGQLRequest>[]
|
||||
) => {
|
||||
setGraphqlCollections(gqlCollections)
|
||||
toast.success(t("import.success"))
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): () => void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -137,7 +137,7 @@
|
||||
@hide-modal="displayModalEditRequest(false)"
|
||||
/>
|
||||
<CollectionsGraphqlImportExport
|
||||
:show="showModalImportExport"
|
||||
v-if="showModalImportExport"
|
||||
@hide-modal="displayModalImportExport(false)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
@dragend="draggingToRoot = false"
|
||||
>
|
||||
<div
|
||||
class="sticky z-10 flex flex-shrink-0 flex-col overflow-x-auto border-b border-dividerLight bg-primary"
|
||||
class="sticky z-10 flex flex-shrink-0 flex-col overflow-x-auto bg-primary border-b border-dividerLight"
|
||||
: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 h-8 w-full bg-transparent p-4 py-2"
|
||||
class="flex w-full bg-transparent px-4 py-2"
|
||||
:placeholder="t('action.search')"
|
||||
:disabled="collectionsType.type === 'team-collections'"
|
||||
/>
|
||||
@@ -41,6 +41,7 @@
|
||||
@export-data="exportData"
|
||||
@remove-collection="removeCollection"
|
||||
@remove-folder="removeFolder"
|
||||
@share-request="shareRequest"
|
||||
@drop-collection="dropCollection"
|
||||
@update-request-order="updateRequestOrder"
|
||||
@update-collection-order="updateCollectionOrder"
|
||||
@@ -71,6 +72,7 @@
|
||||
@export-data="exportData"
|
||||
@remove-collection="removeCollection"
|
||||
@remove-folder="removeFolder"
|
||||
@share-request="shareRequest"
|
||||
@edit-request="editRequest"
|
||||
@duplicate-request="duplicateRequest"
|
||||
@remove-request="removeRequest"
|
||||
@@ -138,17 +140,13 @@
|
||||
@hide-modal="showConfirmModal = false"
|
||||
@resolve="resolveConfirmModal"
|
||||
/>
|
||||
|
||||
<CollectionsImportExport
|
||||
: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"
|
||||
v-if="showModalImportExport"
|
||||
:collections-type="collectionsType"
|
||||
@hide-modal="displayModalImportExport(false)"
|
||||
/>
|
||||
|
||||
<TeamsAdd
|
||||
:show="showTeamModalAdd"
|
||||
@hide-modal="displayTeamModalAdd(false)"
|
||||
@@ -197,7 +195,6 @@ import {
|
||||
createChildCollection,
|
||||
renameCollection,
|
||||
deleteCollection,
|
||||
importJSONToTeam,
|
||||
moveRESTTeamCollection,
|
||||
updateOrderRESTTeamCollection,
|
||||
} from "~/helpers/backend/mutations/TeamCollection"
|
||||
@@ -212,12 +209,9 @@ 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,
|
||||
@@ -229,7 +223,7 @@ import {
|
||||
resetTeamRequestsContext,
|
||||
} from "~/helpers/collection/collection"
|
||||
import { currentReorderingStatus$ } from "~/newstore/reordering"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
import { useService } from "dioc/vue"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
@@ -303,12 +297,6 @@ 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)
|
||||
@@ -412,14 +400,12 @@ const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
|
||||
})
|
||||
|
||||
const hasTeamWriteAccess = computed(() => {
|
||||
if (!collectionsType.value.selectedTeam) return false
|
||||
if (collectionsType.value.type !== "team-collections") {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
collectionsType.value.type === "team-collections" &&
|
||||
collectionsType.value.selectedTeam.myRole !== "VIEWER"
|
||||
)
|
||||
return true
|
||||
else return false
|
||||
const role = collectionsType.value.selectedTeam?.myRole
|
||||
return role === "OWNER" || role === "EDITOR"
|
||||
})
|
||||
|
||||
const filteredCollections = computed(() => {
|
||||
@@ -1069,7 +1055,7 @@ const onRemoveCollection = () => {
|
||||
const collectionIndex = editingCollectionIndex.value
|
||||
|
||||
const collectionToRemove =
|
||||
collectionIndex || collectionIndex == 0
|
||||
collectionIndex || collectionIndex === 0
|
||||
? navigateToFolderWithIndexPath(restCollectionStore.value.state, [
|
||||
collectionIndex,
|
||||
])
|
||||
@@ -1468,9 +1454,8 @@ const checkIfCollectionIsAParentOfTheChildren = (
|
||||
)
|
||||
if (isEqual(slicedDestinationCollectionPath, collectionDraggedPath)) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
@@ -1491,9 +1476,8 @@ const isMoveToSameLocation = (
|
||||
|
||||
if (isEqual(draggedItemParentPathArr, destinationPathArr)) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1673,25 +1657,22 @@ const isSameSameParent = (
|
||||
const dragedItemParent = draggedItemIndex.slice(0, -1)
|
||||
|
||||
return dragedItemParent.join("/") === destinationCollectionIndex
|
||||
} 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)) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
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)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1833,33 +1814,6 @@ 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.
|
||||
@@ -1928,88 +1882,15 @@ const exportData = async (
|
||||
}
|
||||
}
|
||||
|
||||
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"))
|
||||
const shareRequest = ({ request }: { request: HoppRESTRequest }) => {
|
||||
if (currentUser.value) {
|
||||
// opens the share request modal
|
||||
invokeAction("share.request", {
|
||||
request,
|
||||
})
|
||||
} else {
|
||||
invokeAction("modals.login.toggle")
|
||||
}
|
||||
|
||||
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) => {
|
||||
@@ -2041,37 +1922,36 @@ const getErrorMessage = (err: GQLError<string>) => {
|
||||
console.error(err)
|
||||
if (err.type === "network_error") {
|
||||
return t("error.network_error")
|
||||
} 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")
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
212
packages/hoppscotch-common/src/components/embeds/index.vue
Normal file
212
packages/hoppscotch-common/src/components/embeds/index.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<div class="flex flex-1 flex-col">
|
||||
<header
|
||||
class="flex flex-1 flex-shrink-0 items-center justify-between space-x-2 overflow-x-auto overflow-y-hidden px-2 py-2"
|
||||
>
|
||||
<div class="flex flex-1 items-center justify-between space-x-2">
|
||||
<HoppButtonSecondary
|
||||
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||
:label="t('app.name')"
|
||||
to="https://hoppscotch.io/"
|
||||
blank
|
||||
/>
|
||||
<div class="flex">
|
||||
<HoppSmartItem
|
||||
:label="t('app.open_in_hoppscotch')"
|
||||
:to="sharedRequestURL"
|
||||
blank
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="flex-1">
|
||||
<div
|
||||
class="flex-none flex-shrink-0 bg-primary p-4 sm:flex sm:flex-shrink-0 sm:space-x-2"
|
||||
>
|
||||
<div
|
||||
class="min-w-52 flex flex-1 whitespace-nowrap rounded border border-divider"
|
||||
>
|
||||
<div class="relative flex">
|
||||
<span
|
||||
class="flex justify-center items-center w-26 cursor-pointer rounded-l bg-primaryLight px-4 py-2 font-semibold text-secondaryDark transition"
|
||||
>
|
||||
{{ tab.document.request.method }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-1 whitespace-nowrap rounded-r border-l border-divider bg-primaryLight transition"
|
||||
>
|
||||
<input
|
||||
name="method"
|
||||
:value="tab.document.request.endpoint"
|
||||
class="flex-1 px-4 bg-primary"
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex sm:mt-0">
|
||||
<HoppButtonPrimary
|
||||
id="send"
|
||||
:title="`${t(
|
||||
'action.send'
|
||||
)} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
|
||||
:label="`${!loading ? t('action.send') : t('action.cancel')}`"
|
||||
class="min-w-20 flex-1"
|
||||
@click="!loading ? newSendRequest() : cancelRequest()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:title="`${t(
|
||||
'request.save'
|
||||
)} <kbd>${getSpecialKey()}</kbd><kbd>S</kbd>`"
|
||||
:label="t('request.save')"
|
||||
filled
|
||||
:icon="IconSave"
|
||||
class="flex-1 rounded rounded-r-none"
|
||||
blank
|
||||
:to="sharedRequestURL"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<HttpRequestOptions
|
||||
v-model="tab.document.request"
|
||||
v-model:option-tab="selectedOptionTab"
|
||||
:properties="properties"
|
||||
/>
|
||||
|
||||
<HttpResponse :document="tab.document" :is-embed="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Ref } from "vue"
|
||||
import { computed, useModel } from "vue"
|
||||
import { ref } from "vue"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { useStreamSubscriber } from "~/composables/stream"
|
||||
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||
import { runRESTRequest$ } from "~/helpers/RequestRunner"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||
import IconSave from "~icons/lucide/save"
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const props = defineProps<{
|
||||
modelTab: HoppTab<HoppRESTDocument>
|
||||
properties: string[]
|
||||
sharedRequestID: string
|
||||
}>()
|
||||
|
||||
const tab = useModel(props, "modelTab")
|
||||
|
||||
const selectedOptionTab = ref(props.properties[0])
|
||||
|
||||
const requestCancelFunc: Ref<(() => void) | null> = ref(null)
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const baseURL = import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
|
||||
const sharedRequestURL = computed(() => {
|
||||
return `${baseURL}/r/${props.sharedRequestID}`
|
||||
})
|
||||
|
||||
const { subscribeToStream } = useStreamSubscriber()
|
||||
|
||||
const newSendRequest = async () => {
|
||||
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
|
||||
toast.error(`${t("empty.endpoint")}`)
|
||||
return
|
||||
}
|
||||
|
||||
ensureMethodInEndpoint()
|
||||
|
||||
loading.value = true
|
||||
|
||||
const [cancel, streamPromise] = runRESTRequest$(tab)
|
||||
const streamResult = await streamPromise
|
||||
|
||||
requestCancelFunc.value = cancel
|
||||
if (E.isRight(streamResult)) {
|
||||
subscribeToStream(
|
||||
streamResult.right,
|
||||
(responseState) => {
|
||||
if (loading.value) {
|
||||
// Check exists because, loading can be set to false
|
||||
// when cancelled
|
||||
updateRESTResponse(responseState)
|
||||
}
|
||||
},
|
||||
() => {
|
||||
loading.value = false
|
||||
},
|
||||
() => {
|
||||
// TODO: Change this any to a proper type
|
||||
const result = (streamResult.right as any).value
|
||||
if (
|
||||
result.type === "network_fail" &&
|
||||
result.error?.error === "NO_PW_EXT_HOOK"
|
||||
) {
|
||||
const errorResponse: HoppRESTResponse = {
|
||||
type: "extension_error",
|
||||
error: result.error.humanMessage.heading,
|
||||
component: result.error.component,
|
||||
req: result.req,
|
||||
}
|
||||
updateRESTResponse(errorResponse)
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
)
|
||||
} else {
|
||||
loading.value = false
|
||||
toast.error(`${t("error.script_fail")}`)
|
||||
let error: Error
|
||||
if (typeof streamResult.left === "string") {
|
||||
error = { name: "RequestFailure", message: streamResult.left }
|
||||
} else {
|
||||
error = streamResult.left
|
||||
}
|
||||
updateRESTResponse({
|
||||
type: "script_fail",
|
||||
error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const updateRESTResponse = (response: HoppRESTResponse | null) => {
|
||||
tab.value.document.response = response
|
||||
}
|
||||
|
||||
const newEndpoint = computed(() => {
|
||||
return tab.value.document.request.endpoint
|
||||
})
|
||||
|
||||
const ensureMethodInEndpoint = () => {
|
||||
if (
|
||||
!/^http[s]?:\/\//.test(newEndpoint.value) &&
|
||||
!newEndpoint.value.startsWith("<<")
|
||||
) {
|
||||
const domain = newEndpoint.value.split(/[/:#?]+/)[0]
|
||||
if (domain === "localhost" || /([0-9]+\.)*[0-9]/.test(domain)) {
|
||||
tab.value.document.request.endpoint =
|
||||
"http://" + tab.value.document.request.endpoint
|
||||
} else {
|
||||
tab.value.document.request.endpoint =
|
||||
"https://" + tab.value.document.request.endpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cancelRequest = () => {
|
||||
loading.value = false
|
||||
requestCancelFunc.value?.()
|
||||
|
||||
updateRESTResponse(null)
|
||||
}
|
||||
</script>
|
||||
@@ -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-10 font-semibold">{{
|
||||
<label for="name" class="min-w-[2.5rem] 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-10 font-semibold">{{
|
||||
<label for="value" class="min-w-[2.5rem] 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-10 font-semibold">
|
||||
<label for="scope" class="min-w-[2.5rem] 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-18" />
|
||||
<div class="min-w-[4rem]" />
|
||||
<HoppSmartCheckbox
|
||||
:on="replaceWithVariable"
|
||||
title="t('environment.replace_with_variable'))"
|
||||
:title="t('environment.replace_with_variable')"
|
||||
@change="replaceWithVariable = !replaceWithVariable"
|
||||
/>
|
||||
<label for="replaceWithVariable">
|
||||
@@ -205,15 +205,14 @@ const addEnvironment = async () => {
|
||||
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")
|
||||
case "Forbidden resource":
|
||||
return t("profile.no_permission")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
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>
|
||||
|
||||
@@ -1,154 +1,60 @@
|
||||
<template>
|
||||
<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>
|
||||
<ImportExportBase
|
||||
ref="collections-import-export"
|
||||
modal-title="environment.title"
|
||||
:importer-modules="importerModules"
|
||||
:exporter-modules="exporterModules"
|
||||
@hide-modal="emit('hide-modal')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||
import IconDownload from "~icons/lucide/download"
|
||||
import IconGithub from "~icons/lucide/github"
|
||||
import { computed, ref } from "vue"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { platform } from "~/platform"
|
||||
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 { 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 { TippyComponent } from "vue-tippy"
|
||||
import { CreateTeamEnvironmentMutation } from "~/helpers/backend/graphql"
|
||||
import { postmanEnvImporter } from "~/helpers/import-export/import/postmanEnv"
|
||||
|
||||
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 { platform } from "~/platform"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
teamEnvironments?: TeamEnvironment[]
|
||||
teamId?: string | undefined
|
||||
environmentType: "MY_ENV" | "TEAM_ENV"
|
||||
}>()
|
||||
|
||||
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()
|
||||
)
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
const inputChooseFileToImportFrom = ref<HTMLInputElement>()
|
||||
const isTeamEnvironment = computed(() => {
|
||||
return props.environmentType === "TEAM_ENV"
|
||||
})
|
||||
|
||||
const environmentJson = computed(() => {
|
||||
if (
|
||||
@@ -158,266 +64,249 @@ const environmentJson = computed(() => {
|
||||
const teamEnvironments = props.teamEnvironments.map(
|
||||
(x) => x.environment as Environment
|
||||
)
|
||||
return JSON.stringify(teamEnvironments, null, 2)
|
||||
} else {
|
||||
return JSON.stringify(myEnvironments.value, null, 2)
|
||||
return teamEnvironments
|
||||
}
|
||||
|
||||
return myEnvironments.value
|
||||
})
|
||||
|
||||
const createEnvironmentGist = async () => {
|
||||
if (!currentUser.value) {
|
||||
toast.error(t("profile.no_permission").toString())
|
||||
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)()
|
||||
|
||||
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",
|
||||
},
|
||||
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 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"
|
||||
)
|
||||
|
||||
toast.success(t("export.gist_created").toString())
|
||||
if (E.isLeft(message)) {
|
||||
toast.error(t(message.left))
|
||||
return
|
||||
}
|
||||
|
||||
toast.success(t(message.right))
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_ENVIRONMENT",
|
||||
platform: "rest",
|
||||
})
|
||||
|
||||
window.open(res.data.html_url)
|
||||
} catch (e) {
|
||||
toast.error(t("error.something_went_wrong").toString())
|
||||
console.error(e)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const fileImported = () => {
|
||||
toast.success(t("state.file_imported").toString())
|
||||
}
|
||||
|
||||
const failedImport = () => {
|
||||
toast.error(t("import.failed").toString())
|
||||
}
|
||||
|
||||
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[]) => {
|
||||
loading.value = true
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||
platform: "rest",
|
||||
workspaceType: "team",
|
||||
})
|
||||
|
||||
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 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())
|
||||
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 environments = JSON.parse(content)
|
||||
const accessToken = currentUser.value?.accessToken
|
||||
|
||||
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
|
||||
if (accessToken) {
|
||||
const res = await environmentsGistExporter(
|
||||
JSON.stringify(environmentJson.value),
|
||||
accessToken
|
||||
)
|
||||
|
||||
if (E.isLeft(res)) {
|
||||
toast.error(t("export.failed"))
|
||||
return
|
||||
}
|
||||
importFromHoppscotch(environments)
|
||||
} else {
|
||||
failedImport()
|
||||
}
|
||||
}
|
||||
|
||||
reader.readAsText(inputChooseFileToImportFrom.value.files[0])
|
||||
inputChooseFileToImportFrom.value.value = ""
|
||||
toast.success(t("export.success"))
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_ENVIRONMENT",
|
||||
platform: "rest",
|
||||
})
|
||||
|
||||
window.open(res.right, "_blank")
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const importFromHoppscotch = (environments: Environment[]) => {
|
||||
const importerModules = [
|
||||
HoppEnvironmentsImport,
|
||||
EnvironmentsImportFromGIST,
|
||||
PostmanEnvironmentsImport,
|
||||
]
|
||||
|
||||
const exporterModules = computed(() => {
|
||||
const enabledExporters = [HoppEnvironmentsExport]
|
||||
|
||||
if (platform.platformFeatureFlags.exportAsGIST) {
|
||||
enabledExporters.push(HoppEnvironmentsGistExporter)
|
||||
}
|
||||
|
||||
return enabledExporters
|
||||
})
|
||||
|
||||
const showImportFailedError = () => {
|
||||
toast.error(t("import.failed").toString())
|
||||
}
|
||||
|
||||
const handleImportToStore = async (environments: Environment[]) => {
|
||||
if (props.environmentType === "MY_ENV") {
|
||||
appendEnvironments(environments)
|
||||
fileImported()
|
||||
toast.success(t("state.file_imported"))
|
||||
} else {
|
||||
importToTeams(environments)
|
||||
await 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]
|
||||
const importToTeams = async (content: Environment[]) => {
|
||||
const envImportPromises: Promise<
|
||||
E.Either<GQLError<"">, CreateTeamEnvironmentMutation>
|
||||
>[] = []
|
||||
|
||||
importFromHoppscotch(environments)
|
||||
}
|
||||
for (const [, env] of content.entries()) {
|
||||
const res = createTeamEnvironment(
|
||||
JSON.stringify(env.variables),
|
||||
props.teamId as string,
|
||||
env.name
|
||||
)()
|
||||
|
||||
const exportJSON = async () => {
|
||||
const dataToWrite = environmentJson.value
|
||||
|
||||
const parsedCollections = JSON.parse(dataToWrite)
|
||||
|
||||
if (!parsedCollections.length) {
|
||||
return toast.error(t("error.no_environments_to_export"))
|
||||
envImportPromises.push(res)
|
||||
}
|
||||
|
||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
||||
const url = URL.createObjectURL(file)
|
||||
const res = await Promise.all(envImportPromises)
|
||||
|
||||
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
const failedImports = res.some((r) => E.isLeft(r))
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
const result = await platform.io.saveFileWithDialog({
|
||||
data: dataToWrite,
|
||||
contentType: "application/json",
|
||||
suggestedFilename: filename,
|
||||
filters: [
|
||||
{
|
||||
name: "JSON file",
|
||||
extensions: ["json"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (result.type === "unknown" || result.type === "saved") {
|
||||
toast.success(t("state.download_started").toString())
|
||||
}
|
||||
}
|
||||
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
if (err.type === "network_error") {
|
||||
return t("error.network_error")
|
||||
if (failedImports) {
|
||||
toast.error(t("import.failed"))
|
||||
} else {
|
||||
switch (err.error) {
|
||||
case "team_environment/not_found":
|
||||
return t("team_environment.not_found")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
toast.success(t("import.success"))
|
||||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): () => void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -6,10 +6,9 @@
|
||||
theme="popover"
|
||||
:on-shown="() => envSelectorActions!.focus()"
|
||||
>
|
||||
<span
|
||||
<HoppSmartSelectWrapper
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="`${t('environment.select')}`"
|
||||
class="select-wrapper"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconLayers"
|
||||
@@ -22,7 +21,7 @@
|
||||
"
|
||||
class="flex-1 !justify-start rounded-none pr-8"
|
||||
/>
|
||||
</span>
|
||||
</HoppSmartSelectWrapper>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="envSelectorActions"
|
||||
@@ -207,10 +206,14 @@
|
||||
</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-32 w-1/4 truncate text-tiny font-semibold">
|
||||
<span
|
||||
class="min-w-[9rem] w-1/4 truncate text-tiny font-semibold"
|
||||
>
|
||||
{{ t("environment.name") }}
|
||||
</span>
|
||||
<span class="min-w-32 w-full truncate text-tiny font-semibold">
|
||||
<span
|
||||
class="min-w-[9rem] w-full truncate text-tiny font-semibold"
|
||||
>
|
||||
{{ t("environment.value") }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -219,10 +222,10 @@
|
||||
:key="index"
|
||||
class="flex flex-1 space-x-4"
|
||||
>
|
||||
<span class="min-w-32 w-1/4 truncate text-secondaryLight">
|
||||
<span class="min-w-[9rem] w-1/4 truncate text-secondaryLight">
|
||||
{{ variable.key }}
|
||||
</span>
|
||||
<span class="min-w-32 w-full truncate text-secondaryLight">
|
||||
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
|
||||
{{ variable.value }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -258,10 +261,14 @@
|
||||
</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-32 w-1/4 truncate text-tiny font-semibold">
|
||||
<span
|
||||
class="min-w-[9rem] w-1/4 truncate text-tiny font-semibold"
|
||||
>
|
||||
{{ t("environment.name") }}
|
||||
</span>
|
||||
<span class="min-w-32 w-full truncate text-tiny font-semibold">
|
||||
<span
|
||||
class="min-w-[9rem] w-full truncate text-tiny font-semibold"
|
||||
>
|
||||
{{ t("environment.value") }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -270,10 +277,10 @@
|
||||
:key="index"
|
||||
class="flex flex-1 space-x-4"
|
||||
>
|
||||
<span class="min-w-32 w-1/4 truncate text-secondaryLight">
|
||||
<span class="min-w-[9rem] w-1/4 truncate text-secondaryLight">
|
||||
{{ variable.key }}
|
||||
</span>
|
||||
<span class="min-w-32 w-full truncate text-secondaryLight">
|
||||
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
|
||||
{{ variable.value }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -446,12 +453,11 @@ 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,40 +502,36 @@ const selectedEnv = computed(() => {
|
||||
name: props.modelValue.environment.environment.name,
|
||||
teamEnvID: props.modelValue.environment.id,
|
||||
}
|
||||
} else {
|
||||
return { type: "global", name: "Global" }
|
||||
}
|
||||
} else {
|
||||
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) {
|
||||
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: "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) {
|
||||
return {
|
||||
type: "TEAM_ENV",
|
||||
name: teamEnv.environment.name,
|
||||
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
|
||||
variables: teamEnv.environment.variables,
|
||||
}
|
||||
}
|
||||
return { type: "NO_ENV_SELECTED" }
|
||||
}
|
||||
return { type: "NO_ENV_SELECTED" }
|
||||
})
|
||||
|
||||
// Set the selected environment as initial scope value
|
||||
@@ -577,13 +579,12 @@ const envQuickPeekActions = ref<TippyComponent | null>(null)
|
||||
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")
|
||||
}
|
||||
}
|
||||
switch (err.error) {
|
||||
case "team_environment/not_found":
|
||||
return t("team_environment.not_found")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,9 +593,8 @@ const globalEnvs = useReadonlyStream(globalEnv$, [])
|
||||
const environmentVariables = computed(() => {
|
||||
if (selectedEnv.value.variables) {
|
||||
return selectedEnv.value.variables
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const editGlobalEnv = () => {
|
||||
|
||||
@@ -198,9 +198,8 @@ const workingEnv = computed(() => {
|
||||
type: "MY_ENV",
|
||||
index: props.editingEnvironmentIndex,
|
||||
})
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const envList = useReadonlyStream(environments$, []) || props.envVars()
|
||||
@@ -226,12 +225,11 @@ const liveEnvs = computed(() => {
|
||||
return [
|
||||
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
||||
...globalVars.value.map((x) => ({ ...x, source: "Global" })),
|
||||
]
|
||||
}
|
||||
return [
|
||||
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
||||
...globalVars.value.map((x) => ({ ...x, source: "Global" })),
|
||||
]
|
||||
})
|
||||
|
||||
watch(
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
@hide-modal="displayModalEdit(false)"
|
||||
/>
|
||||
<EnvironmentsImportExport
|
||||
:show="showModalImportExport"
|
||||
v-if="showModalImportExport"
|
||||
environment-type="MY_ENV"
|
||||
@hide-modal="displayModalImportExport(false)"
|
||||
/>
|
||||
|
||||
@@ -205,11 +205,8 @@ const evnExpandError = computed(() => {
|
||||
const liveEnvs = computed(() => {
|
||||
if (evnExpandError.value) {
|
||||
return []
|
||||
} else {
|
||||
return [
|
||||
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
||||
]
|
||||
}
|
||||
return [...vars.value.map((x) => ({ ...x.env, source: editingName.value! }))]
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -338,13 +335,12 @@ const hideModal = () => {
|
||||
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")
|
||||
}
|
||||
}
|
||||
switch (err.error) {
|
||||
case "team_environment/not_found":
|
||||
return t("team_environment.not_found")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -184,13 +184,12 @@ const duplicateEnvironments = () => {
|
||||
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")
|
||||
}
|
||||
}
|
||||
switch (err.error) {
|
||||
case "team_environment/not_found":
|
||||
return t("team_environment.not_found")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
@hide-modal="displayModalEdit(false)"
|
||||
/>
|
||||
<EnvironmentsImportExport
|
||||
:show="showModalImportExport"
|
||||
v-if="showModalImportExport"
|
||||
:team-environments="teamEnvironments"
|
||||
:team-id="team?.id"
|
||||
environment-type="TEAM_ENV"
|
||||
@@ -174,13 +174,12 @@ const resetSelectedData = () => {
|
||||
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")
|
||||
}
|
||||
}
|
||||
switch (err.error) {
|
||||
case "team_environment/not_found":
|
||||
return t("team_environment.not_found")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<span class="select-wrapper">
|
||||
<HoppSmartSelectWrapper>
|
||||
<HoppButtonSecondary
|
||||
class="ml-2 rounded-none pr-8"
|
||||
:label="authName"
|
||||
/>
|
||||
</span>
|
||||
</HoppSmartSelectWrapper>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
@@ -171,7 +171,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="z-9 sticky top-upperTertiaryStickyFold h-full min-w-46 max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
|
||||
class="z-[9] sticky top-upperTertiaryStickyFold h-full min-w-[12rem] max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
|
||||
>
|
||||
<div class="pb-2 text-secondaryLight">
|
||||
{{ t("helpers.authorization") }}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
:title="`${t(
|
||||
'action.download_file'
|
||||
)} <kbd>${getSpecialKey()}</kbd><kbd>J</kbd>`"
|
||||
:icon="downloadResponseIcon"
|
||||
:icon="downloadIcon"
|
||||
@click="downloadResponse"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
@@ -33,9 +33,41 @@
|
||||
:title="`${t(
|
||||
'action.copy'
|
||||
)} <kbd>${getSpecialKey()}</kbd><kbd>.</kbd>`"
|
||||
:icon="copyResponseIcon"
|
||||
@click="copyResponse(response[0].data)"
|
||||
:icon="copyIcon"
|
||||
@click="copyResponse"
|
||||
/>
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => copyInterfaceTippyActions.focus()"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('app.copy_interface_type')"
|
||||
:icon="IconMore"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="copyInterfaceTippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
v-for="(language, index) in interfaceLanguages"
|
||||
:key="index"
|
||||
:label="language"
|
||||
:icon="
|
||||
copiedInterfaceLanguage === language
|
||||
? copyInterfaceIcon
|
||||
: IconCopy
|
||||
"
|
||||
@click="runCopyInterface(language)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="schemaEditor" class="flex flex-1 flex-col"></div>
|
||||
@@ -59,22 +91,22 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import IconDownload from "~icons/lucide/download"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import IconMore from "~icons/lucide/more-horizontal"
|
||||
import { computed, reactive, ref } from "vue"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { useCodemirror } from "@composables/codemirror"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import { GQLResponseEvent } from "~/helpers/graphql/connection"
|
||||
import { platform } from "~/platform"
|
||||
import interfaceLanguages from "~/helpers/utils/interfaceLanguages"
|
||||
import {
|
||||
useCopyInterface,
|
||||
useCopyResponse,
|
||||
useDownloadResponse,
|
||||
} from "~/composables/lens-actions"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -101,6 +133,7 @@ const responseString = computed(() => {
|
||||
})
|
||||
|
||||
const schemaEditor = ref<any | null>(null)
|
||||
const copyInterfaceTippyActions = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
useCodemirror(
|
||||
@@ -118,55 +151,29 @@ useCodemirror(
|
||||
})
|
||||
)
|
||||
|
||||
const downloadResponseIcon = refAutoReset<
|
||||
typeof IconDownload | typeof IconCheck
|
||||
>(IconDownload, 1000)
|
||||
const copyResponseIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
IconCopy,
|
||||
1000
|
||||
const { copyIcon, copyResponse } = useCopyResponse(responseString)
|
||||
const { copyInterfaceIcon, copyInterface } = useCopyInterface(responseString)
|
||||
const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||
"application/json",
|
||||
responseString
|
||||
)
|
||||
|
||||
const copyResponse = (str: string) => {
|
||||
copyToClipboard(str)
|
||||
copyResponseIcon.value = IconCheck
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
}
|
||||
const copiedInterfaceLanguage = ref("")
|
||||
|
||||
const downloadResponse = async (str: string) => {
|
||||
const dataToWrite = str
|
||||
const file = new Blob([dataToWrite!], { type: "application/json" })
|
||||
const url = URL.createObjectURL(file)
|
||||
|
||||
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
const result = await platform.io.saveFileWithDialog({
|
||||
data: dataToWrite,
|
||||
contentType: "application/json",
|
||||
suggestedFilename: filename,
|
||||
filters: [
|
||||
{
|
||||
name: "JSON file",
|
||||
extensions: ["json"],
|
||||
},
|
||||
],
|
||||
const runCopyInterface = (language: string) => {
|
||||
copyInterface(language).then(() => {
|
||||
copiedInterfaceLanguage.value = language
|
||||
})
|
||||
|
||||
if (result.type === "unknown" || result.type === "saved") {
|
||||
downloadResponseIcon.value = IconCheck
|
||||
toast.success(`${t("state.download_started")}`)
|
||||
}
|
||||
}
|
||||
|
||||
defineActionHandler(
|
||||
"response.file.download",
|
||||
() => downloadResponse(responseString.value),
|
||||
() => downloadResponse(),
|
||||
computed(() => !!props.response && props.response.length > 0)
|
||||
)
|
||||
defineActionHandler(
|
||||
"response.copy",
|
||||
() => copyResponse(responseString.value),
|
||||
() => copyResponse(),
|
||||
computed(() => !!props.response && props.response.length > 0)
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
v-model="graphqlFieldsFilterText"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
class="flex h-8 w-full bg-transparent p-4 py-2"
|
||||
class="flex w-full bg-transparent px-4 py-2"
|
||||
:placeholder="`${t('action.search')}`"
|
||||
/>
|
||||
<div class="flex">
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
>
|
||||
<span class="truncate px-2 leading-8">
|
||||
<span class="truncate">
|
||||
{{ tab.document.request.name }}
|
||||
</span>
|
||||
<template #content="{ hide }">
|
||||
|
||||
@@ -26,7 +26,7 @@ const isScalar = computed(() => {
|
||||
|
||||
function resolveRootType(type: GraphQLType) {
|
||||
let t = type as any
|
||||
while (t.ofType != null) t = t.ofType
|
||||
while (t.ofType !== null) t = t.ofType
|
||||
return t
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
v-model="filterText"
|
||||
type="search"
|
||||
autocomplete="off"
|
||||
class="flex h-8 w-full bg-transparent p-4 py-2"
|
||||
class="flex w-full bg-transparent px-4 py-2"
|
||||
:placeholder="`${t('action.search')}`"
|
||||
/>
|
||||
<div class="flex">
|
||||
|
||||
@@ -121,7 +121,8 @@ const duration = computed(() => {
|
||||
return responseDuration > 0
|
||||
? `${t("request.duration")}: ${responseDuration}ms`
|
||||
: t("error.no_duration")
|
||||
} else return t("error.no_duration")
|
||||
}
|
||||
return t("error.no_duration")
|
||||
})
|
||||
|
||||
const entryStatus = computed(() => {
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<span class="select-wrapper">
|
||||
<HoppSmartSelectWrapper>
|
||||
<HoppButtonSecondary
|
||||
class="ml-2 rounded-none pr-8"
|
||||
:label="authName"
|
||||
/>
|
||||
</span>
|
||||
</HoppSmartSelectWrapper>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
@@ -149,7 +149,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="z-9 sticky top-upperTertiaryStickyFold h-full min-w-46 max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
|
||||
class="z-[9] sticky top-upperTertiaryStickyFold h-full min-w-[12rem] max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
|
||||
>
|
||||
<div class="pb-2 text-secondaryLight">
|
||||
{{ t("helpers.authorization") }}
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<span class="select-wrapper">
|
||||
<HoppSmartSelectWrapper>
|
||||
<HoppButtonSecondary
|
||||
:label="body.contentType || t('state.none')"
|
||||
class="ml-2 rounded-none pr-8"
|
||||
/>
|
||||
</span>
|
||||
</HoppSmartSelectWrapper>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
|
||||
@@ -339,7 +339,7 @@ const deleteBodyParam = (index: number) => {
|
||||
}
|
||||
|
||||
workingParams.value = workingParams.value.filter(
|
||||
(_, arrIndex) => arrIndex != index
|
||||
(_, arrIndex) => arrIndex !== index
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
placement="bottom"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<span class="select-wrapper">
|
||||
<HoppSmartSelectWrapper>
|
||||
<HoppButtonSecondary
|
||||
:label="
|
||||
CodegenDefinitions.find((x) => x.name === codegenType)!.caption
|
||||
@@ -25,7 +25,7 @@
|
||||
outline
|
||||
class="flex-1 pr-8"
|
||||
/>
|
||||
</span>
|
||||
</HoppSmartSelectWrapper>
|
||||
<template #content="{ hide }">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<div class="sticky top-0 z-10 flex-shrink-0 overflow-x-auto">
|
||||
@@ -214,10 +214,9 @@ const requestCode = computed(() => {
|
||||
if (O.isSome(result)) {
|
||||
errorState.value = false
|
||||
return result.value
|
||||
} else {
|
||||
errorState.value = true
|
||||
return ""
|
||||
}
|
||||
errorState.value = true
|
||||
return ""
|
||||
})
|
||||
|
||||
// Template refs
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<div ref="preRequestEditor" class="h-full"></div>
|
||||
</div>
|
||||
<div
|
||||
class="z-9 sticky top-upperTertiaryStickyFold h-full min-w-46 max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
|
||||
class="z-[9] sticky top-upperTertiaryStickyFold h-full min-w-[12rem] max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
|
||||
>
|
||||
<div class="pb-2 text-secondaryLight">
|
||||
{{ t("helpers.pre_request_script") }}
|
||||
|
||||
@@ -126,19 +126,19 @@ const linewrapEnabled = ref(true)
|
||||
const rawBodyParameters = ref<any | null>(null)
|
||||
|
||||
const codemirrorValue: Ref<string | undefined> =
|
||||
typeof rawParamsBody.value == "string"
|
||||
typeof rawParamsBody.value === "string"
|
||||
? ref(rawParamsBody.value)
|
||||
: ref(undefined)
|
||||
|
||||
watch(rawParamsBody, (newVal) => {
|
||||
typeof newVal == "string"
|
||||
typeof newVal === "string"
|
||||
? (codemirrorValue.value = newVal)
|
||||
: (codemirrorValue.value = undefined)
|
||||
})
|
||||
|
||||
// propagate the edits from codemirror back to the body
|
||||
watch(codemirrorValue, (updatedValue) => {
|
||||
if (updatedValue && updatedValue != rawParamsBody.value) {
|
||||
if (updatedValue && updatedValue !== rawParamsBody.value) {
|
||||
rawParamsBody.value = updatedValue
|
||||
}
|
||||
})
|
||||
@@ -185,7 +185,7 @@ const prettifyRequestBody = () => {
|
||||
if (body.value.contentType.endsWith("json")) {
|
||||
const jsonObj = JSON.parse(rawParamsBody.value as string)
|
||||
prettifyBody = JSON.stringify(jsonObj, null, 2)
|
||||
} else if (body.value.contentType == "application/xml") {
|
||||
} else if (body.value.contentType === "application/xml") {
|
||||
prettifyBody = prettifyXML(rawParamsBody.value as string)
|
||||
}
|
||||
rawParamsBody.value = prettifyBody
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class="sticky top-0 z-20 flex-none flex-shrink-0 bg-primary p-4 sm:flex sm:flex-shrink-0 sm:space-x-2"
|
||||
>
|
||||
<div
|
||||
class="min-w-52 flex flex-1 whitespace-nowrap rounded border border-divider"
|
||||
class="min-w-[12rem] flex flex-1 whitespace-nowrap rounded border border-divider"
|
||||
>
|
||||
<div class="relative flex">
|
||||
<label for="method">
|
||||
@@ -13,7 +13,7 @@
|
||||
theme="popover"
|
||||
:on-shown="() => methodTippyActions.focus()"
|
||||
>
|
||||
<span class="select-wrapper">
|
||||
<HoppSmartSelectWrapper>
|
||||
<input
|
||||
id="method"
|
||||
class="flex w-26 cursor-pointer rounded-l bg-primaryLight px-4 py-2 font-semibold text-secondaryDark transition"
|
||||
@@ -22,7 +22,7 @@
|
||||
:placeholder="`${t('request.method')}`"
|
||||
@input="onSelectMethod($event)"
|
||||
/>
|
||||
</span>
|
||||
</HoppSmartSelectWrapper>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="methodTippyActions"
|
||||
@@ -34,6 +34,9 @@
|
||||
v-for="(method, index) in methods"
|
||||
:key="`method-${index}`"
|
||||
:label="method"
|
||||
:style="{
|
||||
color: getMethodLabelColor(method),
|
||||
}"
|
||||
@click="
|
||||
() => {
|
||||
updateMethod(method)
|
||||
@@ -67,7 +70,7 @@
|
||||
'action.send'
|
||||
)} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
|
||||
:label="`${!loading ? t('action.send') : t('action.cancel')}`"
|
||||
class="min-w-20 flex-1 rounded-r-none"
|
||||
class="min-w-[5rem] flex-1 rounded-r-none"
|
||||
@click="!loading ? newSendRequest() : cancelRequest()"
|
||||
/>
|
||||
<span class="flex">
|
||||
@@ -179,20 +182,16 @@
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="copyRequestAction"
|
||||
:label="shareButtonText"
|
||||
:icon="copyLinkIcon"
|
||||
:label="t('request.share_request')"
|
||||
:icon="IconShare2"
|
||||
:loading="fetchingShareLink"
|
||||
@click="
|
||||
() => {
|
||||
copyRequest()
|
||||
shareRequest()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
:icon="IconLink2"
|
||||
:label="`${t('request.view_my_links')}`"
|
||||
to="/profile"
|
||||
/>
|
||||
<hr />
|
||||
<HoppSmartItem
|
||||
ref="saveRequestAction"
|
||||
@@ -236,25 +235,20 @@ import { useI18n } from "@composables/i18n"
|
||||
import { useSetting } from "@composables/settings"
|
||||
import { useReadonlyStream, useStreamSubscriber } from "@composables/stream"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { refAutoReset, useVModel } from "@vueuse/core"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { Ref, computed, onBeforeUnmount, ref } from "vue"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
||||
import { runMutation } from "~/helpers/backend/GQLClient"
|
||||
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
|
||||
import { createShortcode } from "~/helpers/backend/mutations/Shortcode"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import { runRESTRequest$ } from "~/helpers/RequestRunner"
|
||||
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { editRESTRequest } from "~/newstore/collections"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconChevronDown from "~icons/lucide/chevron-down"
|
||||
import IconCode2 from "~icons/lucide/code-2"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import IconFileCode from "~icons/lucide/file-code"
|
||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||
import IconLink2 from "~icons/lucide/link-2"
|
||||
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
||||
import IconSave from "~icons/lucide/save"
|
||||
import IconShare2 from "~icons/lucide/share-2"
|
||||
@@ -268,6 +262,7 @@ import { InterceptorService } from "~/services/interceptor.service"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { getMethodLabelColor } from "~/helpers/rest/labelColoring"
|
||||
|
||||
const t = useI18n()
|
||||
const interceptorService = useService(InterceptorService)
|
||||
@@ -279,8 +274,8 @@ const methods = [
|
||||
"PATCH",
|
||||
"DELETE",
|
||||
"HEAD",
|
||||
"CONNECT",
|
||||
"OPTIONS",
|
||||
"CONNECT",
|
||||
"TRACE",
|
||||
"CUSTOM",
|
||||
]
|
||||
@@ -309,8 +304,6 @@ const showCurlImportModal = ref(false)
|
||||
const showCodegenModal = ref(false)
|
||||
const showSaveRequestModal = ref(false)
|
||||
|
||||
const hasNavigatorShare = !!navigator.share
|
||||
|
||||
// Template refs
|
||||
const methodTippyActions = ref<any | null>(null)
|
||||
const sendTippyActions = ref<any | null>(null)
|
||||
@@ -453,62 +446,20 @@ const updateRESTResponse = (response: HoppRESTResponse | null) => {
|
||||
tab.value.document.response = response
|
||||
}
|
||||
|
||||
const copyLinkIcon = refAutoReset<
|
||||
typeof IconShare2 | typeof IconCopy | typeof IconCheck
|
||||
>(hasNavigatorShare ? IconShare2 : IconCopy, 1000)
|
||||
const currentUser = useReadonlyStream(
|
||||
platform.auth.getCurrentUserStream(),
|
||||
platform.auth.getCurrentUser()
|
||||
)
|
||||
|
||||
const shareLink = ref<string | null>("")
|
||||
const fetchingShareLink = ref(false)
|
||||
|
||||
const shareButtonText = computed(() => {
|
||||
if (shareLink.value) {
|
||||
return shareLink.value
|
||||
} else if (fetchingShareLink.value) {
|
||||
return t("state.loading")
|
||||
} else {
|
||||
return t("request.copy_link")
|
||||
}
|
||||
})
|
||||
|
||||
const copyRequest = async () => {
|
||||
if (shareLink.value) {
|
||||
copyShareLink(shareLink.value)
|
||||
} else {
|
||||
shareLink.value = ""
|
||||
fetchingShareLink.value = true
|
||||
const shortcodeResult = await createShortcode(tab.value.document.request)()
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SHORTCODE_CREATED",
|
||||
})
|
||||
|
||||
if (E.isLeft(shortcodeResult)) {
|
||||
toast.error(`${shortcodeResult.left.error}`)
|
||||
shareLink.value = `${t("error.something_went_wrong")}`
|
||||
} else if (E.isRight(shortcodeResult)) {
|
||||
shareLink.value = `/${shortcodeResult.right.createShortcode.id}`
|
||||
copyShareLink(shareLink.value)
|
||||
}
|
||||
fetchingShareLink.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const copyShareLink = (shareLink: string) => {
|
||||
const link = `${
|
||||
import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
|
||||
}/r${shareLink}`
|
||||
if (navigator.share) {
|
||||
const time = new Date().toLocaleTimeString()
|
||||
const date = new Date().toLocaleDateString()
|
||||
navigator.share({
|
||||
title: "Hoppscotch",
|
||||
text: `Hoppscotch • Open source API development ecosystem at ${time} on ${date}`,
|
||||
url: link,
|
||||
const shareRequest = () => {
|
||||
if (currentUser.value) {
|
||||
invokeAction("share.request", {
|
||||
request: tab.value.document.request,
|
||||
})
|
||||
} else {
|
||||
copyLinkIcon.value = IconCheck
|
||||
copyToClipboard(link)
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
invokeAction("modals.login.toggle")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -611,7 +562,7 @@ defineActionHandler("request.send-cancel", () => {
|
||||
else cancelRequest()
|
||||
})
|
||||
defineActionHandler("request.reset", clearContent)
|
||||
defineActionHandler("request.copy-link", copyRequest)
|
||||
defineActionHandler("request.share-request", shareRequest)
|
||||
defineActionHandler("request.method.next", cycleDownMethod)
|
||||
defineActionHandler("request.method.prev", cycleUpMethod)
|
||||
defineActionHandler("request.save", saveRequest)
|
||||
|
||||
@@ -5,13 +5,18 @@
|
||||
render-inactive-tabs
|
||||
>
|
||||
<HoppSmartTab
|
||||
v-if="properties ? properties.includes('parameters') : true"
|
||||
:id="'params'"
|
||||
:label="`${t('tab.parameters')}`"
|
||||
:info="`${newActiveParamsCount$}`"
|
||||
>
|
||||
<HttpParameters v-model="request.params" />
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab :id="'bodyParams'" :label="`${t('tab.body')}`">
|
||||
<HoppSmartTab
|
||||
v-if="properties ? properties.includes('body') : true"
|
||||
:id="'bodyParams'"
|
||||
:label="`${t('tab.body')}`"
|
||||
>
|
||||
<HttpBody
|
||||
v-model:headers="request.headers"
|
||||
v-model:body="request.body"
|
||||
@@ -19,16 +24,22 @@
|
||||
/>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
v-if="properties ? properties.includes('headers') : true"
|
||||
:id="'headers'"
|
||||
:label="`${t('tab.headers')}`"
|
||||
:info="`${newActiveHeadersCount$}`"
|
||||
>
|
||||
<HttpHeaders v-model="request" @change-tab="changeOptionTab" />
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
|
||||
<HoppSmartTab
|
||||
v-if="properties ? properties.includes('authorization') : true"
|
||||
:id="'authorization'"
|
||||
:label="`${t('tab.authorization')}`"
|
||||
>
|
||||
<HttpAuthorization v-model="request.auth" />
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
v-if="properties ? properties.includes('preRequestScript') : true"
|
||||
:id="'preRequestScript'"
|
||||
:label="`${t('tab.pre_request_script')}`"
|
||||
:indicator="
|
||||
@@ -40,6 +51,7 @@
|
||||
<HttpPreRequestScript v-model="request.preRequestScript" />
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
v-if="properties ? properties.includes('tests') : true"
|
||||
:id="'tests'"
|
||||
:label="`${t('tab.tests')}`"
|
||||
:indicator="
|
||||
@@ -76,6 +88,7 @@ const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: HoppRESTRequest
|
||||
optionTab: RESTOptionTabs
|
||||
properties?: string[]
|
||||
}>(),
|
||||
{
|
||||
optionTab: "params",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="relative flex flex-1 flex-col">
|
||||
<HttpResponseMeta :response="doc.response" />
|
||||
<HttpResponseMeta :response="doc.response" :is-embed="isEmbed" />
|
||||
<LensesResponseBodyRenderer
|
||||
v-if="!loading && hasResponse"
|
||||
v-model:document="doc"
|
||||
@@ -15,6 +15,7 @@ import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||
|
||||
const props = defineProps<{
|
||||
document: HoppRESTDocument
|
||||
isEmbed: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -2,8 +2,20 @@
|
||||
<div
|
||||
class="sticky top-0 z-10 flex flex-shrink-0 items-center justify-center overflow-auto overflow-x-auto whitespace-nowrap bg-primary p-4"
|
||||
>
|
||||
<AppShortcutsPrompt v-if="response == null" class="flex-1" />
|
||||
<div v-else class="flex flex-1 flex-col">
|
||||
<AppShortcutsPrompt v-if="response == null && !isEmbed" class="flex-1" />
|
||||
|
||||
<div v-if="response == null && isEmbed">
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('app.documentation')}`"
|
||||
to="https://docs.hoppscotch.io/documentation/features/rest-api-testing#response"
|
||||
:icon="IconExternalLink"
|
||||
blank
|
||||
outline
|
||||
reverse
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="response" class="flex flex-1 flex-col">
|
||||
<div
|
||||
v-if="response.type === 'loading'"
|
||||
class="flex flex-col items-center justify-center"
|
||||
@@ -53,7 +65,12 @@
|
||||
<span v-if="response.statusCode">
|
||||
<span class="text-secondary"> {{ t("response.status") }}: </span>
|
||||
{{ `${response.statusCode}\xA0 • \xA0`
|
||||
}}{{ getStatusCodeReasonPhrase(response.statusCode) }}
|
||||
}}{{
|
||||
getStatusCodeReasonPhrase(
|
||||
response.statusCode,
|
||||
response.statusText
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span v-if="response.meta && response.meta.responseDuration">
|
||||
<span class="text-secondary"> {{ t("response.time") }}: </span>
|
||||
@@ -100,6 +117,7 @@ import { getStatusCodeReasonPhrase } from "~/helpers/utils/statusCodes"
|
||||
import { useService } from "dioc/vue"
|
||||
import { InspectionService } from "~/services/inspection"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import IconExternalLink from "~icons/lucide/external-link"
|
||||
|
||||
const t = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
@@ -107,6 +125,7 @@ const tabs = useService(RESTTabService)
|
||||
|
||||
const props = defineProps<{
|
||||
response: HoppRESTResponse | null | undefined
|
||||
isEmbed?: boolean
|
||||
}>()
|
||||
|
||||
/**
|
||||
|
||||
@@ -26,6 +26,13 @@
|
||||
>
|
||||
<History :page="'rest'" />
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'share-request'"
|
||||
:icon="IconShare2"
|
||||
:label="`${t('tab.shared_requests')}`"
|
||||
>
|
||||
<Share />
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</template>
|
||||
|
||||
@@ -33,6 +40,7 @@
|
||||
import IconClock from "~icons/lucide/clock"
|
||||
import IconLayers from "~icons/lucide/layers"
|
||||
import IconFolder from "~icons/lucide/folder"
|
||||
import IconShare2 from "~icons/lucide/share-2"
|
||||
import { ref } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
|
||||
|
||||
@@ -8,12 +8,11 @@
|
||||
@click.middle="emit('close-tab')"
|
||||
>
|
||||
<span
|
||||
class="text-tiny font-semibold"
|
||||
class="text-tiny font-semibold mr-2"
|
||||
:style="{ color: getMethodLabelColorClassOf(tab.document.request) }"
|
||||
>
|
||||
{{ tab.document.request.method }}
|
||||
</span>
|
||||
|
||||
<tippy
|
||||
ref="options"
|
||||
trigger="manual"
|
||||
@@ -21,7 +20,7 @@
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
>
|
||||
<span class="truncate px-2 leading-8">
|
||||
<span class="truncate">
|
||||
{{ tab.document.request.name }}
|
||||
</span>
|
||||
<template #content="{ hide }">
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<div class="divide-y divide-dividerLight">
|
||||
<div
|
||||
v-if="noEnvSelected && !globalHasAdditions"
|
||||
class="flex bg-error p-4 text-secondaryDark"
|
||||
class="flex bg-info p-4 text-secondaryDark"
|
||||
role="alert"
|
||||
>
|
||||
<icon-lucide-alert-triangle class="svg-icons mr-4" />
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<div ref="testScriptEditor" class="h-full"></div>
|
||||
</div>
|
||||
<div
|
||||
class="z-9 sticky top-upperTertiaryStickyFold h-full min-w-46 max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
|
||||
class="z-[9] sticky top-upperTertiaryStickyFold h-full min-w-[12rem] max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
|
||||
>
|
||||
<div class="pb-2 text-secondaryLight">
|
||||
{{ t("helpers.post_request_tests") }}
|
||||
|
||||
@@ -242,7 +242,7 @@ const urlEncodedParamsRaw = pluckRef(body, "body")
|
||||
|
||||
const urlEncodedParams = computed<RawKeyValueEntry[]>({
|
||||
get() {
|
||||
return typeof urlEncodedParamsRaw.value == "string"
|
||||
return typeof urlEncodedParamsRaw.value === "string"
|
||||
? parseRawKeyValueEntries(urlEncodedParamsRaw.value)
|
||||
: []
|
||||
},
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
theme="popover"
|
||||
:on-shown="() => authTippyActions.focus()"
|
||||
>
|
||||
<span class="select-wrapper">
|
||||
<HoppSmartSelectWrapper>
|
||||
<HoppButtonSecondary
|
||||
:label="auth.addTo || t('state.none')"
|
||||
class="ml-2 rounded-none pr-8"
|
||||
/>
|
||||
</span>
|
||||
</HoppSmartSelectWrapper>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="authTippyActions"
|
||||
|
||||
163
packages/hoppscotch-common/src/components/importExport/Base.vue
Normal file
163
packages/hoppscotch-common/src/components/importExport/Base.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<HoppSmartModal
|
||||
dialog
|
||||
:title="t(modalTitle)"
|
||||
styles="sm:max-w-md"
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #actions>
|
||||
<HoppButtonSecondary
|
||||
v-if="hasPreviousStep"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.go_back')"
|
||||
:icon="IconArrowLeft"
|
||||
@click="goToPreviousStep"
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<component :is="currentStep.component" v-bind="currentStep.props()" />
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import IconArrowLeft from "~icons/lucide/arrow-left"
|
||||
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { PropType, ref } from "vue"
|
||||
|
||||
import { useSteps, defineStep } from "~/composables/step-components"
|
||||
import ImportExportList from "./ImportExportList.vue"
|
||||
|
||||
import ImportExportSourcesList from "./ImportExportSourcesList.vue"
|
||||
import { ImporterOrExporter } from "~/components/importExport/types"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
importerModules: {
|
||||
// type: Array as PropType<ReturnType<typeof defineImporter>[]>,
|
||||
type: Array as PropType<ImporterOrExporter[]>,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
exporterModules: {
|
||||
type: Array as PropType<ImporterOrExporter[]>,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
modalTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
addStep,
|
||||
currentStep,
|
||||
goToStep,
|
||||
goToNextStep,
|
||||
goToPreviousStep,
|
||||
hasPreviousStep,
|
||||
} = useSteps()
|
||||
|
||||
const selectedImporterID = ref<string | null>(null)
|
||||
const selectedSourceID = ref<string | null>(null)
|
||||
|
||||
const chooseImporterOrExporter = defineStep(
|
||||
"choose_importer_or_exporter",
|
||||
ImportExportList,
|
||||
() => ({
|
||||
importers: props.importerModules.map((importer) => ({
|
||||
id: importer.metadata.id,
|
||||
name: importer.metadata.name,
|
||||
title: importer.metadata.title,
|
||||
icon: importer.metadata.icon,
|
||||
disabled: importer.metadata.disabled,
|
||||
})),
|
||||
exporters: props.exporterModules.map((exporter) => ({
|
||||
id: exporter.metadata.id,
|
||||
name: exporter.metadata.name,
|
||||
title: exporter.metadata.title,
|
||||
icon: exporter.metadata.icon,
|
||||
disabled: exporter.metadata.disabled,
|
||||
loading: exporter.metadata.isLoading?.value ?? false,
|
||||
})),
|
||||
"onImporter-selected": (id: string) => {
|
||||
selectedImporterID.value = id
|
||||
|
||||
const selectedImporter = props.importerModules.find(
|
||||
(i) => i.metadata.id === id
|
||||
)
|
||||
|
||||
if (selectedImporter?.supported_sources) goToNextStep()
|
||||
else if (selectedImporter?.component)
|
||||
goToStep(selectedImporter.component.id)
|
||||
},
|
||||
"onExporter-selected": (id: string) => {
|
||||
const selectedExporter = props.exporterModules.find(
|
||||
(i) => i.metadata.id === id
|
||||
)
|
||||
|
||||
if (selectedExporter && selectedExporter.action) {
|
||||
selectedExporter.action()
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const chooseImportSource = defineStep(
|
||||
"choose_import_source",
|
||||
ImportExportSourcesList,
|
||||
() => {
|
||||
const currentImporter = props.importerModules.find(
|
||||
(i) => i.metadata.id === selectedImporterID.value
|
||||
)
|
||||
|
||||
const sources = currentImporter?.supported_sources
|
||||
|
||||
if (!sources)
|
||||
return {
|
||||
sources: [],
|
||||
}
|
||||
|
||||
sources.forEach((source) => {
|
||||
addStep(source.step)
|
||||
})
|
||||
|
||||
return {
|
||||
sources: sources.map((source) => ({
|
||||
id: source.id,
|
||||
name: source.name,
|
||||
icon: source.icon,
|
||||
})),
|
||||
"onImport-source-selected": (sourceID) => {
|
||||
selectedSourceID.value = sourceID
|
||||
|
||||
const sourceStep = sources.find((s) => s.id === sourceID)?.step
|
||||
|
||||
if (sourceStep) {
|
||||
goToStep(sourceStep.id)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
addStep(chooseImporterOrExporter)
|
||||
addStep(chooseImportSource)
|
||||
|
||||
props.importerModules.forEach((importer) => {
|
||||
if (importer.component) {
|
||||
addStep(importer.component)
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const hideModal = () => {
|
||||
// resetImport()
|
||||
emit("hide-modal")
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<HoppSmartExpand>
|
||||
<template #body>
|
||||
<HoppSmartItem
|
||||
v-for="importer in importers"
|
||||
:key="importer.id"
|
||||
:icon="importer.icon"
|
||||
:label="t(`${importer.name}`)"
|
||||
@click="emit('importer-selected', importer.id)"
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartExpand>
|
||||
<hr />
|
||||
<div class="flex flex-col space-y-2">
|
||||
<template v-for="exporter in exporters" :key="exporter.id">
|
||||
<!-- adding the title to a span if the item is visible, otherwise the title won't be shown -->
|
||||
|
||||
<span
|
||||
v-if="exporter.disabled && exporter.title"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t(`${exporter.title}`)"
|
||||
class="flex"
|
||||
>
|
||||
<HoppSmartItem
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="exporter.icon"
|
||||
:label="t(`${exporter.name}`)"
|
||||
:disabled="exporter.disabled"
|
||||
:loading="exporter.loading"
|
||||
@click="emit('exporter-selected', exporter.id)"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<HoppSmartItem
|
||||
v-else
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="exporter.icon"
|
||||
:title="t(`${exporter.title}`)"
|
||||
:label="t(`${exporter.name}`)"
|
||||
:loading="exporter.loading"
|
||||
:disabled="exporter.disabled"
|
||||
@click="emit('exporter-selected', exporter.id)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { Component } from "vue"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
type ImportExportEntryMeta = {
|
||||
id: string
|
||||
name: string
|
||||
icon: Component
|
||||
disabled: boolean
|
||||
title?: string
|
||||
loading?: boolean
|
||||
isVisible?: boolean
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
importers: ImportExportEntryMeta[]
|
||||
exporters: ImportExportEntryMeta[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "importer-selected", importerID: string): void
|
||||
(e: "exporter-selected", exporterID: string): void
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="flex flex-col">
|
||||
<HoppSmartItem
|
||||
v-for="source in sources"
|
||||
:key="source.id"
|
||||
:icon="source.icon"
|
||||
:label="t(`${source.name}`)"
|
||||
@click="emit('import-source-selected', source.id)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { Component } from "vue"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
type ListItemMeta = {
|
||||
id: string
|
||||
name: string
|
||||
icon: Component
|
||||
title?: string
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
sources: ListItemMeta[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "import-source-selected", sourceID: string): void
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<p class="flex items-center">
|
||||
<span
|
||||
class="inline-flex items-center justify-center flex-shrink-0 mr-4 border-4 rounded-full border-primary text-dividerDark"
|
||||
:class="{
|
||||
'!text-green-500': hasFile,
|
||||
}"
|
||||
>
|
||||
<icon-lucide-check-circle class="svg-icons" />
|
||||
</span>
|
||||
<span>
|
||||
{{ t(`${caption}`) }}
|
||||
</span>
|
||||
</p>
|
||||
<div
|
||||
class="flex flex-col ml-10 border border-dashed rounded border-dividerDark"
|
||||
>
|
||||
<input
|
||||
id="inputChooseFileToImportFrom"
|
||||
ref="inputChooseFileToImportFrom"
|
||||
name="inputChooseFileToImportFrom"
|
||||
type="file"
|
||||
class="p-4 cursor-pointer transition file:transition file:cursor-pointer text-secondary hover:text-secondaryDark file:mr-2 file:py-2 file:px-4 file:rounded file:border-0 file:text-secondary hover:file:text-secondaryDark file:bg-primaryLight hover:file:bg-primaryDark"
|
||||
:accept="acceptedFileTypes"
|
||||
@change="onFileChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<HoppButtonPrimary
|
||||
class="w-full"
|
||||
:label="t('import.title')"
|
||||
:disabled="!hasFile"
|
||||
@click="emit('importFromFile', fileContent)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
|
||||
defineProps<{
|
||||
caption: string
|
||||
acceptedFileTypes: string
|
||||
}>()
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const hasFile = ref(false)
|
||||
const fileContent = ref("")
|
||||
|
||||
const inputChooseFileToImportFrom = ref<HTMLInputElement | any>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "importFromFile", content: string): void
|
||||
}>()
|
||||
|
||||
const onFileChange = () => {
|
||||
const inputFileToImport = inputChooseFileToImportFrom.value
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fileContent.value = content
|
||||
|
||||
hasFile.value = !!content?.length
|
||||
}
|
||||
|
||||
reader.readAsText(inputFileToImport.files[0])
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<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 class="my-4">
|
||||
<HoppButtonPrimary
|
||||
class="w-full"
|
||||
:label="t('import.title')"
|
||||
:disabled="!hasSelectedCollectionID"
|
||||
@click="fetchCollectionFromMyCollections"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { computed, ref } from "vue"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { useReadonlyStream } from "~/composables/stream"
|
||||
import { getRESTCollection, restCollections$ } from "~/newstore/collections"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const mySelectedCollectionID = ref<number | undefined>(undefined)
|
||||
|
||||
const hasSelectedCollectionID = computed(() => {
|
||||
return mySelectedCollectionID.value !== undefined
|
||||
})
|
||||
|
||||
const myCollections = useReadonlyStream(restCollections$, [])
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "importFromMyCollection", content: HoppCollection<HoppRESTRequest>): void
|
||||
}>()
|
||||
|
||||
const fetchCollectionFromMyCollections = async () => {
|
||||
if (mySelectedCollectionID.value === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const collection = getRESTCollection(mySelectedCollectionID.value)
|
||||
|
||||
if (collection) {
|
||||
emit("importFromMyCollection", collection)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<p class="flex items-center">
|
||||
<span
|
||||
class="inline-flex items-center justify-center flex-shrink-0 mr-4 border-4 rounded-full border-primary text-dividerDark"
|
||||
:class="{
|
||||
'!text-green-500': hasURL,
|
||||
}"
|
||||
>
|
||||
<icon-lucide-check-circle class="svg-icons" />
|
||||
</span>
|
||||
<span>
|
||||
{{ t(caption) }}
|
||||
</span>
|
||||
</p>
|
||||
<p class="flex flex-col ml-10">
|
||||
<input
|
||||
v-model="inputChooseGistToImportFrom"
|
||||
type="url"
|
||||
class="input"
|
||||
:placeholder="`${t('import.from_url')}`"
|
||||
/>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<HoppButtonPrimary
|
||||
class="w-full"
|
||||
:label="t('import.title')"
|
||||
:disabled="!hasURL"
|
||||
:loading="isFetchingUrl"
|
||||
@click="fetchUrlData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import axios, { AxiosResponse } from "axios"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const props = defineProps<{
|
||||
caption: string
|
||||
fetchLogic?: (url: string) => Promise<AxiosResponse<any>>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "importFromURL", content: unknown): void
|
||||
}>()
|
||||
|
||||
const inputChooseGistToImportFrom = ref<string>("")
|
||||
const hasURL = ref(false)
|
||||
|
||||
const isFetchingUrl = ref(false)
|
||||
|
||||
watch(inputChooseGistToImportFrom, (url) => {
|
||||
hasURL.value = !!url
|
||||
})
|
||||
|
||||
const urlFetchLogic =
|
||||
props.fetchLogic ??
|
||||
async function (url: string) {
|
||||
const res = await axios.get(url, {
|
||||
transitional: {
|
||||
forcedJSONParsing: false,
|
||||
silentJSONParsing: false,
|
||||
clarifyTimeoutError: true,
|
||||
},
|
||||
})
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
async function fetchUrlData() {
|
||||
isFetchingUrl.value = true
|
||||
|
||||
try {
|
||||
const res = await urlFetchLogic(inputChooseGistToImportFrom.value)
|
||||
|
||||
if (res.status === 200) {
|
||||
emit("importFromURL", res.data)
|
||||
}
|
||||
} catch (e) {
|
||||
toast.error(t("import.failed"))
|
||||
console.log(e)
|
||||
} finally {
|
||||
isFetchingUrl.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Component, Ref } from "vue"
|
||||
import { defineStep } from "~/composables/step-components"
|
||||
|
||||
// TODO: move the metadata except disabled and isLoading to importers.ts
|
||||
export type ImporterOrExporter = {
|
||||
metadata: {
|
||||
id: string
|
||||
name: string
|
||||
icon: any
|
||||
title: string
|
||||
disabled: boolean
|
||||
applicableTo: Array<"personal-workspace" | "team-workspace" | "url-import">
|
||||
isLoading?: Ref<boolean>
|
||||
}
|
||||
supported_sources?: {
|
||||
id: string
|
||||
name: string
|
||||
icon: Component
|
||||
step: ReturnType<typeof defineStep>
|
||||
}[]
|
||||
component?: ReturnType<typeof defineStep>
|
||||
action?: (...args: any[]) => any
|
||||
}
|
||||
@@ -44,6 +44,39 @@
|
||||
:icon="copyIcon"
|
||||
@click="copyResponse"
|
||||
/>
|
||||
<tippy
|
||||
v-if="response.body"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => copyInterfaceTippyActions.focus()"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('app.copy_interface_type')"
|
||||
:icon="IconMore"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="copyInterfaceTippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
v-for="(language, index) in interfaceLanguages"
|
||||
:key="index"
|
||||
:label="language"
|
||||
:icon="
|
||||
copiedInterfaceLanguage === language
|
||||
? copyInterfaceIcon
|
||||
: IconCopy
|
||||
"
|
||||
@click="runCopyInterface(language)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -201,7 +234,9 @@
|
||||
<script setup lang="ts">
|
||||
import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import IconFilter from "~icons/lucide/filter"
|
||||
import IconMore from "~icons/lucide/more-horizontal"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import * as LJSON from "lossless-json"
|
||||
import * as O from "fp-ts/Option"
|
||||
import * as E from "fp-ts/Either"
|
||||
@@ -221,9 +256,11 @@ import {
|
||||
useCopyResponse,
|
||||
useResponseBody,
|
||||
useDownloadResponse,
|
||||
useCopyInterface,
|
||||
} from "@composables/lens-actions"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||
import interfaceLanguages from "~/helpers/utils/interfaceLanguages"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -235,6 +272,13 @@ const { responseBodyText } = useResponseBody(props.response)
|
||||
|
||||
const toggleFilter = ref(false)
|
||||
const filterQueryText = ref("")
|
||||
const copiedInterfaceLanguage = ref("")
|
||||
|
||||
const runCopyInterface = (language: string) => {
|
||||
copyInterface(language).then(() => {
|
||||
copiedInterfaceLanguage.value = language
|
||||
})
|
||||
}
|
||||
|
||||
type BodyParseError =
|
||||
| { type: "JSON_PARSE_FAILED" }
|
||||
@@ -269,9 +313,8 @@ const jsonResponseBodyText = computed(() => {
|
||||
),
|
||||
E.map(JSON.stringify)
|
||||
)
|
||||
} else {
|
||||
return E.right(responseBodyText.value)
|
||||
}
|
||||
return E.right(responseBodyText.value)
|
||||
})
|
||||
|
||||
const jsonBodyText = computed(() =>
|
||||
@@ -319,6 +362,7 @@ const filterResponseError = computed(() =>
|
||||
)
|
||||
|
||||
const { copyIcon, copyResponse } = useCopyResponse(jsonBodyText)
|
||||
const { copyInterfaceIcon, copyInterface } = useCopyInterface(jsonBodyText)
|
||||
const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||
"application/json",
|
||||
jsonBodyText
|
||||
@@ -327,6 +371,7 @@ const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
const jsonResponse = ref<any | null>(null)
|
||||
const copyInterfaceTippyActions = ref<any | null>(null)
|
||||
const linewrapEnabled = ref(true)
|
||||
|
||||
const { cursor } = useCodemirror(
|
||||
|
||||
@@ -5,12 +5,11 @@ export default {
|
||||
computed: {
|
||||
responseBodyText() {
|
||||
if (typeof this.response.body === "string") return this.response.body
|
||||
else {
|
||||
const res = new TextDecoder("utf-8").decode(this.response.body)
|
||||
|
||||
// HACK: Temporary trailing null character issue from the extension fix
|
||||
return res.replace(/\0+$/, "")
|
||||
}
|
||||
const res = new TextDecoder("utf-8").decode(this.response.body)
|
||||
|
||||
// HACK: Temporary trailing null character issue from the extension fix
|
||||
return res.replace(/\0+$/, "")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="my-6 block w-full divide-y divide-dividerLight border border-dividerLight lg:my-0 lg:flex lg:divide-x lg:divide-y-0 lg:border-0"
|
||||
>
|
||||
<div class="table-box font-mono text-tiny">
|
||||
{{ shortcode.id }}
|
||||
</div>
|
||||
<div class="table-box" :class="requestLabelColor">
|
||||
{{ parseShortcodeRequest.method }}
|
||||
</div>
|
||||
<div class="table-box">
|
||||
{{ parseShortcodeRequest.endpoint }}
|
||||
</div>
|
||||
<div ref="timeStampRef" class="table-box">
|
||||
{{ dateStamp }}
|
||||
</div>
|
||||
<div class="table-box justify-center">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.open_workspace')"
|
||||
:to="`${shortcodeBaseURL}/r/${shortcode.id}`"
|
||||
blank
|
||||
:icon="IconExternalLink"
|
||||
class="px-3 text-accent hover:text-accent"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.copy')"
|
||||
color="green"
|
||||
:icon="copyIconRefs"
|
||||
class="px-3"
|
||||
@click="copyShortcode(shortcode.id)"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.delete')"
|
||||
:icon="IconTrash"
|
||||
color="red"
|
||||
class="px-3"
|
||||
@click="deleteShortcode(shortcode.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as RR from "fp-ts/ReadonlyRecord"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { translateToNewRequest } from "@hoppscotch/data"
|
||||
import { refAutoReset } from "@vueuse/core"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import { Shortcode } from "~/helpers/shortcodes/Shortcode"
|
||||
import { shortDateTime } from "~/helpers/utils/date"
|
||||
|
||||
import IconTrash from "~icons/lucide/trash"
|
||||
import IconExternalLink from "~icons/lucide/external-link"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const props = defineProps<{
|
||||
shortcode: Shortcode
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "delete-shortcode", codeID: string): void
|
||||
}>()
|
||||
|
||||
const deleteShortcode = (codeID: string) => {
|
||||
emit("delete-shortcode", codeID)
|
||||
}
|
||||
|
||||
const requestMethodLabels = {
|
||||
get: "text-green-500",
|
||||
post: "text-yellow-500",
|
||||
put: "text-blue-500",
|
||||
delete: "text-red-500",
|
||||
default: "text-gray-500",
|
||||
} as const
|
||||
|
||||
const timeStampRef = ref()
|
||||
|
||||
const copyIconRefs = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
||||
IconCopy,
|
||||
1000
|
||||
)
|
||||
|
||||
const parseShortcodeRequest = computed(() =>
|
||||
pipe(props.shortcode.request, JSON.parse, translateToNewRequest)
|
||||
)
|
||||
|
||||
const requestLabelColor = computed(() =>
|
||||
pipe(
|
||||
requestMethodLabels,
|
||||
RR.lookup(parseShortcodeRequest.value.method.toLowerCase()),
|
||||
O.getOrElseW(() => requestMethodLabels.default)
|
||||
)
|
||||
)
|
||||
|
||||
const dateStamp = computed(() => shortDateTime(props.shortcode.createdOn))
|
||||
|
||||
const shortcodeBaseURL =
|
||||
import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
|
||||
|
||||
const copyShortcode = (codeID: string) => {
|
||||
copyToClipboard(`${shortcodeBaseURL}/r/${codeID}`)
|
||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
||||
copyIconRefs.value = IconCheck
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.table-box {
|
||||
@apply flex flex-1 items-center truncate px-4 py-1;
|
||||
}
|
||||
</style>
|
||||
@@ -1,168 +0,0 @@
|
||||
<template>
|
||||
<section class="p-4">
|
||||
<h4 class="font-semibold text-secondaryDark">
|
||||
{{ t("settings.short_codes") }}
|
||||
</h4>
|
||||
<div class="my-1 text-secondaryLight">
|
||||
{{ t("settings.short_codes_description") }}
|
||||
</div>
|
||||
<div class="relative overflow-x-auto py-4">
|
||||
<div v-if="loading" class="flex flex-col items-center justify-center">
|
||||
<HoppSmartSpinner class="mb-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="!loading && myShortcodes.length === 0"
|
||||
:src="`/images/states/${colorMode.value}/add_files.svg`"
|
||||
:alt="`${t('empty.shortcodes')}`"
|
||||
:text="t('empty.shortcodes')"
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
<div v-else-if="!loading">
|
||||
<div
|
||||
class="hidden w-full rounded-t border-x border-t border-dividerLight bg-primaryLight lg:flex"
|
||||
>
|
||||
<div class="flex w-full overflow-y-scroll">
|
||||
<div class="table-box">
|
||||
{{ t("shortcodes.short_code") }}
|
||||
</div>
|
||||
<div class="table-box">
|
||||
{{ t("shortcodes.method") }}
|
||||
</div>
|
||||
<div class="table-box">
|
||||
{{ t("shortcodes.url") }}
|
||||
</div>
|
||||
<div class="table-box">
|
||||
{{ t("shortcodes.created_on") }}
|
||||
</div>
|
||||
<div class="table-box justify-center">
|
||||
{{ t("shortcodes.actions") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex max-h-sm w-full flex-col items-center justify-between divide-dividerLight overflow-y-scroll rounded border border-dividerLight lg:divide-y lg:rounded-t-none"
|
||||
>
|
||||
<ProfileShortcode
|
||||
v-for="(shortcode, shortcodeIndex) in myShortcodes"
|
||||
:key="`shortcode-${shortcodeIndex}`"
|
||||
:shortcode="shortcode"
|
||||
@delete-shortcode="deleteShortcode"
|
||||
/>
|
||||
<HoppSmartIntersection
|
||||
v-if="hasMoreShortcodes && myShortcodes.length > 0"
|
||||
@intersecting="loadMoreShortcodes()"
|
||||
>
|
||||
<div v-if="adapterLoading" class="flex flex-col items-center py-3">
|
||||
<HoppSmartSpinner />
|
||||
</div>
|
||||
</HoppSmartIntersection>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!loading && adapterError"
|
||||
class="flex flex-col items-center py-4"
|
||||
>
|
||||
<icon-lucide-help-circle class="svg-icons mb-4" />
|
||||
{{ getErrorMessage(adapterError) }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watchEffect, computed } from "vue"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
import { onAuthEvent, onLoggedIn } from "@composables/auth"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { usePageHead } from "@composables/head"
|
||||
|
||||
import ShortcodeListAdapter from "~/helpers/shortcodes/ShortcodeListAdapter"
|
||||
import { deleteShortcode as backendDeleteShortcode } from "~/helpers/backend/mutations/Shortcode"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
usePageHead({
|
||||
title: computed(() => t("navigation.profile")),
|
||||
})
|
||||
|
||||
const currentUser = useReadonlyStream(
|
||||
platform.auth.getCurrentUserStream(),
|
||||
platform.auth.getCurrentUser()
|
||||
)
|
||||
|
||||
const displayName = ref(currentUser.value?.displayName)
|
||||
watchEffect(() => (displayName.value = currentUser.value?.displayName))
|
||||
|
||||
const emailAddress = ref(currentUser.value?.email)
|
||||
watchEffect(() => (emailAddress.value = currentUser.value?.email))
|
||||
|
||||
const adapter = new ShortcodeListAdapter(true)
|
||||
const adapterLoading = useReadonlyStream(adapter.loading$, false)
|
||||
const adapterError = useReadonlyStream(adapter.error$, null)
|
||||
const myShortcodes = useReadonlyStream(adapter.shortcodes$, [])
|
||||
const hasMoreShortcodes = useReadonlyStream(adapter.hasMoreShortcodes$, true)
|
||||
|
||||
const loading = computed(
|
||||
() => adapterLoading.value && myShortcodes.value.length === 0
|
||||
)
|
||||
|
||||
onLoggedIn(() => {
|
||||
try {
|
||||
adapter.initialize()
|
||||
} catch (e) {}
|
||||
})
|
||||
|
||||
onAuthEvent((ev) => {
|
||||
if (ev.event === "logout" && adapter.isInitialized()) {
|
||||
adapter.dispose()
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
const deleteShortcode = (codeID: string) => {
|
||||
pipe(
|
||||
backendDeleteShortcode(codeID),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
toast.error(`${getErrorMessage(err)}`)
|
||||
},
|
||||
() => {
|
||||
toast.success(`${t("shortcodes.deleted")}`)
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
|
||||
const loadMoreShortcodes = () => {
|
||||
adapter.loadMore()
|
||||
}
|
||||
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
if (err.type === "network_error") {
|
||||
return t("error.network_error")
|
||||
} else {
|
||||
switch (err.error) {
|
||||
case "shortcode/not_found":
|
||||
return t("shortcodes.not_found")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.table-box {
|
||||
@apply flex flex-1 items-center truncate px-4 py-2;
|
||||
}
|
||||
</style>
|
||||
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-else-if="myTeams.length"
|
||||
class="flex flex-col space-y-2 rounded-lg border border-red-500 bg-error p-4 text-secondaryDark"
|
||||
class="bg-info flex flex-col space-y-2 rounded-lg border border-red-500 p-4 text-secondaryDark"
|
||||
>
|
||||
<h2 class="font-bold text-red-500">
|
||||
{{ t("error.danger_zone") }}
|
||||
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
<div v-else>
|
||||
<div
|
||||
class="mb-4 flex flex-col space-y-2 rounded-lg border border-red-500 bg-error p-4 text-secondaryDark"
|
||||
class="bg-info mb-4 flex flex-col space-y-2 rounded-lg border border-red-500 p-4 text-secondaryDark"
|
||||
>
|
||||
<h2 class="font-bold text-red-500">
|
||||
{{ t("error.danger_zone") }}
|
||||
@@ -173,13 +173,8 @@ const deleteUserAccount = async () => {
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
if (err.type === "network_error") {
|
||||
return t("error.network_error")
|
||||
} else {
|
||||
switch (err.error) {
|
||||
case "shortcode/not_found":
|
||||
return t("shortcodes.not_found")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -33,12 +33,12 @@
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<span class="select-wrapper">
|
||||
<HoppSmartSelectWrapper>
|
||||
<HoppButtonSecondary
|
||||
:label="contentType || t('state.none').toLowerCase()"
|
||||
class="ml-2 rounded-none pr-8"
|
||||
/>
|
||||
</span>
|
||||
</HoppSmartSelectWrapper>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
@@ -279,6 +279,7 @@ defineActionHandler("request.send-cancel", sendMessage)
|
||||
:deep(.cm-panels) {
|
||||
@apply top-upperSecondaryStickyFold #{!important};
|
||||
}
|
||||
|
||||
.eventFeildShown :deep(.cm-panels),
|
||||
.cmResponsePrimaryStickyFold :deep(.cm-panels) {
|
||||
@apply top-upperTertiaryStickyFold #{!important};
|
||||
|
||||
@@ -62,12 +62,12 @@
|
||||
{{ t("mqtt.lw_qos") }}
|
||||
</label>
|
||||
<tippy interactive trigger="click" theme="popover">
|
||||
<span class="select-wrapper">
|
||||
<HoppSmartSelectWrapper>
|
||||
<HoppButtonSecondary
|
||||
class="ml-2 rounded-none pr-8"
|
||||
:label="`${config.lwQos}`"
|
||||
/>
|
||||
</span>
|
||||
</HoppSmartSelectWrapper>
|
||||
<template #content="{ hide }">
|
||||
<div class="flex flex-col" role="menu">
|
||||
<HoppSmartItem
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
autoScrollEnabled ? t('action.turn_off') : t('action.turn_on')
|
||||
}`"
|
||||
:icon="IconChevronsDown"
|
||||
:class="toggleAutoscrollColor"
|
||||
@click="toggleAutoscroll()"
|
||||
:color="autoScrollEnabled ? 'green' : 'red'"
|
||||
@click="autoScrollEnabled = !autoScrollEnabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,7 +60,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, PropType, computed, watch, Ref } from "vue"
|
||||
import { ref, PropType, watch, Ref } from "vue"
|
||||
import IconTrash from "~icons/lucide/trash"
|
||||
import IconArrowUp from "~icons/lucide/arrow-up"
|
||||
import IconArrowDown from "~icons/lucide/arrow-down"
|
||||
@@ -123,12 +123,4 @@ watch(
|
||||
}, 200),
|
||||
{ flush: "post" }
|
||||
)
|
||||
|
||||
const toggleAutoscroll = () => {
|
||||
autoScrollEnabled.value = !autoScrollEnabled.value
|
||||
}
|
||||
|
||||
const toggleAutoscrollColor = computed(() =>
|
||||
autoScrollEnabled.value ? "text-green-500" : "text-red-500"
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="entry.ts !== undefined"
|
||||
class="w-34 hidden items-center px-1 sm:inline-flex"
|
||||
class="w-36 hidden items-center px-1 sm:inline-flex"
|
||||
>
|
||||
<span
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -269,12 +269,12 @@ const ast = computed(() =>
|
||||
|
||||
const editorText = computed(() => {
|
||||
if (selectedTab.value === "json") return jsonBodyText.value
|
||||
else return logPayload.value
|
||||
return logPayload.value
|
||||
})
|
||||
|
||||
const editorMode = computed(() => {
|
||||
if (selectedTab.value === "json") return "application/ld+json"
|
||||
else return "text/plain"
|
||||
return "text/plain"
|
||||
})
|
||||
|
||||
const { cursor } = useCodemirror(
|
||||
|
||||
@@ -9,9 +9,9 @@
|
||||
{{ t("mqtt.qos") }}
|
||||
</label>
|
||||
<tippy interactive trigger="click" theme="popover">
|
||||
<span class="select-wrapper">
|
||||
<HoppSmartSelectWrapper>
|
||||
<HoppButtonSecondary class="pr-8" :label="`${QoS}`" />
|
||||
</span>
|
||||
</HoppSmartSelectWrapper>
|
||||
<template #content="{ hide }">
|
||||
<div class="flex flex-col" role="menu">
|
||||
<HoppSmartItem
|
||||
|
||||
144
packages/hoppscotch-common/src/components/share/CreateModal.vue
Normal file
144
packages/hoppscotch-common/src/components/share/CreateModal.vue
Normal file
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="selectedWidget"
|
||||
class="border divide-y rounded divide-divider border-divider"
|
||||
>
|
||||
<div v-if="loading" class="px-4 py-2">
|
||||
{{ t("shared_requests.creating_widget") }}
|
||||
</div>
|
||||
<div v-else class="px-4 py-2">
|
||||
{{ t("shared_requests.description") }}
|
||||
</div>
|
||||
<div class="flex flex-col divide-y divide-divider">
|
||||
<div class="flex flex-col p-4 space-y-4">
|
||||
<div
|
||||
v-for="widget in widgets"
|
||||
:key="widget.value"
|
||||
class="flex flex-col p-4 border rounded cursor-pointer border-divider hover:bg-dividerLight"
|
||||
:class="{
|
||||
'!border-accentLight': selectedWidget.value === widget.value,
|
||||
}"
|
||||
@click="selectedWidget = widget"
|
||||
>
|
||||
<span class="mb-1 font-bold text-secondaryDark">
|
||||
{{ widget.label }}
|
||||
</span>
|
||||
<span class="text-tiny">
|
||||
{{ widget.info }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-center justify-center p-4">
|
||||
<span
|
||||
class="flex justify-center flex-1 mb-2 text-secondaryLight text-tiny"
|
||||
>
|
||||
{{ t("shared_requests.preview") }}
|
||||
</span>
|
||||
<div class="w-full">
|
||||
<ShareTemplatesEmbeds
|
||||
v-if="selectedWidget.value === 'embed'"
|
||||
:endpoint="request?.endpoint"
|
||||
:method="request?.method"
|
||||
:model-value="embedOption"
|
||||
/>
|
||||
<ShareTemplatesButton
|
||||
v-else-if="selectedWidget.value === 'button'"
|
||||
img="badge.svg"
|
||||
/>
|
||||
<ShareTemplatesLink v-else />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { PropType, ref } from "vue"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
request: {
|
||||
type: Object as PropType<HoppRESTRequest | null>,
|
||||
required: true,
|
||||
},
|
||||
modelValue: {
|
||||
type: Object as PropType<Widget | null>,
|
||||
default: null,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const selectedWidget = useVModel(props, "modelValue")
|
||||
|
||||
type WidgetID = "embed" | "button" | "link"
|
||||
|
||||
type Widget = {
|
||||
value: WidgetID
|
||||
label: string
|
||||
info: string
|
||||
}
|
||||
|
||||
const widgets: Widget[] = [
|
||||
{
|
||||
value: "embed",
|
||||
label: t("shared_requests.embed"),
|
||||
info: t("shared_requests.embed_info"),
|
||||
},
|
||||
{
|
||||
value: "button",
|
||||
label: t("shared_requests.button"),
|
||||
info: t("shared_requests.button_info"),
|
||||
},
|
||||
{
|
||||
value: "link",
|
||||
label: t("shared_requests.link"),
|
||||
info: t("shared_requests.link_info"),
|
||||
},
|
||||
]
|
||||
|
||||
type Tabs = "parameters" | "body" | "headers" | "authorization"
|
||||
|
||||
type EmbedOption = {
|
||||
selectedTab: Tabs
|
||||
tabs: {
|
||||
value: Tabs
|
||||
label: string
|
||||
enabled: boolean
|
||||
}[]
|
||||
theme: "light" | "dark" | "system"
|
||||
}
|
||||
|
||||
const embedOption = ref<EmbedOption>({
|
||||
selectedTab: "parameters",
|
||||
tabs: [
|
||||
{
|
||||
value: "parameters",
|
||||
label: t("tab.parameters"),
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
value: "body",
|
||||
label: t("tab.body"),
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
value: "headers",
|
||||
label: t("tab.headers"),
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
value: "authorization",
|
||||
label: t("tab.authorization"),
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
theme: "system",
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,435 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="selectedWidget"
|
||||
class="border divide-y rounded divide-divider border-divider"
|
||||
>
|
||||
<div v-if="loading" class="px-4 py-2">
|
||||
{{ t("shared_requests.creating_widget") }}
|
||||
</div>
|
||||
<div v-else class="px-4 py-2">
|
||||
{{ t("shared_requests.customize") }}
|
||||
</div>
|
||||
<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 divide-y divide-divider">
|
||||
<div class="flex flex-col p-2 space-y-2">
|
||||
<HoppSmartRadioGroup
|
||||
v-model="selectedWidget.value"
|
||||
:radios="widgets"
|
||||
class="flex !flex-row"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col divide-y divide-divider">
|
||||
<div class="flex items-center justify-center px-6 py-4">
|
||||
<div v-if="selectedWidget.value === 'embed'" class="w-full">
|
||||
<div class="flex flex-col pb-4">
|
||||
<div
|
||||
v-for="option in embedOptions.tabs"
|
||||
:key="option.value"
|
||||
class="flex justify-between py-2"
|
||||
>
|
||||
<span class="capitalize">
|
||||
{{ option.label }}
|
||||
</span>
|
||||
<HoppSmartCheckbox
|
||||
:on="option.enabled"
|
||||
@change="removeEmbedOption(option.value)"
|
||||
>
|
||||
</HoppSmartCheckbox>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>
|
||||
{{ t("shared_requests.theme.title") }}
|
||||
</span>
|
||||
<div>
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions!.focus()"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
class="!py-2 !px-0 capitalize"
|
||||
:label="embedOptions.theme"
|
||||
:icon="embedThemeIcon"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
:label="t('shared_requests.theme.system')"
|
||||
:icon="IconMonitor"
|
||||
:active="embedOptions.theme === 'system'"
|
||||
@click="
|
||||
() => {
|
||||
embedOptions.theme = 'system'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
:label="t('shared_requests.theme.light')"
|
||||
:icon="IconSun"
|
||||
:active="embedOptions.theme === 'light'"
|
||||
@click="
|
||||
() => {
|
||||
embedOptions.theme = 'light'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
:label="t('shared_requests.theme.dark')"
|
||||
:icon="IconMoon"
|
||||
:active="embedOptions.theme === 'dark'"
|
||||
@click="
|
||||
() => {
|
||||
embedOptions.theme = 'dark'
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
class="flex justify-center mb-2 text-secondaryLight text-tiny"
|
||||
>
|
||||
{{ t("shared_requests.preview") }}
|
||||
</span>
|
||||
<ShareTemplatesEmbeds
|
||||
:endpoint="request?.endpoint"
|
||||
:method="request?.method"
|
||||
:model-value="embedOptions"
|
||||
/>
|
||||
<div class="flex items-center justify-center">
|
||||
<HoppButtonSecondary
|
||||
:label="t('shared_requests.copy_html')"
|
||||
class="underline text-secondaryDark"
|
||||
@click="
|
||||
copyContent({
|
||||
widget: 'embed',
|
||||
type: 'html',
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="selectedWidget.value === 'button'"
|
||||
class="flex flex-col space-y-8"
|
||||
>
|
||||
<div
|
||||
v-for="variant in buttonVariants"
|
||||
:key="variant.id"
|
||||
class="flex flex-col"
|
||||
>
|
||||
<span
|
||||
class="flex justify-center mb-2 text-secondaryLight text-tiny"
|
||||
>
|
||||
{{ t("shared_requests.preview") }}
|
||||
</span>
|
||||
<ShareTemplatesButton :img="variant.img" />
|
||||
<div class="flex items-center justify-between">
|
||||
<HoppButtonSecondary
|
||||
:label="t('shared_requests.copy_html')"
|
||||
class="underline text-secondaryDark"
|
||||
@click="
|
||||
copyContent({
|
||||
widget: 'button',
|
||||
type: 'html',
|
||||
id: variant.id,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('shared_requests.copy_markdown')"
|
||||
class="underline text-secondaryDark"
|
||||
@click="
|
||||
copyContent({
|
||||
widget: 'button',
|
||||
type: 'markdown',
|
||||
id: variant.id,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex flex-col space-y-8">
|
||||
<div
|
||||
v-for="variant in linkVariants"
|
||||
:key="variant.type"
|
||||
class="flex flex-col items-center justify-center"
|
||||
>
|
||||
<span
|
||||
class="flex justify-center mb-2 text-secondaryLight text-tiny"
|
||||
>
|
||||
{{ t("shared_requests.preview") }}
|
||||
</span>
|
||||
<ShareTemplatesLink :link="variant.link" :label="variant.label" />
|
||||
<HoppButtonSecondary
|
||||
:label="t(`shared_requests.copy_${variant.type}`)"
|
||||
class="underline text-secondaryDark"
|
||||
@click="
|
||||
copyContent({
|
||||
widget: 'link',
|
||||
type: variant.type,
|
||||
id: variant.id,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { computed, ref } from "vue"
|
||||
import { PropType } from "vue"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import IconMonitor from "~icons/lucide/monitor"
|
||||
import IconSun from "~icons/lucide/sun"
|
||||
import IconMoon from "~icons/lucide/moon"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
request: {
|
||||
type: Object as PropType<HoppRESTRequest | null>,
|
||||
required: true,
|
||||
},
|
||||
modelValue: {
|
||||
type: Object as PropType<Widget | null>,
|
||||
default: null,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
embedOptions: {
|
||||
type: Object as PropType<EmbedOption>,
|
||||
default: () => ({
|
||||
selectedTab: "parameters",
|
||||
tabs: [
|
||||
{
|
||||
value: "parameters",
|
||||
label: "shared_requests.parameters",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
value: "body",
|
||||
label: "shared_requests.body",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
value: "headers",
|
||||
label: "shared_requests.headers",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
value: "authorization",
|
||||
label: "shared_requests.authorization",
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
theme: "system",
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: "copy-shared-request",
|
||||
request: {
|
||||
sharedRequestID: string | undefined
|
||||
content: string | undefined
|
||||
}
|
||||
): void
|
||||
(e: "hide-modal"): void
|
||||
(e: "update:modelValue", value: string): void
|
||||
}>()
|
||||
|
||||
const selectedWidget = useVModel(props, "modelValue")
|
||||
const embedOptions = useVModel(props, "embedOptions")
|
||||
|
||||
type WidgetID = "embed" | "button" | "link"
|
||||
|
||||
type Widget = {
|
||||
value: WidgetID
|
||||
label: string
|
||||
}
|
||||
|
||||
const widgets: Widget[] = [
|
||||
{
|
||||
value: "embed",
|
||||
label: t("shared_requests.embed"),
|
||||
},
|
||||
{
|
||||
value: "button",
|
||||
label: t("shared_requests.button"),
|
||||
},
|
||||
{
|
||||
value: "link",
|
||||
label: t("shared_requests.link"),
|
||||
},
|
||||
]
|
||||
|
||||
type EmbedTabs = "parameters" | "body" | "headers" | "authorization"
|
||||
|
||||
type EmbedOption = {
|
||||
selectedTab: EmbedTabs
|
||||
tabs: {
|
||||
value: EmbedTabs
|
||||
label: string
|
||||
enabled: boolean
|
||||
}[]
|
||||
theme: "light" | "dark" | "system"
|
||||
}
|
||||
const embedThemeIcon = computed(() => {
|
||||
if (embedOptions.value.theme === "system") {
|
||||
return IconMonitor
|
||||
} else if (embedOptions.value.theme === "light") {
|
||||
return IconSun
|
||||
} else {
|
||||
return IconMoon
|
||||
}
|
||||
})
|
||||
|
||||
const removeEmbedOption = (option: EmbedTabs) => {
|
||||
const index = embedOptions.value.tabs.findIndex((tab) => tab.value === option)
|
||||
if (index === -1) return
|
||||
|
||||
//if removed tab is the selected tab, select the next tab with enabled true
|
||||
if (embedOptions.value.selectedTab === option) {
|
||||
const nextTab = embedOptions.value.tabs.find((tab) => tab.enabled)
|
||||
if (nextTab) {
|
||||
embedOptions.value.selectedTab = nextTab.value
|
||||
}
|
||||
}
|
||||
|
||||
embedOptions.value.tabs[index].enabled =
|
||||
!embedOptions.value.tabs[index].enabled
|
||||
}
|
||||
|
||||
type ButtonVariant = {
|
||||
id: string
|
||||
img: string
|
||||
}
|
||||
const buttonVariants: ButtonVariant[] = [
|
||||
{
|
||||
id: "button1",
|
||||
img: "badge.svg",
|
||||
},
|
||||
{
|
||||
id: "button2",
|
||||
img: "badge-light.svg",
|
||||
},
|
||||
{
|
||||
id: "button3",
|
||||
img: "badge-dark.svg",
|
||||
},
|
||||
]
|
||||
|
||||
type LinkVariant = {
|
||||
id: string
|
||||
link?: string
|
||||
label?: string
|
||||
type: "html" | "markdown" | "link"
|
||||
}
|
||||
|
||||
const linkVariants: LinkVariant[] = [
|
||||
{
|
||||
id: "link1",
|
||||
link: props.request?.id,
|
||||
type: "link",
|
||||
},
|
||||
{
|
||||
id: "link2",
|
||||
label: "shared_requests.run_in_hoppscotch",
|
||||
type: "html",
|
||||
},
|
||||
{
|
||||
id: "link3",
|
||||
label: "shared_requests.run_in_hoppscotch",
|
||||
type: "markdown",
|
||||
},
|
||||
]
|
||||
|
||||
const baseURL = import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
|
||||
|
||||
const copyEmbed = () => {
|
||||
return `<iframe src="${baseURL}/e/${props.request?.id}' style='width: 100%; height: 500px; border: 0; border-radius: 4px; overflow: hidden;'></iframe>`
|
||||
}
|
||||
|
||||
const copyButton = (
|
||||
variationID: string,
|
||||
type: "html" | "markdown" | "link"
|
||||
) => {
|
||||
let badge = ""
|
||||
if (variationID === "button1") {
|
||||
badge = "badge.svg"
|
||||
} else if (variationID === "button2") {
|
||||
badge = "badge-light.svg"
|
||||
} else {
|
||||
badge = "badge-dark.svg"
|
||||
}
|
||||
|
||||
if (type === "markdown") {
|
||||
return `[](${baseURL}/r/${props.request?.id})`
|
||||
} else {
|
||||
return `<a href="${baseURL}/r/${props.request?.id}"><img src="${baseURL}/${badge}" alt="Run in Hoppscotch" /></a>`
|
||||
}
|
||||
}
|
||||
|
||||
const copyLink = (variationID: string) => {
|
||||
if (variationID === "link1") {
|
||||
return `${baseURL}/r/${props.request?.id}`
|
||||
} else if (variationID === "link2") {
|
||||
return `<a href="${baseURL}/r/${props.request?.id}">Run in Hoppscotch</a>`
|
||||
} else {
|
||||
return `[Run in Hoppscotch](${baseURL}/r/${props.request?.id})`
|
||||
}
|
||||
}
|
||||
|
||||
const copyContent = ({
|
||||
id,
|
||||
widget,
|
||||
type,
|
||||
}: {
|
||||
id?: string | undefined
|
||||
widget: WidgetID
|
||||
type: "html" | "markdown" | "link"
|
||||
}) => {
|
||||
let content = ""
|
||||
if (widget === "button") {
|
||||
content = copyButton(id!, type)
|
||||
} else if (widget === "link") {
|
||||
content = copyLink(id!)
|
||||
} else {
|
||||
content = copyEmbed()
|
||||
}
|
||||
const copyContent = {
|
||||
sharedRequestID: props.request?.id,
|
||||
content,
|
||||
}
|
||||
emit("copy-shared-request", copyContent)
|
||||
}
|
||||
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
</script>
|
||||
170
packages/hoppscotch-common/src/components/share/Modal.vue
Normal file
170
packages/hoppscotch-common/src/components/share/Modal.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
dialog
|
||||
:title="
|
||||
step === 1 ? t('modal.share_request') : t('modal.customize_request')
|
||||
"
|
||||
styles="sm:max-w-md"
|
||||
@close="hideModal"
|
||||
>
|
||||
<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>
|
||||
<ShareCreateModal
|
||||
v-else-if="step === 1"
|
||||
v-model="selectedWidget"
|
||||
:request="request"
|
||||
:loading="loading"
|
||||
@create-shared-request="createSharedRequest"
|
||||
/>
|
||||
<ShareCustomizeModal
|
||||
v-else-if="step === 2"
|
||||
v-model="selectedWidget"
|
||||
v-model:embed-options="embedOptions"
|
||||
:request="request"
|
||||
:loading="loading"
|
||||
@copy-shared-request="copySharedRequest"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-start flex-1">
|
||||
<HoppButtonPrimary
|
||||
v-if="step === 1"
|
||||
:label="t('action.create')"
|
||||
:loading="loading"
|
||||
@click="createSharedRequest"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="step === 1 ? t('action.cancel') : t('action.close')"
|
||||
class="ml-2"
|
||||
filled
|
||||
outline
|
||||
@click="hideModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { PropType } from "vue"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
type EmbedTabs = "parameters" | "body" | "headers" | "authorization"
|
||||
|
||||
type EmbedOption = {
|
||||
selectedTab: EmbedTabs
|
||||
tabs: {
|
||||
value: EmbedTabs
|
||||
label: string
|
||||
enabled: boolean
|
||||
}[]
|
||||
theme: "light" | "dark" | "system"
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
request: {
|
||||
type: Object as PropType<HoppRESTRequest | null>,
|
||||
required: true,
|
||||
},
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: true,
|
||||
},
|
||||
modelValue: {
|
||||
type: Object as PropType<Widget | null>,
|
||||
default: null,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
},
|
||||
embedOptions: {
|
||||
type: Object as PropType<EmbedOption>,
|
||||
default: () => ({
|
||||
selectedTab: "parameters",
|
||||
tabs: [
|
||||
{
|
||||
value: "parameters",
|
||||
label: "shared_requests.parameters",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
value: "body",
|
||||
label: "shared_requests.body",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
value: "headers",
|
||||
label: "shared_requests.headers",
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
value: "authorization",
|
||||
label: "shared_requests.authorization",
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
theme: "system",
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
type WidgetID = "embed" | "button" | "link"
|
||||
|
||||
type Widget = {
|
||||
value: WidgetID
|
||||
label: string
|
||||
info: string
|
||||
}
|
||||
|
||||
const selectedWidget = useVModel(props, "modelValue")
|
||||
const embedOptions = useVModel(props, "embedOptions")
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "create-shared-request", request: HoppRESTRequest | null): void
|
||||
(e: "hide-modal"): void
|
||||
(e: "update:modelValue", value: string): void
|
||||
(e: "update:step", value: number): void
|
||||
(
|
||||
e: "copy-shared-request",
|
||||
payload: {
|
||||
sharedRequestID: string | undefined
|
||||
content: string | undefined
|
||||
}
|
||||
): void
|
||||
}>()
|
||||
|
||||
const createSharedRequest = () => {
|
||||
emit("create-shared-request", props.request as HoppRESTRequest)
|
||||
}
|
||||
|
||||
const copySharedRequest = (payload: {
|
||||
sharedRequestID: string | undefined
|
||||
content: string | undefined
|
||||
}) => {
|
||||
emit("copy-shared-request", payload)
|
||||
}
|
||||
|
||||
const hideModal = () => {
|
||||
emit("hide-modal")
|
||||
selectedWidget.value = {
|
||||
value: "embed",
|
||||
label: t("shared_requests.embed"),
|
||||
info: t("shared_requests.embed_info"),
|
||||
}
|
||||
}
|
||||
</script>
|
||||
167
packages/hoppscotch-common/src/components/share/Request.vue
Normal file
167
packages/hoppscotch-common/src/components/share/Request.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-stretch group"
|
||||
@contextmenu.prevent="options!.tippy.show()"
|
||||
>
|
||||
<div
|
||||
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
|
||||
class="flex items-center justify-center flex-1 min-w-0 py-2 cursor-pointer pointer-events-auto"
|
||||
:title="`${timeStamp}`"
|
||||
@click="openInNewTab"
|
||||
>
|
||||
<span
|
||||
class="flex items-center justify-center w-16 px-2 truncate pointer-events-none"
|
||||
:style="{ color: requestLabelColor }"
|
||||
>
|
||||
<span class="font-semibold truncate text-tiny">
|
||||
{{ parseRequest.method }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="flex items-center flex-1 min-w-0 pr-2 transition pointer-events-none group-hover:text-secondaryDark"
|
||||
>
|
||||
<span class="flex-1 truncate">
|
||||
{{ parseRequest.endpoint }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="flex px-2 truncate text-secondaryLight group-hover:text-secondaryDark"
|
||||
>
|
||||
{{ parseRequest.name }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>
|
||||
<tippy
|
||||
ref="options"
|
||||
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"
|
||||
role="menu"
|
||||
@keyup.t="openInNewTabAction?.$el.click()"
|
||||
@keyup.e="customizeAction?.$el.click()"
|
||||
@keyup.delete="deleteAction?.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<HoppSmartItem
|
||||
ref="openInNewTabAction"
|
||||
:icon="IconArrowUpRight"
|
||||
:label="`${t('shared_requests.open_new_tab')}`"
|
||||
:shortcut="['T']"
|
||||
@click="
|
||||
() => {
|
||||
openInNewTab()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="customizeAction"
|
||||
:icon="IconCustomize"
|
||||
:label="`${t('shared_requests.customize')}`"
|
||||
:shortcut="['E']"
|
||||
@click="
|
||||
() => {
|
||||
customizeSharedRequest()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
:label="`${t('action.delete')}`"
|
||||
:shortcut="['⌫']"
|
||||
@click="
|
||||
() => {
|
||||
deleteSharedRequest()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { HoppRESTRequest, translateToNewRequest } from "@hoppscotch/data"
|
||||
import { pipe } from "fp-ts/lib/function"
|
||||
import { ref } from "vue"
|
||||
import { computed } from "vue"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
|
||||
import { Shortcode } from "~/helpers/shortcode/Shortcode"
|
||||
import IconArrowUpRight from "~icons/lucide/arrow-up-right-square"
|
||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||
import IconCustomize from "~icons/lucide/settings-2"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import { shortDateTime } from "~/helpers/utils/date"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
request: Shortcode
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: "customize-shared-request",
|
||||
request: HoppRESTRequest,
|
||||
id: string,
|
||||
embedProperties?: string | null
|
||||
): void
|
||||
(e: "delete-shared-request", codeID: string): void
|
||||
(e: "open-new-tab", request: HoppRESTRequest): void
|
||||
}>()
|
||||
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
const openInNewTabAction = ref<HTMLButtonElement | null>(null)
|
||||
const customizeAction = ref<HTMLButtonElement | null>(null)
|
||||
const deleteAction = ref<HTMLButtonElement | null>(null)
|
||||
const options = ref<any | null>(null)
|
||||
|
||||
const parseRequest = computed(() =>
|
||||
pipe(props.request.request, JSON.parse, translateToNewRequest)
|
||||
)
|
||||
|
||||
const requestLabelColor = computed(() =>
|
||||
getMethodLabelColorClassOf(parseRequest.value)
|
||||
)
|
||||
|
||||
const openInNewTab = () => {
|
||||
emit("open-new-tab", parseRequest.value)
|
||||
}
|
||||
|
||||
const customizeSharedRequest = () => {
|
||||
const embedProperties = props.request.properties
|
||||
emit(
|
||||
"customize-shared-request",
|
||||
parseRequest.value,
|
||||
props.request.id,
|
||||
embedProperties
|
||||
)
|
||||
}
|
||||
|
||||
const deleteSharedRequest = () => {
|
||||
emit("delete-shared-request", props.request.id)
|
||||
}
|
||||
|
||||
const timeStamp = computed(() => shortDateTime(props.request.createdOn))
|
||||
</script>
|
||||
463
packages/hoppscotch-common/src/components/share/index.vue
Normal file
463
packages/hoppscotch-common/src/components/share/index.vue
Normal file
@@ -0,0 +1,463 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="sticky top-0 z-10 flex flex-shrink-0 flex-col overflow-x-auto bg-primary"
|
||||
>
|
||||
<WorkspaceCurrent
|
||||
:section="t('tab.shared_requests')"
|
||||
:is-only-personal="true"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="sticky top-sidebarPrimaryStickyFold z-10 flex flex-1 flex-shrink-0 justify-end overflow-x-auto border-b border-dividerLight bg-primary"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/documentation/features/shared-request"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
:icon="IconHelpCircle"
|
||||
class="py-2"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div v-if="loading" class="flex flex-col items-center justify-center">
|
||||
<HoppSmartSpinner class="mb-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
|
||||
<HoppSmartPlaceholder
|
||||
v-else-if="!currentUser"
|
||||
:src="`/images/states/${colorMode.value}/add_files.svg`"
|
||||
:alt="`${t('empty.shared_requests_logout')}`"
|
||||
:text="`${t('empty.shared_requests_logout')}`"
|
||||
>
|
||||
<HoppButtonPrimary
|
||||
:label="t('auth.login')"
|
||||
@click="invokeAction('modals.login.toggle')"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
|
||||
<template v-else-if="sharedRequests.length">
|
||||
<ShareRequest
|
||||
v-for="request in sharedRequests"
|
||||
:key="request.id"
|
||||
:request="request"
|
||||
@customize-shared-request="customizeSharedRequest"
|
||||
@delete-shared-request="deleteSharedRequest"
|
||||
@open-new-tab="openInNewTab"
|
||||
/>
|
||||
<HoppSmartIntersection
|
||||
v-if="hasMoreSharedRequests"
|
||||
@intersecting="loadMoreSharedRequests"
|
||||
>
|
||||
<div v-if="adapterLoading" class="flex flex-col items-center py-3">
|
||||
<HoppSmartSpinner />
|
||||
</div>
|
||||
</HoppSmartIntersection>
|
||||
</template>
|
||||
|
||||
<div v-else-if="adapterError" class="flex flex-col items-center py-4">
|
||||
<icon-lucide-help-circle class="svg-icons mb-4" />
|
||||
{{ getErrorMessage(adapterError) }}
|
||||
</div>
|
||||
|
||||
<HoppSmartPlaceholder
|
||||
v-else
|
||||
:src="`/images/states/${colorMode.value}/add_files.svg`"
|
||||
:alt="`${t('empty.shared_requests')}`"
|
||||
:text="t('empty.shared_requests')"
|
||||
@drop.stop
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<HoppSmartConfirmModal
|
||||
:show="showConfirmModal"
|
||||
:title="confirmModalTitle"
|
||||
:loading-state="modalLoadingState"
|
||||
@hide-modal="showConfirmModal = false"
|
||||
@resolve="resolveConfirmModal"
|
||||
/>
|
||||
<ShareModal
|
||||
v-model="selectedWidget"
|
||||
v-model:embed-options="embedOptions"
|
||||
:step="step"
|
||||
:request="requestToShare"
|
||||
:show="showShareRequestModal"
|
||||
:loading="shareRequestCreatingLoading"
|
||||
@hide-modal="displayCustomizeRequestModal(false, null)"
|
||||
@copy-shared-request="copySharedRequest"
|
||||
@create-shared-request="createSharedRequest"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import ShortcodeListAdapter from "~/helpers/shortcode/ShortcodeListAdapter"
|
||||
import { useReadonlyStream } from "~/composables/stream"
|
||||
import { onAuthEvent, onLoggedIn } from "~/composables/auth"
|
||||
import { computed } from "vue"
|
||||
import { useColorMode } from "~/composables/theming"
|
||||
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
||||
import { platform } from "~/platform"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import {
|
||||
deleteShortcode as backendDeleteShortcode,
|
||||
createShortcode,
|
||||
updateEmbedProperties,
|
||||
} from "~/helpers/backend/mutations/Shortcode"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { ref } from "vue"
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { useService } from "dioc/vue"
|
||||
import { watch } from "vue"
|
||||
|
||||
const t = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
const toast = useToast()
|
||||
|
||||
const showConfirmModal = ref(false)
|
||||
const confirmModalTitle = ref("")
|
||||
const modalLoadingState = ref(false)
|
||||
|
||||
const showShareRequestModal = ref(false)
|
||||
|
||||
const sharedRequestID = ref("")
|
||||
const shareRequestCreatingLoading = ref(false)
|
||||
|
||||
const requestToShare = ref<HoppRESTRequest | null>(null)
|
||||
|
||||
const embedOptions = ref<EmbedOption>({
|
||||
selectedTab: "parameters",
|
||||
tabs: [
|
||||
{
|
||||
value: "parameters",
|
||||
label: t("tab.parameters"),
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
value: "body",
|
||||
label: t("tab.body"),
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
value: "headers",
|
||||
label: t("tab.headers"),
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
value: "authorization",
|
||||
label: t("tab.authorization"),
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
theme: "system",
|
||||
})
|
||||
|
||||
const updateEmbedProperty = async (
|
||||
shareRequestID: string,
|
||||
properties: string
|
||||
) => {
|
||||
const customizeEmbedResult = await updateEmbedProperties(
|
||||
shareRequestID,
|
||||
properties
|
||||
)()
|
||||
|
||||
if (E.isLeft(customizeEmbedResult)) {
|
||||
toast.error(`${customizeEmbedResult.left.error}`)
|
||||
toast.error(t("error.something_went_wrong"))
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => embedOptions.value,
|
||||
() => {
|
||||
if (
|
||||
requestToShare.value &&
|
||||
requestToShare.value.id &&
|
||||
showShareRequestModal.value
|
||||
) {
|
||||
if (selectedWidget.value.value === "embed") {
|
||||
const properties = {
|
||||
options: embedOptions.value.tabs
|
||||
.filter((tab) => tab.enabled)
|
||||
.map((tab) => tab.value),
|
||||
theme: embedOptions.value.theme,
|
||||
}
|
||||
updateEmbedProperty(requestToShare.value.id, JSON.stringify(properties))
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const restTab = useService(RESTTabService)
|
||||
|
||||
const currentUser = useReadonlyStream(
|
||||
platform.auth.getCurrentUserStream(),
|
||||
platform.auth.getCurrentUser()
|
||||
)
|
||||
|
||||
const step = ref(1)
|
||||
|
||||
type EmbedTabs = "parameters" | "body" | "headers" | "authorization"
|
||||
|
||||
type EmbedOption = {
|
||||
selectedTab: EmbedTabs
|
||||
tabs: {
|
||||
value: EmbedTabs
|
||||
label: string
|
||||
enabled: boolean
|
||||
}[]
|
||||
theme: "light" | "dark" | "system"
|
||||
}
|
||||
|
||||
type WidgetID = "embed" | "button" | "link"
|
||||
|
||||
type Widget = {
|
||||
value: WidgetID
|
||||
label: string
|
||||
info: string
|
||||
}
|
||||
|
||||
const selectedWidget = ref<Widget>({
|
||||
value: "embed",
|
||||
label: t("shared_requests.embed"),
|
||||
info: t("shared_requests.embed_info"),
|
||||
})
|
||||
|
||||
const adapter = new ShortcodeListAdapter(true)
|
||||
const adapterLoading = useReadonlyStream(adapter.loading$, false)
|
||||
const adapterError = useReadonlyStream(adapter.error$, null)
|
||||
const sharedRequests = useReadonlyStream(adapter.shortcodes$, [])
|
||||
const hasMoreSharedRequests = useReadonlyStream(
|
||||
adapter.hasMoreShortcodes$,
|
||||
true
|
||||
)
|
||||
|
||||
const loading = computed(
|
||||
() => adapterLoading.value && sharedRequests.value.length === 0
|
||||
)
|
||||
|
||||
onLoggedIn(() => {
|
||||
try {
|
||||
adapter.initialize()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
|
||||
onAuthEvent((ev) => {
|
||||
if (ev.event === "logout" && adapter.isInitialized()) {
|
||||
adapter.dispose()
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
const deleteSharedRequest = (codeID: string) => {
|
||||
if (currentUser.value) {
|
||||
sharedRequestID.value = codeID
|
||||
confirmModalTitle.value = `${t("confirm.remove_shared_request")}`
|
||||
showConfirmModal.value = true
|
||||
} else {
|
||||
invokeAction("modals.login.toggle")
|
||||
}
|
||||
}
|
||||
|
||||
const onDeleteSharedRequest = () => {
|
||||
modalLoadingState.value = true
|
||||
pipe(
|
||||
backendDeleteShortcode(sharedRequestID.value),
|
||||
TE.match(
|
||||
(err: GQLError<string>) => {
|
||||
toast.error(getErrorMessage(err))
|
||||
showConfirmModal.value = false
|
||||
},
|
||||
() => {
|
||||
toast.success(t("shared_requests.deleted"))
|
||||
sharedRequestID.value = ""
|
||||
modalLoadingState.value = false
|
||||
showConfirmModal.value = false
|
||||
}
|
||||
)
|
||||
)()
|
||||
}
|
||||
|
||||
const loadMoreSharedRequests = () => {
|
||||
adapter.loadMore()
|
||||
}
|
||||
|
||||
const displayShareRequestModal = (show: boolean) => {
|
||||
showShareRequestModal.value = show
|
||||
step.value = 1
|
||||
}
|
||||
|
||||
const displayCustomizeRequestModal = (
|
||||
show: boolean,
|
||||
embedProperties?: string | null
|
||||
) => {
|
||||
showShareRequestModal.value = show
|
||||
step.value = 2
|
||||
if (!embedProperties) {
|
||||
selectedWidget.value = {
|
||||
value: "button",
|
||||
label: t("shared_requests.button"),
|
||||
info: t("shared_requests.button_info"),
|
||||
}
|
||||
embedOptions.value = {
|
||||
selectedTab: "parameters",
|
||||
tabs: [
|
||||
{
|
||||
value: "parameters",
|
||||
label: t("tab.parameters"),
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
value: "body",
|
||||
label: t("tab.body"),
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
value: "headers",
|
||||
label: t("tab.headers"),
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
value: "authorization",
|
||||
label: t("tab.authorization"),
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
theme: "system",
|
||||
}
|
||||
} else {
|
||||
const parsedEmbedProperties = JSON.parse(embedProperties)
|
||||
embedOptions.value = {
|
||||
selectedTab: parsedEmbedProperties.options[0],
|
||||
tabs: embedOptions.value.tabs.map((tab) => {
|
||||
return {
|
||||
...tab,
|
||||
enabled: parsedEmbedProperties.options.includes(tab.value),
|
||||
}
|
||||
}),
|
||||
theme: parsedEmbedProperties.theme,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const createSharedRequest = async (request: HoppRESTRequest | null) => {
|
||||
if (request && selectedWidget.value) {
|
||||
const properties = {
|
||||
options: ["parameters", "body", "headers"],
|
||||
theme: "system",
|
||||
}
|
||||
shareRequestCreatingLoading.value = true
|
||||
const sharedRequestResult = await createShortcode(
|
||||
request,
|
||||
selectedWidget.value.value === "embed"
|
||||
? JSON.stringify(properties)
|
||||
: undefined
|
||||
)()
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SHORTCODE_CREATED",
|
||||
})
|
||||
|
||||
if (E.isLeft(sharedRequestResult)) {
|
||||
toast.error(`${sharedRequestResult.left.error}`)
|
||||
toast.error(t("error.something_went_wrong"))
|
||||
} else if (E.isRight(sharedRequestResult)) {
|
||||
if (sharedRequestResult.right.createShortcode) {
|
||||
shareRequestCreatingLoading.value = false
|
||||
requestToShare.value = {
|
||||
...JSON.parse(sharedRequestResult.right.createShortcode.request),
|
||||
id: sharedRequestResult.right.createShortcode.id,
|
||||
}
|
||||
step.value = 2
|
||||
|
||||
if (sharedRequestResult.right.createShortcode.properties) {
|
||||
const parsedEmbedProperties = JSON.parse(
|
||||
sharedRequestResult.right.createShortcode.properties
|
||||
)
|
||||
|
||||
embedOptions.value = {
|
||||
selectedTab: parsedEmbedProperties.options[0],
|
||||
tabs: embedOptions.value.tabs.map((tab) => {
|
||||
return {
|
||||
...tab,
|
||||
enabled: parsedEmbedProperties.options.includes(tab.value),
|
||||
}
|
||||
}),
|
||||
theme: parsedEmbedProperties.theme,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const customizeSharedRequest = (
|
||||
request: HoppRESTRequest,
|
||||
shredRequestID: string,
|
||||
embedProperties?: string | null
|
||||
) => {
|
||||
requestToShare.value = {
|
||||
...request,
|
||||
id: shredRequestID,
|
||||
}
|
||||
displayCustomizeRequestModal(true, embedProperties)
|
||||
}
|
||||
|
||||
const copySharedRequest = (payload: {
|
||||
sharedRequestID: string | undefined
|
||||
content: string | undefined
|
||||
}) => {
|
||||
if (payload.content) {
|
||||
copyToClipboard(payload.content)
|
||||
toast.success(t("state.copied_to_clipboard"))
|
||||
}
|
||||
}
|
||||
|
||||
const openInNewTab = (request: HoppRESTRequest) => {
|
||||
restTab.createNewTab({
|
||||
isDirty: false,
|
||||
request,
|
||||
})
|
||||
}
|
||||
|
||||
const resolveConfirmModal = (title: string | null) => {
|
||||
if (title === `${t("confirm.remove_shared_request")}`) onDeleteSharedRequest()
|
||||
else {
|
||||
console.error(
|
||||
`Confirm modal title ${title} is not handled by the component`
|
||||
)
|
||||
toast.error(t("error.something_went_wrong"))
|
||||
showConfirmModal.value = false
|
||||
sharedRequestID.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
const getErrorMessage = (err: GQLError<string>) => {
|
||||
if (err.type === "network_error") {
|
||||
return t("error.network_error")
|
||||
} else {
|
||||
switch (err.error) {
|
||||
case "shortcode/not_found":
|
||||
return t("shared_request.not_found")
|
||||
default:
|
||||
return t("error.something_went_wrong")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defineActionHandler("share.request", ({ request }) => {
|
||||
requestToShare.value = request
|
||||
displayShareRequestModal(true)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center p-4 border rounded border-dividerDark">
|
||||
<img :src="img" :alt="t('shared_requests.run_in_hoppscotch')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
|
||||
defineProps<{
|
||||
img: string
|
||||
}>()
|
||||
|
||||
const t = useI18n()
|
||||
</script>
|
||||
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col p-4 border rounded border-dividerDark"
|
||||
:class="{
|
||||
'bg-accentContrast': isEmbedThemeLight,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="flex items-stretch space-x-2"
|
||||
:class="{
|
||||
'bg-accentContrast': isEmbedThemeLight,
|
||||
}"
|
||||
>
|
||||
<span class="flex items-center min-w-0 border rounded border-divider">
|
||||
<span
|
||||
class="flex max-w-[4rem] rounded-l h-full items-center justify-center border-r border-divider text-tiny"
|
||||
:class="{
|
||||
'!border-dividerLight bg-accentContrast text-primary':
|
||||
isEmbedThemeLight,
|
||||
}"
|
||||
>
|
||||
<span class="px-3 truncate">
|
||||
{{ method }}
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
class="px-3 truncate"
|
||||
:class="{
|
||||
'text-primary': isEmbedThemeLight,
|
||||
}"
|
||||
>
|
||||
{{ endpoint }}
|
||||
</span>
|
||||
</span>
|
||||
<button
|
||||
class="flex items-center justify-center flex-shrink-0 px-3 py-2 font-semibold border rounded border-dividerDark bg-primaryDark text-secondary"
|
||||
:class="{
|
||||
'!bg-accentContrast text-primaryLight': isEmbedThemeLight,
|
||||
}"
|
||||
>
|
||||
{{ t("action.send") }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="flex"
|
||||
:class="{
|
||||
'bg-accentContrast text-primary': isEmbedThemeLight,
|
||||
'border-b border-divider pt-2': !noActiveTab,
|
||||
}"
|
||||
>
|
||||
<span
|
||||
v-for="option in embedOptions.tabs"
|
||||
v-show="option.enabled"
|
||||
:key="option.value"
|
||||
class="px-2 py-2"
|
||||
:class="{
|
||||
'border-b border-dividerDark':
|
||||
embedOptions.tabs.filter((tab) => tab.enabled)[0]?.value ===
|
||||
option.value,
|
||||
}"
|
||||
>
|
||||
{{ option.label }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { computed } from "vue"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
|
||||
type Tabs = "parameters" | "body" | "headers" | "authorization"
|
||||
|
||||
type EmbedOption = {
|
||||
selectedTab: Tabs
|
||||
tabs: {
|
||||
value: Tabs
|
||||
label: string
|
||||
enabled: boolean
|
||||
}[]
|
||||
theme: "light" | "dark" | "system"
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
method: string | undefined
|
||||
endpoint: string | undefined
|
||||
modelValue: EmbedOption
|
||||
}>()
|
||||
|
||||
const embedOptions = useVModel(props, "modelValue")
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const noActiveTab = computed(() => {
|
||||
return embedOptions.value.tabs.every((tab) => !tab.enabled)
|
||||
})
|
||||
|
||||
const isEmbedThemeLight = computed(() => embedOptions.value.theme === "light")
|
||||
</script>
|
||||
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center p-4 border rounded border-dividerDark">
|
||||
<span
|
||||
:class="{
|
||||
'border-b border-secondary': label,
|
||||
}"
|
||||
>
|
||||
{{ text }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from "vue"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
link?: string | undefined
|
||||
label?: string | undefined
|
||||
}>()
|
||||
|
||||
const text = computed(() => {
|
||||
return props.label ? t(props.label) : `hopp.sh/r/${props.link ?? "xxxx"}`
|
||||
})
|
||||
</script>
|
||||
@@ -6,7 +6,7 @@
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<span class="select-wrapper">
|
||||
<HoppSmartSelectWrapper>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('settings.choose_language')"
|
||||
@@ -15,7 +15,7 @@
|
||||
outline
|
||||
:label="currentLocale.name"
|
||||
/>
|
||||
</span>
|
||||
</HoppSmartSelectWrapper>
|
||||
<template #content="{ hide }">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<HoppSmartInput
|
||||
|
||||
@@ -124,9 +124,8 @@ onClickOutside(autoCompleteWrapper, () => {
|
||||
const uniqueAutoCompleteSource = computed(() => {
|
||||
if (props.autoCompleteSource) {
|
||||
return [...new Set(props.autoCompleteSource)]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const suggestions = computed(() => {
|
||||
@@ -139,9 +138,8 @@ const suggestions = computed(() => {
|
||||
return uniqueAutoCompleteSource.value.filter((suggestion) =>
|
||||
suggestion.toLowerCase().includes(props.modelValue.toLowerCase())
|
||||
)
|
||||
} else {
|
||||
return uniqueAutoCompleteSource.value ?? []
|
||||
}
|
||||
return uniqueAutoCompleteSource.value ?? []
|
||||
})
|
||||
|
||||
const updateModelValue = (value: string) => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user