Compare commits

...

23 Commits

Author SHA1 Message Date
Liyas Thomas
1dcfc684ef feat: revamped header wireframe 2023-12-01 19:49:23 +05:30
Liyas Thomas
1cc845e17d fix: minor ui improvements (#3603) 2023-11-29 22:45:40 +05:30
James George
60bfb6fe2c refactor: move persistence logic into a dedicated service (#3493) 2023-11-29 22:40:26 +05:30
Joel Jacob Stephen
144d14ab5b fix: email validation failure in cases when email entered is correct when trying to create a team in admin dashboard (#3588)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2023-11-29 21:49:49 +05:30
Anwarul Islam
8f1ca6e282 feat: platform definition added for additional settings components (#3503) 2023-11-29 21:42:51 +05:30
Dante Calderon
a93758c6b7 chore: Remove whitespace in env variables 2023-11-22 19:40:32 +05:30
James George
1829c088cc feat: support for subpath based access in SH apps (#3449)
Co-authored-by: Balu Babu <balub997@gmail.com>
2023-11-22 19:35:35 +05:30
Akash K
ee1425d0dd fix: XML body disappearing with invalid XML (#3567)
fix: catch xmlformatter errors
2023-11-20 15:02:07 +05:30
Joel Jacob Stephen
24ae090916 refactor: allow banner service to hold multiple banners and display the banner with the highest score (#3556) 2023-11-17 20:31:34 +05:30
Nivedin
a3aa9b68fc refactor: interceptor error display in graphql response (#3553) 2023-11-17 17:03:53 +05:30
Joel Jacob Stephen
50f475334e fix: enlarged hoppscotch logo on dashboard login screen (#3559)
fix: resize the dashboard login icon
2023-11-16 22:51:08 +05:30
Andrew Bastin
7b18526f24 chore: merge hoppscotch/main into hoppscotch/release/2023.12.0 2023-11-16 13:51:12 +05:30
Balu Babu
f5d2e4f11f feat: introducing shortcode into admin module (#3504)
* feat: added query in infra to fetch all shortcodes

* feat: added mutation in admin to delete shortcode

* chore: added new tests for methods in shortcode module

* chore: removed .vscode file

* chore: added a new ShortcodeCreator type to output of fetchAllShortcodes query

* chore: shortcodeCreator type is now nullable

* chore: added type defs to fetchAllShortcodes method in admin module

* docs: update code comments

* chore: changed target to prod in hoppscotch-old-backend

---------

Co-authored-by: Mir Arif Hasan <arif.ishan05@gmail.com>
2023-11-10 14:28:02 +05:30
Balu Babu
4caf0053cd feat: introduction of shared-requests (#3476)
* feat: added new property to existing shortcode model in prisma schema

* chore: created shared-requests module

* chore: created shared-request model

* chore: complete sharedRequest query

* chore: completed mutation to create a SharedRequest

* chore: completed subscription to create a SharedRequest

* chore: completed query to fetch all user created shared-requests

* chore: completed mutation to delete a SharedRequest

* chore: completed subscription to delete a SharedRequest

* chore: removed unused dependncues in share-requests module

* chore: added shared-requests into user deletion spec

* test: added all testcases for shared-request module

* test: modified all relevant tests in shortcode module

* chore: added deprecated label to all queries,mutations and subscriptions in the shortcode module

* chore: resolved all comments raised in review

* feat: added ability to update and listen to updates of shared-requests

* chore: added updatedOn field to shortcode model

* chore: fixed issue with updateSharedRequest method

* chore: fixed incorrect value getting updated

* chore: added all test-cases for updateSharedRequest method

* chore: created migration for shared-requests

* chore: moved shared-requests into shortcode module

* chore: added missing import in shortcode tests

* chore: changed properties to embedProperties in shortcode model

* feat: generated migrations file for new schema changes to Shortcodes table

* chore: changed target of old-backend service in docker-compose file

* chore: fixed issue with updatedOn field in shortcodes model

* chore: removed unused dependencies

* fix: handle invalid input for shortcode properties

* Revert "fix: handle invalid input for shortcode properties"

This reverts commit 4dcb0afb18.

* chore: changed updateShortcode method name to updateEmbedProperties

* chore: changed target of hoppscotch-old-backend service to prod

---------

Co-authored-by: Mir Arif Hasan <arif.ishan05@gmail.com>
2023-11-07 17:57:51 +05:30
Andrew Bastin
93ce86f32d chore: merge hoppscotch/release/2023.8.3 into hoppscotch/release/2023.12.0 2023-11-06 18:56:01 +05:30
Joel Jacob Stephen
507fe69efe feat: new banner service and added ability to bind additional services from other platforms (#3474)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2023-11-06 11:41:19 +05:30
Joel Jacob Stephen
23e3739718 feat: introducing a new smart table hoppscotch ui component (#3178)
Co-authored-by: Anwarul Islam <anwaarulislaam@gmail.com>
2023-11-06 11:31:55 +05:30
Andrew Bastin
6daa043a1b chore: merge hoppscotch/release/2023.8.3 into hoppscotch/release/2023.12.0 2023-11-03 10:12:54 +05:30
Joel Jacob Stephen
9dcbc4a126 refactor: updated dashboard gql queries and components to use the new infra type of the updated schema (#3455) 2023-11-01 23:40:19 +05:30
Anwarul Islam
a215860782 feat: replacing windicss by tailwindcss in hoppscotch-ui (#3076)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
Co-authored-by: Joel Jacob Stephen <70131076+JoelJacobStephen@users.noreply.github.com>
Co-authored-by: nivedin <nivedinp@gmail.com>
2023-11-01 20:55:08 +05:30
Mir Arif Hasan
59b5a50a97 HBE-296 feat: introducing 'infra' type and splitting model properties between 'admin' and 'infra' (#3445)
* feat: infra type added in admin module

* feat: infra-resolver added in admin module

* feat: feedback resolved

* feat: deprecated tag added in some admin ResolveFields

* build: update pnpm-lock file

* feat: add field in infra type

* feat: admin extends user partially

* feat: admin extends user with omitting some fields

* chore: remove unused imports

* build: conflict resolve in pnpm lock file
2023-10-30 17:03:43 +06:00
Andrew Bastin
d1c9c3583f chore: merge hoppscotch/release/2023.8.3 into hoppscotch/release/2023.12.0 2023-10-19 09:34:49 +05:30
James George
2462492c86 chore(cli): bump dependencies (#3441)
* chore: bump CLI dependencies

* chore: update package.json

Bump version and specify minimum Node.js version
2023-10-16 18:23:22 +05:30
260 changed files with 9059 additions and 4043 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ services:
environment:
# Edit the below line to match your PostgresDB URL if you have an outside DB (make sure to update the .env file as well)
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
- PORT=3170
- PORT=8080
volumes:
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
# - ./packages/hoppscotch-backend/:/usr/src/app
@@ -26,6 +26,7 @@ services:
hoppscotch-db:
condition: service_healthy
ports:
- "3180:80"
- "3170:3170"
# The main hoppscotch app. This will be hosted at port 3000
@@ -42,7 +43,8 @@ services:
depends_on:
- hoppscotch-backend
ports:
- "3000:8080"
- "3080:80"
- "3000:3000"
# The Self Host dashboard for managing the app. This will be hosted at port 3100
# NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for
@@ -58,7 +60,8 @@ services:
depends_on:
- hoppscotch-backend
ports:
- "3100:8080"
- "3280:80"
- "3100:3100"
# The service that spins up all 3 services at once in one container
hoppscotch-aio:
@@ -76,6 +79,7 @@ services:
- "3000:3000"
- "3100:3100"
- "3170:3170"
- "3080:80"
# The preset DB service, you can delete/comment the below lines if
# you are using an external postgres instance

View File

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

View File

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

View File

@@ -68,11 +68,13 @@ model TeamRequest {
}
model Shortcode {
id String @id
request Json
creatorUid String?
createdOn DateTime @default(now())
id String @id @unique
request Json
embedProperties Json?
creatorUid String?
User User? @relation(fields: [creatorUid], references: [uid])
createdOn DateTime @default(now())
updatedOn DateTime @updatedAt @default(now())
@@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique")
}
@@ -102,6 +104,7 @@ model User {
currentGQLSession Json?
createdOn DateTime @default(now()) @db.Timestamp(3)
invitedUsers InvitedUsers[]
shortcodes Shortcode[]
}
model Account {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,225 @@
import { UseGuards } from '@nestjs/common';
import { Args, ID, Query, ResolveField, Resolver } from '@nestjs/graphql';
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
import { Infra } from './infra.model';
import { AdminService } from './admin.service';
import { GqlAuthGuard } from 'src/guards/gql-auth.guard';
import { GqlAdminGuard } from './guards/gql-admin.guard';
import { User } from 'src/user/user.model';
import { AuthUser } from 'src/types/AuthUser';
import { throwErr } from 'src/utils';
import * as E from 'fp-ts/Either';
import { Admin } from './admin.model';
import { PaginationArgs } from 'src/types/input-types.args';
import { InvitedUser } from './invited-user.model';
import { Team } from 'src/team/team.model';
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
import { GqlAdmin } from './decorators/gql-admin.decorator';
import { ShortcodeWithUserEmail } from 'src/shortcode/shortcode.model';
@UseGuards(GqlThrottlerGuard)
@Resolver(() => Infra)
export class InfraResolver {
constructor(private adminService: AdminService) {}
@Query(() => Infra, {
description: 'Fetch details of the Infrastructure',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
infra(@GqlAdmin() admin: Admin) {
const infra: Infra = { executedBy: admin };
return infra;
}
@ResolveField(() => [User], {
description: 'Returns a list of all admin users in infra',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async admins() {
const admins = await this.adminService.fetchAdmins();
return admins;
}
@ResolveField(() => User, {
description: 'Returns a user info by UID',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async userInfo(
@Args({
name: 'userUid',
type: () => ID,
description: 'The user UID',
})
userUid: string,
): Promise<AuthUser> {
const user = await this.adminService.fetchUserInfo(userUid);
if (E.isLeft(user)) throwErr(user.left);
return user.right;
}
@ResolveField(() => [User], {
description: 'Returns a list of all the users in infra',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async allUsers(@Args() args: PaginationArgs): Promise<AuthUser[]> {
const users = await this.adminService.fetchUsers(args.cursor, args.take);
return users;
}
@ResolveField(() => [InvitedUser], {
description: 'Returns a list of all the invited users',
})
async invitedUsers(): Promise<InvitedUser[]> {
const users = await this.adminService.fetchInvitedUsers();
return users;
}
@ResolveField(() => [Team], {
description: 'Returns a list of all the teams in the infra',
})
async allTeams(@Args() args: PaginationArgs): Promise<Team[]> {
const teams = await this.adminService.fetchAllTeams(args.cursor, args.take);
return teams;
}
@ResolveField(() => Team, {
description: 'Returns a team info by ID when requested by Admin',
})
async teamInfo(
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which info to fetch',
})
teamID: string,
): Promise<Team> {
const team = await this.adminService.getTeamInfo(teamID);
if (E.isLeft(team)) throwErr(team.left);
return team.right;
}
@ResolveField(() => Number, {
description: 'Return count of all the members in a team',
})
async membersCountInTeam(
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which team members to fetch',
nullable: false,
})
teamID: string,
): Promise<number> {
const teamMembersCount = await this.adminService.membersCountInTeam(teamID);
return teamMembersCount;
}
@ResolveField(() => Number, {
description: 'Return count of all the stored collections in a team',
})
async collectionCountInTeam(
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which team members to fetch',
})
teamID: string,
): Promise<number> {
const teamCollCount = await this.adminService.collectionCountInTeam(teamID);
return teamCollCount;
}
@ResolveField(() => Number, {
description: 'Return count of all the stored requests in a team',
})
async requestCountInTeam(
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which team members to fetch',
})
teamID: string,
): Promise<number> {
const teamReqCount = await this.adminService.requestCountInTeam(teamID);
return teamReqCount;
}
@ResolveField(() => Number, {
description: 'Return count of all the stored environments in a team',
})
async environmentCountInTeam(
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which team members to fetch',
})
teamID: string,
): Promise<number> {
const envsCount = await this.adminService.environmentCountInTeam(teamID);
return envsCount;
}
@ResolveField(() => [TeamInvitation], {
description: 'Return all the pending invitations in a team',
})
async pendingInvitationCountInTeam(
@Args({
name: 'teamID',
type: () => ID,
description: 'Team ID for which team members to fetch',
})
teamID: string,
) {
const invitations = await this.adminService.pendingInvitationCountInTeam(
teamID,
);
return invitations;
}
@ResolveField(() => Number, {
description: 'Return total number of Users in organization',
})
async usersCount() {
return this.adminService.getUsersCount();
}
@ResolveField(() => Number, {
description: 'Return total number of Teams in organization',
})
async teamsCount() {
return this.adminService.getTeamsCount();
}
@ResolveField(() => Number, {
description: 'Return total number of Team Collections in organization',
})
async teamCollectionsCount() {
return this.adminService.getTeamCollectionsCount();
}
@ResolveField(() => Number, {
description: 'Return total number of Team Requests in organization',
})
async teamRequestsCount() {
return this.adminService.getTeamRequestsCount();
}
@ResolveField(() => [ShortcodeWithUserEmail], {
description: 'Returns a list of all the shortcodes in the infra',
})
async allShortcodes(
@Args() args: PaginationArgs,
@Args({
name: 'userEmail',
nullable: true,
description: 'Users email to filter shortcodes by',
})
userEmail: string,
) {
return await this.adminService.fetchAllShortcodes(
args.cursor,
args.take,
userEmail,
);
}
}

View File

@@ -318,18 +318,6 @@ export const TEAM_INVITATION_NOT_FOUND =
*/
export const SHORTCODE_NOT_FOUND = 'shortcode/not_found' as const;
/**
* Invalid ShortCode format
* (ShortcodeService)
*/
export const SHORTCODE_INVALID_JSON = 'shortcode/invalid_json' as const;
/**
* ShortCode already exists in DB
* (ShortcodeService)
*/
export const SHORTCODE_ALREADY_EXISTS = 'shortcode/already_exists' as const;
/**
* Invalid or non-existent TEAM ENVIRONMENT ID
* (TeamEnvironmentsService)
@@ -621,3 +609,24 @@ export const MAILER_SMTP_URL_UNDEFINED = 'mailer/smtp_url_undefined' as const;
*/
export const MAILER_FROM_ADDRESS_UNDEFINED =
'mailer/from_address_undefined' as const;
/**
* SharedRequest invalid request JSON format
* (ShortcodeService)
*/
export const SHORTCODE_INVALID_REQUEST_JSON =
'shortcode/request_invalid_format' as const;
/**
* SharedRequest invalid properties JSON format
* (ShortcodeService)
*/
export const SHORTCODE_INVALID_PROPERTIES_JSON =
'shortcode/properties_invalid_format' as const;
/**
* SharedRequest invalid properties not found
* (ShortcodeService)
*/
export const SHORTCODE_PROPERTIES_NOT_FOUND =
'shortcode/properties_not_found' as const;

View File

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

View File

@@ -69,5 +69,7 @@ export type TopicDef = {
[topic: `team_req/${string}/req_deleted`]: string;
[topic: `team/${string}/invite_added`]: TeamInvitation;
[topic: `team/${string}/invite_removed`]: string;
[topic: `shortcode/${string}/${'created' | 'revoked'}`]: Shortcode;
[
topic: `shortcode/${string}/${'created' | 'revoked' | 'updated'}`
]: Shortcode;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,25 @@
/*
* Write hoppscotch-common related custom styles in this file.
* If styles are sharable across all package then write into hoppscotch-ui/assets/scss/styles.scss file.
*/
* {
@apply backface-hidden;
@apply before:backface-hidden;
@apply after:backface-hidden;
backface-visibility: hidden;
-moz-backface-visibility: hidden;
-webkit-backface-visibility: hidden;
&::before {
backface-visibility: hidden;
-moz-backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
&::after {
backface-visibility: hidden;
-moz-backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
@apply selection:bg-accentDark;
@apply selection:text-accentContrast;
@apply overscroll-none;
@@ -11,17 +29,25 @@
@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 {
@apply bg-transparent;
@apply border-solid border-l border-dividerLight border-t-0 border-b-0 border-r-0;
@apply border-b-0 border-l border-r-0 border-t-0 border-solid border-dividerLight;
}
::-webkit-scrollbar-thumb {
@apply bg-divider bg-clip-content;
@apply rounded-full;
@apply border-solid border-transparent border-4;
@apply border-4 border-solid border-transparent;
@apply hover:bg-dividerDark;
@apply hover:bg-clip-content;
}
@@ -54,7 +80,7 @@ html {
body {
@apply bg-primary;
@apply text-secondary text-body;
@apply text-body text-secondary;
@apply font-medium;
@apply select-none;
@apply overflow-x-hidden;
@@ -124,8 +150,8 @@ a {
&.link {
@apply items-center;
@apply py-0.5 px-1;
@apply -my-0.5 -mx-1;
@apply px-1 py-0.5;
@apply -mx-1 -my-0.5;
@apply text-accent;
@apply rounded;
@apply hover:text-accentDark;
@@ -140,7 +166,7 @@ a {
@apply shadow-none #{!important};
@apply fixed;
@apply inline-flex;
@apply -mt-7.5;
@apply -mt-8;
}
}
@@ -154,7 +180,7 @@ a {
@apply flex;
@apply text-tiny text-primary;
@apply font-semibold;
@apply py-1 px-2;
@apply px-2 py-1;
@apply truncate;
@apply leading-normal;
@apply items-center;
@@ -162,7 +188,7 @@ a {
kbd {
@apply hidden;
@apply font-sans;
@apply bg-gray-500/45;
background-color: rgba(107, 114, 128, 0.45);
@apply text-primaryLight;
@apply rounded-sm;
@apply px-1;
@@ -170,6 +196,12 @@ a {
@apply truncate;
@apply sm:inline-flex;
}
.env-icon {
@apply transition;
@apply inline-flex;
@apply items-center;
}
}
.tippy-svg-arrow {
@@ -195,7 +227,7 @@ a {
@apply max-h-[45vh];
@apply items-stretch;
@apply overflow-y-auto;
@apply text-secondary text-body;
@apply text-body text-secondary;
@apply p-2;
@apply leading-normal;
@apply focus:outline-none;
@@ -234,7 +266,7 @@ hr {
.heading {
@apply font-bold;
@apply text-secondaryDark text-lg;
@apply text-lg text-secondaryDark;
@apply tracking-tight;
}
@@ -243,7 +275,7 @@ hr {
.textarea {
@apply flex;
@apply w-full;
@apply py-2 px-4;
@apply px-4 py-2;
@apply bg-transparent;
@apply rounded;
@apply text-secondaryDark;
@@ -284,7 +316,7 @@ button {
@apply transform;
@apply origin-top-left;
@apply scale-75;
@apply translate-x-1 -translate-y-4;
@apply -translate-y-4 translate-x-1;
}
.floating-input:focus-within ~ label {
@@ -293,7 +325,7 @@ button {
.floating-input ~ .end-actions {
@apply absolute;
@apply right-0.2;
@apply right-[.05rem];
@apply inset-y-0;
@apply flex;
@apply items-center;
@@ -335,23 +367,23 @@ pre.ace_editor {
}
.info-response {
@apply text-pink-500;
color: var(--info-color);
}
.success-response {
@apply text-green-500;
color: var(--success-color);
}
.redir-response {
@apply text-yellow-500;
color: var(--warning-color);
}
.cl-error-response {
@apply text-red-500;
color: var(--cl-error-color);
}
.sv-error-response {
@apply text-red-600;
color: var(--sv-error-color);
}
.missing-data-response {
@@ -366,7 +398,7 @@ pre.ace_editor {
@apply px-4 py-2;
@apply bg-tooltip;
@apply border-secondaryDark;
@apply text-primary text-body;
@apply text-body text-primary;
@apply justify-between;
@apply shadow-lg;
@apply font-semibold;
@@ -394,7 +426,7 @@ pre.ace_editor {
@apply before:opacity-10;
@apply before:inset-0;
@apply before:transition;
@apply before:content-DEFAULT;
@apply before:content-[''];
@apply hover:no-underline;
@apply hover:before:opacity-20;
}
@@ -428,7 +460,7 @@ pre.ace_editor {
@apply before:opacity-0;
@apply before:z-20;
@apply before:transition;
@apply before:content-DEFAULT;
@apply before:content-[''];
@apply hover:before:opacity-100;
}
@@ -501,22 +533,6 @@ pre.ace_editor {
}
}
.cm-panel.cm-search [name="close"] {
@apply flex;
@apply items-center;
@apply justify-center;
@apply min-h-5;
@apply min-w-5;
@apply bg-primaryDark #{!important};
@apply sticky #{!important};
@apply right-0 #{!important};
@apply ml-auto #{!important};
@apply my-auto #{!important};
@apply rounded #{!important};
@apply outline #{!important};
@apply outline-divider #{!important};
}
.shortcut-key {
@apply inline-flex;
@apply font-sans;

View File

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

View File

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

View File

@@ -0,0 +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;
}
@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;
}
@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;
}
@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;
}
@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;
}
@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;
}
@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;
}
@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;
}
@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;
}

View File

@@ -0,0 +1,81 @@
@mixin base-theme {
--font-sans: "Inter Variable", sans-serif;
--font-icon: "Material Symbols Rounded Variable";
--font-mono: "Roboto Mono Variable", monospace;
--font-size-body: 0.75rem;
--font-size-tiny: 0.688rem;
--line-height-body: 1rem;
--upper-primary-sticky-fold: 4.125rem;
--upper-secondary-sticky-fold: 6.188rem;
--upper-tertiary-sticky-fold: 8.25rem;
--upper-fourth-sticky-fold: 10.2rem;
--upper-mobile-primary-sticky-fold: 6.625rem;
--upper-mobile-secondary-sticky-fold: 8.688rem;
--upper-mobile-sticky-fold: 10.75rem;
--upper-mobile-tertiary-sticky-fold: 8.25rem;
--lower-primary-sticky-fold: 3rem;
--lower-secondary-sticky-fold: 5.063rem;
--lower-tertiary-sticky-fold: 7.125rem;
--lower-fourth-sticky-fold: 9.188rem;
--sidebar-primary-sticky-fold: 2rem;
}
@mixin dark-theme {
--primary-color: #181818;
--primary-light-color: #1c1c1e;
--primary-dark-color: #262626;
--primary-contrast-color: #171717;
--secondary-color: #a3a3a3;
--secondary-light-color: #737373;
--secondary-dark-color: #fafafa;
--divider-color: #262626;
--divider-light-color: #1f1f1f;
--divider-dark-color: #2d2d2d;
--error-color: #292524;
--tooltip-color: #f5f5f5;
--popover-color: #1b1b1b;
--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-dark-color: #181818;
--primary-contrast-color: #0f0f0f;
--secondary-color: #a3a3a3;
--secondary-light-color: #737373;
--secondary-dark-color: #f5f5f5;
--divider-color: #1c1c1e;
--divider-light-color: #181818;
--divider-dark-color: #323232;
--error-color: #1c1917;
--tooltip-color: #f5f5f5;
--popover-color: #0f0f0f;
--editor-theme: "twilight";
}

View File

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

View File

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

View File

@@ -332,9 +332,13 @@
"url": "URL"
},
"header": {
"install_pwa": "Install app",
"install_pwa": "Add to Home Screen",
"login": "Login",
"save_workspace": "Save My Workspace"
"save_workspace": "Save My Workspace",
"download_app": "Download app",
"menu": "Menu",
"go_back": "Go back",
"go_forward": "Go forward"
},
"helpers": {
"authorization": "The authorization header will be automatically generated when you send the request.",
@@ -497,7 +501,8 @@
"enter_curl": "Enter cURL command",
"generate_code": "Generate code",
"generated_code": "Generated code",
"go_to_authorization_tab": "Go to Authorization",
"go_to_authorization_tab": "Go to Authorization tab",
"go_to_body_tab": "Go to Body tab",
"header_list": "Header List",
"invalid_name": "Please provide a name for the request",
"method": "Method",
@@ -599,6 +604,7 @@
"use_experimental_url_bar": "Use experimental URL bar with environment highlighting",
"user": "User",
"verified_email": "Verified email",
"additional": "Additional Settings",
"verify_email": "Verify email"
},
"shortcodes": {

View File

@@ -92,6 +92,7 @@
"url": "^0.11.1",
"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",
@@ -135,6 +136,7 @@
"@vue/compiler-sfc": "^3.3.4",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/runtime-core": "^3.3.4",
"autoprefixer": "^10.4.14",
"cross-env": "^7.0.3",
"dotenv": "^16.3.1",
"eslint": "^8.47.0",
@@ -143,23 +145,25 @@
"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",
"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",
"vite-plugin-html-config": "^1.0.11",
"vite-plugin-inspect": "^0.7.38",
"vite-plugin-pages": "^0.31.0",
"vite-plugin-pages-sitemap": "^1.6.1",
"vite-plugin-pwa": "^0.16.4",
"vite-plugin-vue-layouts": "^0.8.0",
"vite-plugin-windicss": "^1.9.1",
"vitest": "^0.34.2",
"vue-tsc": "^1.8.8",
"windicss": "^3.5.6"
"vue-tsc": "^1.8.8"
}
}

View File

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

View File

@@ -9,6 +9,7 @@ declare module 'vue' {
export interface GlobalComponents {
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
AppBanner: typeof import('./components/app/Banner.vue')['default']
AppContextMenu: typeof import('./components/app/ContextMenu.vue')['default']
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
AppFooter: typeof import('./components/app/Footer.vue')['default']
@@ -92,11 +93,13 @@ declare module 'vue' {
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
@@ -140,9 +143,11 @@ declare module 'vue' {
HttpTests: typeof import('./components/http/Tests.vue')['default']
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
IconLucideActivity: typeof import('~icons/lucide/activity')['default']
IconLucideAlertCircle: typeof import('~icons/lucide/alert-circle')['default']
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
IconLucideBrush: typeof import('~icons/lucide/brush')['default']
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
@@ -152,6 +157,7 @@ declare module 'vue' {
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
IconLucideRss: typeof import('~icons/lucide/rss')['default']
IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideUsers: typeof import('~icons/lucide/users')['default']
InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default']
@@ -200,6 +206,7 @@ declare module 'vue' {
SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default']
SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.vue')['default']
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
SmartTable: typeof import('./../../hoppscotch-ui/src/components/smart/Table.vue')['default']
SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default']
SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
SmartTree: typeof import('./../../hoppscotch-ui/src/components/smart/Tree.vue')['default']

View File

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

View File

@@ -0,0 +1,56 @@
<template>
<div
:role="bannerRole"
class="flex items-center px-4 py-2 text-tiny"
: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' : ''">
{{ banner.text(t) }}
</span>
</span>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue"
import { BannerContent, BannerType } from "~/services/banner.service"
import { useI18n } from "@composables/i18n"
import IconAlertCircle from "~icons/lucide/alert-circle"
import IconAlertTriangle from "~icons/lucide/alert-triangle"
import IconInfo from "~icons/lucide/info"
const props = defineProps<{
banner: BannerContent
}>()
const t = useI18n()
const ariaRoles: Record<BannerType, string> = {
error: "alert",
warning: "status",
info: "status",
}
const bgColors: Record<BannerType, string> = {
error: "bg-red-700",
warning: "bg-yellow-700",
info: "bg-stone-800",
}
const icons = {
info: IconInfo,
warning: IconAlertCircle,
error: IconAlertTriangle,
}
const bannerColor = computed(() => bgColors[props.banner.type])
const bannerIcon = computed(() => icons[props.banner.type])
const bannerRole = computed(() => ariaRoles[props.banner.type])
</script>

View File

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

View File

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

View File

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

View File

@@ -2,29 +2,53 @@
<div>
<header
ref="headerRef"
class="flex items-center justify-between flex-1 flex-shrink-0 px-2 py-2 space-x-2 overflow-x-auto overflow-y-hidden"
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 items-center justify-start flex-1 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="tracking-wide !font-bold !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark uppercase"
:label="t('app.name')"
to="/"
/>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('header.menu')"
:icon="IconMenu"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
/>
<HoppButtonSecondary
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
:label="t('app.name')"
to="/"
/>
</div>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('header.go_back')"
:icon="IconArrowLeft"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
@click="router.back()"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('header.go_forward')"
:icon="IconArrowRight"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
@click="router.forward()"
/>
</div>
</div>
<div class="inline-flex items-center justify-center flex-1 space-x-2">
<div class="col-span-1 flex items-center justify-between space-x-2">
<button
class="flex flex-1 items-center justify-between px-2 py-1 self-stretch bg-primaryDark transition text-secondaryLight cursor-text rounded border border-dividerDark max-w-60 hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
class="flex h-full flex-1 cursor-text items-center justify-between 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">
<icon-lucide-search class="mr-2 svg-icons" />
<icon-lucide-search class="svg-icons mr-2" />
{{ t("app.search") }}
</span>
<span class="flex space-x-1">
@@ -32,192 +56,224 @@
<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 items-center justify-end flex-1 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="hidden md:flex bg-green-500/15 py-1.75 border border-green-600/25 !text-green-500 hover:bg-green-400/10 focus-visible:bg-green-400/10 focus-visible:border-green-800/50 !focus-visible:text-green-600 hover:border-green-800/50 !hover:text-green-600"
@click="invokeAction('modals.login.toggle')"
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="`${
mdAndLarger ? t('support.title') : t('app.options')
} <kbd>?</kbd>`"
:icon="IconLifeBuoy"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
@click="invokeAction('modals.support.toggle')"
/>
<HoppButtonPrimary
:label="t('header.login')"
@click="invokeAction('modals.login.toggle')"
/>
</div>
<div 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 border divide-x rounded bg-green-500/15 divide-green-600/25 border-green-600/25 focus-within:bg-green-400/10 focus-within:border-green-800/50 focus-within:divide-green-800/50 hover:bg-green-400/10 hover:border-green-800/50 hover:divide-green-800/50"
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('team.invite_tooltip')"
:icon="IconUserPlus"
class="py-1.75 !text-green-500 !focus-visible:text-green-600 !hover:text-green-600"
@click="handleInvite()"
/>
<HoppButtonSecondary
v-if="
workspace.type === 'team' &&
selectedTeam &&
selectedTeam?.myRole === 'OWNER'
"
v-tippy="{ theme: 'tooltip' }"
:title="t('team.edit')"
:icon="IconSettings"
class="py-1.75 !text-green-500 !focus-visible:text-green-600 !hover:text-green-600"
@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="pr-8 select-wrapper rounded bg-blue-500/15 py-1.75 border border-blue-600/25 !text-blue-500 focus-visible:bg-blue-400/10 focus-visible:border-blue-800/50 !focus-visible:text-blue-600 hover:bg-blue-400/10 hover:border-blue-800/50 !hover:text-blue-600"
/>
<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">
<span>
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => tippyActions.focus()"
:on-shown="() => downloadActions.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
<HoppButtonSecondary
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'
"
:title="t('header.download_app')"
:icon="IconDownload"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
ref="downloadActions"
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 font-semibold truncate">
{{
currentUser.displayName ||
t("profile.default_hopp_displayname")
}}
</span>
<span class="inline-flex truncate text-secondaryLight">
{{ currentUser.email }}
</span>
</div>
<hr />
<HoppSmartItem
ref="profile"
to="/profile"
:icon="IconUser"
:label="t('navigation.profile')"
:shortcut="['P']"
@click="hide()"
:label="t('header.download_app')"
:icon="IconDownload"
/>
<HoppSmartItem
ref="settings"
to="/settings"
:icon="IconSettings"
:label="t('navigation.settings')"
:shortcut="['S']"
@click="hide()"
/>
<FirebaseLogout
ref="logout"
:shortcut="['L']"
@confirm-logout="hide()"
v-if="showInstallButton"
:label="t('header.install_pwa')"
:icon="IconPlusSquare"
@click="installPWA()"
/>
</div>
</template>
</tippy>
</span>
</div>
<div class="flex">
<div
v-if="currentUser === null"
class="inline-flex items-center space-x-2"
>
<HoppButtonSecondary
:icon="IconUploadCloud"
:label="t('header.save_workspace')"
class="!focus-visible:text-emerald-600 !hover:text-emerald-600 hidden h-8 border border-emerald-600/25 bg-emerald-500/10 !text-emerald-500 hover:border-emerald-600/20 hover:bg-emerald-600/20 focus-visible:border-emerald-600/20 focus-visible:bg-emerald-600/20 md:flex"
@click="invokeAction('modals.login.toggle')"
/>
<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.teamMembers.length > 1
"
:team-members="selectedTeam.teamMembers"
show-count
class="mx-2"
@handle-click="handleTeamEdit()"
/>
<div
class="flex h-8 divide-x divide-emerald-600/25 rounded border border-emerald-600/25 bg-emerald-500/10 focus-within:divide-emerald-600/20 focus-within:border-emerald-600/20 focus-within:bg-emerald-600/20 hover:divide-emerald-600/20 hover:border-emerald-600/20 hover:bg-emerald-600/20"
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('team.invite_tooltip')"
:icon="IconUserPlus"
class="!focus-visible:text-emerald-600 !hover:text-emerald-600 !text-emerald-500"
@click="handleInvite()"
/>
<HoppButtonSecondary
v-if="
workspace.type === 'team' &&
selectedTeam &&
selectedTeam?.myRole === 'OWNER'
"
v-tippy="{ theme: 'tooltip' }"
:title="t('team.edit')"
:icon="IconSettings"
class="!focus-visible:text-emerald-600 !hover:text-emerald-600 !text-emerald-500"
@click="handleTeamEdit()"
/>
</div>
<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 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"
/>
<template #content="{ hide }">
<div
ref="accountActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
@click="hide()"
>
<WorkspaceSelector />
</div>
</template>
</tippy>
<span class="px-2">
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => tippyActions.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-emerald-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-emerald-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 text-tiny">
<span class="inline-flex truncate font-semibold">
{{
currentUser.displayName ||
t("profile.default_hopp_displayname")
}}
</span>
<span class="inline-flex truncate text-secondaryLight">
{{ currentUser.email }}
</span>
</div>
<hr />
<HoppSmartItem
ref="profile"
to="/profile"
:icon="IconUser"
:label="t('navigation.profile')"
:shortcut="['P']"
@click="hide()"
/>
<HoppSmartItem
ref="settings"
to="/settings"
:icon="IconSettings"
:label="t('navigation.settings')"
:shortcut="['S']"
@click="hide()"
/>
<FirebaseLogout
ref="logout"
:shortcut="['L']"
@confirm-logout="hide()"
/>
</div>
</template>
</tippy>
</span>
</div>
</div>
</div>
</header>
<AppAnnouncement v-if="!network.isOnline" />
<AppBanner v-if="bannerContent" :banner="bannerContent" />
<TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" />
<TeamsInvite
v-if="workspace.type === 'team' && workspace.teamID"
@@ -233,7 +289,6 @@
@invite-team="inviteTeam(editingTeamName, editingTeamID)"
@refetch-teams="refetchTeams"
/>
<HoppSmartConfirmModal
:show="confirmRemove"
:title="t('confirm.remove_team')"
@@ -263,12 +318,23 @@ import IconUploadCloud from "~icons/lucide/upload-cloud"
import IconUser from "~icons/lucide/user"
import IconUserPlus from "~icons/lucide/user-plus"
import IconUsers from "~icons/lucide/users"
import IconPlusSquare from "~icons/lucide/plus-square"
import IconArrowLeft from "~icons/lucide/arrow-left"
import IconArrowRight from "~icons/lucide/arrow-right"
import IconMenu from "~icons/lucide/align-left"
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
import {
BannerService,
BannerContent,
BANNER_PRIORITY_HIGH,
} from "~/services/banner.service"
import { useRouter } from "vue-router"
const t = useI18n()
const toast = useToast()
const router = useRouter()
/**
* Once the PWA code is initialized, this holds a method
@@ -283,7 +349,31 @@ const showTeamsModal = ref(false)
const breakpoints = useBreakpoints(breakpointsTailwind)
const mdAndLarger = breakpoints.greater("md")
const banner = useService(BannerService)
const bannerContent = computed(() => banner.content.value?.content)
let bannerID: number | null = null
const offlineBanner: BannerContent = {
type: "info",
text: (t) => t("helpers.offline"),
alternateText: (t) => t("helpers.offline_short"),
score: BANNER_PRIORITY_HIGH,
}
const network = reactive(useNetwork())
const isOnline = computed(() => network.isOnline)
// Show the offline banner if the user is offline
watch(isOnline, () => {
if (!isOnline.value) {
bannerID = banner.showBanner(offlineBanner)
return
} else {
if (banner.content && bannerID) {
banner.removeBanner(bannerID)
}
}
})
const currentUser = useReadonlyStream(
platform.auth.getProbableUserStream(),
@@ -416,6 +506,7 @@ const profile = ref<any | null>(null)
const settings = ref<any | null>(null)
const logout = ref<any | null>(null)
const accountActions = ref<any | null>(null)
const downloadActions = ref<any | null>(null)
defineActionHandler("modals.team.edit", handleTeamEdit)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
<template>
<button
ref="el"
class="flex items-center flex-1 px-6 py-4 font-medium space-x-4 transition cursor-pointer relative search-entry focus:outline-none"
class="search-entry relative flex flex-1 cursor-pointer items-center space-x-4 px-6 py-4 font-medium transition focus:outline-none"
:class="{ 'active bg-primaryLight text-secondaryDark': active }"
tabindex="-1"
@click="emit('action')"
@@ -9,7 +9,7 @@
>
<component
:is="entry.icon"
class="opacity-50 svg-icons"
class="svg-icons opacity-50"
:class="{ 'opacity-100': active }"
/>
<template
@@ -112,9 +112,9 @@ watch(
@apply after:left-0;
@apply after:bottom-0;
@apply after:bg-transparent;
@apply after:z-2;
@apply after:z-10;
@apply after:w-0.5;
@apply after:content-DEFAULT;
@apply after:content-[''];
&.active {
@apply after:bg-accentLight;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,16 +12,16 @@
@dragleave="ordering = false"
@dragend="resetDragState"
></div>
<div class="flex flex-col relative">
<div class="relative flex flex-col">
<div
class="absolute bg-accent opacity-0 pointer-events-none inset-0 z-1 transition"
class="z-1 pointer-events-none absolute inset-0 bg-accent opacity-0 transition"
:class="{
'opacity-25':
dragging && notSameDestination && notSameParentDestination,
}"
></div>
<div
class="flex items-stretch group relative z-3 cursor-pointer pointer-events-auto"
class="z-3 group pointer-events-auto relative flex cursor-pointer items-stretch"
:draggable="!hasNoTeamAccess"
@dragstart="dragStart"
@drop="handelDrop($event)"
@@ -36,11 +36,11 @@
@contextmenu.prevent="options?.tippy.show()"
>
<div
class="flex items-center justify-center flex-1 min-w-0"
class="flex min-w-0 flex-1 items-center justify-center"
@click="emit('toggle-children')"
>
<span
class="flex items-center justify-center px-4 pointer-events-none"
class="pointer-events-none flex items-center justify-center px-4"
>
<HoppSmartSpinner v-if="isCollLoading" />
<component
@@ -51,7 +51,7 @@
/>
</span>
<span
class="flex flex-1 min-w-0 py-2 pr-2 transition pointer-events-none group-hover:text-secondaryDark"
class="pointer-events-none flex min-w-0 flex-1 py-2 pr-2 transition group-hover:text-secondaryDark"
>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ collectionName }}

View File

@@ -26,7 +26,7 @@
<div v-if="step.name === 'FILE_IMPORT'" class="space-y-4">
<p class="flex items-center">
<span
class="inline-flex items-center justify-center flex-shrink-0 mr-4 border-4 rounded-full border-primary text-dividerDark"
class="mr-4 inline-flex flex-shrink-0 items-center justify-center rounded-full border-4 border-primary text-dividerDark"
:class="{
'!text-green-500': hasFile,
}"
@@ -38,14 +38,14 @@
</span>
</p>
<p
class="flex flex-col ml-10 border border-dashed rounded border-dividerDark"
class="ml-10 flex flex-col rounded border border-dashed 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"
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"
/>
@@ -54,7 +54,7 @@
<div v-else-if="step.name === 'URL_IMPORT'" class="space-y-4">
<p class="flex items-center">
<span
class="inline-flex items-center justify-center flex-shrink-0 mr-4 border-4 rounded-full border-primary text-dividerDark"
class="mr-4 inline-flex flex-shrink-0 items-center justify-center rounded-full border-4 border-primary text-dividerDark"
:class="{
'!text-green-500': hasGist,
}"
@@ -65,7 +65,7 @@
{{ t(`${step.metadata.caption}`) }}
</span>
</p>
<p class="flex flex-col ml-10">
<p class="ml-10 flex flex-col">
<input
v-model="inputChooseGistToImportFrom"
type="url"

View File

@@ -1,7 +1,7 @@
<template>
<div class="flex flex-col flex-1">
<div class="flex flex-1 flex-col">
<div
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
class="sticky z-10 flex flex-1 justify-between border-b border-dividerLight bg-primary"
:style="
saveRequest
? 'top: calc(var(--upper-primary-sticky-fold) - var(--line-height-body))'
@@ -31,7 +31,7 @@
/>
</span>
</div>
<div class="flex flex-col flex-1">
<div class="flex flex-1 flex-col">
<HoppSmartTree :adapter="myAdapter">
<template
#content="{ node, toggleChildren, isOpen, highlightChildren }"
@@ -248,7 +248,7 @@
:text="`${t('state.nothing_found')}${filterText}`"
>
<template #icon>
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
</template>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
@@ -258,10 +258,10 @@
:text="t('empty.collections')"
>
<div class="flex flex-col items-center space-y-4">
<span class="text-secondaryLight text-center">
<span class="text-center text-secondaryLight">
{{ t("collection.import_or_create") }}
</span>
<div class="flex gap-4 flex-col items-stretch">
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"

View File

@@ -13,7 +13,7 @@
@dragend="resetDragState"
></div>
<div
class="flex items-stretch group"
class="group flex items-stretch"
:draggable="!hasNoTeamAccess"
@drop="handelDrop"
@dragstart="dragStart"
@@ -23,12 +23,13 @@
@contextmenu.prevent="options?.tippy.show()"
>
<div
class="flex items-center justify-center flex-1 min-w-0 cursor-pointer pointer-events-auto"
class="pointer-events-auto flex min-w-0 flex-1 cursor-pointer items-center justify-center"
@click="selectRequest()"
>
<span
class="flex items-center justify-center w-16 px-2 truncate pointer-events-none"
class="pointer-events-none flex w-16 items-center justify-center truncate px-2"
:class="requestLabelColor"
:style="{ color: requestLabelColor }"
>
<component
:is="IconCheckCircle"
@@ -37,12 +38,12 @@
:class="{ 'text-accent': isSelected }"
/>
<HoppSmartSpinner v-else-if="isRequestLoading" />
<span v-else class="font-semibold truncate text-tiny">
<span v-else class="truncate text-tiny font-semibold">
{{ request.method }}
</span>
</span>
<span
class="flex items-center flex-1 min-w-0 py-2 pr-2 pointer-events-none transition group-hover:text-secondaryDark"
class="pointer-events-none flex min-w-0 flex-1 items-center py-2 pr-2 transition group-hover:text-secondaryDark"
>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ request.name }}
@@ -50,15 +51,15 @@
<span
v-if="isActive"
v-tippy="{ theme: 'tooltip' }"
class="relative h-1.5 w-1.5 flex flex-shrink-0 mx-3"
class="relative mx-3 flex h-1.5 w-1.5 flex-shrink-0"
:title="`${t('collection.request_in_use')}`"
>
<span
class="absolute inline-flex flex-shrink-0 w-full h-full bg-green-500 rounded-full opacity-75 animate-ping"
class="absolute inline-flex h-full w-full flex-shrink-0 animate-ping rounded-full bg-green-500 opacity-75"
>
</span>
<span
class="relative inline-flex flex-shrink-0 rounded-full h-1.5 w-1.5 bg-green-500"
class="relative inline-flex h-1.5 w-1.5 flex-shrink-0 rounded-full bg-green-500"
></span>
</span>
</span>

View File

@@ -1,7 +1,7 @@
<template>
<div class="flex flex-col flex-1">
<div class="flex flex-1 flex-col">
<div
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
class="sticky z-10 flex flex-1 justify-between border-b border-dividerLight bg-primary"
:style="
saveRequest
? 'top: calc(var(--upper-primary-sticky-fold) - var(--line-height-body))'
@@ -269,10 +269,10 @@
@drop.stop
>
<div class="flex flex-col items-center space-y-4">
<span class="text-secondaryLight text-center">
<span class="text-center text-secondaryLight">
{{ t("collection.import_or_create") }}
</span>
<div class="flex gap-4 flex-col items-stretch">
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@
@dragend="draggingToRoot = false"
>
<div
class="sticky z-10 flex flex-col flex-shrink-0 overflow-x-auto border-b bg-primary border-dividerLight"
class="sticky z-10 flex flex-shrink-0 flex-col overflow-x-auto border-b border-dividerLight bg-primary"
:class="{ 'rounded-t': saveRequest }"
:style="
saveRequest ? 'top: calc(-1 * var(--line-height-body))' : 'top: 0'
@@ -22,7 +22,7 @@
v-model="filterTexts"
type="search"
autocomplete="off"
class="flex w-full p-4 py-2 bg-transparent h-8"
class="flex h-8 w-full bg-transparent p-4 py-2"
:placeholder="t('action.search')"
:disabled="collectionsType.type === 'team-collections'"
/>
@@ -85,12 +85,12 @@
@display-modal-import-export="displayModalImportExport(true)"
/>
<div
class="hidden bg-primaryDark flex-col flex-1 items-center py-15 justify-center px-4 text-secondaryLight"
class="py-15 hidden flex-1 flex-col items-center justify-center bg-primaryDark px-4 text-secondaryLight"
:class="{
'!flex': draggingToRoot && currentReorderingStatus.type !== 'request',
}"
>
<icon-lucide-list-end class="svg-icons !w-8 !h-8" />
<icon-lucide-list-end class="svg-icons !h-8 !w-8" />
</div>
<CollectionsAdd
:show="showModalAdd"

View File

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

View File

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

View File

@@ -5,9 +5,9 @@
@close="hideModal"
>
<template #body>
<div class="flex space-y-4 flex-1 flex-col">
<div class="flex items-center space-x-8 ml-2">
<label for="name" class="font-semibold min-w-10">{{
<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">{{
t("environment.name")
}}</label>
<input
@@ -17,8 +17,8 @@
class="input"
/>
</div>
<div class="flex items-center space-x-8 ml-2">
<label for="value" class="font-semibold min-w-10">{{
<div class="ml-2 flex items-center space-x-8">
<label for="value" class="min-w-10 font-semibold">{{
t("environment.value")
}}</label>
<input
@@ -28,17 +28,17 @@
:placeholder="t('environment.value')"
/>
</div>
<div class="flex items-center space-x-8 ml-2">
<label for="scope" class="font-semibold min-w-10">
<div class="ml-2 flex items-center space-x-8">
<label for="scope" class="min-w-10 font-semibold">
{{ t("environment.scope") }}
</label>
<div
class="relative flex flex-1 flex-col border border-divider rounded focus-visible:border-dividerDark"
class="relative flex flex-1 flex-col rounded border border-divider focus-visible:border-dividerDark"
>
<EnvironmentsSelector v-model="scope" :is-scope-selector="true" />
</div>
</div>
<div v-if="replaceWithVariable" class="flex space-x-2 mt-3">
<div v-if="replaceWithVariable" class="mt-3 flex space-x-2">
<div class="min-w-18" />
<HoppSmartCheckbox
:on="replaceWithVariable"

View File

@@ -20,7 +20,7 @@
: `${t('environment.select')}`
: ''
"
class="flex-1 !justify-start pr-8 rounded-none"
class="flex-1 !justify-start rounded-none pr-8"
/>
</span>
<template #content="{ hide }">
@@ -101,7 +101,7 @@
<img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
class="mb-2 inline-flex h-16 w-16 flex-col object-contain object-center"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-2 text-center">
@@ -148,7 +148,7 @@
<img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
class="mb-2 inline-flex h-16 w-16 flex-col object-contain object-center"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-2 text-center">
@@ -160,7 +160,7 @@
v-if="!teamListLoading && teamAdapterError"
class="flex flex-col items-center py-4"
>
<icon-lucide-help-circle class="mb-4 svg-icons" />
<icon-lucide-help-circle class="svg-icons mb-4" />
{{ getErrorMessage(teamAdapterError) }}
</div>
</HoppSmartTab>
@@ -190,7 +190,7 @@
@keyup.escape="hide()"
>
<div
class="sticky top-0 font-semibold truncate flex items-center justify-between text-secondaryDark bg-primary border border-divider rounded pl-4"
class="sticky top-0 flex items-center justify-between truncate rounded border border-divider bg-primary pl-4 font-semibold text-secondaryDark"
>
{{ t("environment.global_variables") }}
<HoppButtonSecondary
@@ -205,12 +205,12 @@
"
/>
</div>
<div class="my-2 flex flex-col flex-1 space-y-2 pl-4 pr-2">
<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="w-1/4 min-w-32 truncate text-tiny font-semibold">
<span class="min-w-32 w-1/4 truncate text-tiny font-semibold">
{{ t("environment.name") }}
</span>
<span class="w-full min-w-32 truncate text-tiny font-semibold">
<span class="min-w-32 w-full truncate text-tiny font-semibold">
{{ t("environment.value") }}
</span>
</div>
@@ -219,10 +219,10 @@
:key="index"
class="flex flex-1 space-x-4"
>
<span class="text-secondaryLight w-1/4 min-w-32 truncate">
<span class="min-w-32 w-1/4 truncate text-secondaryLight">
{{ variable.key }}
</span>
<span class="text-secondaryLight w-full min-w-32 truncate">
<span class="min-w-32 w-full truncate text-secondaryLight">
{{ variable.value }}
</span>
</div>
@@ -231,7 +231,7 @@
</div>
</div>
<div
class="sticky top-0 mt-2 font-semibold truncate flex items-center justify-between text-secondaryDark bg-primary border border-divider rounded pl-4"
class="sticky top-0 mt-2 flex items-center justify-between truncate rounded border border-divider bg-primary pl-4 font-semibold text-secondaryDark"
:class="{
'bg-primaryLight': !selectedEnv.variables,
}"
@@ -252,16 +252,16 @@
</div>
<div
v-if="selectedEnv.type === 'NO_ENV_SELECTED'"
class="text-secondaryLight my-2 flex flex-col flex-1 pl-4"
class="my-2 flex flex-1 flex-col pl-4 text-secondaryLight"
>
{{ t("environment.no_active_environment") }}
</div>
<div v-else class="my-2 flex flex-col flex-1 space-y-2 pl-4 pr-2">
<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="w-1/4 min-w-32 truncate text-tiny font-semibold">
<span class="min-w-32 w-1/4 truncate text-tiny font-semibold">
{{ t("environment.name") }}
</span>
<span class="w-full min-w-32 truncate text-tiny font-semibold">
<span class="min-w-32 w-full truncate text-tiny font-semibold">
{{ t("environment.value") }}
</span>
</div>
@@ -270,10 +270,10 @@
:key="index"
class="flex flex-1 space-x-4"
>
<span class="text-secondaryLight w-1/4 min-w-32 truncate">
<span class="min-w-32 w-1/4 truncate text-secondaryLight">
{{ variable.key }}
</span>
<span class="text-secondaryLight w-full min-w-32 truncate">
<span class="min-w-32 w-full truncate text-secondaryLight">
{{ variable.value }}
</span>
</div>

View File

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

View File

@@ -16,7 +16,7 @@
@submit="saveEnvironment"
/>
<div class="flex items-center justify-between flex-1">
<div class="flex flex-1 items-center justify-between">
<label for="variableList" class="p-4">
{{ t("environment.variable_list") }}
</label>
@@ -37,11 +37,11 @@
</div>
<div
v-if="evnExpandError"
class="w-full px-4 py-2 mb-2 overflow-auto font-mono text-red-400 whitespace-normal rounded bg-primaryLight"
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
>
{{ t("environment.nested_overflow") }}
</div>
<div class="border rounded divide-y divide-dividerLight border-divider">
<div class="divide-y divide-dividerLight rounded border border-divider">
<div
v-for="({ id, env }, index) in vars"
:key="`variable-${id}-${index}`"
@@ -50,7 +50,7 @@
<input
v-model="env.key"
v-focus
class="flex flex-1 px-4 py-2 bg-transparent"
class="flex flex-1 bg-transparent px-4 py-2"
:placeholder="`${t('count.variable', { count: index + 1 })}`"
:name="'param' + index"
/>

View File

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

View File

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

View File

@@ -16,7 +16,7 @@
@submit="saveEnvironment"
/>
<div class="flex items-center justify-between flex-1">
<div class="flex flex-1 items-center justify-between">
<label for="variableList" class="p-4">
{{ t("environment.variable_list") }}
</label>
@@ -37,11 +37,11 @@
</div>
<div
v-if="evnExpandError"
class="w-full px-4 py-2 mb-2 overflow-auto font-mono text-red-400 whitespace-normal rounded bg-primaryLight"
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
>
{{ t("environment.nested_overflow") }}
</div>
<div class="border rounded divide-y divide-dividerLight border-divider">
<div class="divide-y divide-dividerLight rounded border border-divider">
<div
v-for="({ id, env }, index) in vars"
:key="`variable-${id}-${index}`"
@@ -50,7 +50,7 @@
<input
v-model="env.key"
v-focus
class="flex flex-1 px-4 py-2 bg-transparent"
class="flex flex-1 bg-transparent px-4 py-2"
:class="isViewer && 'opacity-25'"
:placeholder="`${t('count.variable', { count: index + 1 })}`"
:name="'param' + index"

View File

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

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div
class="sticky z-10 flex justify-between flex-1 flex-shrink-0 overflow-x-auto border-b top-upperPrimaryStickyFold border-dividerLight bg-primary"
class="sticky top-upperPrimaryStickyFold z-10 flex flex-1 flex-shrink-0 justify-between overflow-x-auto border-b border-dividerLight bg-primary"
>
<HoppButtonSecondary
v-if="team === undefined || team.myRole === 'VIEWER'"
@@ -50,10 +50,10 @@
:text="t('empty.environments')"
>
<div class="flex flex-col items-center space-y-4">
<span class="text-secondaryLight text-center">
<span class="text-center text-secondaryLight">
{{ t("environment.import_or_create") }}
</span>
<div class="flex gap-4 flex-col items-stretch">
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
@@ -94,7 +94,7 @@
v-if="!loading && adapterError"
class="flex flex-col items-center py-4"
>
<icon-lucide-help-circle class="mb-4 svg-icons" />
<icon-lucide-help-circle class="svg-icons mb-4" />
{{ getErrorMessage(adapterError) }}
</div>
<EnvironmentsTeamsDetails

View File

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

View File

@@ -1,10 +1,10 @@
<template>
<div class="flex flex-col flex-1">
<div class="flex flex-1 flex-col">
<div
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between pl-4 border-y bg-primary border-dividerLight"
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between border-y border-dividerLight bg-primary pl-4"
>
<span class="flex items-center">
<label class="font-semibold truncate text-secondaryLight">
<label class="truncate font-semibold text-secondaryLight">
{{ t("authorization.type") }}
</label>
<tippy
@@ -15,7 +15,7 @@
>
<span class="select-wrapper">
<HoppButtonSecondary
class="pr-8 ml-2 rounded-none"
class="ml-2 rounded-none pr-8"
:label="authName"
/>
</span>
@@ -171,7 +171,7 @@
</div>
</div>
<div
class="sticky flex-shrink-0 h-full p-4 overflow-auto overflow-x-auto bg-primary top-upperTertiaryStickyFold min-w-46 max-w-1/3 z-9"
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"
>
<div class="pb-2 text-secondaryLight">
{{ t("helpers.authorization") }}

View File

@@ -23,7 +23,7 @@
<div v-if="gqlField.deprecationReason">
<span
v-tippy="{ theme: 'tomato' }"
class="!text-red-500 hover:!text-red-600 text-xs flex items-center gap-2 cursor-pointer"
class="flex cursor-pointer items-center gap-2 text-xs !text-red-500 hover:!text-red-600"
:title="gqlField.deprecationReason"
>
<IconAlertTriangle /> {{ t("state.deprecated") }}

View File

@@ -1,6 +1,6 @@
<template>
<div
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between pl-4 border-y bg-primary border-dividerLight"
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between border-y border-dividerLight bg-primary pl-4"
>
<label class="font-semibold text-secondaryLight">
{{ t("tab.headers") }}
@@ -42,7 +42,7 @@
/>
</div>
</div>
<div v-if="bulkMode" ref="bulkEditor" class="flex flex-col flex-1"></div>
<div v-if="bulkMode" ref="bulkEditor" class="flex flex-1 flex-col"></div>
<div v-else>
<draggable
v-model="workingHeaders"
@@ -56,7 +56,7 @@
>
<template #item="{ element: header, index }">
<div
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
class="draggable-content group flex divide-x divide-dividerLight border-b border-dividerLight"
>
<span>
<HoppButtonSecondary
@@ -69,9 +69,9 @@
: null,
}"
:icon="IconGripVertical"
class="cursor-auto text-primary hover:text-primary"
class="opacity-0"
:class="{
'draggable-handle group-hover:text-secondaryLight !cursor-grab':
'draggable-handle cursor-grab group-hover:opacity-100':
index !== workingHeaders?.length - 1,
}"
tabindex="-1"
@@ -91,7 +91,7 @@
px-4
truncate
"
class="flex-1 !flex"
class="!flex flex-1"
@input="
updateHeader(index, {
id: header.id,
@@ -102,7 +102,7 @@
"
/>
<input
class="flex flex-1 px-4 py-2 bg-transparent"
class="flex flex-1 bg-transparent px-4 py-2"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:name="`value ${String(index)}`"
:value="header.value"

View File

@@ -1,6 +1,6 @@
<template>
<div
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between pl-4 border-y bg-primary border-dividerLight"
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between border-y border-dividerLight bg-primary pl-4"
>
<label class="font-semibold text-secondaryLight">
{{ t("request.query") }}
@@ -16,7 +16,7 @@
:title="`${t('request.stop')}`"
:label="`${t('request.stop')}`"
:icon="IconStop"
class="rounded-none !text-accent !hover:text-accentDark"
class="!hover:text-accentDark rounded-none !text-accent"
@click="unsubscribe()"
/>
@@ -31,7 +31,7 @@
:label="`${selectedOperation.name?.value ?? t('request.run')}`"
:icon="IconPlay"
:disabled="!selectedOperation"
class="rounded-none !text-accent !hover:text-accentDark"
class="!hover:text-accentDark rounded-none !text-accent"
@click="runQuery(selectedOperation)"
/>
@@ -79,7 +79,7 @@
/>
</div>
</div>
<div ref="queryEditor" class="flex flex-col flex-1"></div>
<div ref="queryEditor" class="flex flex-1 flex-col"></div>
</template>
<script setup lang="ts">

View File

@@ -1,6 +1,6 @@
<template>
<div
class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary"
class="sticky top-0 z-10 flex flex-shrink-0 space-x-2 overflow-x-auto bg-primary p-4"
>
<div class="inline-flex flex-1 space-x-2">
<input
@@ -9,7 +9,7 @@
type="url"
autocomplete="off"
spellcheck="false"
class="w-full px-4 py-2 border rounded bg-primaryLight border-divider text-secondaryDark"
class="w-full rounded border border-divider bg-primaryLight px-4 py-2 text-secondaryDark"
:placeholder="`${t('request.url')}`"
:disabled="connected"
@keyup.enter="onConnectClick"

View File

@@ -1,5 +1,5 @@
<template>
<div class="flex flex-col flex-1 h-full">
<div class="flex h-full flex-1 flex-col">
<HoppSmartTabs
v-model="selectedOptionTab"
styles="sticky top-0 bg-primary z-10 border-b-0"
@@ -51,7 +51,7 @@
<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { completePageProgress, startPageProgress } from "@modules/loadingbar"
import { completePageProgress, startPageProgress } from "~/modules/loadingbar"
import * as gql from "graphql"
import { clone } from "lodash-es"
import { computed, ref, watch } from "vue"
@@ -63,6 +63,7 @@ import {
GQLResponseEvent,
runGQLOperation,
gqlMessageEvent,
connection,
} from "~/helpers/graphql/connection"
import { useService } from "dioc/vue"
import { InterceptorService } from "~/services/interceptor.service"
@@ -152,13 +153,7 @@ const runQuery = async (
toast.success(t("authorization.graphql_headers"))
}
} catch (e: any) {
console.log(e)
// response.value = [`${e}`]
completePageProgress()
toast.error(
`${t("error.something_went_wrong")}. ${t("error.check_console_details")}`,
{}
)
console.error(e)
}
platform.analytics?.logEvent({
@@ -177,7 +172,10 @@ watch(
}
try {
if (event?.operationType !== "subscription") {
if (
event?.type === "response" &&
event?.operationType !== "subscription"
) {
// response.value = [event]
emit("update:response", [event])
} else {
@@ -192,6 +190,26 @@ watch(
{ deep: true }
)
watch(
() => connection,
(newVal) => {
if (newVal.error && newVal.state === "DISCONNECTED") {
const response = [
{
type: "error",
error: {
message: newVal.error.message(t),
type: newVal.error.type,
component: newVal.error.component,
},
},
]
emit("update:response", response)
}
},
{ deep: true }
)
const hideRequestModal = () => {
showSaveRequestModal.value = false
}

View File

@@ -1,10 +1,15 @@
<template>
<div class="flex flex-col flex-1 overflow-auto whitespace-nowrap">
<div v-if="response?.length === 1" class="flex flex-col flex-1">
<div class="flex flex-1 flex-col overflow-auto whitespace-nowrap">
<div
v-if="
response && response.length === 1 && response[0].type === 'response'
"
class="flex flex-1 flex-col"
>
<div
class="sticky top-0 z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight"
class="sticky top-0 z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4"
>
<label class="font-semibold truncate text-secondaryLight">
<label class="truncate font-semibold text-secondaryLight">
{{ t("response.title") }}
</label>
<div class="flex items-center">
@@ -33,11 +38,18 @@
/>
</div>
</div>
<div ref="schemaEditor" class="flex flex-col flex-1"></div>
<div ref="schemaEditor" class="flex flex-1 flex-col"></div>
</div>
<component
:is="response[0].error.component"
v-else-if="
response && response[0].type === 'error' && response[0].error.component
"
class="flex-1"
/>
<div
v-else-if="response && response?.length > 1"
class="flex flex-col flex-1"
class="flex flex-1 flex-col"
>
<GraphqlSubscriptionLog :log="response" />
</div>
@@ -74,8 +86,16 @@ const props = withDefaults(
)
const responseString = computed(() => {
if (props.response?.length === 1) {
return JSON.stringify(JSON.parse(props.response[0].data), null, 2)
const response = props.response
if (response && response[0].type === "error") {
return ""
} else if (
response &&
response.length === 1 &&
response[0].type === "response" &&
response[0].data
) {
return JSON.stringify(JSON.parse(response[0].data), null, 2)
}
return ""
})

View File

@@ -30,7 +30,7 @@
v-model="graphqlFieldsFilterText"
type="search"
autocomplete="off"
class="flex w-full p-4 py-2 bg-transparent h-8"
class="flex h-8 w-full bg-transparent p-4 py-2"
:placeholder="`${t('action.search')}`"
/>
<div class="flex">
@@ -112,9 +112,9 @@
<HoppSmartTab :id="'schema'" :icon="IconBox" :label="`${t('tab.schema')}`">
<div
v-if="schemaString"
class="sticky top-0 z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight"
class="sticky top-0 z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4"
>
<label class="font-semibold truncate text-secondaryLight">
<label class="truncate font-semibold text-secondaryLight">
{{ t("graphql.schema") }}
</label>
<div class="flex">
@@ -149,7 +149,7 @@
<div
v-if="schemaString"
ref="schemaEditor"
class="flex flex-col flex-1"
class="flex flex-1 flex-col"
></div>
<HoppSmartPlaceholder
v-else

View File

@@ -1,7 +1,7 @@
<template>
<div ref="container" class="flex flex-col flex-1">
<div ref="container" class="flex flex-1 flex-col">
<div
class="sticky top-0 z-10 flex items-center justify-between flex-none pl-4 border-b bg-primary border-dividerLight"
class="sticky top-0 z-10 flex flex-none items-center justify-between border-b border-dividerLight bg-primary pl-4"
>
<label for="log" class="py-2 font-semibold text-secondaryLight">
{{ "Subscription Log" }}
@@ -43,13 +43,17 @@
class="overflow-y-auto border-b border-dividerLight"
>
<div
class="flex flex-col h-auto h-full border-r divide-y divide-dividerLight border-dividerLight"
class="flex h-auto h-full flex-col divide-y divide-dividerLight border-r border-dividerLight"
>
<RealtimeLogEntry
v-for="(entry, index) in log"
:key="`entry-${index}`"
:is-open="log.length - 1 === index"
:entry="{ ts: entry.time, source: 'info', payload: entry.data }"
:entry="{
ts: entry.type === 'response' ? entry.time : undefined,
source: 'info',
payload: entry.type === 'response' ? entry.data : '',
}"
/>
</div>
</div>

View File

@@ -2,7 +2,7 @@
<div
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
:title="tab.document.request.name"
class="truncate px-2 flex items-center"
class="flex items-center truncate px-2"
@dblclick="emit('open-rename-modal')"
@contextmenu.prevent="options?.tippy?.show()"
@click.middle="emit('close-tab')"
@@ -14,7 +14,7 @@
theme="popover"
:on-shown="() => tippyActions!.focus()"
>
<span class="leading-8 px-2 truncate">
<span class="truncate px-2 leading-8">
{{ tab.document.request.name }}
</span>
<template #content="{ hide }">

View File

@@ -6,7 +6,7 @@
<span v-else-if="isEnum" class="text-accent">enum </span>
{{ gqlType.name }}
</div>
<div v-if="gqlType.description" class="py-2 text-secondaryLight type-desc">
<div v-if="gqlType.description" class="type-desc py-2 text-secondaryLight">
{{ gqlType.description }}
</div>
<div v-if="interfaces.length > 0">
@@ -18,7 +18,7 @@
<GraphqlTypeLink
:gql-type="gqlInterface"
:jump-type-callback="jumpTypeCallback"
class="pl-4 border-l-2 border-divider"
class="border-l-2 border-divider pl-4"
/>
</div>
</div>
@@ -29,7 +29,7 @@
:key="`child-${index}`"
:gql-type="child"
:jump-type-callback="jumpTypeCallback"
class="pl-4 border-l-2 border-divider"
class="border-l-2 border-divider pl-4"
/>
</div>
<div v-if="gqlType.getFields">
@@ -37,7 +37,7 @@
<GraphqlField
v-for="(field, index) in gqlType.getFields()"
:key="`field-${index}`"
class="pl-4 border-l-2 border-divider"
class="border-l-2 border-divider pl-4"
:gql-field="field"
:is-highlighted="isFieldHighlighted({ field })"
:jump-type-callback="jumpTypeCallback"
@@ -48,7 +48,7 @@
<div
v-for="(value, index) in gqlType.getValues()"
:key="`value-${index}`"
class="pl-4 border-l-2 border-divider"
class="border-l-2 border-divider pl-4"
v-text="value.name"
></div>
</div>

View File

@@ -1,6 +1,6 @@
<template>
<div
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between pl-4 border-y bg-primary border-dividerLight"
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between border-y border-dividerLight bg-primary pl-4"
>
<label class="font-semibold text-secondaryLight">
{{ t("request.variables") }}
@@ -16,7 +16,7 @@
:title="`${t('request.stop')}`"
:label="`${t('request.stop')}`"
:icon="IconStop"
class="rounded-none !text-accent !hover:text-accentDark"
class="!hover:text-accentDark rounded-none !text-accent"
@click="unsubscribe()"
/>
<HoppButtonSecondary
@@ -30,7 +30,7 @@
:label="`${selectedOperation.name?.value ?? t('request.run')}`"
:icon="IconPlay"
:disabled="!selectedOperation"
class="rounded-none !text-accent !hover:text-accentDark"
class="!hover:text-accentDark rounded-none !text-accent"
@click="runQuery(selectedOperation)"
/>
<HoppButtonSecondary
@@ -67,7 +67,7 @@
/>
</div>
</div>
<div ref="variableEditor" class="flex flex-col flex-1"></div>
<div ref="variableEditor" class="flex flex-1 flex-col"></div>
</template>
<script setup lang="ts">

View File

@@ -1,5 +1,5 @@
<template>
<div class="flex flex-col group">
<div class="group flex flex-col">
<div class="flex items-center">
<span
v-tippy="{
@@ -7,7 +7,7 @@
delay: [500, 20],
content: entry.updatedOn ? shortDateTime(entry.updatedOn) : null,
}"
class="flex flex-1 min-w-0 py-2 pl-4 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
class="flex min-w-0 flex-1 cursor-pointer py-2 pl-4 pr-2 transition group-hover:text-secondaryDark"
data-testid="restore_history_entry"
@click="useEntry"
>
@@ -36,7 +36,7 @@
:title="!entry.star ? t('add.star') : t('remove.star')"
:icon="entry.star ? IconStarOff : IconStar"
color="yellow"
:class="{ 'group-hover:inline-flex hidden': !entry.star }"
:class="{ 'hidden group-hover:inline-flex': !entry.star }"
data-testid="star_button"
@click="emit('toggle-star')"
/>
@@ -45,7 +45,7 @@
<span
v-for="(line, index) in query"
:key="`line-${index}`"
class="px-4 font-mono truncate whitespace-pre cursor-pointer text-secondaryLight"
class="cursor-pointer truncate whitespace-pre px-4 font-mono text-secondaryLight"
data-testid="restore_history_entry"
@click="useEntry"
>{{ line }}</span

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div
class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto border-b bg-primary border-dividerLight"
class="sticky top-0 z-10 flex flex-shrink-0 flex-col overflow-x-auto border-b border-dividerLight bg-primary"
>
<WorkspaceCurrent :section="t('tab.history')" />
<div class="flex">
@@ -9,7 +9,7 @@
v-model="filterText"
type="search"
autocomplete="off"
class="flex w-full p-4 py-2 bg-transparent h-8"
class="flex h-8 w-full bg-transparent p-4 py-2"
:placeholder="`${t('action.search')}`"
/>
<div class="flex">
@@ -69,13 +69,13 @@
open
>
<summary
class="flex items-center justify-between flex-1 min-w-0 transition cursor-pointer focus:outline-none text-secondaryLight text-tiny group"
class="group flex min-w-0 flex-1 cursor-pointer items-center justify-between text-tiny text-secondaryLight transition focus:outline-none"
>
<span
class="inline-flex items-center justify-center px-4 py-2 transition group-hover:text-secondary truncate"
class="inline-flex items-center justify-center truncate px-4 py-2 transition group-hover:text-secondary"
>
<icon-lucide-chevron-right
class="mr-2 indicator flex flex-shrink-0"
class="indicator mr-2 flex flex-shrink-0"
/>
<span
:class="[
@@ -124,7 +124,7 @@
:text="`${t('state.nothing_found')} ‟${filterText || filterSelection}”`"
>
<template #icon>
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
</template>
<HoppButtonSecondary
:label="t('action.clear')"

View File

@@ -1,17 +1,17 @@
<template>
<div
class="flex items-stretch group"
class="group flex items-stretch"
@contextmenu.prevent="options!.tippy.show()"
>
<span
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
class="flex items-center justify-center w-16 px-2 truncate cursor-pointer"
class="flex w-16 cursor-pointer items-center justify-center truncate px-2"
:class="entryStatus.className"
data-testid="restore_history_entry"
:title="`${duration}`"
@click="emit('use-entry')"
>
<span class="font-semibold truncate text-tiny">
<span class="truncate text-tiny font-semibold">
{{ entry.request.method }}
</span>
</span>
@@ -21,7 +21,7 @@
delay: [500, 20],
content: entry.updatedOn ? shortDateTime(entry.updatedOn) : null,
}"
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
class="flex min-w-0 flex-1 cursor-pointer py-2 pr-2 transition group-hover:text-secondaryDark"
data-testid="restore_history_entry"
@click="emit('use-entry')"
>
@@ -74,7 +74,7 @@
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="!entry.star ? t('add.star') : t('remove.star')"
:class="{ 'group-hover:inline-flex hidden': !entry.star }"
:class="{ 'hidden group-hover:inline-flex': !entry.star }"
:icon="entry.star ? IconStarOff : IconStar"
color="yellow"
data-testid="star_button"

View File

@@ -1,10 +1,10 @@
<template>
<div class="flex flex-col flex-1">
<div class="flex flex-1 flex-col">
<div
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-upperMobileSecondaryStickyFold sm:top-upperSecondaryStickyFold"
class="sticky top-upperMobileSecondaryStickyFold z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4 sm:top-upperSecondaryStickyFold"
>
<span class="flex items-center">
<label class="font-semibold truncate text-secondaryLight">
<label class="truncate font-semibold text-secondaryLight">
{{ t("authorization.type") }}
</label>
<tippy
@@ -15,7 +15,7 @@
>
<span class="select-wrapper">
<HoppButtonSecondary
class="pr-8 ml-2 rounded-none"
class="ml-2 rounded-none pr-8"
:label="authName"
/>
</span>
@@ -149,7 +149,7 @@
</div>
</div>
<div
class="sticky flex-shrink-0 h-full p-4 overflow-auto overflow-x-auto bg-primary top-upperTertiaryStickyFold min-w-46 max-w-1/3 z-9"
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"
>
<div class="pb-2 text-secondaryLight">
{{ t("helpers.authorization") }}

View File

@@ -1,10 +1,10 @@
<template>
<div class="flex flex-col flex-1">
<div class="flex flex-1 flex-col">
<div
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-upperMobileSecondaryStickyFold sm:top-upperSecondaryStickyFold"
class="sticky top-upperMobileSecondaryStickyFold z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4 sm:top-upperSecondaryStickyFold"
>
<span class="flex items-center">
<label class="font-semibold truncate text-secondaryLight">
<label class="truncate font-semibold text-secondaryLight">
{{ t("request.content_type") }}
</label>
<tippy
@@ -16,13 +16,13 @@
<span class="select-wrapper">
<HoppButtonSecondary
:label="body.contentType || t('state.none')"
class="pr-8 ml-2 rounded-none"
class="ml-2 rounded-none pr-8"
/>
</span>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col space-y-2 divide-y focus:outline-none divide-dividerLight"
class="flex flex-col space-y-2 divide-y divide-dividerLight focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
@@ -46,8 +46,8 @@
:key="`contentTypeItems-${contentTypeItemsIndex}`"
class="flex flex-col text-left"
>
<div class="flex px-4 py-2 rounded">
<span class="font-bold text-tiny text-secondaryLight">
<div class="flex rounded px-4 py-2">
<span class="text-tiny font-bold text-secondaryLight">
{{ t(contentTypeItems.title) }}
</span>
</div>

View File

@@ -1,9 +1,9 @@
<template>
<div>
<div
class="sticky z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight top-upperMobileStickyFold sm:top-upperMobileTertiaryStickyFold"
class="sticky top-upperMobileStickyFold z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4 sm:top-upperMobileTertiaryStickyFold"
>
<label class="font-semibold truncate text-secondaryLight">
<label class="truncate font-semibold text-secondaryLight">
{{ t("request.body") }}
</label>
<div class="flex">
@@ -41,7 +41,7 @@
>
<template #item="{ element: { entry }, index }">
<div
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
class="draggable-content group flex divide-x divide-dividerLight border-b border-dividerLight"
>
<span>
<HoppButtonSecondary
@@ -54,9 +54,9 @@
: null,
}"
:icon="IconGripVertical"
class="cursor-auto text-primary hover:text-primary"
class="opacity-0"
:class="{
'draggable-handle group-hover:text-secondaryLight !cursor-grab':
'draggable-handle cursor-grab group-hover:opacity-100':
index !== workingParams?.length - 1,
}"
tabindex="-1"
@@ -75,7 +75,7 @@
"
/>
<div v-if="entry.isFile" class="file-chips-container">
<div class="space-x-2 file-chips-wrapper">
<div class="file-chips-wrapper space-x-1">
<HoppSmartFileChip
v-for="(file, fileIndex) in entry.value"
:key="`param-${index}-file-${fileIndex}`"
@@ -104,7 +104,7 @@
:name="`attachment${index}`"
type="file"
multiple
class="p-1 transition cursor-pointer file:transition file:cursor-pointer text-secondaryLight hover:text-secondaryDark file:mr-2 file:py-1 file:px-4 file:rounded file:border-0 file:text-tiny text-tiny file:text-secondary hover:file:text-secondaryDark file:bg-primaryLight hover:file:bg-primaryDark"
class="cursor-pointer p-1 text-tiny text-secondaryLight transition file:mr-2 file:cursor-pointer file:rounded file:border-0 file:bg-primaryLight file:px-4 file:py-1 file:text-tiny file:text-secondary file:transition hover:text-secondaryDark hover:file:bg-primaryDark hover:file:text-secondaryDark"
@change="setRequestAttachment(index, entry, $event)"
/>
</label>

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