Compare commits

...

59 Commits

Author SHA1 Message Date
Nivedin
a60303ef7c chore: add empty result placeholder 2023-07-10 15:29:52 +05:30
Andrew Bastin
53e4863a80 fix: error out when arrow key navigation done on spotlight when no results 2023-07-09 21:56:08 +05:30
Andrew Bastin
3340d0813c chore: remove fuse.js dependency 2023-07-05 11:52:36 +05:30
Andrew Bastin
7c5722586c refactor: move shortcuts to use minisearch 2023-07-05 11:51:15 +05:30
Andrew Bastin
6b38363fab chore: add test case to user searcher to register with spotlight on init 2023-07-04 15:03:50 +05:30
Andrew Bastin
14202a3eed chore: introduce tests for history searcher 2023-07-04 14:55:44 +05:30
Andrew Bastin
ea03223b8e fix: history returning no entries until query update and minisearch conflicts 2023-07-04 14:54:30 +05:30
Andrew Bastin
235deb113c chore: update vitest to be able to parse Vue components 2023-07-04 14:51:31 +05:30
Andrew Bastin
833e11ab0b feat: general spotlight improvements and introducing user searcher 2023-07-04 12:54:57 +05:30
Andrew Bastin
0d101673d2 chore: update vitest config to support loading icons 2023-07-04 12:52:26 +05:30
Andrew Bastin
6fe565c30f feat: add action handler to login and logout components 2023-07-03 23:13:36 +05:30
Andrew Bastin
4164de5a9e refactor: ability for defineActionHandler to be able to control binding 2023-07-03 23:10:27 +05:30
Andrew Bastin
1ff35f45ee feat: introduce debug service 2023-07-03 23:01:46 +05:30
Andrew Bastin
3bf8288de3 feat: initial reworked spotlight implementation 2023-07-03 12:03:17 +05:30
Andrew Bastin
8c48d41eed refactor: expose function to get a service instance from dioc through module 2023-07-03 11:41:05 +05:30
Andrew Bastin
38215be3bd refactor: provide global i18n function through the module 2023-07-03 11:39:00 +05:30
Andrew Bastin
edf57da9be chore: add minisearch and bump @vueuse/core dep 2023-07-03 11:36:55 +05:30
Andrew Bastin
be61b62825 feat: add clear history action 2023-07-03 11:23:02 +05:30
Andrew Bastin
9dbce74f5e chore: bump version to 2023.4.7 2023-06-27 15:43:03 +05:30
Liyas Thomas
db1cf5cc08 fix: explicitly added background color 2023-06-27 15:43:03 +05:30
Liyas Thomas
09360abf81 fix: text overflow on details summary label (#3160)
Co-authored-by: Nivedin <nivedinp@gmail.com>
2023-06-27 15:42:58 +05:30
Andrew Bastin
355bd62b8d feat: introduce more events into the analytics pipeline (#3156) 2023-06-27 15:37:25 +05:30
James Butler
5650de1183 fix: self-host unable to use Azure oauth (#3138) 2023-06-27 15:37:25 +05:30
Akash K
2ee8614b93 fix: use --location param for url when parsing curl (#3152) 2023-06-27 15:37:25 +05:30
Ankit Sridhar
5632334c9a fix: remove existing team invitation for an invitee when adding invitee to team by admin (HBE-229) (#3157) 2023-06-27 15:37:25 +05:30
Anwarul Islam
780dd8a713 fix: graphql authorization headers (#3136) 2023-06-27 15:37:25 +05:30
Nivedin
7db3c6d290 fix: unified bg color in collection tree (#3155) 2023-06-27 15:37:25 +05:30
Omer Baflah
c765270dfe fix: correct typos (#3153) 2023-06-27 15:37:25 +05:30
Webysther Sperandio
03f667c21d feat: custom location on admin redirect to base (#3103) 2023-06-27 15:37:25 +05:30
Balázs Úr
f79f3078dc chore(i18n): updated hungarian translation (#3151) 2023-06-27 15:37:25 +05:30
Nivedin
6e29a2f6d4 fix: shortcode resolution screen is stuck on invalid shortcodes (#3142)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-06-27 15:37:25 +05:30
Balu Babu
6304fd50c3 fix: fixed issue with team-invitations and new user accounts (#3137) 2023-06-27 15:37:25 +05:30
Andrew Bastin
6f35574d68 refactor: move hoppscotch-common tests to vitest (#3154) 2023-06-22 00:36:25 +05:30
Anwarul Islam
fc3e3aeaec feat: placeholder component in hoppscotch-ui (#3123)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-06-21 00:09:16 +05:30
Joel Jacob Stephen
331d482b22 feat: introducing i18n support to admin dashboard (#3051) 2023-06-16 09:47:00 +05:30
Andrew Bastin
b07243f131 chore: merge main@2023.4.6 into release/2023.8.0 2023-06-16 09:45:05 +05:30
Andrew Bastin
e3dd9e99a1 chore: bump version to 2023.4.6 2023-06-12 10:43:44 +05:30
Hoai-Thu Vuong
e3091cb6db chore(i18n): fix typo in translation of clear_all (#3133) 2023-06-12 10:31:58 +05:30
Akash K
270f796683 fix: fix url getting overridden when query params are present (#3130) 2023-06-09 21:53:55 +05:30
Anwarul Islam
24c6bce02d fix: failed to execute 'observe' on 'IntersectionObserver' (#3122) 2023-06-09 09:40:09 +05:30
Anwarul Islam
2db567589f fix: collection request name edit issue (#3115)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
Co-authored-by: Nivedin <nivedinp@gmail.com>
2023-06-09 09:36:41 +05:30
Liyas Thomas
1fe83ebdc8 chore: updated i18n strings (#3106) 2023-06-07 23:59:04 +05:30
islamzeki
8320d4f222 chore(i18n): update tr.json 2023-06-07 23:56:49 +05:30
Liyas Thomas
e76c1bc64c fix: stack order of tab inside environment selector (#3108) 2023-06-07 23:47:24 +05:30
Nivedin
1f3f8464ea fix: team environment lost when route changes (#3113)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-06-07 23:46:09 +05:30
Andrew Bastin
81a7e23a12 feat: introduce dioc into hoppscotch-common 2023-06-07 15:20:49 +05:30
Liyas Thomas
e75391cdf1 chore: updated icon with correct size (#3105) 2023-06-04 23:46:47 -04:00
Andrew Bastin
a213c0c26c chore: bump version to 2023.4.5 2023-06-04 23:41:01 -04:00
Andrew Bastin
15424903ed fix: stop logging DATABASE_URL in logs 2023-06-04 23:33:32 -04:00
Andrew Bastin
1cce117b0a chore: bump version to 2023.4.4 2023-06-02 11:06:51 -04:00
Liyas Thomas
abc7b4b6f3 chore: improve mobile responsiveness on environment selector (#3100) 2023-06-02 10:56:18 -04:00
Ankit Sridhar
05e32ef9e4 fix: update team invitation link to domain specified in .env [HBE-202] (#3096) 2023-05-31 10:36:34 -04:00
Nivedin
f0a1fc319c fix: sync popup firing multiple times (#3063) 2023-05-30 23:36:37 -04:00
Allen Zhang
385cabc6aa fix: update package.json script (#3083) 2023-05-30 17:50:47 -04:00
Liyas Thomas
397b26a9f3 chore: environment selector with new ux (#3052)
Co-authored-by: Nivedin <nivedinp@gmail.com>
2023-05-30 17:47:37 -04:00
Nivedin
9a40058329 fix: set team environment from test (#3059) 2023-05-30 17:38:28 -04:00
Akash K
7ec2380ed5 chore: update wss url to ws in .env.example (#3081) 2023-05-29 20:23:02 -04:00
安正超
3d4825305d chore(i18n): Update zh-CN translations (#3068) 2023-05-29 20:19:43 -04:00
Nivedin
26e564288b feat: prettify XML response (#3079) 2023-05-29 20:18:19 -04:00
236 changed files with 7824 additions and 2381 deletions

View File

@@ -31,6 +31,7 @@ MICROSOFT_CLIENT_ID="************************************************"
MICROSOFT_CLIENT_SECRET="************************************************" MICROSOFT_CLIENT_SECRET="************************************************"
MICROSOFT_CALLBACK_URL="http://localhost:3170/v1/auth/microsoft/callback" MICROSOFT_CALLBACK_URL="http://localhost:3170/v1/auth/microsoft/callback"
MICROSOFT_SCOPE="user.read" MICROSOFT_SCOPE="user.read"
MICROSOFT_TENANT="common"
# Mailer config # Mailer config
MAILER_SMTP_URL="smtps://user@domain.com:pass@smtp.domain.com" MAILER_SMTP_URL="smtps://user@domain.com:pass@smtp.domain.com"
@@ -51,7 +52,7 @@ VITE_ADMIN_URL=http://localhost:3100
# Backend URLs # Backend URLs
VITE_BACKEND_GQL_URL=http://localhost:3170/graphql VITE_BACKEND_GQL_URL=http://localhost:3170/graphql
VITE_BACKEND_WS_URL=wss://localhost:3170/graphql VITE_BACKEND_WS_URL=ws://localhost:3170/graphql
VITE_BACKEND_API_URL=http://localhost:3170/v1 VITE_BACKEND_API_URL=http://localhost:3170/v1
# Terms Of Service And Privacy Policy Links (Optional) # Terms Of Service And Privacy Policy Links (Optional)

42
.github/workflows/ui.yml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: Deploy to Netlify (ui)
on:
push:
branches: [main]
# run this workflow only if an update is made to the ui package
paths:
- "packages/hoppscotch-ui/**"
workflow_dispatch:
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup environment
run: mv .env.example .env
- name: Setup pnpm
uses: pnpm/action-setup@v2.2.4
with:
version: 8
run_install: true
- name: Setup node
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: pnpm
- name: Build site
run: pnpm run generate-ui
# Deploy the ui site with netlify-cli
- name: Deploy to Netlify (ui)
run: npx netlify-cli deploy --dir=packages/hoppscotch-ui/.histoire/dist --prod
env:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_UI_SITE_ID }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}

View File

@@ -11,7 +11,7 @@
"dev": "pnpm -r do-dev", "dev": "pnpm -r do-dev",
"gen-gql": "cross-env GQL_SCHEMA_EMIT_LOCATION='../../../gql-gen/backend-schema.gql' pnpm -r generate-gql-sdl", "gen-gql": "cross-env GQL_SCHEMA_EMIT_LOCATION='../../../gql-gen/backend-schema.gql' pnpm -r generate-gql-sdl",
"generate": "pnpm -r do-build-prod", "generate": "pnpm -r do-build-prod",
"start": "http-server packages/hoppscotch-web/dist -p 3000", "start": "http-server packages/hoppscotch-selfhost-web/dist -p 3000",
"lint": "pnpm -r do-lint", "lint": "pnpm -r do-lint",
"typecheck": "pnpm -r do-typecheck", "typecheck": "pnpm -r do-typecheck",
"lintfix": "pnpm -r do-lintfix", "lintfix": "pnpm -r do-lintfix",

24
packages/dioc/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

141
packages/dioc/README.md Normal file
View File

@@ -0,0 +1,141 @@
# dioc
A small and lightweight dependency injection / inversion of control system.
### About
`dioc` is a really simple **DI/IOC** system where you write services (which are singletons per container) that can depend on each other and emit events that can be listened upon.
### Demo
```ts
import { Service, Container } from "dioc"
// Here is a simple service, which you can define by extending the Service class
// and providing an ID static field (of type string)
export class PersistenceService extends Service {
// This should be unique for each container
public static ID = "PERSISTENCE_SERVICE"
public read(key: string): string | undefined {
// ...
}
public write(key: string, value: string) {
// ...
}
}
type TodoServiceEvent =
| { type: "TODO_CREATED"; index: number }
| { type: "TODO_DELETED"; index: number }
// Services have a built in event system
// Define the generic argument to say what are the possible emitted values
export class TodoService extends Service<TodoServiceEvent> {
public static ID = "TODO_SERVICE"
// Inject persistence service into this service
private readonly persistence = this.bind(PersistenceService)
public todos = []
// Service constructors cannot have arguments
constructor() {
super()
this.todos = JSON.parse(this.persistence.read("todos") ?? "[]")
}
public addTodo(text: string) {
// ...
// You can access services via the bound fields
this.persistence.write("todos", JSON.stringify(this.todos))
// This is how you emit an event
this.emit({
type: "TODO_CREATED",
index,
})
}
public removeTodo(index: number) {
// ...
this.emit({
type: "TODO_DELETED",
index,
})
}
}
// Services need a container to run in
const container = new Container()
// You can initialize and get services using Container#bind
// It will automatically initialize the service (and its dependencies)
const todoService = container.bind(TodoService) // Returns an instance of TodoService
```
### Demo (Unit Test)
`dioc/testing` contains `TestContainer` which lets you bind mocked services to the container.
```ts
import { TestContainer } from "dioc/testing"
import { TodoService, PersistenceService } from "./demo.ts" // The above demo code snippet
import { describe, it, expect, vi } from "vitest"
describe("TodoService", () => {
it("addTodo writes to persistence", () => {
const container = new TestContainer()
const writeFn = vi.fn()
// The first parameter is the service to mock and the second parameter
// is the mocked service fields and functions
container.bindMock(PersistenceService, {
read: () => undefined, // Not really important for this test
write: writeFn,
})
// the peristence service bind in TodoService will now use the
// above defined mocked implementation
const todoService = container.bind(TodoService)
todoService.addTodo("sup")
expect(writeFn).toHaveBeenCalledOnce()
expect(writeFn).toHaveBeenCalledWith("todos", JSON.stringify(["sup"]))
})
})
```
### Demo (Vue)
`dioc/vue` contains a Vue Plugin and a `useService` composable that allows Vue components to use the defined services.
In the app entry point:
```ts
import { createApp } from "vue"
import { diocPlugin } from "dioc/vue"
const app = createApp()
app.use(diocPlugin, {
container: new Container(), // You can pass in the container you want to provide to the components here
})
```
In your Vue components:
```vue
<script setup>
import { TodoService } from "./demo.ts" // The above demo
import { useService } from "dioc/vue"
const todoService = useService(TodoService) // Returns an instance of the TodoService class
</script>
```

2
packages/dioc/index.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export { default } from "./dist/main.d.ts"
export * from "./dist/main.d.ts"

View File

@@ -0,0 +1,147 @@
import { Service } from "./service"
import { Observable, Subject } from 'rxjs'
/**
* Stores the current container instance in the current operating context.
*
* NOTE: This should not be used outside of dioc library code
*/
export let currentContainer: Container | null = null
/**
* The events emitted by the container
*
* `SERVICE_BIND` - emitted when a service is bound to the container directly or as a dependency to another service
* `SERVICE_INIT` - emitted when a service is initialized
*/
export type ContainerEvent =
| {
type: 'SERVICE_BIND';
/** The Service ID of the service being bounded (the dependency) */
boundeeID: string;
/**
* The Service ID of the bounder that is binding the boundee (the dependent)
*
* NOTE: This will be undefined if the service is bound directly to the container
*/
bounderID: string | undefined
}
| {
type: 'SERVICE_INIT';
/** The Service ID of the service being initialized */
serviceID: string
}
/**
* The dependency injection container, allows for services to be initialized and maintains the dependency trees.
*/
export class Container {
/** Used during the `bind` operation to detect circular dependencies */
private bindStack: string[] = []
/** The map of bound services to their IDs */
protected boundMap = new Map<string, Service<unknown>>()
/** The RxJS observable representing the event stream */
protected event$ = new Subject<ContainerEvent>()
/**
* Returns whether a container has the given service bound
* @param service The service to check for
*/
public hasBound<
T extends typeof Service<any> & { ID: string }
>(service: T): boolean {
return this.boundMap.has(service.ID)
}
/**
* Returns the service bound to the container with the given ID or if not found, undefined.
*
* NOTE: This is an advanced method and should not be used as much as possible.
*
* @param serviceID The ID of the service to get
*/
public getBoundServiceWithID(serviceID: string): Service<unknown> | undefined {
return this.boundMap.get(serviceID)
}
/**
* Binds a service to the container. This is equivalent to marking a service as a dependency.
* @param service The class reference of a service to bind
* @param bounder The class reference of the service that is binding the service (if bound directly to the container, this should be undefined)
*/
public bind<T extends typeof Service<any> & { ID: string }>(
service: T,
bounder: ((typeof Service<T>) & { ID: string }) | undefined = undefined
): InstanceType<T> {
// We need to store the current container in a variable so that we can restore it after the bind operation
const oldCurrentContainer = currentContainer;
currentContainer = this;
// If the service is already bound, return the existing instance
if (this.hasBound(service)) {
this.event$.next({
type: 'SERVICE_BIND',
boundeeID: service.ID,
bounderID: bounder?.ID // Return the bounder ID if it is defined, else assume its the container
})
return this.boundMap.get(service.ID) as InstanceType<T> // Casted as InstanceType<T> because service IDs and types are expected to match
}
// Detect circular dependency and throw error
if (this.bindStack.findIndex((serviceID) => serviceID === service.ID) !== -1) {
const circularServices = `${this.bindStack.join(' -> ')} -> ${service.ID}`
throw new Error(`Circular dependency detected.\nChain: ${circularServices}`)
}
// Push the service ID onto the bind stack to detect circular dependencies
this.bindStack.push(service.ID)
// Initialize the service and emit events
// NOTE: We need to cast the service to any as TypeScript thinks that the service is abstract
const instance: Service<any> = new (service as any)()
this.boundMap.set(service.ID, instance)
this.bindStack.pop()
this.event$.next({
type: 'SERVICE_INIT',
serviceID: service.ID,
})
this.event$.next({
type: 'SERVICE_BIND',
boundeeID: service.ID,
bounderID: bounder?.ID
})
// Restore the current container
currentContainer = oldCurrentContainer;
// We expect the return type to match the service definition
return instance as InstanceType<T>
}
/**
* Returns an iterator of the currently bound service IDs and their instances
*/
public getBoundServices(): IterableIterator<[string, Service<any>]> {
return this.boundMap.entries()
}
/**
* Returns the public container event stream
*/
public getEventStream(): Observable<ContainerEvent> {
return this.event$.asObservable()
}
}

View File

@@ -0,0 +1,2 @@
export * from "./container"
export * from "./service"

View File

@@ -0,0 +1,65 @@
import { Observable, Subject } from 'rxjs'
import { Container, currentContainer } from './container'
/**
* A Dioc service that can bound to a container and can bind dependency services.
*
* NOTE: Services cannot have a constructor that takes arguments.
*
* @template EventDef The type of events that can be emitted by the service. These will be accessible by event streams
*/
export abstract class Service<EventDef = {}> {
/**
* The internal event stream of the service
*/
private event$ = new Subject<EventDef>()
/** The container the service is bound to */
#container: Container
constructor() {
if (!currentContainer) {
throw new Error(
`Tried to initialize service with no container (ID: ${ (this.constructor as any).ID })`
)
}
this.#container = currentContainer
}
/**
* Binds a dependency service into this service.
* @param service The class reference of the service to bind
*/
protected bind<T extends typeof Service<any> & { ID: string }>(service: T): InstanceType<T> {
if (!currentContainer) {
throw new Error('No currentContainer defined.')
}
return currentContainer.bind(service, this.constructor as typeof Service<any> & { ID: string })
}
/**
* Returns the container the service is bound to
*/
protected getContainer(): Container {
return this.#container
}
/**
* Emits an event on the service's event stream
* @param event The event to emit
*/
protected emit(event: EventDef) {
this.event$.next(event)
}
/**
* Returns the event stream of the service
*/
public getEventStream(): Observable<EventDef> {
return this.event$.asObservable()
}
}

View File

@@ -0,0 +1,33 @@
import { Container, Service } from "./main";
/**
* A container that can be used for writing tests, contains additional methods
* for binding suitable for writing tests. (see `bindMock`).
*/
export class TestContainer extends Container {
/**
* Binds a mock service to the container.
*
* @param service
* @param mock
*/
public bindMock<
T extends typeof Service<any> & { ID: string },
U extends Partial<InstanceType<T>>
>(service: T, mock: U): U {
if (this.boundMap.has(service.ID)) {
throw new Error(`Service '${service.ID}' already bound to container. Did you already call bindMock on this ?`)
}
this.boundMap.set(service.ID, mock as any)
this.event$.next({
type: "SERVICE_BIND",
boundeeID: service.ID,
bounderID: undefined,
})
return mock
}
}

34
packages/dioc/lib/vue.ts Normal file
View File

@@ -0,0 +1,34 @@
import { Plugin, inject } from "vue"
import { Container } from "./container"
import { Service } from "./service"
const VUE_CONTAINER_KEY = Symbol()
// TODO: Some Vue version issue with plugin generics is breaking type checking
/**
* The Vue Dioc Plugin, this allows the composables to work and access the container
*
* NOTE: Make sure you add `vue` as dependency to be able to use this plugin (duh)
*/
export const diocPlugin: Plugin = {
install(app, { container }) {
app.provide(VUE_CONTAINER_KEY, container)
}
}
/**
* A composable that binds a service to a Vue Component
*
* @param service The class reference of the service to bind
*/
export function useService<
T extends typeof Service<any> & { ID: string }
>(service: T): InstanceType<T> {
const container = inject(VUE_CONTAINER_KEY) as Container | undefined | null
if (!container) {
throw new Error("Container not found, did you forget to install the dioc plugin?")
}
return container.bind(service)
}

View File

@@ -0,0 +1,54 @@
{
"name": "dioc",
"private": true,
"version": "0.1.0",
"type": "module",
"files": [
"dist",
"index.d.ts"
],
"main": "./dist/counter.umd.cjs",
"module": "./dist/counter.js",
"types": "./index.d.ts",
"exports": {
".": {
"types": "./dist/main.d.ts",
"require": "./dist/index.cjs",
"import": "./dist/index.js"
},
"./vue": {
"types": "./dist/vue.d.ts",
"require": "./dist/vue.cjs",
"import": "./dist/vue.js"
},
"./testing": {
"types": "./dist/testing.d.ts",
"require": "./dist/testing.cjs",
"import": "./dist/testing.js"
}
},
"scripts": {
"dev": "vite",
"build": "vite build && tsc --emitDeclarationOnly",
"prepare": "pnpm run build",
"test": "vitest run",
"do-test": "pnpm run test",
"test:watch": "vitest"
},
"devDependencies": {
"typescript": "^4.9.4",
"vite": "^4.0.4",
"vitest": "^0.29.3"
},
"dependencies": {
"rxjs": "^7.8.1"
},
"peerDependencies": {
"vue": "^3.2.25"
},
"peerDependenciesMeta": {
"vue": {
"optional": true
}
}
}

View File

@@ -0,0 +1,262 @@
import { it, expect, describe, vi } from "vitest"
import { Service } from "../lib/service"
import { Container, currentContainer, ContainerEvent } from "../lib/container"
class TestServiceA extends Service {
public static ID = "TestServiceA"
}
class TestServiceB extends Service {
public static ID = "TestServiceB"
// Marked public to allow for testing
public readonly serviceA = this.bind(TestServiceA)
}
describe("Container", () => {
describe("getBoundServiceWithID", () => {
it("returns the service instance if it is bound to the container", () => {
const container = new Container()
const service = container.bind(TestServiceA)
expect(container.getBoundServiceWithID(TestServiceA.ID)).toBe(service)
})
it("returns undefined if the service is not bound to the container", () => {
const container = new Container()
expect(container.getBoundServiceWithID(TestServiceA.ID)).toBeUndefined()
})
})
describe("bind", () => {
it("correctly binds the service to it", () => {
const container = new Container()
const service = container.bind(TestServiceA)
// @ts-expect-error getContainer is defined as a protected property, but we are leveraging it here to check
expect(service.getContainer()).toBe(container)
})
it("after bind, the current container is set back to its previous value", () => {
const originalValue = currentContainer
const container = new Container()
container.bind(TestServiceA)
expect(currentContainer).toBe(originalValue)
})
it("dependent services are registered in the same container", () => {
const container = new Container()
const serviceB = container.bind(TestServiceB)
// @ts-expect-error getContainer is defined as a protected property, but we are leveraging it here to check
expect(serviceB.serviceA.getContainer()).toBe(container)
})
it("binding an already initialized service returns the initialized instance (services are singletons)", () => {
const container = new Container()
const serviceA = container.bind(TestServiceA)
const serviceA2 = container.bind(TestServiceA)
expect(serviceA).toBe(serviceA2)
})
it("binding a service which is a dependency of another service returns the same instance created from the dependency resolution (services are singletons)", () => {
const container = new Container()
const serviceB = container.bind(TestServiceB)
const serviceA = container.bind(TestServiceA)
expect(serviceB.serviceA).toBe(serviceA)
})
it("binding an initialized service as a dependency returns the same instance", () => {
const container = new Container()
const serviceA = container.bind(TestServiceA)
const serviceB = container.bind(TestServiceB)
expect(serviceB.serviceA).toBe(serviceA)
})
it("container emits an init event when an uninitialized service is initialized via bind and event only called once", () => {
const container = new Container()
const serviceFunc = vi.fn<
[ContainerEvent & { type: "SERVICE_INIT" }],
void
>()
container.getEventStream().subscribe((ev) => {
if (ev.type === "SERVICE_INIT") {
serviceFunc(ev)
}
})
const instance = container.bind(TestServiceA)
expect(serviceFunc).toHaveBeenCalledOnce()
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
type: "SERVICE_INIT",
serviceID: TestServiceA.ID,
})
})
it("the bind event emitted has an undefined bounderID when the service is bound directly to the container", () => {
const container = new Container()
const serviceFunc = vi.fn<
[ContainerEvent & { type: "SERVICE_BIND" }],
void
>()
container.getEventStream().subscribe((ev) => {
if (ev.type === "SERVICE_BIND") {
serviceFunc(ev)
}
})
container.bind(TestServiceA)
expect(serviceFunc).toHaveBeenCalledOnce()
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
type: "SERVICE_BIND",
boundeeID: TestServiceA.ID,
bounderID: undefined,
})
})
it("the bind event emitted has the correct bounderID when the service is bound to another service", () => {
const container = new Container()
const serviceFunc = vi.fn<
[ContainerEvent & { type: "SERVICE_BIND" }],
void
>()
container.getEventStream().subscribe((ev) => {
// We only care about the bind event of TestServiceA
if (ev.type === "SERVICE_BIND" && ev.boundeeID === TestServiceA.ID) {
serviceFunc(ev)
}
})
container.bind(TestServiceB)
expect(serviceFunc).toHaveBeenCalledOnce()
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
type: "SERVICE_BIND",
boundeeID: TestServiceA.ID,
bounderID: TestServiceB.ID,
})
})
})
describe("hasBound", () => {
it("returns true if the given service is bound to the container", () => {
const container = new Container()
container.bind(TestServiceA)
expect(container.hasBound(TestServiceA)).toEqual(true)
})
it("returns false if the given service is not bound to the container", () => {
const container = new Container()
expect(container.hasBound(TestServiceA)).toEqual(false)
})
it("returns true when the service is bound because it is a dependency of another service", () => {
const container = new Container()
container.bind(TestServiceB)
expect(container.hasBound(TestServiceA)).toEqual(true)
})
})
describe("getEventStream", () => {
it("returns an observable which emits events correctly when services are initialized", () => {
const container = new Container()
const serviceFunc = vi.fn<
[ContainerEvent & { type: "SERVICE_INIT" }],
void
>()
container.getEventStream().subscribe((ev) => {
if (ev.type === "SERVICE_INIT") {
serviceFunc(ev)
}
})
container.bind(TestServiceB)
expect(serviceFunc).toHaveBeenCalledTimes(2)
expect(serviceFunc).toHaveBeenNthCalledWith(1, <ContainerEvent>{
type: "SERVICE_INIT",
serviceID: TestServiceA.ID,
})
expect(serviceFunc).toHaveBeenNthCalledWith(2, <ContainerEvent>{
type: "SERVICE_INIT",
serviceID: TestServiceB.ID,
})
})
it("returns an observable which emits events correctly when services are bound", () => {
const container = new Container()
const serviceFunc = vi.fn<
[ContainerEvent & { type: "SERVICE_BIND" }],
void
>()
container.getEventStream().subscribe((ev) => {
if (ev.type === "SERVICE_BIND") {
serviceFunc(ev)
}
})
container.bind(TestServiceB)
expect(serviceFunc).toHaveBeenCalledTimes(2)
expect(serviceFunc).toHaveBeenNthCalledWith(1, <ContainerEvent>{
type: "SERVICE_BIND",
boundeeID: TestServiceA.ID,
bounderID: TestServiceB.ID,
})
expect(serviceFunc).toHaveBeenNthCalledWith(2, <ContainerEvent>{
type: "SERVICE_BIND",
boundeeID: TestServiceB.ID,
bounderID: undefined,
})
})
})
describe("getBoundServices", () => {
it("returns an iterator over all services bound to the container in the format [service id, service instance]", () => {
const container = new Container()
const instanceB = container.bind(TestServiceB)
const instanceA = instanceB.serviceA
expect(Array.from(container.getBoundServices())).toEqual([
[TestServiceA.ID, instanceA],
[TestServiceB.ID, instanceB],
])
})
it("returns an empty iterator if no services are bound", () => {
const container = new Container()
expect(Array.from(container.getBoundServices())).toEqual([])
})
})
})

View File

@@ -0,0 +1,66 @@
import { describe, expect, it, vi } from "vitest"
import { Service, Container } from "../lib/main"
class TestServiceA extends Service {
public static ID = "TestServiceA"
}
class TestServiceB extends Service<"test"> {
public static ID = "TestServiceB"
// Marked public to allow for testing
public readonly serviceA = this.bind(TestServiceA)
public emitTestEvent() {
this.emit("test")
}
}
describe("Service", () => {
describe("constructor", () => {
it("throws an error if the service is initialized without a container", () => {
expect(() => new TestServiceA()).toThrowError(
"Tried to initialize service with no container (ID: TestServiceA)"
)
})
})
describe("bind", () => {
it("correctly binds the dependency service using the container", () => {
const container = new Container()
const serviceA = container.bind(TestServiceA)
const serviceB = container.bind(TestServiceB)
expect(serviceB.serviceA).toBe(serviceA)
})
})
describe("getContainer", () => {
it("returns the container the service is bound to", () => {
const container = new Container()
const serviceA = container.bind(TestServiceA)
// @ts-expect-error getContainer is a protected member, we are just using it to help with testing
expect(serviceA.getContainer()).toBe(container)
})
})
describe("getEventStream", () => {
it("returns the valid event stream of the service", () => {
const container = new Container()
const serviceB = container.bind(TestServiceB)
const serviceFunc = vi.fn()
serviceB.getEventStream().subscribe(serviceFunc)
serviceB.emitTestEvent()
expect(serviceFunc).toHaveBeenCalledOnce()
expect(serviceFunc).toHaveBeenCalledWith("test")
})
})
})

View File

@@ -0,0 +1,92 @@
import { describe, expect, it, vi } from "vitest"
import { TestContainer } from "../lib/testing"
import { Service } from "../lib/service"
import { ContainerEvent } from "../lib/container"
class TestServiceA extends Service {
public static ID = "TestServiceA"
public test() {
return "real"
}
}
class TestServiceB extends Service {
public static ID = "TestServiceB"
// declared public to help with testing
public readonly serviceA = this.bind(TestServiceA)
public test() {
return this.serviceA.test()
}
}
describe("TestContainer", () => {
describe("bindMock", () => {
it("returns the fake service defined", () => {
const container = new TestContainer()
const fakeService = {
test: () => "fake",
}
const result = container.bindMock(TestServiceA, fakeService)
expect(result).toBe(fakeService)
})
it("new services bound to the container get the mock service", () => {
const container = new TestContainer()
const fakeServiceA = {
test: () => "fake",
}
container.bindMock(TestServiceA, fakeServiceA)
const serviceB = container.bind(TestServiceB)
expect(serviceB.serviceA).toBe(fakeServiceA)
})
it("container emits SERVICE_BIND event", () => {
const container = new TestContainer()
const fakeServiceA = {
test: () => "fake",
}
const serviceFunc = vi.fn<[ContainerEvent, void]>()
container.getEventStream().subscribe((ev) => {
serviceFunc(ev)
})
container.bindMock(TestServiceA, fakeServiceA)
expect(serviceFunc).toHaveBeenCalledOnce()
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
type: "SERVICE_BIND",
boundeeID: TestServiceA.ID,
bounderID: undefined,
})
})
it("throws if service already bound", () => {
const container = new TestContainer()
const fakeServiceA = {
test: () => "fake",
}
container.bindMock(TestServiceA, fakeServiceA)
expect(() => {
container.bindMock(TestServiceA, fakeServiceA)
}).toThrowError(
"Service 'TestServiceA' already bound to container. Did you already call bindMock on this ?"
)
})
})
})

2
packages/dioc/testing.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export { default } from "./dist/testing.d.ts"
export * from "./dist/testing.d.ts"

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"declaration": true,
"sourceMap": true,
"outDir": "dist",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"skipLibCheck": true
},
"include": ["lib"]
}

View File

@@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
export default defineConfig({
build: {
lib: {
entry: {
index: './lib/main.ts',
vue: './lib/vue.ts',
testing: './lib/testing.ts',
},
},
rollupOptions: {
external: ['vue'],
}
},
})

View File

@@ -0,0 +1,7 @@
import { defineConfig } from "vitest/config"
export default defineConfig({
test: {
}
})

2
packages/dioc/vue.d.ts vendored Normal file
View File

@@ -0,0 +1,2 @@
export { default } from "./dist/vue.d.ts"
export * from "./dist/vue.d.ts"

View File

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

View File

@@ -236,11 +236,11 @@ export class AdminService {
const user = await this.userService.findUserByEmail(userEmail); const user = await this.userService.findUserByEmail(userEmail);
if (O.isNone(user)) return E.left(USER_NOT_FOUND); if (O.isNone(user)) return E.left(USER_NOT_FOUND);
const isUserAlreadyMember = await this.teamService.getTeamMemberTE( const teamMember = await this.teamService.getTeamMemberTE(
teamID, teamID,
user.value.uid, user.value.uid,
)(); )();
if (E.left(isUserAlreadyMember)) { if (E.isLeft(teamMember)) {
const addedUser = await this.teamService.addMemberToTeamWithEmail( const addedUser = await this.teamService.addMemberToTeamWithEmail(
teamID, teamID,
userEmail, userEmail,
@@ -248,6 +248,18 @@ export class AdminService {
); );
if (E.isLeft(addedUser)) return E.left(addedUser.left); if (E.isLeft(addedUser)) return E.left(addedUser.left);
const userInvitation =
await this.teamInvitationService.getTeamInviteByEmailAndTeamID(
userEmail,
teamID,
);
if (E.isRight(userInvitation)) {
await this.teamInvitationService.revokeInvitation(
userInvitation.right.id,
)();
}
return E.right(addedUser.right); return E.right(addedUser.right);
} }

View File

@@ -17,7 +17,7 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) {
clientSecret: process.env.MICROSOFT_CLIENT_SECRET, clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
callbackURL: process.env.MICROSOFT_CALLBACK_URL, callbackURL: process.env.MICROSOFT_CALLBACK_URL,
scope: [process.env.MICROSOFT_SCOPE], scope: [process.env.MICROSOFT_SCOPE],
passReqToCallback: true, tenant: process.env.MICROSOFT_TENANT,
store: true, store: true,
}); });
} }

View File

@@ -23,7 +23,7 @@ export const AUTH_FAIL = 'auth/fail';
export const JSON_INVALID = 'json_invalid'; export const JSON_INVALID = 'json_invalid';
/** /**
* Tried to delete an user data document from fb firestore but failed. * Tried to delete a user data document from fb firestore but failed.
* (FirebaseService) * (FirebaseService)
*/ */
export const USER_FB_DOCUMENT_DELETION_FAILED = export const USER_FB_DOCUMENT_DELETION_FAILED =
@@ -231,7 +231,7 @@ export const TEAM_COLL_INVALID_JSON = 'team_coll/invalid_json';
export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const; export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const;
/** /**
* Tried to perform action on a request that doesn't accept their member role level * Tried to perform an action on a request that doesn't accept their member role level
* (GqlRequestTeamMemberGuard) * (GqlRequestTeamMemberGuard)
*/ */
export const TEAM_REQ_NOT_REQUIRED_ROLE = 'team_req/not_required_role'; export const TEAM_REQ_NOT_REQUIRED_ROLE = 'team_req/not_required_role';
@@ -262,7 +262,7 @@ export const TEAM_REQ_REORDERING_FAILED = 'team_req/reordering_failed' as const;
export const SENDER_EMAIL_INVALID = 'mailer/sender_email_invalid' as const; export const SENDER_EMAIL_INVALID = 'mailer/sender_email_invalid' as const;
/** /**
* Tried to perform action on a request when the user is not even member of the team * Tried to perform an action on a request when the user is not even a member of the team
* (GqlRequestTeamMemberGuard, GqlCollectionTeamMemberGuard) * (GqlRequestTeamMemberGuard, GqlCollectionTeamMemberGuard)
*/ */
export const TEAM_REQ_NOT_MEMBER = 'team_req/not_member'; export const TEAM_REQ_NOT_MEMBER = 'team_req/not_member';
@@ -307,7 +307,7 @@ export const SHORTCODE_INVALID_JSON = 'shortcode/invalid_json' as const;
export const SHORTCODE_ALREADY_EXISTS = 'shortcode/already_exists' as const; export const SHORTCODE_ALREADY_EXISTS = 'shortcode/already_exists' as const;
/** /**
* Invalid or non-existent TEAM ENVIRONMMENT ID * Invalid or non-existent TEAM ENVIRONMENT ID
* (TeamEnvironmentsService) * (TeamEnvironmentsService)
*/ */
export const TEAM_ENVIRONMENT_NOT_FOUND = 'team_environment/not_found' as const; export const TEAM_ENVIRONMENT_NOT_FOUND = 'team_environment/not_found' as const;
@@ -340,7 +340,7 @@ export const USER_SETTINGS_NULL_SETTINGS =
'user_settings/null_settings' as const; 'user_settings/null_settings' as const;
/* /*
* Global environment doesnt exists for the user * Global environment doesn't exist for the user
* (UserEnvironmentsService) * (UserEnvironmentsService)
*/ */
export const USER_ENVIRONMENT_GLOBAL_ENV_DOES_NOT_EXISTS = export const USER_ENVIRONMENT_GLOBAL_ENV_DOES_NOT_EXISTS =

View File

@@ -9,7 +9,6 @@ import { emitGQLSchemaFile } from './gql-schema';
async function bootstrap() { async function bootstrap() {
console.log(`Running in production: ${process.env.PRODUCTION}`); console.log(`Running in production: ${process.env.PRODUCTION}`);
console.log(`Port: ${process.env.PORT}`); console.log(`Port: ${process.env.PORT}`);
console.log(`Database: ${process.env.DATABASE_URL}`);
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);

View File

@@ -3,6 +3,7 @@ import * as T from 'fp-ts/Task';
import * as O from 'fp-ts/Option'; import * as O from 'fp-ts/Option';
import * as TO from 'fp-ts/TaskOption'; import * as TO from 'fp-ts/TaskOption';
import * as TE from 'fp-ts/TaskEither'; import * as TE from 'fp-ts/TaskEither';
import * as E from 'fp-ts/Either';
import { pipe, flow, constVoid } from 'fp-ts/function'; import { pipe, flow, constVoid } from 'fp-ts/function';
import { PrismaService } from 'src/prisma/prisma.service'; import { PrismaService } from 'src/prisma/prisma.service';
import { Team, TeamMemberRole } from 'src/team/team.model'; import { Team, TeamMemberRole } from 'src/team/team.model';
@@ -10,6 +11,7 @@ import { Email } from 'src/types/Email';
import { User } from 'src/user/user.model'; import { User } from 'src/user/user.model';
import { TeamService } from 'src/team/team.service'; import { TeamService } from 'src/team/team.service';
import { import {
INVALID_EMAIL,
TEAM_INVITE_ALREADY_MEMBER, TEAM_INVITE_ALREADY_MEMBER,
TEAM_INVITE_EMAIL_DO_NOT_MATCH, TEAM_INVITE_EMAIL_DO_NOT_MATCH,
TEAM_INVITE_MEMBER_HAS_INVITE, TEAM_INVITE_MEMBER_HAS_INVITE,
@@ -19,6 +21,7 @@ import { TeamInvitation } from './team-invitation.model';
import { MailerService } from 'src/mailer/mailer.service'; import { MailerService } from 'src/mailer/mailer.service';
import { UserService } from 'src/user/user.service'; import { UserService } from 'src/user/user.service';
import { PubSubService } from 'src/pubsub/pubsub.service'; import { PubSubService } from 'src/pubsub/pubsub.service';
import { validateEmail } from '../utils';
@Injectable() @Injectable()
export class TeamInvitationService { export class TeamInvitationService {
@@ -63,6 +66,32 @@ export class TeamInvitationService {
); );
} }
/**
* Get the team invite for an invitee with email and teamID.
* @param inviteeEmail invitee email
* @param teamID team id
* @returns an Either of team invitation for the invitee or error
*/
async getTeamInviteByEmailAndTeamID(inviteeEmail: string, teamID: string) {
const isEmailValid = validateEmail(inviteeEmail);
if (!isEmailValid) return E.left(INVALID_EMAIL);
try {
const teamInvite = await this.prisma.teamInvitation.findUniqueOrThrow({
where: {
teamID_inviteeEmail: {
inviteeEmail: inviteeEmail,
teamID: teamID,
},
},
});
return E.right(teamInvite);
} catch (e) {
return E.left(TEAM_INVITE_NO_INVITE_FOUND);
}
}
createInvitation( createInvitation(
creator: User, creator: User,
team: Team, team: Team,
@@ -126,7 +155,7 @@ export class TeamInvitationService {
template: 'team-invitation', template: 'team-invitation',
variables: { variables: {
invitee: creator.displayName ?? 'A Hoppscotch User', invitee: creator.displayName ?? 'A Hoppscotch User',
action_url: `https://hoppscotch.io/join-team?id=${invitation.id}`, action_url: `${process.env.VITE_BASE_URL}/join-team?id=${invitation.id}`,
invite_team_name: team.name, invite_team_name: team.name,
}, },
}), }),

View File

@@ -32,7 +32,7 @@ export class TeamInviteViewerGuard implements CanActivate {
// Get user // Get user
TE.bindW('user', ({ gqlCtx }) => TE.bindW('user', ({ gqlCtx }) =>
pipe( pipe(
O.fromNullable(gqlCtx.getContext<{ user?: User }>().user), O.fromNullable(gqlCtx.getContext().req.user),
TE.fromOption(() => BUG_AUTH_NO_USER_CTX), TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
), ),
), ),

View File

@@ -33,7 +33,7 @@ export class TeamInviteeGuard implements CanActivate {
// Get user // Get user
TE.bindW('user', ({ gqlCtx }) => TE.bindW('user', ({ gqlCtx }) =>
pipe( pipe(
O.fromNullable(gqlCtx.getContext<{ user?: User }>().user), O.fromNullable(gqlCtx.getContext().req.user),
TE.fromOption(() => BUG_AUTH_NO_USER_CTX), TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
), ),
), ),

View File

@@ -2,7 +2,7 @@ import { HttpStatus } from '@nestjs/common';
/** /**
** Custom interface to handle errors specific to Auth module ** Custom interface to handle errors specific to Auth module
** Since its REST we need to return HTTP status code along with error message ** Since its REST we need to return the HTTP status code along with the error message
*/ */
export type AuthError = { export type AuthError = {
message: string; message: string;

View File

@@ -6,7 +6,6 @@ module.exports = {
env: { env: {
browser: true, browser: true,
node: true, node: true,
jest: true,
}, },
parserOptions: { parserOptions: {
sourceType: "module", sourceType: "module",

View File

@@ -432,6 +432,7 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Reaksie liggaam", "body": "Reaksie liggaam",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Opskrifte", "headers": "Opskrifte",
@@ -445,6 +446,7 @@
"status": "Status", "status": "Status",
"time": "Tyd", "time": "Tyd",
"title": "Reaksie", "title": "Reaksie",
"video": "Video",
"waiting_for_connection": "wag vir verbinding", "waiting_for_connection": "wag vir verbinding",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "هيئة الاستجابة", "body": "هيئة الاستجابة",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "الرؤوس", "headers": "الرؤوس",
@@ -445,6 +446,7 @@
"status": "حالة", "status": "حالة",
"time": "وقت", "time": "وقت",
"title": "إجابة", "title": "إجابة",
"video": "Video",
"waiting_for_connection": "في انتظار الاتصال", "waiting_for_connection": "في انتظار الاتصال",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "Visualitzar els meus enllaços" "view_my_links": "Visualitzar els meus enllaços"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Cos de resposta", "body": "Cos de resposta",
"filter_response_body": "Filtrar el cos de la resposta JSON (utilitza la sintaxi JSONPath)", "filter_response_body": "Filtrar el cos de la resposta JSON (utilitza la sintaxi JSONPath)",
"headers": "Capçaleres", "headers": "Capçaleres",
@@ -445,6 +446,7 @@
"status": "Estat", "status": "Estat",
"time": "Temps", "time": "Temps",
"title": "Resposta", "title": "Resposta",
"video": "Video",
"waiting_for_connection": "esperant la connexió", "waiting_for_connection": "esperant la connexió",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -19,7 +19,7 @@
"edit": "编辑", "edit": "编辑",
"filter": "过滤", "filter": "过滤",
"go_back": "返回", "go_back": "返回",
"go_forward": "Go forward", "go_forward": "前进",
"group_by": "分组方式", "group_by": "分组方式",
"label": "标签", "label": "标签",
"learn_more": "了解更多", "learn_more": "了解更多",
@@ -40,9 +40,9 @@
"start": "开始", "start": "开始",
"starting": "正在开始", "starting": "正在开始",
"stop": "停止", "stop": "停止",
"to_close": "关闭", "to_close": "关闭",
"to_navigate": "定位", "to_navigate": "定位",
"to_select": "选择", "to_select": "选择",
"turn_off": "关闭", "turn_off": "关闭",
"turn_on": "开启", "turn_on": "开启",
"undo": "撤消", "undo": "撤消",
@@ -118,16 +118,16 @@
}, },
"collection": { "collection": {
"created": "集合已创建", "created": "集合已创建",
"different_parent": "Cannot reorder collection with different parent", "different_parent": "不能用不同的父类来重新排序集合",
"edit": "编辑集合", "edit": "编辑集合",
"invalid_name": "请提供有效的集合名称", "invalid_name": "请提供有效的集合名称",
"invalid_root_move": "Collection already in the root", "invalid_root_move": "该集合已经在根级了",
"moved": "Moved Successfully", "moved": "移动完成",
"my_collections": "我的集合", "my_collections": "我的集合",
"name": "我的新集合", "name": "我的新集合",
"name_length_insufficient": "集合名字至少需要 3 个字符", "name_length_insufficient": "集合名字至少需要 3 个字符",
"new": "新建集合", "new": "新建集合",
"order_changed": "Collection Order Updated", "order_changed": "集合顺序已更新",
"renamed": "集合已更名", "renamed": "集合已更名",
"request_in_use": "请求正在使用中", "request_in_use": "请求正在使用中",
"save_as": "另存为", "save_as": "另存为",
@@ -147,7 +147,7 @@
"remove_team": "你确定要删除该团队吗?", "remove_team": "你确定要删除该团队吗?",
"remove_telemetry": "你确定要退出遥测服务吗?", "remove_telemetry": "你确定要退出遥测服务吗?",
"request_change": "你确定你要放弃当前的请求,未保存的修改将被丢失。", "request_change": "你确定你要放弃当前的请求,未保存的修改将被丢失。",
"save_unsaved_tab": "Do you want to save changes made in this tab?", "save_unsaved_tab": "你想保存在此标签页中所作的修改吗?",
"sync": "您确定要同步该工作区吗?" "sync": "您确定要同步该工作区吗?"
}, },
"count": { "count": {
@@ -177,7 +177,7 @@
"members": "团队为空", "members": "团队为空",
"parameters": "该请求没有任何参数", "parameters": "该请求没有任何参数",
"pending_invites": "此团队无待办邀请", "pending_invites": "此团队无待办邀请",
"profile": "登录以查看你的个人档案", "profile": "登录以查看你的个人资料",
"protocols": "协议为空", "protocols": "协议为空",
"schema": "连接至 GraphQL 端点", "schema": "连接至 GraphQL 端点",
"shortcodes": "Shortcodes 为空", "shortcodes": "Shortcodes 为空",
@@ -209,7 +209,7 @@
"browser_support_sse": "该浏览器似乎不支持 SSE。", "browser_support_sse": "该浏览器似乎不支持 SSE。",
"check_console_details": "检查控制台日志以获悉详情", "check_console_details": "检查控制台日志以获悉详情",
"curl_invalid_format": "cURL 格式不正确", "curl_invalid_format": "cURL 格式不正确",
"danger_zone": "Danger zone", "danger_zone": "危险区域",
"delete_account": "您的帐号目前为这些团队的拥有者:", "delete_account": "您的帐号目前为这些团队的拥有者:",
"delete_account_description": "您在删除帐号前必须先将您自己从团队中移除、转移拥有权,或是删除团队。", "delete_account_description": "您在删除帐号前必须先将您自己从团队中移除、转移拥有权,或是删除团队。",
"empty_req_name": "空请求名称", "empty_req_name": "空请求名称",
@@ -219,7 +219,7 @@
"incorrect_email": "电子邮箱错误", "incorrect_email": "电子邮箱错误",
"invalid_link": "无效链接", "invalid_link": "无效链接",
"invalid_link_description": "你点击的链接无效或已过期。", "invalid_link_description": "你点击的链接无效或已过期。",
"json_parsing_failed": "Invalid JSON", "json_parsing_failed": "不合法的 JSON",
"json_prettify_invalid_body": "无法美化无效的请求头,处理 JSON 语法错误并重试", "json_prettify_invalid_body": "无法美化无效的请求头,处理 JSON 语法错误并重试",
"network_error": "好像发生了网络错误,请重试。", "network_error": "好像发生了网络错误,请重试。",
"network_fail": "无法发送请求", "network_fail": "无法发送请求",
@@ -316,14 +316,14 @@
"zen_mode": "ZEN 模式" "zen_mode": "ZEN 模式"
}, },
"modal": { "modal": {
"close_unsaved_tab": "You have unsaved changes", "close_unsaved_tab": "有未保存的变更",
"collections": "集合", "collections": "集合",
"confirm": "确认", "confirm": "确认",
"edit_request": "编辑请求", "edit_request": "编辑请求",
"import_export": "导入/导出" "import_export": "导入/导出"
}, },
"mqtt": { "mqtt": {
"already_subscribed": "您已经订阅了此主。", "already_subscribed": "您已经订阅了此主。",
"clean_session": "清除会话", "clean_session": "清除会话",
"clear_input": "清除输入", "clear_input": "清除输入",
"clear_input_on_send": "发送后清除输入", "clear_input_on_send": "发送后清除输入",
@@ -355,7 +355,7 @@
"navigation": { "navigation": {
"doc": "文档", "doc": "文档",
"graphql": "GraphQL", "graphql": "GraphQL",
"profile": "个人档案", "profile": "个人资料",
"realtime": "实时", "realtime": "实时",
"rest": "REST", "rest": "REST",
"settings": "设置" "settings": "设置"
@@ -377,7 +377,7 @@
"owner_description": "所有者可以添加、编辑和删除请求、集合及团队成员。", "owner_description": "所有者可以添加、编辑和删除请求、集合及团队成员。",
"roles": "角色", "roles": "角色",
"roles_description": "角色用以控制共享集合的访问权限。", "roles_description": "角色用以控制共享集合的访问权限。",
"updated": "档案已更新", "updated": "已更新",
"viewer": "查看者", "viewer": "查看者",
"viewer_description": "查看者只可查看与使用请求。" "viewer_description": "查看者只可查看与使用请求。"
}, },
@@ -396,8 +396,8 @@
"text": "文字" "text": "文字"
}, },
"copy_link": "复制链接", "copy_link": "复制链接",
"different_collection": "Cannot reorder requests from different collections", "different_collection": "不能对来自不同集合的请求进行重新排序",
"duplicated": "Request duplicated", "duplicated": "重复的请求",
"duration": "持续时间", "duration": "持续时间",
"enter_curl": "输入 cURL", "enter_curl": "输入 cURL",
"generate_code": "生成代码", "generate_code": "生成代码",
@@ -405,10 +405,10 @@
"header_list": "请求头列表", "header_list": "请求头列表",
"invalid_name": "请提供请求名称", "invalid_name": "请提供请求名称",
"method": "方法", "method": "方法",
"moved": "Request moved", "moved": "请求移动完成",
"name": "请求名称", "name": "请求名称",
"new": "新请求", "new": "新请求",
"order_changed": "Request Order Updated", "order_changed": "请求顺序更新完成",
"override": "覆盖", "override": "覆盖",
"override_help": "设置 <kbd>Content-Type</kbd> 头", "override_help": "设置 <kbd>Content-Type</kbd> 头",
"overriden": "覆盖", "overriden": "覆盖",
@@ -432,6 +432,7 @@
"view_my_links": "查看我的链接" "view_my_links": "查看我的链接"
}, },
"response": { "response": {
"audio": "Audio",
"body": "响应体", "body": "响应体",
"filter_response_body": "筛选JSON响应本体使用JSONPath语法", "filter_response_body": "筛选JSON响应本体使用JSONPath语法",
"headers": "响应头", "headers": "响应头",
@@ -445,6 +446,7 @@
"status": "状态", "status": "状态",
"time": "时间", "time": "时间",
"title": "响应", "title": "响应",
"video": "Video",
"waiting_for_connection": "等待连接", "waiting_for_connection": "等待连接",
"xml": "XML" "xml": "XML"
}, },
@@ -479,10 +481,10 @@
"language": "语言", "language": "语言",
"light_mode": "亮色", "light_mode": "亮色",
"official_proxy_hosting": "官方代理由 Hoppscotch 托管。", "official_proxy_hosting": "官方代理由 Hoppscotch 托管。",
"profile": "个人档案", "profile": "个人资料",
"profile_description": "更新你的档案详情", "profile_description": "更新你的资料",
"profile_email": "电子邮箱地址", "profile_email": "电子邮箱地址",
"profile_name": "档案名称", "profile_name": "名称",
"proxy": "网络代理", "proxy": "网络代理",
"proxy_url": "代理网址", "proxy_url": "代理网址",
"proxy_use_toggle": "使用代理中间件发送请求", "proxy_use_toggle": "使用代理中间件发送请求",
@@ -532,7 +534,7 @@
"documentation": "前往文档页面", "documentation": "前往文档页面",
"forward": "前往下一页面", "forward": "前往下一页面",
"graphql": "前往 GraphQL 页面", "graphql": "前往 GraphQL 页面",
"profile": "前往个人档案页面", "profile": "前往个人资料页面",
"realtime": "前往实时页面", "realtime": "前往实时页面",
"rest": "前往 REST 页面", "rest": "前往 REST 页面",
"settings": "前往设置页面", "settings": "前往设置页面",
@@ -574,7 +576,7 @@
}, },
"socketio": { "socketio": {
"communication": "通讯", "communication": "通讯",
"connection_not_authorized": "此SocketIO连接未使用任何验证。", "connection_not_authorized": "此 SocketIO 连接未使用任何验证。",
"event_name": "事件名称", "event_name": "事件名称",
"events": "事件", "events": "事件",
"log": "日志", "log": "日志",
@@ -614,12 +616,12 @@
"none": "无", "none": "无",
"nothing_found": "没有找到", "nothing_found": "没有找到",
"published_error": "将信息:{topic}发布至主题:{message}时发生错误", "published_error": "将信息:{topic}发布至主题:{message}时发生错误",
"published_message": "已将此信息:{message}发布至主题:{topic}", "published_message": "已将此信息:{message} 发布至主题:{topic}",
"reconnection_error": "重连失败", "reconnection_error": "重连失败",
"subscribed_failed": "无法订阅此主{topic}", "subscribed_failed": "无法订阅此主{topic}",
"subscribed_success": "成功订阅此主{topic}", "subscribed_success": "成功订阅此主{topic}",
"unsubscribed_failed": "无法取消订阅此主{topic}", "unsubscribed_failed": "无法取消订阅此主{topic}",
"unsubscribed_success": "成功取消订阅此主{topic}", "unsubscribed_success": "成功取消订阅此主{topic}",
"waiting_send_request": "等待发送请求" "waiting_send_request": "等待发送请求"
}, },
"support": { "support": {
@@ -639,7 +641,7 @@
"body": "请求体", "body": "请求体",
"collections": "集合", "collections": "集合",
"documentation": "帮助文档", "documentation": "帮助文档",
"environments": "Environments", "environments": "环境",
"headers": "请求头", "headers": "请求头",
"history": "历史记录", "history": "历史记录",
"mqtt": "MQTT", "mqtt": "MQTT",
@@ -664,7 +666,7 @@
"email_do_not_match": "邮箱无法与你的帐户信息匹配。请联系你的团队者。", "email_do_not_match": "邮箱无法与你的帐户信息匹配。请联系你的团队者。",
"exit": "退出团队", "exit": "退出团队",
"exit_disabled": "团队所有者无法退出团队", "exit_disabled": "团队所有者无法退出团队",
"invalid_coll_id": "Invalid collection ID", "invalid_coll_id": "无效的集合 ID",
"invalid_email_format": "电子邮箱格式无效", "invalid_email_format": "电子邮箱格式无效",
"invalid_id": "无效的团队 ID请联系你的团队者。", "invalid_id": "无效的团队 ID请联系你的团队者。",
"invalid_invite_link": "无效的邀请链接", "invalid_invite_link": "无效的邀请链接",
@@ -688,7 +690,7 @@
"member_removed": "用户已移除", "member_removed": "用户已移除",
"member_role_updated": "用户角色已更新", "member_role_updated": "用户角色已更新",
"members": "成员", "members": "成员",
"more_members": "+{count} more", "more_members": "+{count} 更多",
"name_length_insufficient": "团队名称至少为 6 个字符", "name_length_insufficient": "团队名称至少为 6 个字符",
"name_updated": "团队名称已更新", "name_updated": "团队名称已更新",
"new": "新团队", "new": "新团队",
@@ -696,13 +698,13 @@
"new_name": "我的新团队", "new_name": "我的新团队",
"no_access": "你没有编辑集合的权限", "no_access": "你没有编辑集合的权限",
"no_invite_found": "未找到邀请。请联系你的团队者。", "no_invite_found": "未找到邀请。请联系你的团队者。",
"no_request_found": "Request not found.", "no_request_found": "请求不存在",
"not_found": "没有找到团队,请联系您的团队所有者。", "not_found": "没有找到团队,请联系您的团队所有者。",
"not_valid_viewer": "你不是有效的查看者。请联系你的团队者。", "not_valid_viewer": "你不是有效的查看者。请联系你的团队者。",
"parent_coll_move": "Cannot move collection to a child collection", "parent_coll_move": "不能将集合移动到一个子集合",
"pending_invites": "待办邀请", "pending_invites": "待办邀请",
"permissions": "权限", "permissions": "权限",
"same_target_destination": "Same target and destination", "same_target_destination": "目标相同",
"saved": "团队已保存", "saved": "团队已保存",
"select_a_team": "选择团队", "select_a_team": "选择团队",
"title": "团队", "title": "团队",
@@ -732,9 +734,9 @@
"url": "URL" "url": "URL"
}, },
"workspace": { "workspace": {
"change": "Change workspace", "change": "切换工作空间",
"personal": "My Workspace", "personal": "我的工作空间",
"team": "Team Workspace", "team": "团队工作空间",
"title": "Workspaces" "title": "工作空间"
} }
} }

View File

@@ -432,6 +432,7 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Odpovědní orgán", "body": "Odpovědní orgán",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Záhlaví", "headers": "Záhlaví",
@@ -445,6 +446,7 @@
"status": "Postavení", "status": "Postavení",
"time": "Čas", "time": "Čas",
"title": "Odezva", "title": "Odezva",
"video": "Video",
"waiting_for_connection": "čekání na připojení", "waiting_for_connection": "čekání na připojení",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Svarorgan", "body": "Svarorgan",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Overskrifter", "headers": "Overskrifter",
@@ -445,6 +446,7 @@
"status": "Status", "status": "Status",
"time": "Tid", "time": "Tid",
"title": "Respons", "title": "Respons",
"video": "Video",
"waiting_for_connection": "venter på forbindelse", "waiting_for_connection": "venter på forbindelse",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Antworttext", "body": "Antworttext",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Header", "headers": "Header",
@@ -445,6 +446,7 @@
"status": "Status", "status": "Status",
"time": "Zeit", "time": "Zeit",
"title": "Antwort", "title": "Antwort",
"video": "Video",
"waiting_for_connection": "auf Verbindung warten", "waiting_for_connection": "auf Verbindung warten",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "Προβολή των links μου" "view_my_links": "Προβολή των links μου"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Σώμα απόκρισης", "body": "Σώμα απόκρισης",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Κεφαλίδες", "headers": "Κεφαλίδες",
@@ -445,6 +446,7 @@
"status": "Κατάσταση", "status": "Κατάσταση",
"time": "χρόνος", "time": "χρόνος",
"title": "Απάντηση", "title": "Απάντηση",
"video": "Video",
"waiting_for_connection": "περιμένοντας τη σύνδεση", "waiting_for_connection": "περιμένοντας τη σύνδεση",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -4,6 +4,7 @@
"cancel": "Cancel", "cancel": "Cancel",
"choose_file": "Choose a file", "choose_file": "Choose a file",
"clear": "Clear", "clear": "Clear",
"clear_history": "Clear All History",
"clear_all": "Clear all", "clear_all": "Clear all",
"close": "Close", "close": "Close",
"connect": "Connect", "connect": "Connect",
@@ -582,6 +583,11 @@
"log": "Log", "log": "Log",
"url": "URL" "url": "URL"
}, },
"spotlight": {
"section": {
"user": "User"
}
},
"sse": { "sse": {
"event_type": "Event type", "event_type": "Event type",
"log": "Log", "log": "Log",

View File

@@ -432,6 +432,7 @@
"view_my_links": "Ver mis enlaces" "view_my_links": "Ver mis enlaces"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Cuerpo de respuesta", "body": "Cuerpo de respuesta",
"filter_response_body": "Filtrar el cuerpo de la respuesta JSON (utiliza la sintaxis JSONPath)", "filter_response_body": "Filtrar el cuerpo de la respuesta JSON (utiliza la sintaxis JSONPath)",
"headers": "Encabezados", "headers": "Encabezados",
@@ -445,6 +446,7 @@
"status": "Estado", "status": "Estado",
"time": "Tiempo", "time": "Tiempo",
"title": "Respuesta", "title": "Respuesta",
"video": "Video",
"waiting_for_connection": "esperando la conexión", "waiting_for_connection": "esperando la conexión",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Vastauselin", "body": "Vastauselin",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Otsikot", "headers": "Otsikot",
@@ -445,6 +446,7 @@
"status": "Tila", "status": "Tila",
"time": "Aika", "time": "Aika",
"title": "Vastaus", "title": "Vastaus",
"video": "Video",
"waiting_for_connection": "yhteyttä odotellessa", "waiting_for_connection": "yhteyttä odotellessa",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "Voir mes liens" "view_my_links": "Voir mes liens"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Corps de réponse", "body": "Corps de réponse",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "En-têtes", "headers": "En-têtes",
@@ -445,6 +446,7 @@
"status": "Statut", "status": "Statut",
"time": "Temps", "time": "Temps",
"title": "Réponse", "title": "Réponse",
"video": "Video",
"waiting_for_connection": "En attente de connexion", "waiting_for_connection": "En attente de connexion",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "גוף תגובה", "body": "גוף תגובה",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "כותרות", "headers": "כותרות",
@@ -445,6 +446,7 @@
"status": "סטָטוּס", "status": "סטָטוּס",
"time": "זְמַן", "time": "זְמַן",
"title": "תְגוּבָה", "title": "תְגוּבָה",
"video": "Video",
"waiting_for_connection": "מחכה לחיבור", "waiting_for_connection": "מחכה לחיבור",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -433,6 +433,7 @@
"view_my_links": "मेरे लिंक देखें" "view_my_links": "मेरे लिंक देखें"
}, },
"response": { "response": {
"audio": "Audio",
"body": "प्रतिक्रिया निकाय", "body": "प्रतिक्रिया निकाय",
"filter_response_body": "फ़िल्टर JSON रिस्पांस बॉडी (JSONPATH सिंटैक्स का उपयोग करता है)", "filter_response_body": "फ़िल्टर JSON रिस्पांस बॉडी (JSONPATH सिंटैक्स का उपयोग करता है)",
"headers": "हेडर", "headers": "हेडर",
@@ -446,6 +447,7 @@
"status": "दर्जा", "status": "दर्जा",
"time": "समय", "time": "समय",
"title": "जवाब", "title": "जवाब",
"video": "Video",
"waiting_for_connection": "जुडने के लिए इंतजार", "waiting_for_connection": "जुडने के लिए इंतजार",
"xml": "एक्सएमएल" "xml": "एक्सएमएल"
}, },

View File

@@ -5,29 +5,29 @@
"choose_file": "Válasszon egy fájlt", "choose_file": "Válasszon egy fájlt",
"clear": "Törlés", "clear": "Törlés",
"clear_all": "Összes törlése", "clear_all": "Összes törlése",
"close": "Close", "close": "Bezárás",
"connect": "Kapcsolódás", "connect": "Kapcsolódás",
"connecting": "Connecting", "connecting": "Kapcsolódás",
"copy": "Másolás", "copy": "Másolás",
"delete": "Törlés", "delete": "Törlés",
"disconnect": "Leválasztás", "disconnect": "Leválasztás",
"dismiss": "Eltüntetés", "dismiss": "Eltüntetés",
"dont_save": "Ne mentse", "dont_save": "Ne mentse",
"download_file": "Fájl letöltése", "download_file": "Fájl letöltése",
"drag_to_reorder": "Drag to reorder", "drag_to_reorder": "Húzza az átrendezéshez",
"duplicate": "Kettőzés", "duplicate": "Kettőzés",
"edit": "Szerkesztés", "edit": "Szerkesztés",
"filter": "Filter", "filter": "Szűrő",
"go_back": "Vissza", "go_back": "Vissza",
"go_forward": "Go forward", "go_forward": "Előre",
"group_by": "Group by", "group_by": "Csoportosítás",
"label": "Címke", "label": "Címke",
"learn_more": "Tudjon meg többet", "learn_more": "Tudjon meg többet",
"less": "Kevesebb", "less": "Kevesebb",
"more": "Több", "more": "Több",
"new": "Új", "new": "Új",
"no": "Nem", "no": "Nem",
"open_workspace": "Open workspace", "open_workspace": "Munkaterület megnyitása",
"paste": "Beillesztés", "paste": "Beillesztés",
"prettify": "Csinosítás", "prettify": "Csinosítás",
"remove": "Eltávolítás", "remove": "Eltávolítás",
@@ -38,7 +38,7 @@
"search": "Keresés", "search": "Keresés",
"send": "Küldés", "send": "Küldés",
"start": "Indítás", "start": "Indítás",
"starting": "Starting", "starting": "Indítás",
"stop": "Leállítás", "stop": "Leállítás",
"to_close": "a bezáráshoz", "to_close": "a bezáráshoz",
"to_navigate": "a navigáláshoz", "to_navigate": "a navigáláshoz",
@@ -118,16 +118,16 @@
}, },
"collection": { "collection": {
"created": "Gyűjtemény létrehozva", "created": "Gyűjtemény létrehozva",
"different_parent": "Cannot reorder collection with different parent", "different_parent": "Nem lehet átrendezni a különböző szülővel rendelkező gyűjteményt",
"edit": "Gyűjtemény szerkesztése", "edit": "Gyűjtemény szerkesztése",
"invalid_name": "Adjon nevet a gyűjteménynek", "invalid_name": "Adjon nevet a gyűjteménynek",
"invalid_root_move": "Collection already in the root", "invalid_root_move": "A gyűjtemény már a gyökérben van",
"moved": "Moved Successfully", "moved": "Sikeresen áthelyezve",
"my_collections": "Saját gyűjtemények", "my_collections": "Saját gyűjtemények",
"name": "Saját új gyűjtemény", "name": "Saját új gyűjtemény",
"name_length_insufficient": "A gyűjtemény nevének legalább 3 karakter hosszúságúnak kell lennie", "name_length_insufficient": "A gyűjtemény nevének legalább 3 karakter hosszúságúnak kell lennie",
"new": "Új gyűjtemény", "new": "Új gyűjtemény",
"order_changed": "Collection Order Updated", "order_changed": "Gyűjtemény sorrendje frissítve",
"renamed": "Gyűjtemény átnevezve", "renamed": "Gyűjtemény átnevezve",
"request_in_use": "A kérés használatban", "request_in_use": "A kérés használatban",
"save_as": "Mentés másként", "save_as": "Mentés másként",
@@ -147,7 +147,7 @@
"remove_team": "Biztosan törölni szeretné ezt a csapatot?", "remove_team": "Biztosan törölni szeretné ezt a csapatot?",
"remove_telemetry": "Biztosan ki szeretné kapcsolni a telemetriát?", "remove_telemetry": "Biztosan ki szeretné kapcsolni a telemetriát?",
"request_change": "Biztosan el szeretné vetni a jelenlegi kérést? Minden mentetlen változtatás el fog veszni.", "request_change": "Biztosan el szeretné vetni a jelenlegi kérést? Minden mentetlen változtatás el fog veszni.",
"save_unsaved_tab": "Do you want to save changes made in this tab?", "save_unsaved_tab": "Szeretné menteni az ezen a lapon elvégzett változtatásokat?",
"sync": "Szeretné visszaállítani a munkaterületét a felhőből? Ez el fogja vetni a helyi folyamatát." "sync": "Szeretné visszaállítani a munkaterületét a felhőből? Ez el fogja vetni a helyi folyamatát."
}, },
"count": { "count": {
@@ -180,8 +180,8 @@
"profile": "Jelentkezzen be a profilja megtekintéséhez", "profile": "Jelentkezzen be a profilja megtekintéséhez",
"protocols": "A protokollok üresek", "protocols": "A protokollok üresek",
"schema": "Kapcsolódjon egy GraphQL-végponthoz a séma megtekintéséhez", "schema": "Kapcsolódjon egy GraphQL-végponthoz a séma megtekintéséhez",
"shortcodes": "Shortcodes are empty", "shortcodes": "A rövid kódok üresek",
"subscription": "Subscriptions are empty", "subscription": "A feliratkozások üresek",
"team_name": "A csapat neve üres", "team_name": "A csapat neve üres",
"teams": "Ön nem tartozik semmilyen csapathoz", "teams": "Ön nem tartozik semmilyen csapathoz",
"tests": "Nincsenek tesztek ehhez a kéréshez" "tests": "Nincsenek tesztek ehhez a kéréshez"
@@ -194,13 +194,13 @@
"deleted": "Környezet törlése", "deleted": "Környezet törlése",
"edit": "Környezet szerkesztése", "edit": "Környezet szerkesztése",
"invalid_name": "Adjon nevet a környezetnek", "invalid_name": "Adjon nevet a környezetnek",
"my_environments": "My Environments", "my_environments": "Saját környezetek",
"nested_overflow": "az egymásba ágyazott környezeti változók 10 szintre vannak korlátozva", "nested_overflow": "az egymásba ágyazott környezeti változók 10 szintre vannak korlátozva",
"new": "Új környezet", "new": "Új környezet",
"no_environment": "Nincs környezet", "no_environment": "Nincs környezet",
"no_environment_description": "Nem lettek környezetek kiválasztva. Válassza ki, hogy mit kell tenni a következő változókkal.", "no_environment_description": "Nem lettek környezetek kiválasztva. Válassza ki, hogy mit kell tenni a következő változókkal.",
"select": "Környezet kiválasztása", "select": "Környezet kiválasztása",
"team_environments": "Team Environments", "team_environments": "Csapatkörnyezetek",
"title": "Környezetek", "title": "Környezetek",
"updated": "Környezet frissítve", "updated": "Környezet frissítve",
"variable_list": "Változólista" "variable_list": "Változólista"
@@ -209,9 +209,9 @@
"browser_support_sse": "Úgy tűnik, hogy ez a böngésző nem támogatja a kiszolgáló által küldött eseményeket.", "browser_support_sse": "Úgy tűnik, hogy ez a böngésző nem támogatja a kiszolgáló által küldött eseményeket.",
"check_console_details": "Nézze meg a konzolnaplót a részletekért.", "check_console_details": "Nézze meg a konzolnaplót a részletekért.",
"curl_invalid_format": "A cURL nincs megfelelően formázva", "curl_invalid_format": "A cURL nincs megfelelően formázva",
"danger_zone": "Danger zone", "danger_zone": "Veszélyes zóna",
"delete_account": "Your account is currently an owner in these teams:", "delete_account": "Az Ön fiókja jelenleg tulajdonos ezekben a csapatokban:",
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.", "delete_account_description": "El kell távolítani magát, át kell adnia a tulajdonjogot vagy törölnie kell ezeket a csapatokat, mielőtt törölhetné a fiókját.",
"empty_req_name": "Üres kérésnév", "empty_req_name": "Üres kérésnév",
"f12_details": "(F12 a részletekért)", "f12_details": "(F12 a részletekért)",
"gql_prettify_invalid_query": "Nem sikerült csinosítani egy érvénytelen lekérdezést, oldja meg a lekérdezés szintaktikai hibáit, és próbálja újra", "gql_prettify_invalid_query": "Nem sikerült csinosítani egy érvénytelen lekérdezést, oldja meg a lekérdezés szintaktikai hibáit, és próbálja újra",
@@ -219,13 +219,13 @@
"incorrect_email": "Hibás e-mail", "incorrect_email": "Hibás e-mail",
"invalid_link": "Érvénytelen hivatkozás", "invalid_link": "Érvénytelen hivatkozás",
"invalid_link_description": "A kattintott hivatkozás érvénytelen vagy lejárt.", "invalid_link_description": "A kattintott hivatkozás érvénytelen vagy lejárt.",
"json_parsing_failed": "Invalid JSON", "json_parsing_failed": "Érvénytelen JSON",
"json_prettify_invalid_body": "Nem sikerült csinosítani egy érvénytelen törzset, oldja meg a JSON szintaktikai hibáit, és próbálja újra", "json_prettify_invalid_body": "Nem sikerült csinosítani egy érvénytelen törzset, oldja meg a JSON szintaktikai hibáit, és próbálja újra",
"network_error": "Úgy tűnik, hogy hálózati hiba van. Próbálja újra.", "network_error": "Úgy tűnik, hogy hálózati hiba van. Próbálja újra.",
"network_fail": "Nem sikerült elküldeni a kérést", "network_fail": "Nem sikerült elküldeni a kérést",
"no_duration": "Nincs időtartam", "no_duration": "Nincs időtartam",
"no_results_found": "No matches found", "no_results_found": "Nincs találat",
"page_not_found": "This page could not be found", "page_not_found": "Ez az oldal nem található",
"script_fail": "Nem sikerült végrehajtani a kérés előtti parancsfájlt", "script_fail": "Nem sikerült végrehajtani a kérés előtti parancsfájlt",
"something_went_wrong": "Valami elromlott", "something_went_wrong": "Valami elromlott",
"test_script_fail": "Nem sikerült végrehajtani a kérés utáni parancsfájlt" "test_script_fail": "Nem sikerült végrehajtani a kérés utáni parancsfájlt"
@@ -238,9 +238,9 @@
"title": "Exportálás" "title": "Exportálás"
}, },
"filter": { "filter": {
"all": "All", "all": "Összes",
"none": "None", "none": "Nincs",
"starred": "Starred" "starred": "Csillagozott"
}, },
"folder": { "folder": {
"created": "Mappa létrehozva", "created": "Mappa létrehozva",
@@ -256,7 +256,7 @@
"subscriptions": "Feliratkozások" "subscriptions": "Feliratkozások"
}, },
"group": { "group": {
"time": "Time", "time": "Idő",
"url": "URL" "url": "URL"
}, },
"header": { "header": {
@@ -316,32 +316,32 @@
"zen_mode": "Zen mód" "zen_mode": "Zen mód"
}, },
"modal": { "modal": {
"close_unsaved_tab": "You have unsaved changes", "close_unsaved_tab": "Elmentetlen változtatásai vannak",
"collections": "Gyűjtemények", "collections": "Gyűjtemények",
"confirm": "Megerősítés", "confirm": "Megerősítés",
"edit_request": "Kérés szerkesztése", "edit_request": "Kérés szerkesztése",
"import_export": "Importálás és exportálás" "import_export": "Importálás és exportálás"
}, },
"mqtt": { "mqtt": {
"already_subscribed": "You are already subscribed to this topic.", "already_subscribed": "Ön már feliratkozott erre a témára.",
"clean_session": "Clean Session", "clean_session": "Munkamenet törlése",
"clear_input": "Clear input", "clear_input": "Bevitel törlése",
"clear_input_on_send": "Clear input on send", "clear_input_on_send": "Bevitel törlése küldéskor",
"client_id": "Client ID", "client_id": "Ügyfél-azonosító",
"color": "Pick a color", "color": "Válasszon színt",
"communication": "Kommunikáció", "communication": "Kommunikáció",
"connection_config": "Connection Config", "connection_config": "Kapcsolat beállításai",
"connection_not_authorized": "This MQTT connection does not use any authentication.", "connection_not_authorized": "Ez az MQTT-kapcsolat nem használ semmilyen hitelesítést.",
"invalid_topic": "Please provide a topic for the subscription", "invalid_topic": "Adjon témát a feliratkozáshoz",
"keep_alive": "Keep Alive", "keep_alive": "Életben tartás",
"log": "Napló", "log": "Napló",
"lw_message": "Last-Will Message", "lw_message": "Utolsó kívánság üzenet",
"lw_qos": "Last-Will QoS", "lw_qos": "Utolsó kívánság QoS",
"lw_retain": "Last-Will Retain", "lw_retain": "Utolsó kívánság megtartás",
"lw_topic": "Last-Will Topic", "lw_topic": "Utolsó kívánság téma",
"message": "Üzenet", "message": "Üzenet",
"new": "New Subscription", "new": "Új feliratkozás",
"not_connected": "Please start a MQTT connection first.", "not_connected": "Először indítson egy MQTT-kapcsolatot.",
"publish": "Közzététel", "publish": "Közzététel",
"qos": "QoS", "qos": "QoS",
"ssl": "SSL", "ssl": "SSL",
@@ -368,7 +368,7 @@
}, },
"profile": { "profile": {
"app_settings": "Alkalmazás beállításai", "app_settings": "Alkalmazás beállításai",
"default_hopp_displayname": "Unnamed User", "default_hopp_displayname": "Névtelen felhasználó",
"editor": "Szerkesztő", "editor": "Szerkesztő",
"editor_description": "A szerkesztők hozzáadhatnak, szerkeszthetnek és törölhetnek kéréseket.", "editor_description": "A szerkesztők hozzáadhatnak, szerkeszthetnek és törölhetnek kéréseket.",
"email_verification_mail": "Egy ellenőrző e-mail el lett küldve az e-mail-címére. Kattintson a hivatkozásra az e-mail-címe ellenőrzéséhez.", "email_verification_mail": "Egy ellenőrző e-mail el lett küldve az e-mail-címére. Kattintson a hivatkozásra az e-mail-címe ellenőrzéséhez.",
@@ -391,26 +391,26 @@
"choose_language": "Nyelv kiválasztása", "choose_language": "Nyelv kiválasztása",
"content_type": "Tartalom típusa", "content_type": "Tartalom típusa",
"content_type_titles": { "content_type_titles": {
"others": "Others", "others": "Egyebek",
"structured": "Structured", "structured": "Szerkesztett",
"text": "Text" "text": "Szöveg"
}, },
"copy_link": "Hivatkozás másolása", "copy_link": "Hivatkozás másolása",
"different_collection": "Cannot reorder requests from different collections", "different_collection": "Nem lehet átrendezni a különböző gyűjteményekből érkező kéréseket",
"duplicated": "Request duplicated", "duplicated": "Kérés megkettőzve",
"duration": "Időtartam", "duration": "Időtartam",
"enter_curl": "cURL megadása", "enter_curl": "cURL-parancs megadása",
"generate_code": "Kód előállítása", "generate_code": "Kód előállítása",
"generated_code": "Előállított kód", "generated_code": "Előállított kód",
"header_list": "Fejléclista", "header_list": "Fejléclista",
"invalid_name": "Adjon nevet a kérésnek", "invalid_name": "Adjon nevet a kérésnek",
"method": "Módszer", "method": "Módszer",
"moved": "Request moved", "moved": "Kérés áthelyezve",
"name": "Kérés neve", "name": "Kérés neve",
"new": "Új kérés", "new": "Új kérés",
"order_changed": "Request Order Updated", "order_changed": "Kérés sorrendje frissítve",
"override": "Felülbírálás", "override": "Felülbírálás",
"override_help": "A <kbd>Content-Type</kbd> beállítása a fejlécekben", "override_help": "<kbd>Content-Type</kbd> beállítása a fejlécekben",
"overriden": "Felülbírálva", "overriden": "Felülbírálva",
"parameter_list": "Lekérdezési paraméterek", "parameter_list": "Lekérdezési paraméterek",
"parameters": "Paraméterek", "parameters": "Paraméterek",
@@ -429,11 +429,12 @@
"type": "Kérés típusa", "type": "Kérés típusa",
"url": "URL", "url": "URL",
"variables": "Változók", "variables": "Változók",
"view_my_links": "View my links" "view_my_links": "Saját hivatkozások megtekintése"
}, },
"response": { "response": {
"audio": "Hang",
"body": "Válasz törzse", "body": "Válasz törzse",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "JSON-válasz törzsének szűrése (JSONPath szintaxist használ)",
"headers": "Fejlécek", "headers": "Fejlécek",
"html": "HTML", "html": "HTML",
"image": "Kép", "image": "Kép",
@@ -445,13 +446,14 @@
"status": "Állapot", "status": "Állapot",
"time": "Idő", "time": "Idő",
"title": "Válasz", "title": "Válasz",
"video": "Videó",
"waiting_for_connection": "várakozás kapcsolódásra", "waiting_for_connection": "várakozás kapcsolódásra",
"xml": "XML" "xml": "XML"
}, },
"settings": { "settings": {
"accent_color": "Kiemelőszín", "accent_color": "Kiemelőszín",
"account": "Fiók", "account": "Fiók",
"account_deleted": "Your account has been deleted", "account_deleted": "A fiókja törölve lett",
"account_description": "A fiókbeállítások személyre szabása.", "account_description": "A fiókbeállítások személyre szabása.",
"account_email_description": "Az Ön elsődleges e-mail-címe.", "account_email_description": "Az Ön elsődleges e-mail-címe.",
"account_name_description": "Ez a megjelenített neve.", "account_name_description": "Ez a megjelenített neve.",
@@ -460,8 +462,8 @@
"change_font_size": "Betűméret megváltoztatása", "change_font_size": "Betűméret megváltoztatása",
"choose_language": "Nyelv kiválasztása", "choose_language": "Nyelv kiválasztása",
"dark_mode": "Sötét", "dark_mode": "Sötét",
"delete_account": "Delete account", "delete_account": "Fiók törlése",
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.", "delete_account_description": "Ha törli a fiókját, akkor az összes adata véglegesen törlésre kerül. Ezt a műveletet nem lehet visszavonni.",
"expand_navigation": "Navigáció kinyitása", "expand_navigation": "Navigáció kinyitása",
"experiments": "Kísérletek", "experiments": "Kísérletek",
"experiments_notice": "Ez olyan kísérletek gyűjteménye, amelyeken dolgozunk, és amelyek hasznosak, szórakoztatóak lehetnek, mindkettő, vagy egyik sem. Ezek nem véglegesek és nem stabilak, ezért ha valami túl furcsa dolog történik, ne essen pánikba. Egyszerűen kapcsolja ki a hibás dolgot. Viccet félretéve, ", "experiments_notice": "Ez olyan kísérletek gyűjteménye, amelyeken dolgozunk, és amelyek hasznosak, szórakoztatóak lehetnek, mindkettő, vagy egyik sem. Ezek nem véglegesek és nem stabilak, ezért ha valami túl furcsa dolog történik, ne essen pánikba. Egyszerűen kapcsolja ki a hibás dolgot. Viccet félretéve, ",
@@ -488,8 +490,8 @@
"proxy_use_toggle": "A proxy középprogram használata a kérések küldéséhez", "proxy_use_toggle": "A proxy középprogram használata a kérések küldéséhez",
"read_the": "Olvassa el:", "read_the": "Olvassa el:",
"reset_default": "Visszaállítás az alapértelmezettre", "reset_default": "Visszaállítás az alapértelmezettre",
"short_codes": "Short codes", "short_codes": "Rövid kódok",
"short_codes_description": "Short codes which were created by you.", "short_codes_description": "Az Ön által létrehozott rövid kódok.",
"sidebar_on_left": "Oldalsáv a bal oldalon", "sidebar_on_left": "Oldalsáv a bal oldalon",
"sync": "Szinkronizálás", "sync": "Szinkronizálás",
"sync_collections": "Gyűjtemények", "sync_collections": "Gyűjtemények",
@@ -503,16 +505,16 @@
"theme_description": "Az alkalmazás témájának személyre szabása.", "theme_description": "Az alkalmazás témájának személyre szabása.",
"use_experimental_url_bar": "Kísérleti URL-sáv használata a környezet kiemelésével", "use_experimental_url_bar": "Kísérleti URL-sáv használata a környezet kiemelésével",
"user": "Felhasználó", "user": "Felhasználó",
"verified_email": "Verified email", "verified_email": "Ellenőrzött e-mail-cím",
"verify_email": "E-mail-cím ellenőrzése" "verify_email": "E-mail-cím ellenőrzése"
}, },
"shortcodes": { "shortcodes": {
"actions": "Actions", "actions": "Műveletek",
"created_on": "Created on", "created_on": "Létrehozva",
"deleted": "Shortcode deleted", "deleted": "Rövid kód törölve",
"method": "Method", "method": "Módszer",
"not_found": "Shortcode not found", "not_found": "A rövid kód nem található",
"short_code": "Short code", "short_code": "Rövid kód",
"url": "URL" "url": "URL"
}, },
"shortcut": { "shortcut": {
@@ -554,9 +556,9 @@
"title": "Kérés" "title": "Kérés"
}, },
"response": { "response": {
"copy": "Copy response to clipboard", "copy": "Válasz másolása a vágólapra",
"download": "Download response as file", "download": "Válasz letöltés fájlként",
"title": "Response" "title": "Válasz"
}, },
"theme": { "theme": {
"black": "Téma átváltása fekete módra", "black": "Téma átváltása fekete módra",
@@ -574,8 +576,8 @@
}, },
"socketio": { "socketio": {
"communication": "Kommunikáció", "communication": "Kommunikáció",
"connection_not_authorized": "This SocketIO connection does not use any authentication.", "connection_not_authorized": "Ez a SocketIO-kapcsolat nem használ semmilyen hitelesítést.",
"event_name": "Esemény neve", "event_name": "Esemény vagy téma neve",
"events": "Események", "events": "Események",
"log": "Napló", "log": "Napló",
"url": "URL" "url": "URL"
@@ -592,9 +594,9 @@
"connected": "Kapcsolódva", "connected": "Kapcsolódva",
"connected_to": "Kapcsolódva ehhez: {name}", "connected_to": "Kapcsolódva ehhez: {name}",
"connecting_to": "Kapcsolódás ehhez: {name}…", "connecting_to": "Kapcsolódás ehhez: {name}…",
"connection_error": "Failed to connect", "connection_error": "Nem sikerült kapcsolódni",
"connection_failed": "Connection failed", "connection_failed": "A kapcsolódás sikertelen",
"connection_lost": "Connection lost", "connection_lost": "A kapcsolat elveszett",
"copied_to_clipboard": "Vágólapra másolva", "copied_to_clipboard": "Vágólapra másolva",
"deleted": "Törölve", "deleted": "Törölve",
"deprecated": "ELAVULT", "deprecated": "ELAVULT",
@@ -609,17 +611,17 @@
"history_deleted": "Előzmények törölve", "history_deleted": "Előzmények törölve",
"linewrap": "Sorok tördelése", "linewrap": "Sorok tördelése",
"loading": "Betöltés…", "loading": "Betöltés…",
"message_received": "Message: {message} arrived on topic: {topic}", "message_received": "Üzenet: {message} érkezett ehhez a témához: {topic}",
"mqtt_subscription_failed": "Something went wrong while subscribing to topic: {topic}", "mqtt_subscription_failed": "Valami elromlott a következő témára való feliratkozás során: {topic}",
"none": "Nincs", "none": "Nincs",
"nothing_found": "Semmi sem található ehhez:", "nothing_found": "Semmi sem található ehhez:",
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}", "published_error": "Valami elromlott a következő üzenet közzététele során: {topic}, ehhez a témához: {message}",
"published_message": "Published message: {message} to topic: {topic}", "published_message": "Közzétett üzenet: {message}, ehhez a témához: {topic}",
"reconnection_error": "Failed to reconnect", "reconnection_error": "Nem sikerült újrakapcsolódni",
"subscribed_failed": "Failed to subscribe to topic: {topic}", "subscribed_failed": "Nem sikerült feliratkozni erre a témára: {topic}",
"subscribed_success": "Successfully subscribed to topic: {topic}", "subscribed_success": "Sikeresen feliratkozott erre a témára: {topic}",
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}", "unsubscribed_failed": "Nem sikerült leiratkozni erről a témáról: {topic}",
"unsubscribed_success": "Successfully unsubscribed from topic: {topic}", "unsubscribed_success": "Sikeresen leiratkozott erről a témáról: {topic}",
"waiting_send_request": "Várakozás a kérés elküldésére" "waiting_send_request": "Várakozás a kérés elküldésére"
}, },
"support": { "support": {
@@ -639,7 +641,7 @@
"body": "Törzs", "body": "Törzs",
"collections": "Gyűjtemények", "collections": "Gyűjtemények",
"documentation": "Dokumentáció", "documentation": "Dokumentáció",
"environments": "Environments", "environments": "Környezetek",
"headers": "Fejlécek", "headers": "Fejlécek",
"history": "Előzmények", "history": "Előzmények",
"mqtt": "MQTT", "mqtt": "MQTT",
@@ -664,7 +666,7 @@
"email_do_not_match": "Az e-mail-cím nem egyezik a fiókja részleteivel. Vegye fel a kapcsolatot a csapat tulajdonosával.", "email_do_not_match": "Az e-mail-cím nem egyezik a fiókja részleteivel. Vegye fel a kapcsolatot a csapat tulajdonosával.",
"exit": "Kilépés a csapatból", "exit": "Kilépés a csapatból",
"exit_disabled": "Csak a tulajdonos nem léphet ki a csapatból", "exit_disabled": "Csak a tulajdonos nem léphet ki a csapatból",
"invalid_coll_id": "Invalid collection ID", "invalid_coll_id": "Érvénytelen gyűjteményazonosító",
"invalid_email_format": "Az e-mail formátuma érvénytelen", "invalid_email_format": "Az e-mail formátuma érvénytelen",
"invalid_id": "Érvénytelen csapatazonosító. Vegye fel a kapcsolatot a csapat tulajdonosával.", "invalid_id": "Érvénytelen csapatazonosító. Vegye fel a kapcsolatot a csapat tulajdonosával.",
"invalid_invite_link": "Érvénytelen meghívási hivatkozás", "invalid_invite_link": "Érvénytelen meghívási hivatkozás",
@@ -688,7 +690,7 @@
"member_removed": "Felhasználó eltávolítva", "member_removed": "Felhasználó eltávolítva",
"member_role_updated": "Felhasználói szerepek frissítve", "member_role_updated": "Felhasználói szerepek frissítve",
"members": "Tagok", "members": "Tagok",
"more_members": "+{count} more", "more_members": "+{count} további",
"name_length_insufficient": "A csapat nevének legalább 6 karakter hosszúságúnak kell lennie", "name_length_insufficient": "A csapat nevének legalább 6 karakter hosszúságúnak kell lennie",
"name_updated": "Csapatnév frissítve", "name_updated": "Csapatnév frissítve",
"new": "Új csapat", "new": "Új csapat",
@@ -696,13 +698,13 @@
"new_name": "Saját új csapat", "new_name": "Saját új csapat",
"no_access": "Nincs szerkesztési jogosultsága ezekhez a gyűjteményekhez", "no_access": "Nincs szerkesztési jogosultsága ezekhez a gyűjteményekhez",
"no_invite_found": "A meghívás nem található. Vegye fel a kapcsolatot a csapat tulajdonosával.", "no_invite_found": "A meghívás nem található. Vegye fel a kapcsolatot a csapat tulajdonosával.",
"no_request_found": "Request not found.", "no_request_found": "A kérés nem található.",
"not_found": "A csapat nem található. Vegye fel a kapcsolatot a csapat tulajdonosával.", "not_found": "A csapat nem található. Vegye fel a kapcsolatot a csapat tulajdonosával.",
"not_valid_viewer": "Ön nem érvényes megtekintő. Vegye fel a kapcsolatot a csapat tulajdonosával.", "not_valid_viewer": "Ön nem érvényes megtekintő. Vegye fel a kapcsolatot a csapat tulajdonosával.",
"parent_coll_move": "Cannot move collection to a child collection", "parent_coll_move": "Nem lehet áthelyezni a gyűjteményt egy gyermekgyűjteménybe",
"pending_invites": "Függőben lévő meghívások", "pending_invites": "Függőben lévő meghívások",
"permissions": "Jogosultságok", "permissions": "Jogosultságok",
"same_target_destination": "Same target and destination", "same_target_destination": "Ugyanaz a cél és célhely",
"saved": "Csapat elmentve", "saved": "Csapat elmentve",
"select_a_team": "Csapat kiválasztása", "select_a_team": "Csapat kiválasztása",
"title": "Csapatok", "title": "Csapatok",
@@ -710,9 +712,9 @@
"we_sent_invite_link_description": "Kérje meg az összes meghívottat, hogy nézzék meg a beérkező leveleiket. Kattintsanak a hivatkozásra a csapathoz való csatlakozáshoz." "we_sent_invite_link_description": "Kérje meg az összes meghívottat, hogy nézzék meg a beérkező leveleiket. Kattintsanak a hivatkozásra a csapathoz való csatlakozáshoz."
}, },
"team_environment": { "team_environment": {
"deleted": "Environment Deleted", "deleted": "Környezet törölve",
"duplicate": "Environment Duplicated", "duplicate": "Környezet megkettőzve",
"not_found": "Environment not found." "not_found": "A környezet nem található."
}, },
"test": { "test": {
"failed": "teszt sikertelen", "failed": "teszt sikertelen",
@@ -732,9 +734,9 @@
"url": "URL" "url": "URL"
}, },
"workspace": { "workspace": {
"change": "Change workspace", "change": "Munkaterület váltása",
"personal": "My Workspace", "personal": "Saját munkaterület",
"team": "Team Workspace", "team": "Csapat-munkaterület",
"title": "Workspaces" "title": "Munkaterületek"
} }
} }

View File

@@ -432,6 +432,7 @@
"view_my_links": "Lihat tautan saya" "view_my_links": "Lihat tautan saya"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Response Body", "body": "Response Body",
"filter_response_body": "Filter body respons JSON (menggunakan sintaks JSONPath)", "filter_response_body": "Filter body respons JSON (menggunakan sintaks JSONPath)",
"headers": "Headers", "headers": "Headers",
@@ -445,6 +446,7 @@
"status": "Status", "status": "Status",
"time": "Waktu", "time": "Waktu",
"title": "Response", "title": "Response",
"video": "Video",
"waiting_for_connection": "Menunggu koneksi", "waiting_for_connection": "Menunggu koneksi",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Corpo della risposta", "body": "Corpo della risposta",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Intestazioni", "headers": "Intestazioni",
@@ -445,6 +446,7 @@
"status": "Stato", "status": "Stato",
"time": "Tempo impiegato", "time": "Tempo impiegato",
"title": "Risposta", "title": "Risposta",
"video": "Video",
"waiting_for_connection": "In attesa di connessione", "waiting_for_connection": "In attesa di connessione",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "自分のリンクを見る" "view_my_links": "自分のリンクを見る"
}, },
"response": { "response": {
"audio": "Audio",
"body": "レスポンスボディ", "body": "レスポンスボディ",
"filter_response_body": "JSONレスポンスボディをフィルタ (JSONPathシンタックスを使用)", "filter_response_body": "JSONレスポンスボディをフィルタ (JSONPathシンタックスを使用)",
"headers": "ヘッダー", "headers": "ヘッダー",
@@ -445,6 +446,7 @@
"status": "ステータス", "status": "ステータス",
"time": "時間", "time": "時間",
"title": "レスポンス", "title": "レスポンス",
"video": "Video",
"waiting_for_connection": "接続を待っています", "waiting_for_connection": "接続を待っています",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "내 링크 보기" "view_my_links": "내 링크 보기"
}, },
"response": { "response": {
"audio": "Audio",
"body": "응답 본문", "body": "응답 본문",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "헤더", "headers": "헤더",
@@ -445,6 +446,7 @@
"status": "상태", "status": "상태",
"time": "시간", "time": "시간",
"title": "제목", "title": "제목",
"video": "Video",
"waiting_for_connection": "연결 대기 중", "waiting_for_connection": "연결 대기 중",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Reactie inhoud", "body": "Reactie inhoud",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Headers", "headers": "Headers",
@@ -445,6 +446,7 @@
"status": "Status", "status": "Status",
"time": "Tijd", "time": "Tijd",
"title": "Antwoord", "title": "Antwoord",
"video": "Video",
"waiting_for_connection": "wachten op verbinding", "waiting_for_connection": "wachten op verbinding",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Svarkropp", "body": "Svarkropp",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Overskrifter", "headers": "Overskrifter",
@@ -445,6 +446,7 @@
"status": "Status", "status": "Status",
"time": "Tid", "time": "Tid",
"title": "Respons", "title": "Respons",
"video": "Video",
"waiting_for_connection": "venter på tilkobling", "waiting_for_connection": "venter på tilkobling",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Ciało odpowiedzi", "body": "Ciało odpowiedzi",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Nagłówki", "headers": "Nagłówki",
@@ -445,6 +446,7 @@
"status": "Status", "status": "Status",
"time": "Czas", "time": "Czas",
"title": "Odpowiedź", "title": "Odpowiedź",
"video": "Video",
"waiting_for_connection": "oczekiwanie na połączenie", "waiting_for_connection": "oczekiwanie na połączenie",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Corpo de Resposta", "body": "Corpo de Resposta",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Cabeçalhos", "headers": "Cabeçalhos",
@@ -445,6 +446,7 @@
"status": "Status", "status": "Status",
"time": "Tempo", "time": "Tempo",
"title": "Resposta", "title": "Resposta",
"video": "Video",
"waiting_for_connection": "aguardando conexão", "waiting_for_connection": "aguardando conexão",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Corpo de Resposta", "body": "Corpo de Resposta",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Cabeçalhos", "headers": "Cabeçalhos",
@@ -445,6 +446,7 @@
"status": "Status", "status": "Status",
"time": "Tempo", "time": "Tempo",
"title": "Resposta", "title": "Resposta",
"video": "Video",
"waiting_for_connection": "aguardando conexão", "waiting_for_connection": "aguardando conexão",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "Vizualizare link-uri" "view_my_links": "Vizualizare link-uri"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Corpul de răspuns", "body": "Corpul de răspuns",
"filter_response_body": "Filtrează corpul răspunsului JSON (folosește sintaxa JSONPath)", "filter_response_body": "Filtrează corpul răspunsului JSON (folosește sintaxa JSONPath)",
"headers": "Anteturi", "headers": "Anteturi",
@@ -445,6 +446,7 @@
"status": "Stare", "status": "Stare",
"time": "Timp", "time": "Timp",
"title": "Raspuns", "title": "Raspuns",
"video": "Video",
"waiting_for_connection": "Așteptând conexiunea", "waiting_for_connection": "Așteptând conexiunea",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Тело ответа", "body": "Тело ответа",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Заголовки", "headers": "Заголовки",
@@ -445,6 +446,7 @@
"status": "Статус", "status": "Статус",
"time": "Время", "time": "Время",
"title": "Ответ", "title": "Ответ",
"video": "Video",
"waiting_for_connection": "Ожидание соединения", "waiting_for_connection": "Ожидание соединения",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Тело за одговор", "body": "Тело за одговор",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Заглавља", "headers": "Заглавља",
@@ -445,6 +446,7 @@
"status": "Статус", "status": "Статус",
"time": "време", "time": "време",
"title": "Одговор", "title": "Одговор",
"video": "Video",
"waiting_for_connection": "чека везу", "waiting_for_connection": "чека везу",
"xml": "КСМЛ" "xml": "КСМЛ"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Svarskommitté", "body": "Svarskommitté",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Rubriker", "headers": "Rubriker",
@@ -445,6 +446,7 @@
"status": "Status", "status": "Status",
"time": "Tid", "time": "Tid",
"title": "Svar", "title": "Svar",
"video": "Video",
"waiting_for_connection": "väntar på anslutning", "waiting_for_connection": "väntar på anslutning",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -122,7 +122,7 @@
"edit": "Koleksiyonu düzenle", "edit": "Koleksiyonu düzenle",
"invalid_name": "Lütfen koleksiyon için geçerli bir ad girin", "invalid_name": "Lütfen koleksiyon için geçerli bir ad girin",
"invalid_root_move": "Collection already in the root", "invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully", "moved": "Başarıyla taşındı",
"my_collections": "Koleksiyonlarım", "my_collections": "Koleksiyonlarım",
"name": "Yeni Koleksiyonum", "name": "Yeni Koleksiyonum",
"name_length_insufficient": "Koleksiyon adı en az 3 karakter uzunluğunda olmalıdır", "name_length_insufficient": "Koleksiyon adı en az 3 karakter uzunluğunda olmalıdır",
@@ -147,7 +147,7 @@
"remove_team": "Bu takımı silmek istediğinizden emin misiniz?", "remove_team": "Bu takımı silmek istediğinizden emin misiniz?",
"remove_telemetry": "Telemetriden çıkmak istediğinizden emin misiniz?", "remove_telemetry": "Telemetriden çıkmak istediğinizden emin misiniz?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.", "request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"save_unsaved_tab": "Do you want to save changes made in this tab?", "save_unsaved_tab": "Bu sekmede yapılan değişiklikleri kaydetmek istiyor musunuz?",
"sync": "Bu çalışma alanını senkronize etmek istediğinizden emin misiniz?" "sync": "Bu çalışma alanını senkronize etmek istediğinizden emin misiniz?"
}, },
"count": { "count": {
@@ -368,9 +368,9 @@
}, },
"profile": { "profile": {
"app_settings": "Uygulama ayarları", "app_settings": "Uygulama ayarları",
"default_hopp_displayname": "Unnamed User", "default_hopp_displayname": "Adsız Kullanıcı",
"editor": "Düzenleyici", "editor": "Editör",
"editor_description": "Editors can add, edit, and delete requests.", "editor_description": "Editörler istekleri ekleyebilir, düzenleyebilir ve silebilir.",
"email_verification_mail": "Doğrulama bağlantısı e-postanıza gönderildi. E-postanızı doğrulamak için gelen bağlantıya tıklayınız.", "email_verification_mail": "Doğrulama bağlantısı e-postanıza gönderildi. E-postanızı doğrulamak için gelen bağlantıya tıklayınız.",
"no_permission": "Bu eylemi gerçekleştirmek için gerekli yetkiniz yok.", "no_permission": "Bu eylemi gerçekleştirmek için gerekli yetkiniz yok.",
"owner": "Kurucu", "owner": "Kurucu",
@@ -432,6 +432,7 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Yanıt gövdesi", "body": "Yanıt gövdesi",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Başlıklar", "headers": "Başlıklar",
@@ -445,6 +446,7 @@
"status": "Durum", "status": "Durum",
"time": "Zaman", "time": "Zaman",
"title": "Cevap", "title": "Cevap",
"video": "Video",
"waiting_for_connection": "Bağlantı için bekleniyor", "waiting_for_connection": "Bağlantı için bekleniyor",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "檢視我的連結" "view_my_links": "檢視我的連結"
}, },
"response": { "response": {
"audio": "Audio",
"body": "回應本體", "body": "回應本體",
"filter_response_body": "篩選 JSON 回應本體 (使用 JSONPath 語法)", "filter_response_body": "篩選 JSON 回應本體 (使用 JSONPath 語法)",
"headers": "回應標頭", "headers": "回應標頭",
@@ -445,6 +446,7 @@
"status": "狀態", "status": "狀態",
"time": "時間", "time": "時間",
"title": "回應", "title": "回應",
"video": "Video",
"waiting_for_connection": "等待連線", "waiting_for_connection": "等待連線",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -432,6 +432,7 @@
"view_my_links": "Переглянути мої посилання" "view_my_links": "Переглянути мої посилання"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Орган реагування", "body": "Орган реагування",
"filter_response_body": "Фільтр тіла відповідей JSON (використовує синтаксис JSONPath)", "filter_response_body": "Фільтр тіла відповідей JSON (використовує синтаксис JSONPath)",
"headers": "Заголовки", "headers": "Заголовки",
@@ -445,6 +446,7 @@
"status": "Статус", "status": "Статус",
"time": "Час", "time": "Час",
"title": "Відповідь", "title": "Відповідь",
"video": "Video",
"waiting_for_connection": "очікування підключення", "waiting_for_connection": "очікування підключення",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -4,7 +4,7 @@
"cancel": "Hủy bỏ", "cancel": "Hủy bỏ",
"choose_file": "Chọn một tệp", "choose_file": "Chọn một tệp",
"clear": "Thông thoáng", "clear": "Thông thoáng",
"clear_all": "Quet sạch tât cả", "clear_all": "Quet sạch tt cả",
"close": "Close", "close": "Close",
"connect": "Liên kết", "connect": "Liên kết",
"connecting": "Connecting", "connecting": "Connecting",
@@ -432,6 +432,7 @@
"view_my_links": "View my links" "view_my_links": "View my links"
}, },
"response": { "response": {
"audio": "Audio",
"body": "Cơ quan phản hồi", "body": "Cơ quan phản hồi",
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)", "filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
"headers": "Tiêu đề", "headers": "Tiêu đề",
@@ -445,6 +446,7 @@
"status": "Tình trạng", "status": "Tình trạng",
"time": "Thời gian", "time": "Thời gian",
"title": "Phản ứng", "title": "Phản ứng",
"video": "Video",
"waiting_for_connection": "Đang đợi kết nối", "waiting_for_connection": "Đang đợi kết nối",
"xml": "XML" "xml": "XML"
}, },

View File

@@ -1,9 +1,11 @@
{ {
"name": "@hoppscotch/common", "name": "@hoppscotch/common",
"private": true, "private": true,
"version": "2023.4.3", "version": "2023.4.7",
"scripts": { "scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*", "dev": "pnpm exec npm-run-all -p -l dev:*",
"test": "vitest --run",
"test:watch": "vitest",
"dev:vite": "vite", "dev:vite": "vite",
"dev:gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml --watch dotenv_config_path=\"../../.env\"", "dev:gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml --watch dotenv_config_path=\"../../.env\"",
"lint": "eslint src --ext .ts,.js,.vue --ignore-path .gitignore .", "lint": "eslint src --ext .ts,.js,.vue --ignore-path .gitignore .",
@@ -13,6 +15,7 @@
"preview": "vite preview", "preview": "vite preview",
"gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml dotenv_config_path=\"../../.env\"", "gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml dotenv_config_path=\"../../.env\"",
"postinstall": "pnpm run gql-codegen", "postinstall": "pnpm run gql-codegen",
"do-test": "pnpm run test",
"do-lint": "pnpm run prod-lint", "do-lint": "pnpm run prod-lint",
"do-typecheck": "pnpm run lint", "do-typecheck": "pnpm run lint",
"do-lintfix": "pnpm run lintfix" "do-lintfix": "pnpm run lintfix"
@@ -43,11 +46,12 @@
"@urql/exchange-auth": "^0.1.7", "@urql/exchange-auth": "^0.1.7",
"@urql/exchange-graphcache": "^4.4.3", "@urql/exchange-graphcache": "^4.4.3",
"@vitejs/plugin-legacy": "^2.3.0", "@vitejs/plugin-legacy": "^2.3.0",
"@vueuse/core": "^8.7.5", "@vueuse/core": "^8.9.4",
"@vueuse/head": "^0.7.9", "@vueuse/head": "^0.7.9",
"acorn-walk": "^8.2.0", "acorn-walk": "^8.2.0",
"axios": "^0.21.4", "axios": "^0.21.4",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"dioc": "workspace:^",
"esprima": "^4.0.1", "esprima": "^4.0.1",
"events": "^3.3.0", "events": "^3.3.0",
"fp-ts": "^2.12.1", "fp-ts": "^2.12.1",
@@ -63,6 +67,7 @@
"jsonpath-plus": "^7.0.0", "jsonpath-plus": "^7.0.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"lossless-json": "^2.0.8", "lossless-json": "^2.0.8",
"minisearch": "^6.1.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
"paho-mqtt": "^1.1.0", "paho-mqtt": "^1.1.0",
"path": "^0.12.7", "path": "^0.12.7",
@@ -92,6 +97,7 @@
"vuedraggable-es": "^4.1.1", "vuedraggable-es": "^4.1.1",
"wonka": "^4.0.15", "wonka": "^4.0.15",
"workbox-window": "^6.5.4", "workbox-window": "^6.5.4",
"xml-formatter": "^3.4.1",
"yargs-parser": "^21.1.1" "yargs-parser": "^21.1.1"
}, },
"devDependencies": { "devDependencies": {
@@ -107,6 +113,7 @@
"@graphql-typed-document-node/core": "^3.1.1", "@graphql-typed-document-node/core": "^3.1.1",
"@iconify-json/lucide": "^1.1.40", "@iconify-json/lucide": "^1.1.40",
"@intlify/vite-plugin-vue-i18n": "^7.0.0", "@intlify/vite-plugin-vue-i18n": "^7.0.0",
"@relmify/jest-fp-ts": "^2.1.1",
"@rushstack/eslint-patch": "^1.1.4", "@rushstack/eslint-patch": "^1.1.4",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
"@types/lodash-es": "^4.17.6", "@types/lodash-es": "^4.17.6",
@@ -145,6 +152,7 @@
"vite-plugin-pwa": "^0.13.1", "vite-plugin-pwa": "^0.13.1",
"vite-plugin-vue-layouts": "^0.7.0", "vite-plugin-vue-layouts": "^0.7.0",
"vite-plugin-windicss": "^1.8.8", "vite-plugin-windicss": "^1.8.8",
"vitest": "^0.32.2",
"vue-tsc": "^0.38.2", "vue-tsc": "^0.38.2",
"windicss": "^3.5.6" "windicss": "^3.5.6"
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -11,20 +11,20 @@ declare module '@vue/runtime-core' {
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default'] AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default'] AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
AppFooter: typeof import('./components/app/Footer.vue')['default'] AppFooter: typeof import('./components/app/Footer.vue')['default']
AppFuse: typeof import('./components/app/Fuse.vue')['default']
AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default'] AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default']
AppHeader: typeof import('./components/app/Header.vue')['default'] AppHeader: typeof import('./components/app/Header.vue')['default']
AppInterceptor: typeof import('./components/app/Interceptor.vue')['default'] AppInterceptor: typeof import('./components/app/Interceptor.vue')['default']
AppLogo: typeof import('./components/app/Logo.vue')['default'] AppLogo: typeof import('./components/app/Logo.vue')['default']
AppOptions: typeof import('./components/app/Options.vue')['default'] AppOptions: typeof import('./components/app/Options.vue')['default']
AppPaneLayout: typeof import('./components/app/PaneLayout.vue')['default'] AppPaneLayout: typeof import('./components/app/PaneLayout.vue')['default']
AppPowerSearch: typeof import('./components/app/PowerSearch.vue')['default']
AppPowerSearchEntry: typeof import('./components/app/PowerSearchEntry.vue')['default']
AppShare: typeof import('./components/app/Share.vue')['default'] AppShare: typeof import('./components/app/Share.vue')['default']
AppShortcuts: typeof import('./components/app/Shortcuts.vue')['default'] AppShortcuts: typeof import('./components/app/Shortcuts.vue')['default']
AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default'] AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default'] AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
AppSidenav: typeof import('./components/app/Sidenav.vue')['default'] AppSidenav: typeof import('./components/app/Sidenav.vue')['default']
AppSpotlight: typeof import('./components/app/spotlight/index.vue')['default']
AppSpotlightEntry: typeof import('./components/app/spotlight/Entry.vue')['default']
AppSpotlightEntryHistory: typeof import('./components/app/spotlight/entry/History.vue')['default']
AppSupport: typeof import('./components/app/Support.vue')['default'] AppSupport: typeof import('./components/app/Support.vue')['default']
ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default'] ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default']
ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default'] ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default']
@@ -86,6 +86,7 @@ declare module '@vue/runtime-core' {
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink'] HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal'] HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture'] HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder']
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing'] HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup'] HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver'] HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
@@ -130,15 +131,18 @@ declare module '@vue/runtime-core' {
IconLucideLayers: typeof import('~icons/lucide/layers')['default'] IconLucideLayers: typeof import('~icons/lucide/layers')['default']
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default'] IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
IconLucideMinus: typeof import('~icons/lucide/minus')['default'] IconLucideMinus: typeof import('~icons/lucide/minus')['default']
IconLucideRefreshCw: typeof import('~icons/lucide/refresh-cw')['default']
IconLucideSearch: typeof import('~icons/lucide/search')['default'] IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideUsers: typeof import('~icons/lucide/users')['default'] IconLucideUsers: typeof import('~icons/lucide/users')['default']
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default'] LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default'] LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
LensesRenderersHTMLLensRenderer: typeof import('./components/lenses/renderers/HTMLLensRenderer.vue')['default'] LensesRenderersHTMLLensRenderer: typeof import('./components/lenses/renderers/HTMLLensRenderer.vue')['default']
LensesRenderersImageLensRenderer: typeof import('./components/lenses/renderers/ImageLensRenderer.vue')['default'] LensesRenderersImageLensRenderer: typeof import('./components/lenses/renderers/ImageLensRenderer.vue')['default']
LensesRenderersJSONLensRenderer: typeof import('./components/lenses/renderers/JSONLensRenderer.vue')['default'] LensesRenderersJSONLensRenderer: typeof import('./components/lenses/renderers/JSONLensRenderer.vue')['default']
LensesRenderersPDFLensRenderer: typeof import('./components/lenses/renderers/PDFLensRenderer.vue')['default'] LensesRenderersPDFLensRenderer: typeof import('./components/lenses/renderers/PDFLensRenderer.vue')['default']
LensesRenderersRawLensRenderer: typeof import('./components/lenses/renderers/RawLensRenderer.vue')['default'] LensesRenderersRawLensRenderer: typeof import('./components/lenses/renderers/RawLensRenderer.vue')['default']
LensesRenderersVideoLensRenderer: typeof import('./components/lenses/renderers/VideoLensRenderer.vue')['default']
LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default'] LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default']
LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default'] LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default']
ProfileShortcode: typeof import('./components/profile/Shortcode.vue')['default'] ProfileShortcode: typeof import('./components/profile/Shortcode.vue')['default']
@@ -165,6 +169,7 @@ declare module '@vue/runtime-core' {
SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default'] SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default']
SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.vue')['default'] SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.vue')['default']
SmartPicture: typeof import('./../../hoppscotch-ui/src/components/smart/Picture.vue')['default'] SmartPicture: typeof import('./../../hoppscotch-ui/src/components/smart/Picture.vue')['default']
SmartPlaceholder: typeof import('./../../hoppscotch-ui/src/components/smart/Placeholder.vue')['default']
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default'] SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default'] SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default'] SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']

View File

@@ -1,70 +0,0 @@
<template>
<div key="outputHash" class="flex flex-col flex-1 overflow-auto">
<div class="flex flex-col">
<AppPowerSearchEntry
v-for="(shortcut, shortcutIndex) in searchResults"
:key="`shortcut-${shortcutIndex}`"
:active="shortcutIndex === selectedEntry"
:shortcut="shortcut.item"
@action="emit('action', shortcut.item.action)"
@mouseover="selectedEntry = shortcutIndex"
/>
</div>
<div
v-if="searchResults.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
<span class="my-2 text-center">
{{ t("state.nothing_found") }} "{{ search }}"
</span>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onUnmounted, onMounted } from "vue"
import Fuse from "fuse.js"
import { useArrowKeysNavigation } from "~/helpers/powerSearchNavigation"
import { HoppAction } from "~/helpers/actions"
import { useI18n } from "@composables/i18n"
const t = useI18n()
const props = defineProps<{
input: Record<string, any>[]
search: string
}>()
const emit = defineEmits<{
(e: "action", action: HoppAction): void
}>()
const options = {
keys: ["keys", "label", "action", "tags"],
}
const fuse = new Fuse(props.input, options)
const searchResults = computed(() => fuse.search(props.search))
const searchResultsItems = computed(() =>
searchResults.value.map((searchResult) => searchResult.item)
)
const emitSearchAction = (action: HoppAction) => emit("action", action)
const { bindArrowKeysListeners, unbindArrowKeysListeners, selectedEntry } =
useArrowKeysNavigation(searchResultsItems, {
onEnter: emitSearchAction,
stopPropagation: true,
})
onMounted(() => {
bindArrowKeysListeners()
})
onUnmounted(() => {
unbindArrowKeysListeners()
})
</script>

View File

@@ -242,7 +242,7 @@ import { pwaDefferedPrompt, installPWA } from "@modules/pwa"
import { platform } from "~/platform" import { platform } from "~/platform"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useReadonlyStream } from "@composables/stream" import { useReadonlyStream } from "@composables/stream"
import { invokeAction } from "@helpers/actions" import { defineActionHandler, invokeAction } from "@helpers/actions"
import { workspaceStatus$, updateWorkspaceTeamName } from "~/newstore/workspace" import { workspaceStatus$, updateWorkspaceTeamName } from "~/newstore/workspace"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter" import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
import { onLoggedIn } from "~/composables/auth" import { onLoggedIn } from "~/composables/auth"
@@ -374,4 +374,12 @@ const profile = ref<any | null>(null)
const settings = ref<any | null>(null) const settings = ref<any | null>(null)
const logout = ref<any | null>(null) const logout = ref<any | null>(null)
const accountActions = ref<any | null>(null) const accountActions = ref<any | null>(null)
defineActionHandler(
"user.login",
() => {
invokeAction("modals.login.toggle")
},
computed(() => !currentUser.value)
)
</script> </script>

View File

@@ -1,122 +0,0 @@
<template>
<HoppSmartModal
v-if="show"
styles="sm:max-w-lg"
full-width
@close="emit('hide-modal')"
>
<template #body>
<div class="flex flex-col border-b transition border-dividerLight">
<input
id="command"
v-model="search"
v-focus
type="text"
autocomplete="off"
name="command"
:placeholder="`${t('app.type_a_command_search')}`"
class="flex flex-shrink-0 p-6 text-base bg-transparent text-secondaryDark"
/>
<div
class="flex flex-shrink-0 text-tiny text-secondaryLight px-4 pb-4 justify-between whitespace-nowrap overflow-auto <sm:hidden"
>
<div class="flex items-center">
<kbd class="shortcut-key"></kbd>
<kbd class="shortcut-key"></kbd>
<span class="ml-2 truncate">
{{ t("action.to_navigate") }}
</span>
<kbd class="shortcut-key"></kbd>
<span class="ml-2 truncate">
{{ t("action.to_select") }}
</span>
</div>
<div class="flex items-center">
<kbd class="shortcut-key">ESC</kbd>
<span class="ml-2 truncate">
{{ t("action.to_close") }}
</span>
</div>
</div>
</div>
<AppFuse
v-if="search && show"
:input="fuse"
:search="search"
@action="runAction"
/>
<div
v-else
class="flex flex-col flex-1 overflow-auto space-y-4 divide-y divide-dividerLight"
>
<div
v-for="(map, mapIndex) in mappings"
:key="`map-${mapIndex}`"
class="flex flex-col"
>
<h5 class="px-6 py-2 my-2 text-secondaryLight">
{{ t(map.section) }}
</h5>
<AppPowerSearchEntry
v-for="(shortcut, shortcutIndex) in map.shortcuts"
:key="`map-${mapIndex}-shortcut-${shortcutIndex}`"
:shortcut="shortcut"
:active="shortcutsItems.indexOf(shortcut) === selectedEntry"
@action="runAction"
@mouseover="selectedEntry = shortcutsItems.indexOf(shortcut)"
/>
</div>
</div>
</template>
</HoppSmartModal>
</template>
<script setup lang="ts">
import { ref, computed, watch } from "vue"
import { HoppAction, invokeAction } from "~/helpers/actions"
import { spotlight as mappings, fuse } from "@helpers/shortcuts"
import { useArrowKeysNavigation } from "~/helpers/powerSearchNavigation"
import { useI18n } from "@composables/i18n"
const t = useI18n()
const props = defineProps<{
show: boolean
}>()
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const search = ref("")
const hideModal = () => {
search.value = ""
emit("hide-modal")
}
const runAction = (command: HoppAction) => {
invokeAction(command)
hideModal()
}
const shortcutsItems = computed(() =>
mappings.reduce(
(shortcuts, section) => [...shortcuts, ...section.shortcuts],
[]
)
)
const { bindArrowKeysListeners, unbindArrowKeysListeners, selectedEntry } =
useArrowKeysNavigation(shortcutsItems, {
onEnter: runAction,
})
watch(
() => props.show,
(show) => {
if (show) bindArrowKeysListeners()
else unbindArrowKeysListeners()
}
)
</script>

View File

@@ -1,68 +0,0 @@
<template>
<button
class="flex items-center flex-1 px-6 py-3 font-medium transition cursor-pointer search-entry focus:outline-none"
:class="{ active: active }"
tabindex="-1"
@click="emit('action', shortcut.action)"
@keydown.enter="emit('action', shortcut.action)"
>
<component
:is="shortcut.icon"
class="mr-4 transition opacity-50 svg-icons"
:class="{ 'opacity-100 text-secondaryDark': active }"
/>
<span
class="flex flex-1 mr-4 transition"
:class="{ 'text-secondaryDark': active }"
>
{{ t(shortcut.label) }}
</span>
<kbd
v-for="(key, keyIndex) in shortcut.keys"
:key="`key-${String(keyIndex)}`"
class="shortcut-key"
>
{{ key }}
</kbd>
</button>
</template>
<script setup lang="ts">
import type { Component } from "vue"
import { useI18n } from "@composables/i18n"
const t = useI18n()
defineProps<{
shortcut: {
label: string
keys: string[]
action: string
icon: object | Component
}
active: boolean
}>()
const emit = defineEmits<{
(e: "action", action: string): void
}>()
</script>
<style lang="scss" scoped>
.search-entry {
@apply relative;
@apply after:absolute;
@apply after:top-0;
@apply after:left-0;
@apply after:bottom-0;
@apply after:bg-transparent;
@apply after:z-2;
@apply after:w-0.5;
@apply after:content-DEFAULT;
&.active {
@apply bg-primaryLight;
@apply after:bg-accentLight;
}
}
</style>

View File

@@ -14,45 +14,18 @@
/> />
</div> </div>
</div> </div>
<div v-if="filterText" class="flex flex-col divide-y divide-dividerLight"> <div class="flex flex-col divide-y divide-dividerLight">
<details <HoppSmartPlaceholder v-if="isEmpty(shortcutsResults)">
v-for="(map, mapIndex) in searchResults"
:key="`map-${mapIndex}`"
class="flex flex-col"
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"
>
<icon-lucide-chevron-right class="mr-2 indicator" />
<span
class="font-semibold truncate capitalize-first text-secondaryDark"
>
{{ t(map.item.section) }}
</span>
</summary>
<div class="flex flex-col px-6 pb-4 space-y-2">
<AppShortcutsEntry
v-for="(shortcut, index) in map.item.shortcuts"
:key="`shortcut-${index}`"
:shortcut="shortcut"
/>
</div>
</details>
<div
v-if="searchResults.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
>
<icon-lucide-search class="pb-2 opacity-75 svg-icons" /> <icon-lucide-search class="pb-2 opacity-75 svg-icons" />
<span class="my-2 text-center"> <span class="my-2 text-center flex flex-col">
{{ t("state.nothing_found") }} "{{ filterText }}" {{ t("state.nothing_found") }}
<span class="break-all">"{{ filterText }}"</span>
</span> </span>
</div> </HoppSmartPlaceholder>
</div>
<div v-else class="flex flex-col divide-y divide-dividerLight">
<details <details
v-for="(map, mapIndex) in mappings" v-for="(sectionResults, sectionTitle) in shortcutsResults"
:key="`map-${mapIndex}`" v-else
:key="`section-${sectionTitle}`"
class="flex flex-col" class="flex flex-col"
open open
> >
@@ -63,13 +36,13 @@
<span <span
class="font-semibold truncate capitalize-first text-secondaryDark" class="font-semibold truncate capitalize-first text-secondaryDark"
> >
{{ t(map.section) }} {{ sectionTitle }}
</span> </span>
</summary> </summary>
<div class="flex flex-col px-6 pb-4 space-y-2"> <div class="flex flex-col px-6 pb-4 space-y-2">
<AppShortcutsEntry <AppShortcutsEntry
v-for="(shortcut, shortcutIndex) in map.shortcuts" v-for="(shortcut, index) in sectionResults"
:key="`map-${mapIndex}-shortcut-${shortcutIndex}`" :key="`shortcut-${index}`"
:shortcut="shortcut" :shortcut="shortcut"
/> />
</div> </div>
@@ -80,10 +53,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from "vue" import { computed, onBeforeMount, ref } from "vue"
import Fuse from "fuse.js" import { ShortcutDef, getShortcuts } from "~/helpers/shortcuts"
import mappings from "~/helpers/shortcuts" import MiniSearch from "minisearch"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { groupBy, isEmpty } from "lodash-es"
const t = useI18n() const t = useI18n()
@@ -91,15 +65,33 @@ defineProps<{
show: boolean show: boolean
}>() }>()
const options = { const minisearch = new MiniSearch({
keys: ["shortcuts.label"], fields: ["label", "keys", "section"],
} idField: "label",
storeFields: ["label", "keys", "section"],
searchOptions: {
fuzzy: true,
prefix: true,
},
})
const fuse = new Fuse(mappings, options) const shortcuts = getShortcuts(t)
onBeforeMount(() => {
minisearch.addAllAsync(shortcuts)
})
const filterText = ref("") const filterText = ref("")
const searchResults = computed(() => fuse.search(filterText.value)) const shortcutsResults = computed(() => {
// If there are no search text, return all the shortcuts
const results =
filterText.value.length > 0
? minisearch.search(filterText.value)
: shortcuts
return groupBy(results, "section") as Record<string, ShortcutDef[]>
})
const emit = defineEmits<{ const emit = defineEmits<{
(e: "close"): void (e: "close"): void

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="flex items-center py-1"> <div class="flex items-center py-1">
<span class="flex flex-1 mr-4"> <span class="flex flex-1 mr-4">
{{ t(shortcut.label) }} {{ shortcut.label }}
</span> </span>
<kbd <kbd
v-for="(key, index) in shortcut.keys" v-for="(key, index) in shortcut.keys"
@@ -14,14 +14,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from "@composables/i18n" import { ShortcutDef } from "~/helpers/shortcuts"
const t = useI18n()
defineProps<{ defineProps<{
shortcut: { shortcut: ShortcutDef
label: string
keys: string[]
}
}>() }>()
</script> </script>

View File

@@ -0,0 +1,123 @@
<template>
<button
ref="el"
class="flex items-center flex-1 px-6 py-3 font-medium transition cursor-pointer search-entry focus:outline-none"
:class="{ active: active }"
tabindex="-1"
@click="emit('action')"
@keydown.enter="emit('action')"
>
<component
:is="entry.icon"
class="mr-4 transition opacity-50 svg-icons"
:class="{ 'opacity-100 text-secondaryDark': active }"
/>
<span
v-if="entry.text.type === 'text' && typeof entry.text.text === 'string'"
class="flex flex-1 mr-4 transition"
:class="{ 'text-secondaryDark': active }"
>
{{ entry.text.text }}
</span>
<span
v-else-if="entry.text.type === 'text' && Array.isArray(entry.text.text)"
class="flex flex-1 mr-4 transition"
:class="{ 'text-secondaryDark': active }"
>
<span
v-for="(labelPart, labelPartIndex) in entry.text.text"
:key="`label-${labelPart}-${labelPartIndex}`"
>
{{ labelPart }}
<icon-lucide-chevron-right
v-if="labelPartIndex < entry.text.text.length - 1"
class="inline"
/>
</span>
</span>
<span v-else-if="entry.text.type === 'custom'">
<component
:is="entry.text.component"
v-bind="entry.text.componentProps"
/>
</span>
<span v-if="formattedShortcutKeys">
<kbd
v-for="(key, keyIndex) in formattedShortcutKeys"
:key="`key-${String(keyIndex)}`"
class="shortcut-key"
>
{{ key }}
</kbd>
</span>
</button>
</template>
<script lang="ts">
import { getPlatformSpecialKey } from "~/helpers/platformutils"
import { SpotlightSearcherResult } from "~/services/spotlight"
const SPECIAL_KEY_CHARS: Record<string, string> = {
ctrl: getPlatformSpecialKey(),
alt: getPlatformAlternateKey(),
up: "↑",
down: "↓",
enter: "↩",
}
</script>
<script setup lang="ts">
import { computed, watch, ref } from "vue"
import { capitalize } from "lodash-es"
import { getPlatformAlternateKey } from "~/helpers/platformutils"
const el = ref<HTMLElement>()
const props = defineProps<{
entry: SpotlightSearcherResult
active: boolean
}>()
const formattedShortcutKeys = computed(() =>
props.entry.meta?.keyboardShortcut?.map((key) => {
return SPECIAL_KEY_CHARS[key] ?? capitalize(key)
})
)
const emit = defineEmits<{
(e: "action"): void
}>()
watch(
() => props.active,
(active) => {
if (active) {
el.value?.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "nearest",
})
}
}
)
</script>
<style lang="scss" scoped>
.search-entry {
@apply relative;
@apply after:absolute;
@apply after:top-0;
@apply after:left-0;
@apply after:bottom-0;
@apply after:bg-transparent;
@apply after:z-2;
@apply after:w-0.5;
@apply after:content-DEFAULT;
&.active {
@apply bg-primaryLight;
@apply after:bg-accentLight;
}
}
</style>

View File

@@ -0,0 +1,40 @@
<template>
<span class="flex flex-row space-x-2">
<span>{{ dateTimeText }}</span>
<icon-lucide-chevron-right class="inline" />
<span class="truncate" :class="entryStatus.className">
<span class="font-semibold truncate text-tiny">
{{ historyEntry.request.method }}
</span>
</span>
<span>
{{ historyEntry.request.endpoint }}
</span>
</span>
</template>
<script setup lang="ts">
import { computed } from "vue"
import findStatusGroup from "~/helpers/findStatusGroup"
import { shortDateTime } from "~/helpers/utils/date"
import { RESTHistoryEntry } from "~/newstore/history"
const props = defineProps<{
historyEntry: RESTHistoryEntry
}>()
const dateTimeText = computed(() =>
shortDateTime(props.historyEntry.updatedOn!)
)
const entryStatus = computed(() => {
const foundStatusGroup = findStatusGroup(
props.historyEntry.responseMeta.statusCode
)
return (
foundStatusGroup || {
className: "",
}
)
})
</script>

View File

@@ -0,0 +1,228 @@
<template>
<HoppSmartModal
v-if="show"
styles="sm:max-w-lg"
full-width
@close="emit('hide-modal')"
>
<template #body>
<div class="flex flex-col border-b transition border-dividerLight">
<div class="flex items-center p-6 space-x-2">
<input
id="command"
v-model="search"
v-focus
type="text"
autocomplete="off"
name="command"
:placeholder="`${t('app.type_a_command_search')}`"
class="flex flex-1 text-base bg-transparent text-secondaryDark"
/>
<icon-lucide-refresh-cw
v-if="searchSession?.loading"
class="animate-spin"
/>
</div>
<div
class="flex flex-shrink-0 text-tiny text-secondaryLight px-4 pb-4 justify-between whitespace-nowrap overflow-auto <sm:hidden"
>
<div class="flex items-center">
<kbd class="shortcut-key"></kbd>
<kbd class="shortcut-key"></kbd>
<span class="ml-2 truncate">
{{ t("action.to_navigate") }}
</span>
<kbd class="shortcut-key"></kbd>
<span class="ml-2 truncate">
{{ t("action.to_select") }}
</span>
</div>
<div class="flex items-center">
<kbd class="shortcut-key">ESC</kbd>
<span class="ml-2 truncate">
{{ t("action.to_close") }}
</span>
</div>
</div>
</div>
<div
v-if="searchSession"
class="flex flex-col flex-1 overflow-auto space-y-4 divide-y divide-dividerLight"
>
<div
v-for="([sectionID, sectionResult], sectionIndex) in scoredResults"
:key="`section-${sectionID}`"
class="flex flex-col"
>
<h5 class="px-6 py-2 my-2 text-secondaryLight">
{{ sectionResult.title }}
</h5>
<AppSpotlightEntry
v-for="(result, entryIndex) in sectionResult.results"
:key="`result-${result.id}`"
:entry="result"
:active="isEqual(selectedEntry, [sectionIndex, entryIndex])"
@mouseover="selectedEntry = [sectionIndex, entryIndex]"
@action="runAction(sectionID, result)"
/>
</div>
</div>
<HoppSmartPlaceholder
v-if="scoredResults.length === 0 && search.length > 0"
:text="`${t('state.nothing_found')} ‟${search}”`"
>
<template #icon>
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
</template>
</HoppSmartPlaceholder>
</template>
</HoppSmartModal>
</template>
<script setup lang="ts">
import { ref, computed, watch } from "vue"
import { useService } from "dioc/vue"
import { useI18n } from "@composables/i18n"
import {
SpotlightService,
SpotlightSearchState,
SpotlightSearcherResult,
} from "~/services/spotlight"
import { isEqual } from "lodash-es"
import { HistorySpotlightSearcherService } from "~/services/spotlight/searchers/history.searcher"
import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/user.searcher"
const t = useI18n()
const props = defineProps<{
show: boolean
}>()
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const spotlightService = useService(SpotlightService)
useService(HistorySpotlightSearcherService)
useService(UserSpotlightSearcherService)
const search = ref("")
const searchSession = ref<SpotlightSearchState>()
const stopSearchSession = ref<() => void>()
const scoredResults = computed(() =>
Object.entries(searchSession.value?.results ?? {}).sort(
([, sectionA], [, sectionB]) => sectionB.avgScore - sectionA.avgScore
)
)
const { selectedEntry } = newUseArrowKeysForNavigation()
watch(
() => props.show,
(show) => {
search.value = ""
if (show) {
const [session, onSessionEnd] =
spotlightService.createSearchSession(search)
searchSession.value = session.value
stopSearchSession.value = onSessionEnd
} else {
stopSearchSession.value?.()
stopSearchSession.value = undefined
searchSession.value = undefined
}
}
)
function runAction(searcherID: string, result: SpotlightSearcherResult) {
spotlightService.selectSearchResult(searcherID, result)
emit("hide-modal")
}
function newUseArrowKeysForNavigation() {
const selectedEntry = ref<[number, number]>([0, 0]) // [sectionIndex, entryIndex]
watch(search, () => {
selectedEntry.value = [0, 0]
})
const onArrowDown = () => {
// If no entries, do nothing
if (scoredResults.value.length === 0) return
const [sectionIndex, entryIndex] = selectedEntry.value
const [, section] = scoredResults.value[sectionIndex]
if (entryIndex < section.results.length - 1) {
selectedEntry.value = [sectionIndex, entryIndex + 1]
} else if (sectionIndex < scoredResults.value.length - 1) {
selectedEntry.value = [sectionIndex + 1, 0]
} else {
selectedEntry.value = [0, 0]
}
}
const onArrowUp = () => {
// If no entries, do nothing
if (scoredResults.value.length === 0) return
const [sectionIndex, entryIndex] = selectedEntry.value
if (entryIndex > 0) {
selectedEntry.value = [sectionIndex, entryIndex - 1]
} else if (sectionIndex > 0) {
const [, section] = scoredResults.value[sectionIndex - 1]
selectedEntry.value = [sectionIndex - 1, section.results.length - 1]
} else {
selectedEntry.value = [
scoredResults.value.length - 1,
scoredResults.value[scoredResults.value.length - 1][1].results.length -
1,
]
}
}
function handleKeyPress(e: KeyboardEvent) {
if (e.key === "ArrowUp") {
e.preventDefault()
e.stopPropagation()
onArrowUp()
} else if (e.key === "ArrowDown") {
e.preventDefault()
e.stopPropagation()
onArrowDown()
} else if (e.key === "Enter") {
e.preventDefault()
e.stopPropagation()
const [sectionIndex, entryIndex] = selectedEntry.value
const [sectionID, section] = scoredResults.value[sectionIndex]
const result = section.results[entryIndex]
runAction(sectionID, result)
}
}
watch(
() => props.show,
(show) => {
if (show) {
window.addEventListener("keydown", handleKeyPress)
} else {
window.removeEventListener("keydown", handleKeyPress)
}
}
)
return { selectedEntry }
}
</script>

View File

@@ -284,6 +284,14 @@ const importerAction = async (stepResults: StepReturnValue[]) => {
emit("import-to-teams", result) emit("import-to-teams", result)
} else { } else {
appendRESTCollections(result) appendRESTCollections(result)
platform.analytics?.logEvent({
type: "HOPP_IMPORT_COLLECTION",
importer: importerModule.value!.name,
platform: "rest",
workspaceType: "personal",
})
fileImported() fileImported()
} }
} }

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="flex flex-col flex-1 bg-primary"> <div class="flex flex-col flex-1">
<div <div
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight" class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
:style=" :style="
@@ -243,49 +243,33 @@
/> />
</template> </template>
<template #emptyNode="{ node }"> <template #emptyNode="{ node }">
<div <HoppSmartPlaceholder
v-if="filterText.length !== 0 && filteredCollections.length === 0" v-if="filterText.length !== 0 && filteredCollections.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight" :text="`${t('state.nothing_found')}${filterText}`"
> >
<icon-lucide-search class="pb-2 opacity-75 svg-icons" /> <template #icon>
<span class="my-2 text-center"> <icon-lucide-search class="pb-2 opacity-75 svg-icons" />
{{ t("state.nothing_found") }} "{{ filterText }}" </template>
</span> </HoppSmartPlaceholder>
</div> <HoppSmartPlaceholder
<div v-else-if="node === null"> v-else-if="node === null"
<div :src="`/images/states/${colorMode.value}/pack.svg`"
class="flex flex-col items-center justify-center p-4 text-secondaryLight" :alt="`${t('empty.collections')}`"
> :text="t('empty.collections')"
<img
:src="`/images/states/${colorMode.value}/pack.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
:alt="`${t('empty.collections')}`"
/>
<span class="pb-4 text-center">
{{ t("empty.collections") }}
</span>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="emit('display-modal-add')"
/>
</div>
</div>
<div
v-else-if="node.data.type === 'collections'"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
> >
<img <HoppButtonSecondary
:src="`/images/states/${colorMode.value}/pack.svg`" :label="t('add.new')"
loading="lazy" filled
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4" outline
:alt="`${t('empty.collection')}`" @click="emit('display-modal-add')"
/> />
<span class="pb-4 text-center"> </HoppSmartPlaceholder>
{{ t("empty.collection") }} <HoppSmartPlaceholder
</span> v-else-if="node.data.type === 'collections'"
:src="`/images/states/${colorMode.value}/pack.svg`"
:alt="`${t('empty.collections')}`"
:text="t('empty.collections')"
>
<HoppButtonSecondary <HoppButtonSecondary
:label="t('add.new')" :label="t('add.new')"
filled filled
@@ -298,21 +282,14 @@
}) })
" "
/> />
</div> </HoppSmartPlaceholder>
<div <HoppSmartPlaceholder
v-else-if="node.data.type === 'folders'" v-else-if="node.data.type === 'folders'"
class="flex flex-col items-center justify-center p-4 text-secondaryLight" :src="`/images/states/${colorMode.value}/pack.svg`"
:alt="`${t('empty.folder')}`"
:text="t('empty.folder')"
> >
<img </HoppSmartPlaceholder>
:src="`/images/states/${colorMode.value}/pack.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
:alt="`${t('empty.folder')}`"
/>
<span class="text-center">
{{ t("empty.folder") }}
</span>
</div>
</template> </template>
</SmartTree> </SmartTree>
</div> </div>

View File

@@ -89,6 +89,7 @@ import {
import { GQLError } from "~/helpers/backend/GQLClient" import { GQLError } from "~/helpers/backend/GQLClient"
import { computedWithControl } from "@vueuse/core" import { computedWithControl } from "@vueuse/core"
import { currentActiveTab } from "~/helpers/rest/tab" import { currentActiveTab } from "~/helpers/rest/tab"
import { platform } from "~/platform"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -223,6 +224,13 @@ const saveRequestAs = async () => {
}, },
} }
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
platform: "rest",
workspaceType: "personal",
})
requestSaved() requestSaved()
} else if (picked.value.pickedType === "my-folder") { } else if (picked.value.pickedType === "my-folder") {
if (!isHoppRESTRequest(requestUpdated)) if (!isHoppRESTRequest(requestUpdated))
@@ -243,6 +251,13 @@ const saveRequestAs = async () => {
}, },
} }
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
platform: "rest",
workspaceType: "personal",
})
requestSaved() requestSaved()
} else if (picked.value.pickedType === "my-request") { } else if (picked.value.pickedType === "my-request") {
if (!isHoppRESTRequest(requestUpdated)) if (!isHoppRESTRequest(requestUpdated))
@@ -264,17 +279,38 @@ const saveRequestAs = async () => {
}, },
} }
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: false,
platform: "rest",
workspaceType: "personal",
})
requestSaved() requestSaved()
} else if (picked.value.pickedType === "teams-collection") { } else if (picked.value.pickedType === "teams-collection") {
if (!isHoppRESTRequest(requestUpdated)) if (!isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request") throw new Error("requestUpdated is not a REST Request")
updateTeamCollectionOrFolder(picked.value.collectionID, requestUpdated) updateTeamCollectionOrFolder(picked.value.collectionID, requestUpdated)
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
platform: "rest",
workspaceType: "team",
})
} else if (picked.value.pickedType === "teams-folder") { } else if (picked.value.pickedType === "teams-folder") {
if (!isHoppRESTRequest(requestUpdated)) if (!isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request") throw new Error("requestUpdated is not a REST Request")
updateTeamCollectionOrFolder(picked.value.folderID, requestUpdated) updateTeamCollectionOrFolder(picked.value.folderID, requestUpdated)
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
platform: "rest",
workspaceType: "team",
})
} else if (picked.value.pickedType === "teams-request") { } else if (picked.value.pickedType === "teams-request") {
if (!isHoppRESTRequest(requestUpdated)) if (!isHoppRESTRequest(requestUpdated))
throw new Error("requestUpdated is not a REST Request") throw new Error("requestUpdated is not a REST Request")
@@ -292,6 +328,13 @@ const saveRequestAs = async () => {
title: requestUpdated.name, title: requestUpdated.name,
} }
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: false,
platform: "rest",
workspaceType: "team",
})
pipe( pipe(
updateTeamRequest(picked.value.requestID, data), updateTeamRequest(picked.value.requestID, data),
TE.match( TE.match(
@@ -313,6 +356,13 @@ const saveRequestAs = async () => {
requestUpdated as HoppGQLRequest requestUpdated as HoppGQLRequest
) )
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: false,
platform: "gql",
workspaceType: "team",
})
requestSaved() requestSaved()
} else if (picked.value.pickedType === "gql-my-folder") { } else if (picked.value.pickedType === "gql-my-folder") {
// TODO: Check for GQL request ? // TODO: Check for GQL request ?
@@ -321,6 +371,13 @@ const saveRequestAs = async () => {
requestUpdated as HoppGQLRequest requestUpdated as HoppGQLRequest
) )
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
platform: "gql",
workspaceType: "team",
})
requestSaved() requestSaved()
} else if (picked.value.pickedType === "gql-my-collection") { } else if (picked.value.pickedType === "gql-my-collection") {
// TODO: Check for GQL request ? // TODO: Check for GQL request ?
@@ -329,6 +386,13 @@ const saveRequestAs = async () => {
requestUpdated as HoppGQLRequest requestUpdated as HoppGQLRequest
) )
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
platform: "gql",
workspaceType: "team",
})
requestSaved() requestSaved()
} }
} }

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="flex flex-col flex-1 bg-primary"> <div class="flex flex-col flex-1">
<div <div
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight" class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
:style=" :style="
@@ -262,67 +262,53 @@
</template> </template>
<template #emptyNode="{ node }"> <template #emptyNode="{ node }">
<div v-if="node === null"> <div v-if="node === null">
<div <div @drop="(e) => e.stopPropagation()">
class="flex flex-col items-center justify-center p-4 text-secondaryLight" <HoppSmartPlaceholder
@drop="(e) => e.stopPropagation()"
>
<img
:src="`/images/states/${colorMode.value}/pack.svg`" :src="`/images/states/${colorMode.value}/pack.svg`"
loading="lazy" :alt="`${t('empty.collections')}`"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4" :text="t('empty.collections')"
:alt="`${t('empty.collection')}`" >
/> <HoppButtonSecondary
<span class="pb-4 text-center"> v-if="hasNoTeamAccess"
{{ t("empty.collections") }} v-tippy="{ theme: 'tooltip' }"
</span> disabled
<HoppButtonSecondary filled
v-if="hasNoTeamAccess" outline
v-tippy="{ theme: 'tooltip' }" :title="t('team.no_access')"
disabled :label="t('action.new')"
filled />
outline <HoppButtonSecondary
:title="t('team.no_access')" v-else
:label="t('action.new')" :icon="IconPlus"
/> :label="t('action.new')"
<HoppButtonSecondary filled
v-else outline
:icon="IconPlus" @click="emit('display-modal-add')"
:label="t('action.new')" />
filled </HoppSmartPlaceholder>
outline
@click="emit('display-modal-add')"
/>
</div> </div>
</div> </div>
<div <div
v-else-if="node.data.type === 'collections'" v-else-if="node.data.type === 'collections'"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
@drop="(e) => e.stopPropagation()" @drop="(e) => e.stopPropagation()"
> >
<img <HoppSmartPlaceholder
:src="`/images/states/${colorMode.value}/pack.svg`" :src="`/images/states/${colorMode.value}/pack.svg`"
loading="lazy" :alt="`${t('empty.collections')}`"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4" :text="t('empty.collections')"
:alt="`${t('empty.collection')}`" >
/> </HoppSmartPlaceholder>
<span class="pb-4 text-center">
{{ t("empty.collections") }}
</span>
</div> </div>
<div <div
v-else-if="node.data.type === 'folders'" v-else-if="node.data.type === 'folders'"
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
@drop="(e) => e.stopPropagation()" @drop="(e) => e.stopPropagation()"
> >
<img <HoppSmartPlaceholder
:src="`/images/states/${colorMode.value}/pack.svg`" :src="`/images/states/${colorMode.value}/pack.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
:alt="`${t('empty.folder')}`" :alt="`${t('empty.folder')}`"
/> :text="t('empty.folder')"
<span class="text-center"> >
{{ t("empty.folder") }} </HoppSmartPlaceholder>
</span>
</div> </div>
</template> </template>
</SmartTree> </SmartTree>

View File

@@ -46,6 +46,7 @@ import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { HoppGQLRequest, makeCollection } from "@hoppscotch/data" import { HoppGQLRequest, makeCollection } from "@hoppscotch/data"
import { addGraphqlCollection } from "~/newstore/collections" import { addGraphqlCollection } from "~/newstore/collections"
import { platform } from "~/platform"
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -79,6 +80,13 @@ export default defineComponent({
) )
this.hideModal() this.hideModal()
platform.analytics?.logEvent({
type: "HOPP_CREATE_COLLECTION",
isRootCollection: true,
platform: "gql",
workspaceType: "personal",
})
}, },
hideModal() { hideModal() {
this.name = null this.name = null

View File

@@ -171,21 +171,14 @@
@duplicate-request="$emit('duplicate-request', $event)" @duplicate-request="$emit('duplicate-request', $event)"
@select="$emit('select', $event)" @select="$emit('select', $event)"
/> />
<div <HoppSmartPlaceholder
v-if=" v-if="
collection.folders.length === 0 && collection.requests.length === 0 collection.folders.length === 0 && collection.requests.length === 0
" "
class="flex flex-col items-center justify-center p-4 text-secondaryLight" :src="`/images/states/${colorMode.value}/pack.svg`"
:alt="`${t('empty.collection')}`"
:text="t('empty.collection')"
> >
<img
:src="`/images/states/${colorMode.value}/pack.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
:alt="`${t('empty.collection')}`"
/>
<span class="pb-4 text-center">
{{ t("empty.collection") }}
</span>
<HoppButtonSecondary <HoppButtonSecondary
:label="t('add.new')" :label="t('add.new')"
filled filled
@@ -196,7 +189,7 @@
}) })
" "
/> />
</div> </HoppSmartPlaceholder>
</div> </div>
</div> </div>
<HoppSmartConfirmModal <HoppSmartConfirmModal

View File

@@ -160,25 +160,19 @@
@duplicate-request="emit('duplicate-request', $event)" @duplicate-request="emit('duplicate-request', $event)"
@select="emit('select', $event)" @select="emit('select', $event)"
/> />
<div
<HoppSmartPlaceholder
v-if=" v-if="
folder.folders && folder.folders &&
folder.folders.length === 0 && folder.folders.length === 0 &&
folder.requests && folder.requests &&
folder.requests.length === 0 folder.requests.length === 0
" "
class="flex flex-col items-center justify-center p-4 text-secondaryLight" :src="`/images/states/${colorMode.value}/pack.svg`"
:alt="`${t('empty.folder')}`"
:text="t('empty.folder')"
> >
<img </HoppSmartPlaceholder>
:src="`/images/states/${colorMode.value}/pack.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
:alt="`${t('empty.folder')}`"
/>
<span class="text-center">
{{ t("empty.folder") }}
</span>
</div>
</div> </div>
</div> </div>
<HoppSmartConfirmModal <HoppSmartConfirmModal

View File

@@ -244,6 +244,14 @@ const importFromJSON = () => {
return return
} }
appendGraphqlCollections(collections) appendGraphqlCollections(collections)
platform.analytics?.logEvent({
type: "HOPP_IMPORT_COLLECTION",
importer: "json",
workspaceType: "personal",
platform: "gql",
})
fileImported() fileImported()
} }
reader.readAsText(inputChooseFileToImportFrom.value.files[0]) reader.readAsText(inputChooseFileToImportFrom.value.files[0])
@@ -257,6 +265,12 @@ const exportJSON = () => {
const url = URL.createObjectURL(file) const url = URL.createObjectURL(file)
a.href = url a.href = url
platform?.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "json",
platform: "gql",
})
// TODO: get uri from meta // TODO: get uri from meta
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json` a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
document.body.appendChild(a) document.body.appendChild(a)

View File

@@ -60,35 +60,27 @@
@select="$emit('select', $event)" @select="$emit('select', $event)"
/> />
</div> </div>
<div <HoppSmartPlaceholder
v-if="collections.length === 0" v-if="collections.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight" :src="`/images/states/${colorMode.value}/pack.svg`"
:alt="`${t('empty.collections')}`"
:text="t('empty.collections')"
> >
<img
:src="`/images/states/${colorMode.value}/pack.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="t('empty.collections')"
/>
<span class="pb-4 text-center">
{{ t("empty.collections") }}
</span>
<HoppButtonSecondary <HoppButtonSecondary
:label="t('add.new')" :label="t('add.new')"
filled filled
outline outline
@click="displayModalAdd(true)" @click="displayModalAdd(true)"
/> />
</div> </HoppSmartPlaceholder>
<div <HoppSmartPlaceholder
v-if="!(filteredCollections.length !== 0 || collections.length === 0)" v-if="!(filteredCollections.length !== 0 || collections.length === 0)"
class="flex flex-col items-center justify-center p-4 text-secondaryLight" :text="`${t('state.nothing_found')}${filterText}`"
> >
<icon-lucide-search class="pb-2 opacity-75 svg-icons" /> <template #icon>
<span class="my-2 text-center"> <icon-lucide-search class="pb-2 opacity-75 svg-icons" />
{{ t("state.nothing_found") }} "{{ filterText }}" </template>
</span> </HoppSmartPlaceholder>
</div>
<CollectionsGraphqlAdd <CollectionsGraphqlAdd
:show="showModalAdd" :show="showModalAdd"
@hide-modal="displayModalAdd(false)" @hide-modal="displayModalAdd(false)"
@@ -153,6 +145,7 @@ import IconArchive from "~icons/lucide/archive"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useReadonlyStream } from "@composables/stream" import { useReadonlyStream } from "@composables/stream"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { platform } from "~/platform"
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -285,6 +278,13 @@ export default defineComponent({
response: "", response: "",
}) })
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
platform: "gql",
createdNow: true,
workspaceType: "personal",
})
this.displayModalAddRequest(false) this.displayModalAddRequest(false)
}, },
addRequest(payload) { addRequest(payload) {
@@ -294,6 +294,14 @@ export default defineComponent({
}, },
onAddFolder({ name, path }) { onAddFolder({ name, path }) {
addGraphqlFolder(name, path) addGraphqlFolder(name, path)
platform.analytics?.logEvent({
type: "HOPP_CREATE_COLLECTION",
isRootCollection: false,
platform: "gql",
workspaceType: "personal",
})
this.displayModalAddFolder(false) this.displayModalAddFolder(false)
}, },
addFolder(payload) { addFolder(payload) {

View File

@@ -125,8 +125,8 @@
@hide-modal="displayModalEditFolder(false)" @hide-modal="displayModalEditFolder(false)"
/> />
<CollectionsEditRequest <CollectionsEditRequest
v-model="editingRequestName"
:show="showModalEditRequest" :show="showModalEditRequest"
v-bind:model-value="editingRequest ? editingRequest.name : ''"
:loading-state="modalLoadingState" :loading-state="modalLoadingState"
@submit="updateEditingRequest" @submit="updateEditingRequest"
@hide-modal="displayModalEditRequest(false)" @hide-modal="displayModalEditRequest(false)"
@@ -157,7 +157,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, PropType, ref, watch } from "vue" import { computed, nextTick, PropType, ref, watch } from "vue"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { Picked } from "~/helpers/types/HoppPicked" import { Picked } from "~/helpers/types/HoppPicked"
@@ -288,6 +288,7 @@ const editingFolder = ref<
const editingFolderName = ref<string | null>(null) const editingFolderName = ref<string | null>(null)
const editingFolderPath = ref<string | null>(null) const editingFolderPath = ref<string | null>(null)
const editingRequest = ref<HoppRESTRequest | null>(null) const editingRequest = ref<HoppRESTRequest | null>(null)
const editingRequestName = ref("")
const editingRequestIndex = ref<number | null>(null) const editingRequestIndex = ref<number | null>(null)
const editingRequestID = ref<string | null>(null) const editingRequestID = ref<string | null>(null)
@@ -598,11 +599,25 @@ const addNewRootCollection = (name: string) => {
}) })
) )
platform.analytics?.logEvent({
type: "HOPP_CREATE_COLLECTION",
platform: "rest",
workspaceType: "personal",
isRootCollection: true,
})
displayModalAdd(false) displayModalAdd(false)
} else if (hasTeamWriteAccess.value) { } else if (hasTeamWriteAccess.value) {
if (!collectionsType.value.selectedTeam) return if (!collectionsType.value.selectedTeam) return
modalLoadingState.value = true modalLoadingState.value = true
platform.analytics?.logEvent({
type: "HOPP_CREATE_COLLECTION",
platform: "rest",
workspaceType: "team",
isRootCollection: true,
})
pipe( pipe(
createNewRootCollection(name, collectionsType.value.selectedTeam.id), createNewRootCollection(name, collectionsType.value.selectedTeam.id),
TE.match( TE.match(
@@ -651,6 +666,13 @@ const onAddRequest = (requestName: string) => {
}, },
}) })
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
workspaceType: "personal",
createdNow: true,
platform: "rest",
})
displayModalAddRequest(false) displayModalAddRequest(false)
} else if (hasTeamWriteAccess.value) { } else if (hasTeamWriteAccess.value) {
const folder = editingFolder.value const folder = editingFolder.value
@@ -666,6 +688,13 @@ const onAddRequest = (requestName: string) => {
title: requestName, title: requestName,
} }
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
workspaceType: "team",
platform: "rest",
createdNow: true,
})
pipe( pipe(
createRequestInCollection(folder.id, data), createRequestInCollection(folder.id, data),
TE.match( TE.match(
@@ -711,6 +740,14 @@ const onAddFolder = (folderName: string) => {
if (collectionsType.value.type === "my-collections") { if (collectionsType.value.type === "my-collections") {
if (!path) return if (!path) return
addRESTFolder(folderName, path) addRESTFolder(folderName, path)
platform.analytics?.logEvent({
type: "HOPP_CREATE_COLLECTION",
workspaceType: "personal",
isRootCollection: false,
platform: "rest",
})
displayModalAddFolder(false) displayModalAddFolder(false)
} else if (hasTeamWriteAccess.value) { } else if (hasTeamWriteAccess.value) {
const folder = editingFolder.value const folder = editingFolder.value
@@ -718,6 +755,13 @@ const onAddFolder = (folderName: string) => {
modalLoadingState.value = true modalLoadingState.value = true
platform.analytics?.logEvent({
type: "HOPP_CREATE_COLLECTION",
workspaceType: "personal",
isRootCollection: false,
platform: "rest",
})
pipe( pipe(
createChildCollection(folderName, folder.id), createChildCollection(folderName, folder.id),
TE.match( TE.match(
@@ -860,6 +904,7 @@ const editRequest = (payload: {
}) => { }) => {
const { folderPath, requestIndex, request } = payload const { folderPath, requestIndex, request } = payload
editingRequest.value = request editingRequest.value = request
editingRequestName.value = request.name ?? ""
if (collectionsType.value.type === "my-collections" && folderPath) { if (collectionsType.value.type === "my-collections" && folderPath) {
editingFolderPath.value = folderPath editingFolderPath.value = folderPath
editingRequestIndex.value = parseInt(requestIndex) editingRequestIndex.value = parseInt(requestIndex)
@@ -893,6 +938,9 @@ const updateEditingRequest = (newName: string) => {
if (possibleActiveTab) { if (possibleActiveTab) {
possibleActiveTab.value.document.request.name = requestUpdated.name possibleActiveTab.value.document.request.name = requestUpdated.name
nextTick(() => {
possibleActiveTab.value.document.isDirty = false
})
} }
displayModalEditRequest(false) displayModalEditRequest(false)
@@ -931,6 +979,9 @@ const updateEditingRequest = (newName: string) => {
if (possibleTab) { if (possibleTab) {
possibleTab.value.document.request.name = requestName possibleTab.value.document.request.name = requestName
nextTick(() => {
possibleTab.value.document.isDirty = false
})
} }
} }
} }
@@ -1876,6 +1927,12 @@ const exportData = async (
} }
const exportJSONCollection = async () => { const exportJSONCollection = async () => {
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "json",
platform: "rest",
})
await getJSONCollection() await getJSONCollection()
initializeDownloadCollection(collectionJSON.value, null) initializeDownloadCollection(collectionJSON.value, null)
@@ -1887,6 +1944,12 @@ const createCollectionGist = async () => {
return return
} }
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "gist",
platform: "rest",
})
creatingGistCollection.value = true creatingGistCollection.value = true
await getJSONCollection() await getJSONCollection()
@@ -1917,6 +1980,12 @@ const importToTeams = async (collection: HoppCollection<HoppRESTRequest>[]) => {
importingMyCollections.value = true importingMyCollections.value = true
platform.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION",
exporter: "import-to-teams",
platform: "rest",
})
pipe( pipe(
importJSONToTeam( importJSONToTeam(
JSON.stringify(collection), JSON.stringify(collection),

View File

@@ -190,6 +190,12 @@ const createEnvironmentGist = async () => {
) )
toast.success(t("export.gist_created").toString()) toast.success(t("export.gist_created").toString())
platform.analytics?.logEvent({
type: "HOPP_EXPORT_ENVIRONMENT",
platform: "rest",
})
window.open(res.data.html_url) window.open(res.data.html_url)
} catch (e) { } catch (e) {
toast.error(t("error.something_went_wrong").toString()) toast.error(t("error.something_went_wrong").toString())
@@ -249,6 +255,13 @@ const openDialogChooseFileToImportFrom = () => {
const importToTeams = async (content: Environment[]) => { const importToTeams = async (content: Environment[]) => {
loading.value = true loading.value = true
platform.analytics?.logEvent({
type: "HOPP_IMPORT_ENVIRONMENT",
platform: "rest",
workspaceType: "team",
})
for (const [i, env] of content.entries()) { for (const [i, env] of content.entries()) {
if (i === content.length - 1) { if (i === content.length - 1) {
await pipe( await pipe(
@@ -301,6 +314,12 @@ const importFromJSON = () => {
return return
} }
platform.analytics?.logEvent({
type: "HOPP_IMPORT_ENVIRONMENT",
platform: "rest",
workspaceType: "personal",
})
const reader = new FileReader() const reader = new FileReader()
reader.onload = ({ target }) => { reader.onload = ({ target }) => {
@@ -352,6 +371,7 @@ const importFromPostman = ({
const environment: Environment = { name, variables: [] } const environment: Environment = { name, variables: [] }
values.forEach(({ key, value }) => environment.variables.push({ key, value })) values.forEach(({ key, value }) => environment.variables.push({ key, value }))
const environments = [environment] const environments = [environment]
importFromHoppscotch(environments) importFromHoppscotch(environments)
} }

View File

@@ -11,13 +11,14 @@
class="bg-transparent border-b border-dividerLight select-wrapper" class="bg-transparent border-b border-dividerLight select-wrapper"
> >
<HoppButtonSecondary <HoppButtonSecondary
v-if="selectedEnv.type !== 'NO_ENV_SELECTED'" :icon="IconLayers"
:label="selectedEnv.name" :label="
class="flex-1 !justify-start pr-8 rounded-none" mdAndLarger
/> ? selectedEnv.type !== 'NO_ENV_SELECTED'
<HoppButtonSecondary ? selectedEnv.name
v-else : `${t('environment.select')}`
:label="`${t('environment.select')}`" : ''
"
class="flex-1 !justify-start pr-8 rounded-none" class="flex-1 !justify-start pr-8 rounded-none"
/> />
</span> </span>
@@ -46,93 +47,136 @@
} }
" "
/> />
<div v-if="environmentType === 'my-environments'" class="flex flex-col"> <HoppSmartTabs
<hr v-if="myEnvironments.length > 0" /> v-model="selectedEnvTab"
<HoppSmartItem styles="sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-10 top-0 bg-primary"
v-for="(gen, index) in myEnvironments" render-inactive-tabs
:key="`gen-${index}`" >
:label="gen.name" <HoppSmartTab
:info-icon="index === selectedEnv.index ? IconCheck : undefined" :id="'my-environments'"
:active-info-icon="index === selectedEnv.index" :label="`${t('environment.my_environments')}`"
@click="
() => {
selectedEnvironmentIndex = { type: 'MY_ENV', index: index }
hide()
}
"
/>
</div>
<div v-else class="flex flex-col">
<div
v-if="teamEnvLoading"
class="flex flex-col items-center justify-center p-4"
> >
<HoppSmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<hr v-if="teamEnvironmentList.length > 0" />
<div v-if="isTeamSelected" class="flex flex-col">
<HoppSmartItem <HoppSmartItem
v-for="(gen, index) in teamEnvironmentList" v-for="(gen, index) in myEnvironments"
:key="`gen-team-${index}`" :key="`gen-${index}`"
:label="gen.environment.name" :icon="IconLayers"
:info-icon=" :label="gen.name"
gen.id === selectedEnv.teamEnvID ? IconCheck : undefined :info-icon="index === selectedEnv.index ? IconCheck : undefined"
" :active-info-icon="index === selectedEnv.index"
:active-info-icon="gen.id === selectedEnv.teamEnvID"
@click=" @click="
() => { () => {
selectedEnvironmentIndex = { selectedEnvironmentIndex = { type: 'MY_ENV', index: index }
type: 'TEAM_ENV',
teamEnvID: gen.id,
teamID: gen.teamID,
environment: gen.environment,
}
hide() hide()
} }
" "
/> />
</div> <HoppSmartPlaceholder
<div v-if="myEnvironments.length === 0"
v-if="!teamEnvLoading && isAdapterError" :src="`/images/states/${colorMode.value}/blockchain.svg`"
class="flex flex-col items-center py-4" :alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
>
</HoppSmartPlaceholder>
</HoppSmartTab>
<HoppSmartTab
:id="'team-environments'"
:label="`${t('environment.team_environments')}`"
:disabled="!isTeamSelected || workspace.type === 'personal'"
> >
<icon-lucide-help-circle class="mb-4 svg-icons" /> <div
{{ errorMessage }} v-if="teamListLoading"
</div> class="flex flex-col items-center justify-center p-4"
</div> >
<HoppSmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div>
<div v-else-if="isTeamSelected" class="flex flex-col">
<HoppSmartItem
v-for="(gen, index) in teamEnvironmentList"
:key="`gen-team-${index}`"
:icon="IconLayers"
:label="gen.environment.name"
:info-icon="
gen.id === selectedEnv.teamEnvID ? IconCheck : undefined
"
:active-info-icon="gen.id === selectedEnv.teamEnvID"
@click="
() => {
selectedEnvironmentIndex = {
type: 'TEAM_ENV',
teamEnvID: gen.id,
teamID: gen.teamID,
environment: gen.environment,
}
hide()
}
"
/>
<HoppSmartPlaceholder
v-if="teamEnvironmentList.length === 0"
:src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
>
</HoppSmartPlaceholder>
</div>
<div
v-if="!teamListLoading && teamAdapterError"
class="flex flex-col items-center py-4"
>
<icon-lucide-help-circle class="mb-4 svg-icons" />
{{ getErrorMessage(teamAdapterError) }}
</div>
</HoppSmartTab>
</HoppSmartTabs>
</div> </div>
</template> </template>
</tippy> </tippy>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from "vue" import { computed, ref, watch } from "vue"
import IconCheck from "~icons/lucide/check" import IconCheck from "~icons/lucide/check"
import IconLayers from "~icons/lucide/layers"
import { TippyComponent } from "vue-tippy" import { TippyComponent } from "vue-tippy"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { GQLError } from "~/helpers/backend/GQLClient" import { GQLError } from "~/helpers/backend/GQLClient"
import { Environment } from "@hoppscotch/data" import { useReadonlyStream, useStream } from "~/composables/stream"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { useStream } from "~/composables/stream"
import { import {
environments$,
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
setSelectedEnvironmentIndex, setSelectedEnvironmentIndex,
} from "~/newstore/environments" } from "~/newstore/environments"
import { changeWorkspace, workspaceStatus$ } from "~/newstore/workspace"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { useColorMode } from "@composables/theming"
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
import { useLocalState } from "~/newstore/localstate"
import { onLoggedIn } from "~/composables/auth"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
const breakpoints = useBreakpoints(breakpointsTailwind)
const mdAndLarger = breakpoints.greater("md")
const t = useI18n() const t = useI18n()
const colorMode = useColorMode()
type EnvironmentType = "my-environments" | "team-environments" type EnvironmentType = "my-environments" | "team-environments"
const props = defineProps<{ const myEnvironments = useReadonlyStream(environments$, [])
environmentType: EnvironmentType
myEnvironments: Environment[] const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
teamEnvironmentList: TeamEnvironment[]
teamEnvLoading: boolean const teamEnvListAdapter = new TeamEnvironmentAdapter(undefined)
isAdapterError: boolean const teamListLoading = useReadonlyStream(teamEnvListAdapter.loading$, false)
errorMessage: GQLError<string> const teamAdapterError = useReadonlyStream(teamEnvListAdapter.error$, null)
isTeamSelected: boolean const teamEnvironmentList = useReadonlyStream(
}>() teamEnvListAdapter.teamEnvironmentList$,
[]
)
const selectedEnvironmentIndex = useStream( const selectedEnvironmentIndex = useStream(
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
@@ -140,15 +184,67 @@ const selectedEnvironmentIndex = useStream(
setSelectedEnvironmentIndex setSelectedEnvironmentIndex
) )
const isTeamSelected = computed(
() => workspace.value.type === "team" && workspace.value.teamID !== undefined
)
const selectedEnvTab = ref<EnvironmentType>("my-environments")
watch(
() => workspace.value,
(newVal) => {
if (newVal.type === "personal") {
selectedEnvTab.value = "my-environments"
} else {
selectedEnvTab.value = "team-environments"
if (newVal.teamID) {
teamEnvListAdapter.changeTeamID(newVal.teamID)
}
}
}
)
// TeamList-Adapter
const teamListAdapter = new TeamListAdapter(true)
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
const teamListFetched = ref(false)
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
onLoggedIn(() => {
!teamListAdapter.isInitialized && teamListAdapter.initialize()
})
const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
REMEMBERED_TEAM_ID.value = team.id
changeWorkspace({
teamID: team.id,
teamName: team.name,
type: "team",
})
}
watch(
() => myTeams.value,
(newTeams) => {
if (newTeams && !teamListFetched.value) {
teamListFetched.value = true
if (REMEMBERED_TEAM_ID.value) {
const team = newTeams.find((t) => t.id === REMEMBERED_TEAM_ID.value)
if (team) switchToTeamWorkspace(team)
}
}
}
)
const selectedEnv = computed(() => { const selectedEnv = computed(() => {
if (selectedEnvironmentIndex.value.type === "MY_ENV") { if (selectedEnvironmentIndex.value.type === "MY_ENV") {
return { return {
type: "MY_ENV", type: "MY_ENV",
index: selectedEnvironmentIndex.value.index, index: selectedEnvironmentIndex.value.index,
name: props.myEnvironments[selectedEnvironmentIndex.value.index].name, name: myEnvironments.value[selectedEnvironmentIndex.value.index].name,
} }
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") { } else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
const teamEnv = props.teamEnvironmentList.find( const teamEnv = teamEnvironmentList.value.find(
(env) => (env) =>
env.id === env.id ===
(selectedEnvironmentIndex.value.type === "TEAM_ENV" && (selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
@@ -170,4 +266,17 @@ const selectedEnv = computed(() => {
// Template refs // Template refs
const tippyActions = ref<TippyComponent | null>(null) const tippyActions = ref<TippyComponent | null>(null)
const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") {
return t("error.network_error")
} else {
switch (err.error) {
case "team_environment/not_found":
return t("team_environment.not_found")
default:
return t("error.something_went_wrong")
}
}
}
</script> </script>

View File

@@ -4,15 +4,6 @@
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-col flex-shrink-0 overflow-x-auto bg-primary"
> >
<WorkspaceCurrent :section="t('tab.environments')" /> <WorkspaceCurrent :section="t('tab.environments')" />
<EnvironmentsSelector
:environment-type="environmentType.type"
:my-environments="myEnvironments"
:team-env-loading="loading"
:team-environment-list="teamEnvironmentList"
:is-adapter-error="adapterError !== null"
:error-message="adapterError ? getErrorMessage(adapterError) : ''"
:is-team-selected="environmentType.selectedTeam !== undefined"
/>
<EnvironmentsMyEnvironment <EnvironmentsMyEnvironment
environment-index="Global" environment-index="Global"
:environment="globalEnvironment" :environment="globalEnvironment"
@@ -46,13 +37,11 @@ import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { useReadonlyStream, useStream } from "@composables/stream" import { useReadonlyStream, useStream } from "@composables/stream"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { import {
environments$,
globalEnv$, globalEnv$,
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
setSelectedEnvironmentIndex, setSelectedEnvironmentIndex,
} from "~/newstore/environments" } from "~/newstore/environments"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter" import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { GQLError } from "~/helpers/backend/GQLClient"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { workspaceStatus$ } from "~/newstore/workspace" import { workspaceStatus$ } from "~/newstore/workspace"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter" import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
@@ -147,24 +136,19 @@ onLoggedIn(() => {
const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" }) const workspace = useReadonlyStream(workspaceStatus$, { type: "personal" })
// Used to switch environment type and team when user switch workspace in the global workspace switcher // Switch to my environments if workspace is personal and to team environments if workspace is team
// Check if there is a teamID in the workspace, if yes, switch to team environment and select the team // also resets selected environment if workspace is personal and the previous selected environment was a team environment
// If there is no teamID, switch to my environment
watch(workspace, (newWorkspace) => { watch(workspace, (newWorkspace) => {
if (newWorkspace.type === "personal") { if (newWorkspace.type === "personal") {
switchToMyEnvironments() switchToMyEnvironments()
setSelectedEnvironmentIndex({
type: "NO_ENV_SELECTED",
})
} else if (newWorkspace.type === "team") {
const team = myTeams.value?.find((t) => t.id === newWorkspace.teamID)
updateSelectedTeam(team)
if (selectedEnvironmentIndex.value.type !== "MY_ENV") { if (selectedEnvironmentIndex.value.type !== "MY_ENV") {
setSelectedEnvironmentIndex({ setSelectedEnvironmentIndex({
type: "NO_ENV_SELECTED", type: "NO_ENV_SELECTED",
}) })
} }
} else if (newWorkspace.type === "team") {
const team = myTeams.value?.find((t) => t.id === newWorkspace.teamID)
updateSelectedTeam(team)
} }
}) })
@@ -207,8 +191,6 @@ defineActionHandler(
} }
) )
const myEnvironments = useReadonlyStream(environments$, [])
const selectedEnvironmentIndex = useStream( const selectedEnvironmentIndex = useStream(
selectedEnvironmentIndex$, selectedEnvironmentIndex$,
{ type: "NO_ENV_SELECTED" }, { type: "NO_ENV_SELECTED" },
@@ -251,17 +233,4 @@ watch(
}, },
{ deep: true } { deep: true }
) )
const getErrorMessage = (err: GQLError<string>) => {
if (err.type === "network_error") {
return t("error.network_error")
} else {
switch (err.error) {
case "team_environment/not_found":
return t("team_environment.not_found")
default:
return t("error.something_went_wrong")
}
}
}
</script> </script>

View File

@@ -79,26 +79,19 @@
/> />
</div> </div>
</div> </div>
<div <HoppSmartPlaceholder
v-if="vars.length === 0" v-if="vars.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight" :src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
> >
<img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-4 text-center">
{{ t("empty.environments") }}
</span>
<HoppButtonSecondary <HoppButtonSecondary
:label="`${t('add.new')}`" :label="`${t('add.new')}`"
filled filled
class="mb-4" class="mb-4"
@click="addEnvironmentVariable" @click="addEnvironmentVariable"
/> />
</div> </HoppSmartPlaceholder>
</div> </div>
</div> </div>
</template> </template>
@@ -148,6 +141,7 @@ import { useToast } from "@composables/toast"
import { useReadonlyStream } from "@composables/stream" import { useReadonlyStream } from "@composables/stream"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { environmentsStore } from "~/newstore/environments" import { environmentsStore } from "~/newstore/environments"
import { platform } from "~/platform"
type EnvironmentVariable = { type EnvironmentVariable = {
id: number id: number
@@ -165,8 +159,8 @@ const props = withDefaults(
defineProps<{ defineProps<{
show: boolean show: boolean
action: "edit" | "new" action: "edit" | "new"
editingEnvironmentIndex: number | "Global" | null editingEnvironmentIndex?: number | "Global" | null
editingVariableName: string | null editingVariableName?: string | null
envVars?: () => Environment["variables"] envVars?: () => Environment["variables"]
}>(), }>(),
{ {
@@ -311,6 +305,11 @@ const saveEnvironment = () => {
index: envList.value.length - 1, index: envList.value.length - 1,
}) })
toast.success(`${t("environment.created")}`) toast.success(`${t("environment.created")}`)
platform.analytics?.logEvent({
type: "HOPP_CREATE_ENVIRONMENT",
workspaceType: "personal",
})
} else if (props.editingEnvironmentIndex === "Global") { } else if (props.editingEnvironmentIndex === "Global") {
// Editing the Global environment // Editing the Global environment
setGlobalEnvVariables(environmentUpdated.variables) setGlobalEnvVariables(environmentUpdated.variables)

View File

@@ -32,19 +32,12 @@
:environment="environment" :environment="environment"
@edit-environment="editEnvironment(index)" @edit-environment="editEnvironment(index)"
/> />
<div <HoppSmartPlaceholder
v-if="environments.length === 0" v-if="environments.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight" :src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
> >
<img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-4 text-center">
{{ t("empty.environments") }}
</span>
<HoppButtonSecondary <HoppButtonSecondary
:label="`${t('add.new')}`" :label="`${t('add.new')}`"
filled filled
@@ -52,7 +45,7 @@
class="mb-4" class="mb-4"
@click="displayModalAdd(true)" @click="displayModalAdd(true)"
/> />
</div> </HoppSmartPlaceholder>
<EnvironmentsMyDetails <EnvironmentsMyDetails
:show="showModalDetails" :show="showModalDetails"
:action="action" :action="action"

View File

@@ -83,19 +83,12 @@
/> />
</div> </div>
</div> </div>
<div <HoppSmartPlaceholder
v-if="vars.length === 0" v-if="vars.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight" :src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
> >
<img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-4 text-center">
{{ t("empty.environments") }}
</span>
<HoppButtonSecondary <HoppButtonSecondary
v-if="isViewer" v-if="isViewer"
disabled disabled
@@ -110,7 +103,7 @@
class="mb-4" class="mb-4"
@click="addEnvironmentVariable" @click="addEnvironmentVariable"
/> />
</div> </HoppSmartPlaceholder>
</div> </div>
</div> </div>
</template> </template>
@@ -140,7 +133,7 @@ import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option" import * as O from "fp-ts/Option"
import * as TE from "fp-ts/TaskEither" import * as TE from "fp-ts/TaskEither"
import { flow, pipe } from "fp-ts/function" import { flow, pipe } from "fp-ts/function"
import { parseTemplateStringE } from "@hoppscotch/data" import { Environment, parseTemplateStringE } from "@hoppscotch/data"
import { refAutoReset } from "@vueuse/core" import { refAutoReset } from "@vueuse/core"
import { clone } from "lodash-es" import { clone } from "lodash-es"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
@@ -156,6 +149,7 @@ import IconTrash from "~icons/lucide/trash"
import IconTrash2 from "~icons/lucide/trash-2" import IconTrash2 from "~icons/lucide/trash-2"
import IconDone from "~icons/lucide/check" import IconDone from "~icons/lucide/check"
import IconPlus from "~icons/lucide/plus" import IconPlus from "~icons/lucide/plus"
import { platform } from "~/platform"
type EnvironmentVariable = { type EnvironmentVariable = {
id: number id: number
@@ -173,16 +167,20 @@ const props = withDefaults(
defineProps<{ defineProps<{
show: boolean show: boolean
action: "edit" | "new" action: "edit" | "new"
editingEnvironment: TeamEnvironment | null editingEnvironment?: TeamEnvironment | null
editingTeamId: string | undefined editingTeamId: string | undefined
editingVariableName: string | null editingVariableName?: string | null
isViewer: boolean isViewer?: boolean
envVars?: () => Environment["variables"]
}>(), }>(),
{ {
show: false, show: false,
action: "edit", action: "edit",
editingEnvironment: null, editingEnvironment: null,
editingTeamId: "", editingTeamId: "",
editingVariableName: null,
isViewer: false,
envVars: () => [],
} }
) )
@@ -226,10 +224,16 @@ watch(
() => props.show, () => props.show,
(show) => { (show) => {
if (show) { if (show) {
if (props.editingEnvironment === null) { if (props.action === "new") {
name.value = null name.value = null
vars.value = [] vars.value = pipe(
} else { props.envVars() ?? [],
A.map((e: { key: string; value: string }) => ({
id: idTicker.value++,
env: clone(e),
}))
)
} else if (props.editingEnvironment !== null) {
name.value = props.editingEnvironment.environment.name ?? null name.value = props.editingEnvironment.environment.name ?? null
vars.value = pipe( vars.value = pipe(
props.editingEnvironment.environment.variables ?? [], props.editingEnvironment.environment.variables ?? [],
@@ -284,6 +288,11 @@ const saveEnvironment = async () => {
) )
if (props.action === "new") { if (props.action === "new") {
platform.analytics?.logEvent({
type: "HOPP_CREATE_ENVIRONMENT",
workspaceType: "team",
})
await pipe( await pipe(
createTeamEnvironment( createTeamEnvironment(
JSON.stringify(filterdVariables), JSON.stringify(filterdVariables),

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div <div
class="sticky z-10 flex justify-between flex-1 flex-shrink-0 overflow-x-auto border-b top-upperSecondaryStickyFold border-dividerLight bg-primary" class="sticky z-10 flex justify-between flex-1 flex-shrink-0 overflow-x-auto border-b top-upperPrimaryStickyFold border-dividerLight bg-primary"
> >
<HoppButtonSecondary <HoppButtonSecondary
v-if="team === undefined || team.myRole === 'VIEWER'" v-if="team === undefined || team.myRole === 'VIEWER'"
@@ -43,19 +43,12 @@
/> />
</div> </div>
</div> </div>
<div <HoppSmartPlaceholder
v-if="!loading && teamEnvironments.length === 0 && !adapterError" v-if="!loading && teamEnvironments.length === 0 && !adapterError"
class="flex flex-col items-center justify-center p-4 text-secondaryLight" :src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
> >
<img
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.environments')}`"
/>
<span class="pb-4 text-center">
{{ t("empty.environments") }}
</span>
<HoppButtonSecondary <HoppButtonSecondary
v-if="team === undefined || team.myRole === 'VIEWER'" v-if="team === undefined || team.myRole === 'VIEWER'"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
@@ -74,7 +67,7 @@
class="mb-4" class="mb-4"
@click="displayModalAdd(true)" @click="displayModalAdd(true)"
/> />
</div> </HoppSmartPlaceholder>
<div v-else-if="!loading"> <div v-else-if="!loading">
<EnvironmentsTeamsEnvironment <EnvironmentsTeamsEnvironment
v-for="(environment, index) in JSON.parse( v-for="(environment, index) in JSON.parse(

View File

@@ -1,12 +1,12 @@
<template> <template>
<div class="flex" @click="OpenLogoutModal()"> <div class="flex" @click="openLogoutModal()">
<HoppSmartItem <HoppSmartItem
ref="logoutItem" ref="logoutItem"
:icon="IconLogOut" :icon="IconLogOut"
:label="`${t('auth.logout')}`" :label="`${t('auth.logout')}`"
:outline="outline" :outline="outline"
:shortcut="shortcut" :shortcut="shortcut"
@click="OpenLogoutModal()" @click="openLogoutModal()"
/> />
<HoppSmartConfirmModal <HoppSmartConfirmModal
:show="confirmLogout" :show="confirmLogout"
@@ -23,6 +23,7 @@ import IconLogOut from "~icons/lucide/log-out"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { platform } from "~/platform" import { platform } from "~/platform"
import { defineActionHandler } from "~/helpers/actions"
defineProps({ defineProps({
outline: { outline: {
@@ -55,8 +56,12 @@ const logout = async () => {
} }
} }
const OpenLogoutModal = () => { const openLogoutModal = () => {
emit("confirm-logout") emit("confirm-logout")
confirmLogout.value = true confirmLogout.value = true
} }
defineActionHandler("user.logout", () => {
openLogoutModal()
})
</script> </script>

View File

@@ -114,19 +114,12 @@
/> />
</div> </div>
</div> </div>
<div <HoppSmartPlaceholder
v-if="authType === 'none'" v-if="authType === 'none'"
class="flex flex-col items-center justify-center p-4 text-secondaryLight" :src="`/images/states/${colorMode.value}/login.svg`"
:alt="`${t('empty.authorization')}`"
:text="t('empty.authorization')"
> >
<img
:src="`/images/states/${colorMode.value}/login.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.authorization')}`"
/>
<span class="pb-4 text-center">
{{ t("empty.authorization") }}
</span>
<HoppButtonSecondary <HoppButtonSecondary
outline outline
:label="t('app.documentation')" :label="t('app.documentation')"
@@ -136,40 +129,58 @@
reverse reverse
class="mb-4" class="mb-4"
/> />
</div> </HoppSmartPlaceholder>
<div v-else class="flex flex-1 border-b border-dividerLight"> <div v-else class="flex flex-1 border-b border-dividerLight">
<div class="w-2/3 border-r border-dividerLight"> <div class="w-2/3 border-r border-dividerLight">
<div v-if="authType === 'basic'"> <div v-if="authType === 'basic'">
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput <SmartEnvInput
v-model="basicUsername" v-model="basicUsername"
:environment-highlights="false"
:placeholder="t('authorization.username')" :placeholder="t('authorization.username')"
/> />
</div> </div>
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput <SmartEnvInput
v-model="basicPassword" v-model="basicPassword"
:environment-highlights="false"
:placeholder="t('authorization.password')" :placeholder="t('authorization.password')"
/> />
</div> </div>
</div> </div>
<div v-if="authType === 'bearer'"> <div v-if="authType === 'bearer'">
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="bearerToken" placeholder="Token" /> <SmartEnvInput
v-model="bearerToken"
:environment-highlights="false"
placeholder="Token"
/>
</div> </div>
</div> </div>
<div v-if="authType === 'oauth-2'"> <div v-if="authType === 'oauth-2'">
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="oauth2Token" placeholder="Token" /> <SmartEnvInput
v-model="oauth2Token"
:environment-highlights="false"
placeholder="Token"
/>
</div> </div>
<HttpOAuth2Authorization /> <HttpOAuth2Authorization />
</div> </div>
<div v-if="authType === 'api-key'"> <div v-if="authType === 'api-key'">
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="apiKey" placeholder="Key" /> <SmartEnvInput
v-model="apiKey"
:environment-highlights="false"
placeholder="Key"
/>
</div> </div>
<div class="flex flex-1 border-b border-dividerLight"> <div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="apiValue" placeholder="Value" /> <SmartEnvInput
v-model="apiValue"
:environment-highlights="false"
placeholder="Value"
/>
</div> </div>
<div class="flex items-center border-b border-dividerLight"> <div class="flex items-center border-b border-dividerLight">
<span class="flex items-center"> <span class="flex items-center">

View File

@@ -17,6 +17,7 @@
<HoppButtonPrimary <HoppButtonPrimary
id="get" id="get"
name="get" name="get"
:loading="isLoading"
:label="!connected ? t('action.connect') : t('action.disconnect')" :label="!connected ? t('action.connect') : t('action.disconnect')"
class="w-32" class="w-32"
@click="onConnectClick" @click="onConnectClick"
@@ -31,7 +32,12 @@ import { GQLConnection } from "~/helpers/GQLConnection"
import { getCurrentStrategyID } from "~/helpers/network" import { getCurrentStrategyID } from "~/helpers/network"
import { useReadonlyStream, useStream } from "@composables/stream" import { useReadonlyStream, useStream } from "@composables/stream"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { gqlHeaders$, gqlURL$, setGQLURL } from "~/newstore/GQLSession" import {
gqlAuth$,
gqlHeaders$,
gqlURL$,
setGQLURL,
} from "~/newstore/GQLSession"
const t = useI18n() const t = useI18n()
@@ -40,15 +46,21 @@ const props = defineProps<{
}>() }>()
const connected = useReadonlyStream(props.conn.connected$, false) const connected = useReadonlyStream(props.conn.connected$, false)
const isLoading = useReadonlyStream(props.conn.isLoading$, false)
const headers = useReadonlyStream(gqlHeaders$, []) const headers = useReadonlyStream(gqlHeaders$, [])
const auth = useReadonlyStream(gqlAuth$, {
authType: "none",
authActive: true,
})
const url = useStream(gqlURL$, "", setGQLURL) const url = useStream(gqlURL$, "", setGQLURL)
const onConnectClick = () => { const onConnectClick = () => {
if (!connected.value) { if (!connected.value) {
props.conn.connect(url.value, headers.value as any) props.conn.connect(url.value, headers.value as any, auth.value)
platform.analytics?.logHoppRequestRunToAnalytics({ platform.analytics?.logEvent({
type: "HOPP_REQUEST_RUN",
platform: "graphql-schema", platform: "graphql-schema",
strategy: getCurrentStrategyID(), strategy: getCurrentStrategyID(),
}) })

View File

@@ -289,19 +289,12 @@
</div> </div>
</template> </template>
</draggable> </draggable>
<div <HoppSmartPlaceholder
v-if="workingHeaders.length === 0" v-if="workingHeaders.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight" :src="`/images/states/${colorMode.value}/add_category.svg`"
:alt="`${t('empty.headers')}`"
:text="t('empty.headers')"
> >
<img
:src="`/images/states/${colorMode.value}/add_category.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.headers')}`"
/>
<span class="pb-4 text-center">
{{ t("empty.headers") }}
</span>
<HoppButtonSecondary <HoppButtonSecondary
:label="`${t('add.new')}`" :label="`${t('add.new')}`"
filled filled
@@ -309,7 +302,7 @@
class="mb-4" class="mb-4"
@click="addHeader" @click="addHeader"
/> />
</div> </HoppSmartPlaceholder>
</div> </div>
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`"> <HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
@@ -748,7 +741,8 @@ const runQuery = async () => {
console.error(e) console.error(e)
} }
platform.analytics?.logHoppRequestRunToAnalytics({ platform.analytics?.logEvent({
type: "HOPP_REQUEST_RUN",
platform: "graphql-query", platform: "graphql-query",
strategy: getCurrentStrategyID(), strategy: getCurrentStrategyID(),
}) })

View File

@@ -1,12 +1,13 @@
<template> <template>
<div class="flex flex-col flex-1 overflow-auto whitespace-nowrap"> <div class="flex flex-col flex-1 overflow-auto whitespace-nowrap">
<div <HoppSmartPlaceholder
v-if="responseString === 'loading'" v-if="responseString === 'loading'"
class="flex flex-col items-center justify-center p-4 text-secondaryLight" :text="t('state.loading')"
> >
<HoppSmartSpinner class="my-4" /> <template #icon>
<span class="text-secondaryLight">{{ t("state.loading") }}</span> <HoppSmartSpinner class="my-4" />
</div> </template>
</HoppSmartPlaceholder>
<div v-else-if="responseString" class="flex flex-col flex-1"> <div v-else-if="responseString" class="flex flex-col flex-1">
<div <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 items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight"

View File

@@ -24,25 +24,18 @@
:icon="IconBookOpen" :icon="IconBookOpen"
:label="`${t('tab.documentation')}`" :label="`${t('tab.documentation')}`"
> >
<div <HoppSmartPlaceholder
v-if=" v-if="
queryFields.length === 0 && queryFields.length === 0 &&
mutationFields.length === 0 && mutationFields.length === 0 &&
subscriptionFields.length === 0 && subscriptionFields.length === 0 &&
graphqlTypes.length === 0 graphqlTypes.length === 0
" "
class="flex flex-col items-center justify-center p-4 text-secondaryLight" :src="`/images/states/${colorMode.value}/add_comment.svg`"
:alt="`${t('empty.documentation')}`"
:text="t('empty.documentation')"
> >
<img </HoppSmartPlaceholder>
:src="`/images/states/${colorMode.value}/add_comment.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.documentation')}`"
/>
<span class="mb-4 text-center">
{{ t("empty.documentation") }}
</span>
</div>
<div v-else> <div v-else>
<div <div
class="sticky top-0 z-10 flex flex-shrink-0 overflow-x-auto bg-primary" class="sticky top-0 z-10 flex flex-shrink-0 overflow-x-auto bg-primary"
@@ -172,20 +165,13 @@
ref="schemaEditor" ref="schemaEditor"
class="flex flex-col flex-1" class="flex flex-col flex-1"
></div> ></div>
<div <HoppSmartPlaceholder
v-else v-else
class="flex flex-col items-center justify-center p-4 text-secondaryLight" :src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.schema')}`"
:text="t('empty.schema')"
> >
<img </HoppSmartPlaceholder>
:src="`/images/states/${colorMode.value}/blockchain.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.schema')}`"
/>
<span class="mb-4 text-center">
{{ t("empty.schema") }}
</span>
</div>
</HoppSmartTab> </HoppSmartTab>
</HoppSmartTabs> </HoppSmartTabs>
</template> </template>

View File

@@ -72,9 +72,11 @@
class="flex items-center justify-between flex-1 min-w-0 transition cursor-pointer focus:outline-none text-secondaryLight text-tiny group" class="flex items-center justify-between flex-1 min-w-0 transition cursor-pointer focus:outline-none text-secondaryLight text-tiny group"
> >
<span <span
class="inline-flex items-center justify-center px-4 py-2 transition group-hover:text-secondary" class="inline-flex items-center justify-center px-4 py-2 transition group-hover:text-secondary truncate"
> >
<icon-lucide-chevron-right class="mr-2 indicator" /> <icon-lucide-chevron-right
class="mr-2 indicator flex flex-shrink-0"
/>
<span <span
:class="[ :class="[
{ 'capitalize-first': groupSelection === 'TIME' }, { 'capitalize-first': groupSelection === 'TIME' },
@@ -106,31 +108,23 @@
/> />
</details> </details>
</div> </div>
<div <HoppSmartPlaceholder
v-if="history.length === 0" v-if="history.length === 0"
class="flex flex-col items-center justify-center p-4 text-secondaryLight" :src="`/images/states/${colorMode.value}/history.svg`"
:alt="`${t('empty.history')}`"
:text="t('empty.history')"
> >
<img </HoppSmartPlaceholder>
:src="`/images/states/${colorMode.value}/history.svg`" <HoppSmartPlaceholder
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.history')}`"
/>
<span class="mb-4 text-center">
{{ t("empty.history") }}
</span>
</div>
<div
v-else-if=" v-else-if="
Object.keys(filteredHistoryGroups).length === 0 || Object.keys(filteredHistoryGroups).length === 0 ||
filteredHistory.length === 0 filteredHistory.length === 0
" "
class="flex flex-col items-center justify-center p-4 text-secondaryLight" :text="`${t('state.nothing_found')} ‟${filterText || filterSelection}”`"
> >
<icon-lucide-search class="pb-2 opacity-75 svg-icons" /> <template #icon>
<span class="mt-2 mb-4 text-center"> <icon-lucide-search class="pb-2 opacity-75 svg-icons" />
{{ t("state.nothing_found") }} "{{ filterText || filterSelection }}" </template>
</span>
<HoppButtonSecondary <HoppButtonSecondary
:label="t('action.clear')" :label="t('action.clear')"
outline outline
@@ -141,7 +135,7 @@
} }
" "
/> />
</div> </HoppSmartPlaceholder>
<HoppSmartConfirmModal <HoppSmartConfirmModal
:show="confirmRemove" :show="confirmRemove"
:title="`${t('confirm.remove_history')}`" :title="`${t('confirm.remove_history')}`"
@@ -182,6 +176,7 @@ import {
import HistoryRestCard from "./rest/Card.vue" import HistoryRestCard from "./rest/Card.vue"
import HistoryGraphqlCard from "./graphql/Card.vue" import HistoryGraphqlCard from "./graphql/Card.vue"
import { createNewTab } from "~/helpers/rest/tab" import { createNewTab } from "~/helpers/rest/tab"
import { defineActionHandler } from "~/helpers/actions"
type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry
@@ -335,4 +330,8 @@ const toggleStar = (entry: HistoryEntry) => {
toggleRESTHistoryEntryStar(entry as RESTHistoryEntry) toggleRESTHistoryEntryStar(entry as RESTHistoryEntry)
else toggleGraphqlHistoryEntryStar(entry as GQLHistoryEntry) else toggleGraphqlHistoryEntryStar(entry as GQLHistoryEntry)
} }
defineActionHandler("history.clear", () => {
confirmRemove.value = true
})
</script> </script>

View File

@@ -113,17 +113,12 @@
/> />
</div> </div>
</div> </div>
<div <HoppSmartPlaceholder
v-if="auth.authType === 'none'" v-if="auth.authType === 'none'"
class="flex flex-col items-center justify-center p-4 text-secondaryLight" :src="`/images/states/${colorMode.value}/login.svg`"
:alt="`${t('empty.authorization')}`"
:text="t('empty.authorization')"
> >
<img
:src="`/images/states/${colorMode.value}/login.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.authorization')}`"
/>
<span class="pb-4 text-center">{{ t("empty.authorization") }}</span>
<HoppButtonSecondary <HoppButtonSecondary
outline outline
:label="t('app.documentation')" :label="t('app.documentation')"
@@ -133,7 +128,7 @@
reverse reverse
class="mb-4" class="mb-4"
/> />
</div> </HoppSmartPlaceholder>
<div v-else class="flex flex-1 border-b border-dividerLight"> <div v-else class="flex flex-1 border-b border-dividerLight">
<div class="w-2/3 border-r border-dividerLight"> <div class="w-2/3 border-r border-dividerLight">
<div v-if="auth.authType === 'basic'"> <div v-if="auth.authType === 'basic'">

View File

@@ -102,17 +102,12 @@
v-model="body" v-model="body"
/> />
<HttpRawBody v-else-if="body.contentType !== null" v-model="body" /> <HttpRawBody v-else-if="body.contentType !== null" v-model="body" />
<div <HoppSmartPlaceholder
v-if="body.contentType == null" v-if="body.contentType == null"
class="flex flex-col items-center justify-center p-4 text-secondaryLight" :src="`/images/states/${colorMode.value}/upload_single_file.svg`"
:alt="`${t('empty.body')}`"
:text="t('empty.body')"
> >
<img
:src="`/images/states/${colorMode.value}/upload_single_file.svg`"
loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="`${t('empty.body')}`"
/>
<span class="pb-4 text-center">{{ t("empty.body") }}</span>
<HoppButtonSecondary <HoppButtonSecondary
outline outline
:label="`${t('app.documentation')}`" :label="`${t('app.documentation')}`"
@@ -122,7 +117,7 @@
reverse reverse
class="mb-4" class="mb-4"
/> />
</div> </HoppSmartPlaceholder>
</div> </div>
</template> </template>

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