Compare commits
37 Commits
test/backe
...
pr/AndrewB
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a60303ef7c | ||
|
|
53e4863a80 | ||
|
|
3340d0813c | ||
|
|
7c5722586c | ||
|
|
6b38363fab | ||
|
|
14202a3eed | ||
|
|
ea03223b8e | ||
|
|
235deb113c | ||
|
|
833e11ab0b | ||
|
|
0d101673d2 | ||
|
|
6fe565c30f | ||
|
|
4164de5a9e | ||
|
|
1ff35f45ee | ||
|
|
3bf8288de3 | ||
|
|
8c48d41eed | ||
|
|
38215be3bd | ||
|
|
edf57da9be | ||
|
|
be61b62825 | ||
|
|
9dbce74f5e | ||
|
|
db1cf5cc08 | ||
|
|
09360abf81 | ||
|
|
355bd62b8d | ||
|
|
5650de1183 | ||
|
|
2ee8614b93 | ||
|
|
5632334c9a | ||
|
|
780dd8a713 | ||
|
|
7db3c6d290 | ||
|
|
c765270dfe | ||
|
|
03f667c21d | ||
|
|
f79f3078dc | ||
|
|
6e29a2f6d4 | ||
|
|
6304fd50c3 | ||
|
|
6f35574d68 | ||
|
|
fc3e3aeaec | ||
|
|
331d482b22 | ||
|
|
b07243f131 | ||
|
|
81a7e23a12 |
@@ -31,6 +31,7 @@ MICROSOFT_CLIENT_ID="************************************************"
|
||||
MICROSOFT_CLIENT_SECRET="************************************************"
|
||||
MICROSOFT_CALLBACK_URL="http://localhost:3170/v1/auth/microsoft/callback"
|
||||
MICROSOFT_SCOPE="user.read"
|
||||
MICROSOFT_TENANT="common"
|
||||
|
||||
# Mailer config
|
||||
MAILER_SMTP_URL="smtps://user@domain.com:pass@smtp.domain.com"
|
||||
|
||||
42
.github/workflows/ui.yml
vendored
Normal file
42
.github/workflows/ui.yml
vendored
Normal 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 }}
|
||||
24
packages/dioc/.gitignore
vendored
Normal file
24
packages/dioc/.gitignore
vendored
Normal 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
141
packages/dioc/README.md
Normal 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
2
packages/dioc/index.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from "./dist/main.d.ts"
|
||||
export * from "./dist/main.d.ts"
|
||||
147
packages/dioc/lib/container.ts
Normal file
147
packages/dioc/lib/container.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
2
packages/dioc/lib/main.ts
Normal file
2
packages/dioc/lib/main.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./container"
|
||||
export * from "./service"
|
||||
65
packages/dioc/lib/service.ts
Normal file
65
packages/dioc/lib/service.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
33
packages/dioc/lib/testing.ts
Normal file
33
packages/dioc/lib/testing.ts
Normal 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
34
packages/dioc/lib/vue.ts
Normal 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)
|
||||
}
|
||||
54
packages/dioc/package.json
Normal file
54
packages/dioc/package.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
262
packages/dioc/test/container.spec.ts
Normal file
262
packages/dioc/test/container.spec.ts
Normal 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([])
|
||||
})
|
||||
})
|
||||
})
|
||||
66
packages/dioc/test/service.spec.ts
Normal file
66
packages/dioc/test/service.spec.ts
Normal 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")
|
||||
})
|
||||
})
|
||||
})
|
||||
92
packages/dioc/test/test-container.spec.ts
Normal file
92
packages/dioc/test/test-container.spec.ts
Normal 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
2
packages/dioc/testing.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from "./dist/testing.d.ts"
|
||||
export * from "./dist/testing.d.ts"
|
||||
21
packages/dioc/tsconfig.json
Normal file
21
packages/dioc/tsconfig.json
Normal 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"]
|
||||
}
|
||||
16
packages/dioc/vite.config.ts
Normal file
16
packages/dioc/vite.config.ts
Normal 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'],
|
||||
}
|
||||
},
|
||||
})
|
||||
7
packages/dioc/vitest.config.ts
Normal file
7
packages/dioc/vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from "vitest/config"
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
|
||||
}
|
||||
})
|
||||
2
packages/dioc/vue.d.ts
vendored
Normal file
2
packages/dioc/vue.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from "./dist/vue.d.ts"
|
||||
export * from "./dist/vue.d.ts"
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hoppscotch-backend",
|
||||
"version": "2023.4.6",
|
||||
"version": "2023.4.7",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -236,11 +236,11 @@ export class AdminService {
|
||||
const user = await this.userService.findUserByEmail(userEmail);
|
||||
if (O.isNone(user)) return E.left(USER_NOT_FOUND);
|
||||
|
||||
const isUserAlreadyMember = await this.teamService.getTeamMemberTE(
|
||||
const teamMember = await this.teamService.getTeamMemberTE(
|
||||
teamID,
|
||||
user.value.uid,
|
||||
)();
|
||||
if (E.left(isUserAlreadyMember)) {
|
||||
if (E.isLeft(teamMember)) {
|
||||
const addedUser = await this.teamService.addMemberToTeamWithEmail(
|
||||
teamID,
|
||||
userEmail,
|
||||
@@ -248,6 +248,18 @@ export class AdminService {
|
||||
);
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) {
|
||||
clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
|
||||
callbackURL: process.env.MICROSOFT_CALLBACK_URL,
|
||||
scope: [process.env.MICROSOFT_SCOPE],
|
||||
passReqToCallback: true,
|
||||
tenant: process.env.MICROSOFT_TENANT,
|
||||
store: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const AUTH_FAIL = 'auth/fail';
|
||||
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)
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* Invalid or non-existent TEAM ENVIRONMMENT ID
|
||||
* Invalid or non-existent TEAM ENVIRONMENT ID
|
||||
* (TeamEnvironmentsService)
|
||||
*/
|
||||
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;
|
||||
|
||||
/*
|
||||
* Global environment doesnt exists for the user
|
||||
* Global environment doesn't exist for the user
|
||||
* (UserEnvironmentsService)
|
||||
*/
|
||||
export const USER_ENVIRONMENT_GLOBAL_ENV_DOES_NOT_EXISTS =
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as T from 'fp-ts/Task';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import * as TO from 'fp-ts/TaskOption';
|
||||
import * as TE from 'fp-ts/TaskEither';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { pipe, flow, constVoid } from 'fp-ts/function';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
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 { TeamService } from 'src/team/team.service';
|
||||
import {
|
||||
INVALID_EMAIL,
|
||||
TEAM_INVITE_ALREADY_MEMBER,
|
||||
TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
||||
TEAM_INVITE_MEMBER_HAS_INVITE,
|
||||
@@ -19,6 +21,7 @@ import { TeamInvitation } from './team-invitation.model';
|
||||
import { MailerService } from 'src/mailer/mailer.service';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { validateEmail } from '../utils';
|
||||
|
||||
@Injectable()
|
||||
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(
|
||||
creator: User,
|
||||
team: Team,
|
||||
|
||||
@@ -32,7 +32,7 @@ export class TeamInviteViewerGuard implements CanActivate {
|
||||
// Get user
|
||||
TE.bindW('user', ({ gqlCtx }) =>
|
||||
pipe(
|
||||
O.fromNullable(gqlCtx.getContext<{ user?: User }>().user),
|
||||
O.fromNullable(gqlCtx.getContext().req.user),
|
||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -33,7 +33,7 @@ export class TeamInviteeGuard implements CanActivate {
|
||||
// Get user
|
||||
TE.bindW('user', ({ gqlCtx }) =>
|
||||
pipe(
|
||||
O.fromNullable(gqlCtx.getContext<{ user?: User }>().user),
|
||||
O.fromNullable(gqlCtx.getContext().req.user),
|
||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { HttpStatus } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
** 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 = {
|
||||
message: string;
|
||||
|
||||
@@ -6,7 +6,6 @@ module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"cancel": "Cancel",
|
||||
"choose_file": "Choose a file",
|
||||
"clear": "Clear",
|
||||
"clear_history": "Clear All History",
|
||||
"clear_all": "Clear all",
|
||||
"close": "Close",
|
||||
"connect": "Connect",
|
||||
@@ -582,6 +583,11 @@
|
||||
"log": "Log",
|
||||
"url": "URL"
|
||||
},
|
||||
"spotlight": {
|
||||
"section": {
|
||||
"user": "User"
|
||||
}
|
||||
},
|
||||
"sse": {
|
||||
"event_type": "Event type",
|
||||
"log": "Log",
|
||||
|
||||
@@ -5,29 +5,29 @@
|
||||
"choose_file": "Válasszon egy fájlt",
|
||||
"clear": "Törlés",
|
||||
"clear_all": "Összes törlése",
|
||||
"close": "Close",
|
||||
"close": "Bezárás",
|
||||
"connect": "Kapcsolódás",
|
||||
"connecting": "Connecting",
|
||||
"connecting": "Kapcsolódás",
|
||||
"copy": "Másolás",
|
||||
"delete": "Törlés",
|
||||
"disconnect": "Leválasztás",
|
||||
"dismiss": "Eltüntetés",
|
||||
"dont_save": "Ne mentse",
|
||||
"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",
|
||||
"edit": "Szerkesztés",
|
||||
"filter": "Filter",
|
||||
"filter": "Szűrő",
|
||||
"go_back": "Vissza",
|
||||
"go_forward": "Go forward",
|
||||
"group_by": "Group by",
|
||||
"go_forward": "Előre",
|
||||
"group_by": "Csoportosítás",
|
||||
"label": "Címke",
|
||||
"learn_more": "Tudjon meg többet",
|
||||
"less": "Kevesebb",
|
||||
"more": "Több",
|
||||
"new": "Új",
|
||||
"no": "Nem",
|
||||
"open_workspace": "Open workspace",
|
||||
"open_workspace": "Munkaterület megnyitása",
|
||||
"paste": "Beillesztés",
|
||||
"prettify": "Csinosítás",
|
||||
"remove": "Eltávolítás",
|
||||
@@ -38,7 +38,7 @@
|
||||
"search": "Keresés",
|
||||
"send": "Küldés",
|
||||
"start": "Indítás",
|
||||
"starting": "Starting",
|
||||
"starting": "Indítás",
|
||||
"stop": "Leállítás",
|
||||
"to_close": "a bezáráshoz",
|
||||
"to_navigate": "a navigáláshoz",
|
||||
@@ -118,16 +118,16 @@
|
||||
},
|
||||
"collection": {
|
||||
"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",
|
||||
"invalid_name": "Adjon nevet a gyűjteménynek",
|
||||
"invalid_root_move": "Collection already in the root",
|
||||
"moved": "Moved Successfully",
|
||||
"invalid_root_move": "A gyűjtemény már a gyökérben van",
|
||||
"moved": "Sikeresen áthelyezve",
|
||||
"my_collections": "Saját gyűjtemények",
|
||||
"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",
|
||||
"new": "Új gyűjtemény",
|
||||
"order_changed": "Collection Order Updated",
|
||||
"order_changed": "Gyűjtemény sorrendje frissítve",
|
||||
"renamed": "Gyűjtemény átnevezve",
|
||||
"request_in_use": "A kérés használatban",
|
||||
"save_as": "Mentés másként",
|
||||
@@ -147,7 +147,7 @@
|
||||
"remove_team": "Biztosan törölni szeretné ezt a csapatot?",
|
||||
"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.",
|
||||
"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."
|
||||
},
|
||||
"count": {
|
||||
@@ -180,8 +180,8 @@
|
||||
"profile": "Jelentkezzen be a profilja megtekintéséhez",
|
||||
"protocols": "A protokollok üresek",
|
||||
"schema": "Kapcsolódjon egy GraphQL-végponthoz a séma megtekintéséhez",
|
||||
"shortcodes": "Shortcodes are empty",
|
||||
"subscription": "Subscriptions are empty",
|
||||
"shortcodes": "A rövid kódok üresek",
|
||||
"subscription": "A feliratkozások üresek",
|
||||
"team_name": "A csapat neve üres",
|
||||
"teams": "Ön nem tartozik semmilyen csapathoz",
|
||||
"tests": "Nincsenek tesztek ehhez a kéréshez"
|
||||
@@ -194,13 +194,13 @@
|
||||
"deleted": "Környezet törlése",
|
||||
"edit": "Környezet szerkesztése",
|
||||
"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",
|
||||
"new": "Új 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.",
|
||||
"select": "Környezet kiválasztása",
|
||||
"team_environments": "Team Environments",
|
||||
"team_environments": "Csapatkörnyezetek",
|
||||
"title": "Környezetek",
|
||||
"updated": "Környezet frissítve",
|
||||
"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.",
|
||||
"check_console_details": "Nézze meg a konzolnaplót a részletekért.",
|
||||
"curl_invalid_format": "A cURL nincs megfelelően formázva",
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "Your account is currently an owner in these teams:",
|
||||
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
|
||||
"danger_zone": "Veszélyes zóna",
|
||||
"delete_account": "Az Ön fiókja jelenleg tulajdonos ezekben a csapatokban:",
|
||||
"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",
|
||||
"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",
|
||||
@@ -219,13 +219,13 @@
|
||||
"incorrect_email": "Hibás e-mail",
|
||||
"invalid_link": "Érvénytelen hivatkozás",
|
||||
"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",
|
||||
"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",
|
||||
"no_duration": "Nincs időtartam",
|
||||
"no_results_found": "No matches found",
|
||||
"page_not_found": "This page could not be found",
|
||||
"no_results_found": "Nincs találat",
|
||||
"page_not_found": "Ez az oldal nem található",
|
||||
"script_fail": "Nem sikerült végrehajtani a kérés előtti parancsfájlt",
|
||||
"something_went_wrong": "Valami elromlott",
|
||||
"test_script_fail": "Nem sikerült végrehajtani a kérés utáni parancsfájlt"
|
||||
@@ -238,9 +238,9 @@
|
||||
"title": "Exportálás"
|
||||
},
|
||||
"filter": {
|
||||
"all": "All",
|
||||
"none": "None",
|
||||
"starred": "Starred"
|
||||
"all": "Összes",
|
||||
"none": "Nincs",
|
||||
"starred": "Csillagozott"
|
||||
},
|
||||
"folder": {
|
||||
"created": "Mappa létrehozva",
|
||||
@@ -256,7 +256,7 @@
|
||||
"subscriptions": "Feliratkozások"
|
||||
},
|
||||
"group": {
|
||||
"time": "Time",
|
||||
"time": "Idő",
|
||||
"url": "URL"
|
||||
},
|
||||
"header": {
|
||||
@@ -316,32 +316,32 @@
|
||||
"zen_mode": "Zen mód"
|
||||
},
|
||||
"modal": {
|
||||
"close_unsaved_tab": "You have unsaved changes",
|
||||
"close_unsaved_tab": "Elmentetlen változtatásai vannak",
|
||||
"collections": "Gyűjtemények",
|
||||
"confirm": "Megerősítés",
|
||||
"edit_request": "Kérés szerkesztése",
|
||||
"import_export": "Importálás és exportálás"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "You are already subscribed to this topic.",
|
||||
"clean_session": "Clean Session",
|
||||
"clear_input": "Clear input",
|
||||
"clear_input_on_send": "Clear input on send",
|
||||
"client_id": "Client ID",
|
||||
"color": "Pick a color",
|
||||
"already_subscribed": "Ön már feliratkozott erre a témára.",
|
||||
"clean_session": "Munkamenet törlése",
|
||||
"clear_input": "Bevitel törlése",
|
||||
"clear_input_on_send": "Bevitel törlése küldéskor",
|
||||
"client_id": "Ügyfél-azonosító",
|
||||
"color": "Válasszon színt",
|
||||
"communication": "Kommunikáció",
|
||||
"connection_config": "Connection Config",
|
||||
"connection_not_authorized": "This MQTT connection does not use any authentication.",
|
||||
"invalid_topic": "Please provide a topic for the subscription",
|
||||
"keep_alive": "Keep Alive",
|
||||
"connection_config": "Kapcsolat beállításai",
|
||||
"connection_not_authorized": "Ez az MQTT-kapcsolat nem használ semmilyen hitelesítést.",
|
||||
"invalid_topic": "Adjon témát a feliratkozáshoz",
|
||||
"keep_alive": "Életben tartás",
|
||||
"log": "Napló",
|
||||
"lw_message": "Last-Will Message",
|
||||
"lw_qos": "Last-Will QoS",
|
||||
"lw_retain": "Last-Will Retain",
|
||||
"lw_topic": "Last-Will Topic",
|
||||
"lw_message": "Utolsó kívánság üzenet",
|
||||
"lw_qos": "Utolsó kívánság QoS",
|
||||
"lw_retain": "Utolsó kívánság megtartás",
|
||||
"lw_topic": "Utolsó kívánság téma",
|
||||
"message": "Üzenet",
|
||||
"new": "New Subscription",
|
||||
"not_connected": "Please start a MQTT connection first.",
|
||||
"new": "Új feliratkozás",
|
||||
"not_connected": "Először indítson egy MQTT-kapcsolatot.",
|
||||
"publish": "Közzététel",
|
||||
"qos": "QoS",
|
||||
"ssl": "SSL",
|
||||
@@ -368,7 +368,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"app_settings": "Alkalmazás beállításai",
|
||||
"default_hopp_displayname": "Unnamed User",
|
||||
"default_hopp_displayname": "Névtelen felhasználó",
|
||||
"editor": "Szerkesztő",
|
||||
"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.",
|
||||
@@ -391,26 +391,26 @@
|
||||
"choose_language": "Nyelv kiválasztása",
|
||||
"content_type": "Tartalom típusa",
|
||||
"content_type_titles": {
|
||||
"others": "Others",
|
||||
"structured": "Structured",
|
||||
"text": "Text"
|
||||
"others": "Egyebek",
|
||||
"structured": "Szerkesztett",
|
||||
"text": "Szöveg"
|
||||
},
|
||||
"copy_link": "Hivatkozás másolása",
|
||||
"different_collection": "Cannot reorder requests from different collections",
|
||||
"duplicated": "Request duplicated",
|
||||
"different_collection": "Nem lehet átrendezni a különböző gyűjteményekből érkező kéréseket",
|
||||
"duplicated": "Kérés megkettőzve",
|
||||
"duration": "Időtartam",
|
||||
"enter_curl": "cURL megadása",
|
||||
"enter_curl": "cURL-parancs megadása",
|
||||
"generate_code": "Kód előállítása",
|
||||
"generated_code": "Előállított kód",
|
||||
"header_list": "Fejléclista",
|
||||
"invalid_name": "Adjon nevet a kérésnek",
|
||||
"method": "Módszer",
|
||||
"moved": "Request moved",
|
||||
"moved": "Kérés áthelyezve",
|
||||
"name": "Kérés neve",
|
||||
"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_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",
|
||||
"parameter_list": "Lekérdezési paraméterek",
|
||||
"parameters": "Paraméterek",
|
||||
@@ -429,12 +429,12 @@
|
||||
"type": "Kérés típusa",
|
||||
"url": "URL",
|
||||
"variables": "Változók",
|
||||
"view_my_links": "View my links"
|
||||
"view_my_links": "Saját hivatkozások megtekintése"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"audio": "Hang",
|
||||
"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",
|
||||
"html": "HTML",
|
||||
"image": "Kép",
|
||||
@@ -446,14 +446,14 @@
|
||||
"status": "Állapot",
|
||||
"time": "Idő",
|
||||
"title": "Válasz",
|
||||
"video": "Video",
|
||||
"video": "Videó",
|
||||
"waiting_for_connection": "várakozás kapcsolódásra",
|
||||
"xml": "XML"
|
||||
},
|
||||
"settings": {
|
||||
"accent_color": "Kiemelőszín",
|
||||
"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_email_description": "Az Ön elsődleges e-mail-címe.",
|
||||
"account_name_description": "Ez a megjelenített neve.",
|
||||
@@ -462,8 +462,8 @@
|
||||
"change_font_size": "Betűméret megváltoztatása",
|
||||
"choose_language": "Nyelv kiválasztása",
|
||||
"dark_mode": "Sötét",
|
||||
"delete_account": "Delete account",
|
||||
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
|
||||
"delete_account": "Fiók törlése",
|
||||
"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",
|
||||
"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, ",
|
||||
@@ -490,8 +490,8 @@
|
||||
"proxy_use_toggle": "A proxy középprogram használata a kérések küldéséhez",
|
||||
"read_the": "Olvassa el:",
|
||||
"reset_default": "Visszaállítás az alapértelmezettre",
|
||||
"short_codes": "Short codes",
|
||||
"short_codes_description": "Short codes which were created by you.",
|
||||
"short_codes": "Rövid kódok",
|
||||
"short_codes_description": "Az Ön által létrehozott rövid kódok.",
|
||||
"sidebar_on_left": "Oldalsáv a bal oldalon",
|
||||
"sync": "Szinkronizálás",
|
||||
"sync_collections": "Gyűjtemények",
|
||||
@@ -505,16 +505,16 @@
|
||||
"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",
|
||||
"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"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "Actions",
|
||||
"created_on": "Created on",
|
||||
"deleted": "Shortcode deleted",
|
||||
"method": "Method",
|
||||
"not_found": "Shortcode not found",
|
||||
"short_code": "Short code",
|
||||
"actions": "Műveletek",
|
||||
"created_on": "Létrehozva",
|
||||
"deleted": "Rövid kód törölve",
|
||||
"method": "Módszer",
|
||||
"not_found": "A rövid kód nem található",
|
||||
"short_code": "Rövid kód",
|
||||
"url": "URL"
|
||||
},
|
||||
"shortcut": {
|
||||
@@ -556,9 +556,9 @@
|
||||
"title": "Kérés"
|
||||
},
|
||||
"response": {
|
||||
"copy": "Copy response to clipboard",
|
||||
"download": "Download response as file",
|
||||
"title": "Response"
|
||||
"copy": "Válasz másolása a vágólapra",
|
||||
"download": "Válasz letöltés fájlként",
|
||||
"title": "Válasz"
|
||||
},
|
||||
"theme": {
|
||||
"black": "Téma átváltása fekete módra",
|
||||
@@ -576,8 +576,8 @@
|
||||
},
|
||||
"socketio": {
|
||||
"communication": "Kommunikáció",
|
||||
"connection_not_authorized": "This SocketIO connection does not use any authentication.",
|
||||
"event_name": "Esemény neve",
|
||||
"connection_not_authorized": "Ez a SocketIO-kapcsolat nem használ semmilyen hitelesítést.",
|
||||
"event_name": "Esemény vagy téma neve",
|
||||
"events": "Események",
|
||||
"log": "Napló",
|
||||
"url": "URL"
|
||||
@@ -594,9 +594,9 @@
|
||||
"connected": "Kapcsolódva",
|
||||
"connected_to": "Kapcsolódva ehhez: {name}",
|
||||
"connecting_to": "Kapcsolódás ehhez: {name}…",
|
||||
"connection_error": "Failed to connect",
|
||||
"connection_failed": "Connection failed",
|
||||
"connection_lost": "Connection lost",
|
||||
"connection_error": "Nem sikerült kapcsolódni",
|
||||
"connection_failed": "A kapcsolódás sikertelen",
|
||||
"connection_lost": "A kapcsolat elveszett",
|
||||
"copied_to_clipboard": "Vágólapra másolva",
|
||||
"deleted": "Törölve",
|
||||
"deprecated": "ELAVULT",
|
||||
@@ -611,17 +611,17 @@
|
||||
"history_deleted": "Előzmények törölve",
|
||||
"linewrap": "Sorok tördelése",
|
||||
"loading": "Betöltés…",
|
||||
"message_received": "Message: {message} arrived on topic: {topic}",
|
||||
"mqtt_subscription_failed": "Something went wrong while subscribing to topic: {topic}",
|
||||
"message_received": "Üzenet: {message} érkezett ehhez a témához: {topic}",
|
||||
"mqtt_subscription_failed": "Valami elromlott a következő témára való feliratkozás során: {topic}",
|
||||
"none": "Nincs",
|
||||
"nothing_found": "Semmi sem található ehhez:",
|
||||
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
|
||||
"published_message": "Published message: {message} to topic: {topic}",
|
||||
"reconnection_error": "Failed to reconnect",
|
||||
"subscribed_failed": "Failed to subscribe to topic: {topic}",
|
||||
"subscribed_success": "Successfully subscribed to topic: {topic}",
|
||||
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
|
||||
"unsubscribed_success": "Successfully unsubscribed from topic: {topic}",
|
||||
"published_error": "Valami elromlott a következő üzenet közzététele során: {topic}, ehhez a témához: {message}",
|
||||
"published_message": "Közzétett üzenet: {message}, ehhez a témához: {topic}",
|
||||
"reconnection_error": "Nem sikerült újrakapcsolódni",
|
||||
"subscribed_failed": "Nem sikerült feliratkozni erre a témára: {topic}",
|
||||
"subscribed_success": "Sikeresen feliratkozott erre a témára: {topic}",
|
||||
"unsubscribed_failed": "Nem sikerült leiratkozni erről a témáról: {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"
|
||||
},
|
||||
"support": {
|
||||
@@ -641,7 +641,7 @@
|
||||
"body": "Törzs",
|
||||
"collections": "Gyűjtemények",
|
||||
"documentation": "Dokumentáció",
|
||||
"environments": "Environments",
|
||||
"environments": "Környezetek",
|
||||
"headers": "Fejlécek",
|
||||
"history": "Előzmények",
|
||||
"mqtt": "MQTT",
|
||||
@@ -666,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.",
|
||||
"exit": "Kilépés 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_id": "Érvénytelen csapatazonosító. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
||||
"invalid_invite_link": "Érvénytelen meghívási hivatkozás",
|
||||
@@ -690,7 +690,7 @@
|
||||
"member_removed": "Felhasználó eltávolítva",
|
||||
"member_role_updated": "Felhasználói szerepek frissítve",
|
||||
"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_updated": "Csapatnév frissítve",
|
||||
"new": "Új csapat",
|
||||
@@ -698,13 +698,13 @@
|
||||
"new_name": "Saját új csapat",
|
||||
"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_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_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",
|
||||
"permissions": "Jogosultságok",
|
||||
"same_target_destination": "Same target and destination",
|
||||
"same_target_destination": "Ugyanaz a cél és célhely",
|
||||
"saved": "Csapat elmentve",
|
||||
"select_a_team": "Csapat kiválasztása",
|
||||
"title": "Csapatok",
|
||||
@@ -712,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."
|
||||
},
|
||||
"team_environment": {
|
||||
"deleted": "Environment Deleted",
|
||||
"duplicate": "Environment Duplicated",
|
||||
"not_found": "Environment not found."
|
||||
"deleted": "Környezet törölve",
|
||||
"duplicate": "Környezet megkettőzve",
|
||||
"not_found": "A környezet nem található."
|
||||
},
|
||||
"test": {
|
||||
"failed": "teszt sikertelen",
|
||||
@@ -734,9 +734,9 @@
|
||||
"url": "URL"
|
||||
},
|
||||
"workspace": {
|
||||
"change": "Change workspace",
|
||||
"personal": "My Workspace",
|
||||
"team": "Team Workspace",
|
||||
"title": "Workspaces"
|
||||
"change": "Munkaterület váltása",
|
||||
"personal": "Saját munkaterület",
|
||||
"team": "Csapat-munkaterület",
|
||||
"title": "Munkaterületek"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"name": "@hoppscotch/common",
|
||||
"private": true,
|
||||
"version": "2023.4.6",
|
||||
"version": "2023.4.7",
|
||||
"scripts": {
|
||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||
"test": "vitest --run",
|
||||
"test:watch": "vitest",
|
||||
"dev:vite": "vite",
|
||||
"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 .",
|
||||
@@ -13,6 +15,7 @@
|
||||
"preview": "vite preview",
|
||||
"gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml dotenv_config_path=\"../../.env\"",
|
||||
"postinstall": "pnpm run gql-codegen",
|
||||
"do-test": "pnpm run test",
|
||||
"do-lint": "pnpm run prod-lint",
|
||||
"do-typecheck": "pnpm run lint",
|
||||
"do-lintfix": "pnpm run lintfix"
|
||||
@@ -43,11 +46,12 @@
|
||||
"@urql/exchange-auth": "^0.1.7",
|
||||
"@urql/exchange-graphcache": "^4.4.3",
|
||||
"@vitejs/plugin-legacy": "^2.3.0",
|
||||
"@vueuse/core": "^8.7.5",
|
||||
"@vueuse/core": "^8.9.4",
|
||||
"@vueuse/head": "^0.7.9",
|
||||
"acorn-walk": "^8.2.0",
|
||||
"axios": "^0.21.4",
|
||||
"buffer": "^6.0.3",
|
||||
"dioc": "workspace:^",
|
||||
"esprima": "^4.0.1",
|
||||
"events": "^3.3.0",
|
||||
"fp-ts": "^2.12.1",
|
||||
@@ -63,6 +67,7 @@
|
||||
"jsonpath-plus": "^7.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lossless-json": "^2.0.8",
|
||||
"minisearch": "^6.1.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"paho-mqtt": "^1.1.0",
|
||||
"path": "^0.12.7",
|
||||
@@ -108,6 +113,7 @@
|
||||
"@graphql-typed-document-node/core": "^3.1.1",
|
||||
"@iconify-json/lucide": "^1.1.40",
|
||||
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
||||
"@relmify/jest-fp-ts": "^2.1.1",
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
@@ -146,6 +152,7 @@
|
||||
"vite-plugin-pwa": "^0.13.1",
|
||||
"vite-plugin-vue-layouts": "^0.7.0",
|
||||
"vite-plugin-windicss": "^1.8.8",
|
||||
"vitest": "^0.32.2",
|
||||
"vue-tsc": "^0.38.2",
|
||||
"windicss": "^3.5.6"
|
||||
}
|
||||
|
||||
@@ -11,20 +11,20 @@ declare module '@vue/runtime-core' {
|
||||
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
|
||||
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.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']
|
||||
AppHeader: typeof import('./components/app/Header.vue')['default']
|
||||
AppInterceptor: typeof import('./components/app/Interceptor.vue')['default']
|
||||
AppLogo: typeof import('./components/app/Logo.vue')['default']
|
||||
AppOptions: typeof import('./components/app/Options.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']
|
||||
AppShortcuts: typeof import('./components/app/Shortcuts.vue')['default']
|
||||
AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
|
||||
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.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']
|
||||
ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.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']
|
||||
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
||||
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
|
||||
HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder']
|
||||
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
|
||||
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
|
||||
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
|
||||
@@ -130,6 +131,7 @@ declare module '@vue/runtime-core' {
|
||||
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
||||
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
||||
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||
IconLucideRefreshCw: typeof import('~icons/lucide/refresh-cw')['default']
|
||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
||||
@@ -167,6 +169,7 @@ declare module '@vue/runtime-core' {
|
||||
SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default']
|
||||
SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.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']
|
||||
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
||||
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
||||
|
||||
@@ -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>
|
||||
@@ -242,7 +242,7 @@ import { pwaDefferedPrompt, installPWA } from "@modules/pwa"
|
||||
import { platform } from "~/platform"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { invokeAction } from "@helpers/actions"
|
||||
import { defineActionHandler, invokeAction } from "@helpers/actions"
|
||||
import { workspaceStatus$, updateWorkspaceTeamName } from "~/newstore/workspace"
|
||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||
import { onLoggedIn } from "~/composables/auth"
|
||||
@@ -374,4 +374,12 @@ const profile = ref<any | null>(null)
|
||||
const settings = ref<any | null>(null)
|
||||
const logout = ref<any | null>(null)
|
||||
const accountActions = ref<any | null>(null)
|
||||
|
||||
defineActionHandler(
|
||||
"user.login",
|
||||
() => {
|
||||
invokeAction("modals.login.toggle")
|
||||
},
|
||||
computed(() => !currentUser.value)
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -14,45 +14,18 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="filterText" class="flex flex-col divide-y divide-dividerLight">
|
||||
<details
|
||||
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"
|
||||
>
|
||||
<div class="flex flex-col divide-y divide-dividerLight">
|
||||
<HoppSmartPlaceholder v-if="isEmpty(shortcutsResults)">
|
||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||
<span class="my-2 text-center">
|
||||
{{ t("state.nothing_found") }} "{{ filterText }}"
|
||||
<span class="my-2 text-center flex flex-col">
|
||||
{{ t("state.nothing_found") }}
|
||||
<span class="break-all">"{{ filterText }}"</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex flex-col divide-y divide-dividerLight">
|
||||
</HoppSmartPlaceholder>
|
||||
<details
|
||||
v-for="(map, mapIndex) in mappings"
|
||||
:key="`map-${mapIndex}`"
|
||||
v-for="(sectionResults, sectionTitle) in shortcutsResults"
|
||||
v-else
|
||||
:key="`section-${sectionTitle}`"
|
||||
class="flex flex-col"
|
||||
open
|
||||
>
|
||||
@@ -63,13 +36,13 @@
|
||||
<span
|
||||
class="font-semibold truncate capitalize-first text-secondaryDark"
|
||||
>
|
||||
{{ t(map.section) }}
|
||||
{{ sectionTitle }}
|
||||
</span>
|
||||
</summary>
|
||||
<div class="flex flex-col px-6 pb-4 space-y-2">
|
||||
<AppShortcutsEntry
|
||||
v-for="(shortcut, shortcutIndex) in map.shortcuts"
|
||||
:key="`map-${mapIndex}-shortcut-${shortcutIndex}`"
|
||||
v-for="(shortcut, index) in sectionResults"
|
||||
:key="`shortcut-${index}`"
|
||||
:shortcut="shortcut"
|
||||
/>
|
||||
</div>
|
||||
@@ -80,10 +53,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue"
|
||||
import Fuse from "fuse.js"
|
||||
import mappings from "~/helpers/shortcuts"
|
||||
import { computed, onBeforeMount, ref } from "vue"
|
||||
import { ShortcutDef, getShortcuts } from "~/helpers/shortcuts"
|
||||
import MiniSearch from "minisearch"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { groupBy, isEmpty } from "lodash-es"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -91,15 +65,33 @@ defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
const options = {
|
||||
keys: ["shortcuts.label"],
|
||||
}
|
||||
const minisearch = new MiniSearch({
|
||||
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 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<{
|
||||
(e: "close"): void
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex items-center py-1">
|
||||
<span class="flex flex-1 mr-4">
|
||||
{{ t(shortcut.label) }}
|
||||
{{ shortcut.label }}
|
||||
</span>
|
||||
<kbd
|
||||
v-for="(key, index) in shortcut.keys"
|
||||
@@ -14,14 +14,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
|
||||
const t = useI18n()
|
||||
import { ShortcutDef } from "~/helpers/shortcuts"
|
||||
|
||||
defineProps<{
|
||||
shortcut: {
|
||||
label: string
|
||||
keys: string[]
|
||||
}
|
||||
shortcut: ShortcutDef
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -284,6 +284,14 @@ const importerAction = async (stepResults: StepReturnValue[]) => {
|
||||
emit("import-to-teams", result)
|
||||
} else {
|
||||
appendRESTCollections(result)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_COLLECTION",
|
||||
importer: importerModule.value!.name,
|
||||
platform: "rest",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
|
||||
fileImported()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 bg-primary">
|
||||
<div class="flex flex-col flex-1">
|
||||
<div
|
||||
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
|
||||
:style="
|
||||
@@ -243,49 +243,33 @@
|
||||
/>
|
||||
</template>
|
||||
<template #emptyNode="{ node }">
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
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}”`"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||
<span class="my-2 text-center">
|
||||
{{ t("state.nothing_found") }} "{{ filterText }}"
|
||||
</span>
|
||||
</div>
|
||||
<div v-else-if="node === null">
|
||||
<div
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
</template>
|
||||
</HoppSmartPlaceholder>
|
||||
<HoppSmartPlaceholder
|
||||
v-else-if="node === null"
|
||||
: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>
|
||||
:text="t('empty.collections')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
@click="emit('display-modal-add')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
</HoppSmartPlaceholder>
|
||||
<HoppSmartPlaceholder
|
||||
v-else-if="node.data.type === 'collections'"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<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>
|
||||
:alt="`${t('empty.collections')}`"
|
||||
:text="t('empty.collections')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
@@ -298,21 +282,14 @@
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
</HoppSmartPlaceholder>
|
||||
<HoppSmartPlaceholder
|
||||
v-else-if="node.data.type === 'folders'"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<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.folder')}`"
|
||||
/>
|
||||
<span class="text-center">
|
||||
{{ t("empty.folder") }}
|
||||
</span>
|
||||
</div>
|
||||
:text="t('empty.folder')"
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
</template>
|
||||
</SmartTree>
|
||||
</div>
|
||||
|
||||
@@ -89,6 +89,7 @@ import {
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import { computedWithControl } from "@vueuse/core"
|
||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -223,6 +224,13 @@ const saveRequestAs = async () => {
|
||||
},
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
createdNow: true,
|
||||
platform: "rest",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
|
||||
requestSaved()
|
||||
} else if (picked.value.pickedType === "my-folder") {
|
||||
if (!isHoppRESTRequest(requestUpdated))
|
||||
@@ -243,6 +251,13 @@ const saveRequestAs = async () => {
|
||||
},
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
createdNow: true,
|
||||
platform: "rest",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
|
||||
requestSaved()
|
||||
} else if (picked.value.pickedType === "my-request") {
|
||||
if (!isHoppRESTRequest(requestUpdated))
|
||||
@@ -264,17 +279,38 @@ const saveRequestAs = async () => {
|
||||
},
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
createdNow: false,
|
||||
platform: "rest",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
|
||||
requestSaved()
|
||||
} else if (picked.value.pickedType === "teams-collection") {
|
||||
if (!isHoppRESTRequest(requestUpdated))
|
||||
throw new Error("requestUpdated is not a REST Request")
|
||||
|
||||
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") {
|
||||
if (!isHoppRESTRequest(requestUpdated))
|
||||
throw new Error("requestUpdated is not a REST Request")
|
||||
|
||||
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") {
|
||||
if (!isHoppRESTRequest(requestUpdated))
|
||||
throw new Error("requestUpdated is not a REST Request")
|
||||
@@ -292,6 +328,13 @@ const saveRequestAs = async () => {
|
||||
title: requestUpdated.name,
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
createdNow: false,
|
||||
platform: "rest",
|
||||
workspaceType: "team",
|
||||
})
|
||||
|
||||
pipe(
|
||||
updateTeamRequest(picked.value.requestID, data),
|
||||
TE.match(
|
||||
@@ -313,6 +356,13 @@ const saveRequestAs = async () => {
|
||||
requestUpdated as HoppGQLRequest
|
||||
)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
createdNow: false,
|
||||
platform: "gql",
|
||||
workspaceType: "team",
|
||||
})
|
||||
|
||||
requestSaved()
|
||||
} else if (picked.value.pickedType === "gql-my-folder") {
|
||||
// TODO: Check for GQL request ?
|
||||
@@ -321,6 +371,13 @@ const saveRequestAs = async () => {
|
||||
requestUpdated as HoppGQLRequest
|
||||
)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
createdNow: true,
|
||||
platform: "gql",
|
||||
workspaceType: "team",
|
||||
})
|
||||
|
||||
requestSaved()
|
||||
} else if (picked.value.pickedType === "gql-my-collection") {
|
||||
// TODO: Check for GQL request ?
|
||||
@@ -329,6 +386,13 @@ const saveRequestAs = async () => {
|
||||
requestUpdated as HoppGQLRequest
|
||||
)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
createdNow: true,
|
||||
platform: "gql",
|
||||
workspaceType: "team",
|
||||
})
|
||||
|
||||
requestSaved()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 bg-primary">
|
||||
<div class="flex flex-col flex-1">
|
||||
<div
|
||||
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
|
||||
:style="
|
||||
@@ -262,19 +262,12 @@
|
||||
</template>
|
||||
<template #emptyNode="{ node }">
|
||||
<div v-if="node === null">
|
||||
<div
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
@drop="(e) => e.stopPropagation()"
|
||||
>
|
||||
<img
|
||||
<div @drop="(e) => e.stopPropagation()">
|
||||
<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.collection')}`"
|
||||
/>
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.collections") }}
|
||||
</span>
|
||||
:alt="`${t('empty.collections')}`"
|
||||
:text="t('empty.collections')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-if="hasNoTeamAccess"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -292,37 +285,30 @@
|
||||
outline
|
||||
@click="emit('display-modal-add')"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="node.data.type === 'collections'"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
@drop="(e) => e.stopPropagation()"
|
||||
>
|
||||
<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.collection')}`"
|
||||
/>
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.collections") }}
|
||||
</span>
|
||||
:alt="`${t('empty.collections')}`"
|
||||
:text="t('empty.collections')"
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="node.data.type === 'folders'"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
@drop="(e) => e.stopPropagation()"
|
||||
>
|
||||
<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>
|
||||
:text="t('empty.folder')"
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</template>
|
||||
</SmartTree>
|
||||
|
||||
@@ -46,6 +46,7 @@ import { useToast } from "@composables/toast"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { HoppGQLRequest, makeCollection } from "@hoppscotch/data"
|
||||
import { addGraphqlCollection } from "~/newstore/collections"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -79,6 +80,13 @@ export default defineComponent({
|
||||
)
|
||||
|
||||
this.hideModal()
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_COLLECTION",
|
||||
isRootCollection: true,
|
||||
platform: "gql",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
},
|
||||
hideModal() {
|
||||
this.name = null
|
||||
|
||||
@@ -171,21 +171,14 @@
|
||||
@duplicate-request="$emit('duplicate-request', $event)"
|
||||
@select="$emit('select', $event)"
|
||||
/>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="
|
||||
collection.folders.length === 0 && collection.requests.length === 0
|
||||
"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<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>
|
||||
:text="t('empty.collection')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
@@ -196,7 +189,7 @@
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</div>
|
||||
<HoppSmartConfirmModal
|
||||
|
||||
@@ -160,25 +160,19 @@
|
||||
@duplicate-request="emit('duplicate-request', $event)"
|
||||
@select="emit('select', $event)"
|
||||
/>
|
||||
<div
|
||||
|
||||
<HoppSmartPlaceholder
|
||||
v-if="
|
||||
folder.folders &&
|
||||
folder.folders.length === 0 &&
|
||||
folder.requests &&
|
||||
folder.requests.length === 0
|
||||
"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<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.folder')}`"
|
||||
/>
|
||||
<span class="text-center">
|
||||
{{ t("empty.folder") }}
|
||||
</span>
|
||||
</div>
|
||||
:text="t('empty.folder')"
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</div>
|
||||
<HoppSmartConfirmModal
|
||||
|
||||
@@ -244,6 +244,14 @@ const importFromJSON = () => {
|
||||
return
|
||||
}
|
||||
appendGraphqlCollections(collections)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_COLLECTION",
|
||||
importer: "json",
|
||||
workspaceType: "personal",
|
||||
platform: "gql",
|
||||
})
|
||||
|
||||
fileImported()
|
||||
}
|
||||
reader.readAsText(inputChooseFileToImportFrom.value.files[0])
|
||||
@@ -257,6 +265,12 @@ const exportJSON = () => {
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
|
||||
platform?.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "json",
|
||||
platform: "gql",
|
||||
})
|
||||
|
||||
// TODO: get uri from meta
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
document.body.appendChild(a)
|
||||
|
||||
@@ -60,35 +60,27 @@
|
||||
@select="$emit('select', $event)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="collections.length === 0"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<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>
|
||||
:alt="`${t('empty.collections')}`"
|
||||
:text="t('empty.collections')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
</HoppSmartPlaceholder>
|
||||
<HoppSmartPlaceholder
|
||||
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}”`"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||
<span class="my-2 text-center">
|
||||
{{ t("state.nothing_found") }} "{{ filterText }}"
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartPlaceholder>
|
||||
<CollectionsGraphqlAdd
|
||||
:show="showModalAdd"
|
||||
@hide-modal="displayModalAdd(false)"
|
||||
@@ -153,6 +145,7 @@ import IconArchive from "~icons/lucide/archive"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -285,6 +278,13 @@ export default defineComponent({
|
||||
response: "",
|
||||
})
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
platform: "gql",
|
||||
createdNow: true,
|
||||
workspaceType: "personal",
|
||||
})
|
||||
|
||||
this.displayModalAddRequest(false)
|
||||
},
|
||||
addRequest(payload) {
|
||||
@@ -294,6 +294,14 @@ export default defineComponent({
|
||||
},
|
||||
onAddFolder({ name, path }) {
|
||||
addGraphqlFolder(name, path)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_COLLECTION",
|
||||
isRootCollection: false,
|
||||
platform: "gql",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
|
||||
this.displayModalAddFolder(false)
|
||||
},
|
||||
addFolder(payload) {
|
||||
|
||||
@@ -599,11 +599,25 @@ const addNewRootCollection = (name: string) => {
|
||||
})
|
||||
)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_COLLECTION",
|
||||
platform: "rest",
|
||||
workspaceType: "personal",
|
||||
isRootCollection: true,
|
||||
})
|
||||
|
||||
displayModalAdd(false)
|
||||
} else if (hasTeamWriteAccess.value) {
|
||||
if (!collectionsType.value.selectedTeam) return
|
||||
modalLoadingState.value = true
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_COLLECTION",
|
||||
platform: "rest",
|
||||
workspaceType: "team",
|
||||
isRootCollection: true,
|
||||
})
|
||||
|
||||
pipe(
|
||||
createNewRootCollection(name, collectionsType.value.selectedTeam.id),
|
||||
TE.match(
|
||||
@@ -652,6 +666,13 @@ const onAddRequest = (requestName: string) => {
|
||||
},
|
||||
})
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
workspaceType: "personal",
|
||||
createdNow: true,
|
||||
platform: "rest",
|
||||
})
|
||||
|
||||
displayModalAddRequest(false)
|
||||
} else if (hasTeamWriteAccess.value) {
|
||||
const folder = editingFolder.value
|
||||
@@ -667,6 +688,13 @@ const onAddRequest = (requestName: string) => {
|
||||
title: requestName,
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
workspaceType: "team",
|
||||
platform: "rest",
|
||||
createdNow: true,
|
||||
})
|
||||
|
||||
pipe(
|
||||
createRequestInCollection(folder.id, data),
|
||||
TE.match(
|
||||
@@ -712,6 +740,14 @@ const onAddFolder = (folderName: string) => {
|
||||
if (collectionsType.value.type === "my-collections") {
|
||||
if (!path) return
|
||||
addRESTFolder(folderName, path)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_COLLECTION",
|
||||
workspaceType: "personal",
|
||||
isRootCollection: false,
|
||||
platform: "rest",
|
||||
})
|
||||
|
||||
displayModalAddFolder(false)
|
||||
} else if (hasTeamWriteAccess.value) {
|
||||
const folder = editingFolder.value
|
||||
@@ -719,6 +755,13 @@ const onAddFolder = (folderName: string) => {
|
||||
|
||||
modalLoadingState.value = true
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_COLLECTION",
|
||||
workspaceType: "personal",
|
||||
isRootCollection: false,
|
||||
platform: "rest",
|
||||
})
|
||||
|
||||
pipe(
|
||||
createChildCollection(folderName, folder.id),
|
||||
TE.match(
|
||||
@@ -1884,6 +1927,12 @@ const exportData = async (
|
||||
}
|
||||
|
||||
const exportJSONCollection = async () => {
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "json",
|
||||
platform: "rest",
|
||||
})
|
||||
|
||||
await getJSONCollection()
|
||||
|
||||
initializeDownloadCollection(collectionJSON.value, null)
|
||||
@@ -1895,6 +1944,12 @@ const createCollectionGist = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "gist",
|
||||
platform: "rest",
|
||||
})
|
||||
|
||||
creatingGistCollection.value = true
|
||||
await getJSONCollection()
|
||||
|
||||
@@ -1925,6 +1980,12 @@ const importToTeams = async (collection: HoppCollection<HoppRESTRequest>[]) => {
|
||||
|
||||
importingMyCollections.value = true
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_COLLECTION",
|
||||
exporter: "import-to-teams",
|
||||
platform: "rest",
|
||||
})
|
||||
|
||||
pipe(
|
||||
importJSONToTeam(
|
||||
JSON.stringify(collection),
|
||||
|
||||
@@ -190,6 +190,12 @@ const createEnvironmentGist = async () => {
|
||||
)
|
||||
|
||||
toast.success(t("export.gist_created").toString())
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_EXPORT_ENVIRONMENT",
|
||||
platform: "rest",
|
||||
})
|
||||
|
||||
window.open(res.data.html_url)
|
||||
} catch (e) {
|
||||
toast.error(t("error.something_went_wrong").toString())
|
||||
@@ -249,6 +255,13 @@ const openDialogChooseFileToImportFrom = () => {
|
||||
|
||||
const importToTeams = async (content: Environment[]) => {
|
||||
loading.value = true
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||
platform: "rest",
|
||||
workspaceType: "team",
|
||||
})
|
||||
|
||||
for (const [i, env] of content.entries()) {
|
||||
if (i === content.length - 1) {
|
||||
await pipe(
|
||||
@@ -301,6 +314,12 @@ const importFromJSON = () => {
|
||||
return
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||
platform: "rest",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = ({ target }) => {
|
||||
@@ -352,6 +371,7 @@ const importFromPostman = ({
|
||||
const environment: Environment = { name, variables: [] }
|
||||
values.forEach(({ key, value }) => environment.variables.push({ key, value }))
|
||||
const environments = [environment]
|
||||
|
||||
importFromHoppscotch(environments)
|
||||
}
|
||||
|
||||
|
||||
@@ -70,20 +70,13 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="myEnvironments.length === 0"
|
||||
class="flex flex-col items-center justify-center text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
/>
|
||||
<span class="pb-2 text-center">
|
||||
{{ t("empty.environments") }}
|
||||
</span>
|
||||
</div>
|
||||
:text="t('empty.environments')"
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
:id="'team-environments'"
|
||||
@@ -119,20 +112,14 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div
|
||||
|
||||
<HoppSmartPlaceholder
|
||||
v-if="teamEnvironmentList.length === 0"
|
||||
class="flex flex-col items-center justify-center text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
/>
|
||||
<span class="pb-2 text-center">
|
||||
{{ t("empty.environments") }}
|
||||
</span>
|
||||
</div>
|
||||
:text="t('empty.environments')"
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
<div
|
||||
v-if="!teamListLoading && teamAdapterError"
|
||||
|
||||
@@ -79,26 +79,19 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="vars.length === 0"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<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>
|
||||
:text="t('empty.environments')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
class="mb-4"
|
||||
@click="addEnvironmentVariable"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -148,6 +141,7 @@ import { useToast } from "@composables/toast"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { environmentsStore } from "~/newstore/environments"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
type EnvironmentVariable = {
|
||||
id: number
|
||||
@@ -311,6 +305,11 @@ const saveEnvironment = () => {
|
||||
index: envList.value.length - 1,
|
||||
})
|
||||
toast.success(`${t("environment.created")}`)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_ENVIRONMENT",
|
||||
workspaceType: "personal",
|
||||
})
|
||||
} else if (props.editingEnvironmentIndex === "Global") {
|
||||
// Editing the Global environment
|
||||
setGlobalEnvVariables(environmentUpdated.variables)
|
||||
|
||||
@@ -32,19 +32,12 @@
|
||||
:environment="environment"
|
||||
@edit-environment="editEnvironment(index)"
|
||||
/>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="environments.length === 0"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<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>
|
||||
:text="t('empty.environments')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
@@ -52,7 +45,7 @@
|
||||
class="mb-4"
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
<EnvironmentsMyDetails
|
||||
:show="showModalDetails"
|
||||
:action="action"
|
||||
|
||||
@@ -83,19 +83,12 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="vars.length === 0"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<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>
|
||||
:text="t('empty.environments')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-if="isViewer"
|
||||
disabled
|
||||
@@ -110,7 +103,7 @@
|
||||
class="mb-4"
|
||||
@click="addEnvironmentVariable"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -156,6 +149,7 @@ import IconTrash from "~icons/lucide/trash"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconDone from "~icons/lucide/check"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
type EnvironmentVariable = {
|
||||
id: number
|
||||
@@ -294,6 +288,11 @@ const saveEnvironment = async () => {
|
||||
)
|
||||
|
||||
if (props.action === "new") {
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_ENVIRONMENT",
|
||||
workspaceType: "team",
|
||||
})
|
||||
|
||||
await pipe(
|
||||
createTeamEnvironment(
|
||||
JSON.stringify(filterdVariables),
|
||||
|
||||
@@ -43,19 +43,12 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="!loading && teamEnvironments.length === 0 && !adapterError"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<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>
|
||||
:text="t('empty.environments')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-if="team === undefined || team.myRole === 'VIEWER'"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
@@ -74,7 +67,7 @@
|
||||
class="mb-4"
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
<div v-else-if="!loading">
|
||||
<EnvironmentsTeamsEnvironment
|
||||
v-for="(environment, index) in JSON.parse(
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="flex" @click="OpenLogoutModal()">
|
||||
<div class="flex" @click="openLogoutModal()">
|
||||
<HoppSmartItem
|
||||
ref="logoutItem"
|
||||
:icon="IconLogOut"
|
||||
:label="`${t('auth.logout')}`"
|
||||
:outline="outline"
|
||||
:shortcut="shortcut"
|
||||
@click="OpenLogoutModal()"
|
||||
@click="openLogoutModal()"
|
||||
/>
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmLogout"
|
||||
@@ -23,6 +23,7 @@ import IconLogOut from "~icons/lucide/log-out"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { platform } from "~/platform"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
|
||||
defineProps({
|
||||
outline: {
|
||||
@@ -55,8 +56,12 @@ const logout = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const OpenLogoutModal = () => {
|
||||
const openLogoutModal = () => {
|
||||
emit("confirm-logout")
|
||||
confirmLogout.value = true
|
||||
}
|
||||
|
||||
defineActionHandler("user.logout", () => {
|
||||
openLogoutModal()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -114,19 +114,12 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="authType === 'none'"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<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>
|
||||
:text="t('empty.authorization')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
outline
|
||||
:label="t('app.documentation')"
|
||||
@@ -136,40 +129,58 @@
|
||||
reverse
|
||||
class="mb-4"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
<div v-else class="flex flex-1 border-b border-dividerLight">
|
||||
<div class="w-2/3 border-r border-dividerLight">
|
||||
<div v-if="authType === 'basic'">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="basicUsername"
|
||||
:environment-highlights="false"
|
||||
:placeholder="t('authorization.username')"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput
|
||||
v-model="basicPassword"
|
||||
:environment-highlights="false"
|
||||
:placeholder="t('authorization.password')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="authType === 'bearer'">
|
||||
<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 v-if="authType === 'oauth-2'">
|
||||
<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>
|
||||
<HttpOAuth2Authorization />
|
||||
</div>
|
||||
<div v-if="authType === 'api-key'">
|
||||
<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 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 class="flex items-center border-b border-dividerLight">
|
||||
<span class="flex items-center">
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<HoppButtonPrimary
|
||||
id="get"
|
||||
name="get"
|
||||
:loading="isLoading"
|
||||
:label="!connected ? t('action.connect') : t('action.disconnect')"
|
||||
class="w-32"
|
||||
@click="onConnectClick"
|
||||
@@ -31,7 +32,12 @@ import { GQLConnection } from "~/helpers/GQLConnection"
|
||||
import { getCurrentStrategyID } from "~/helpers/network"
|
||||
import { useReadonlyStream, useStream } from "@composables/stream"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { gqlHeaders$, gqlURL$, setGQLURL } from "~/newstore/GQLSession"
|
||||
import {
|
||||
gqlAuth$,
|
||||
gqlHeaders$,
|
||||
gqlURL$,
|
||||
setGQLURL,
|
||||
} from "~/newstore/GQLSession"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -40,15 +46,21 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const connected = useReadonlyStream(props.conn.connected$, false)
|
||||
const isLoading = useReadonlyStream(props.conn.isLoading$, false)
|
||||
const headers = useReadonlyStream(gqlHeaders$, [])
|
||||
const auth = useReadonlyStream(gqlAuth$, {
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
})
|
||||
|
||||
const url = useStream(gqlURL$, "", setGQLURL)
|
||||
|
||||
const onConnectClick = () => {
|
||||
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",
|
||||
strategy: getCurrentStrategyID(),
|
||||
})
|
||||
|
||||
@@ -289,19 +289,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="workingHeaders.length === 0"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<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>
|
||||
:text="t('empty.headers')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
@@ -309,7 +302,7 @@
|
||||
class="mb-4"
|
||||
@click="addHeader"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
|
||||
@@ -748,7 +741,8 @@ const runQuery = async () => {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "graphql-query",
|
||||
strategy: getCurrentStrategyID(),
|
||||
})
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 overflow-auto whitespace-nowrap">
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="responseString === 'loading'"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
:text="t('state.loading')"
|
||||
>
|
||||
<template #icon>
|
||||
<HoppSmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartPlaceholder>
|
||||
<div v-else-if="responseString" class="flex flex-col flex-1">
|
||||
<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"
|
||||
|
||||
@@ -24,25 +24,18 @@
|
||||
:icon="IconBookOpen"
|
||||
:label="`${t('tab.documentation')}`"
|
||||
>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="
|
||||
queryFields.length === 0 &&
|
||||
mutationFields.length === 0 &&
|
||||
subscriptionFields.length === 0 &&
|
||||
graphqlTypes.length === 0
|
||||
"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
: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>
|
||||
:text="t('empty.documentation')"
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
<div v-else>
|
||||
<div
|
||||
class="sticky top-0 z-10 flex flex-shrink-0 overflow-x-auto bg-primary"
|
||||
@@ -172,20 +165,13 @@
|
||||
ref="schemaEditor"
|
||||
class="flex flex-col flex-1"
|
||||
></div>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-else
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<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.schema')}`"
|
||||
/>
|
||||
<span class="mb-4 text-center">
|
||||
{{ t("empty.schema") }}
|
||||
</span>
|
||||
</div>
|
||||
:text="t('empty.schema')"
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</template>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
<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
|
||||
:class="[
|
||||
{ 'capitalize-first': groupSelection === 'TIME' },
|
||||
@@ -106,31 +108,23 @@
|
||||
/>
|
||||
</details>
|
||||
</div>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="history.length === 0"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/history.svg`"
|
||||
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
|
||||
:text="t('empty.history')"
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
<HoppSmartPlaceholder
|
||||
v-else-if="
|
||||
Object.keys(filteredHistoryGroups).length === 0 ||
|
||||
filteredHistory.length === 0
|
||||
"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
:text="`${t('state.nothing_found')} ‟${filterText || filterSelection}”`"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||
<span class="mt-2 mb-4 text-center">
|
||||
{{ t("state.nothing_found") }} "{{ filterText || filterSelection }}"
|
||||
</span>
|
||||
</template>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.clear')"
|
||||
outline
|
||||
@@ -141,7 +135,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmRemove"
|
||||
:title="`${t('confirm.remove_history')}`"
|
||||
@@ -182,6 +176,7 @@ import {
|
||||
import HistoryRestCard from "./rest/Card.vue"
|
||||
import HistoryGraphqlCard from "./graphql/Card.vue"
|
||||
import { createNewTab } from "~/helpers/rest/tab"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
|
||||
type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry
|
||||
|
||||
@@ -335,4 +330,8 @@ const toggleStar = (entry: HistoryEntry) => {
|
||||
toggleRESTHistoryEntryStar(entry as RESTHistoryEntry)
|
||||
else toggleGraphqlHistoryEntryStar(entry as GQLHistoryEntry)
|
||||
}
|
||||
|
||||
defineActionHandler("history.clear", () => {
|
||||
confirmRemove.value = true
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -113,17 +113,12 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="auth.authType === 'none'"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<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>
|
||||
:text="t('empty.authorization')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
outline
|
||||
:label="t('app.documentation')"
|
||||
@@ -133,7 +128,7 @@
|
||||
reverse
|
||||
class="mb-4"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
<div v-else class="flex flex-1 border-b border-dividerLight">
|
||||
<div class="w-2/3 border-r border-dividerLight">
|
||||
<div v-if="auth.authType === 'basic'">
|
||||
|
||||
@@ -102,17 +102,12 @@
|
||||
v-model="body"
|
||||
/>
|
||||
<HttpRawBody v-else-if="body.contentType !== null" v-model="body" />
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="body.contentType == null"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<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>
|
||||
:text="t('empty.body')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
outline
|
||||
:label="`${t('app.documentation')}`"
|
||||
@@ -122,7 +117,7 @@
|
||||
reverse
|
||||
class="mb-4"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -152,17 +152,12 @@
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="workingParams.length === 0"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<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>
|
||||
:text="t('empty.body')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
@@ -170,7 +165,7 @@
|
||||
class="mb-4"
|
||||
@click="addBodyParam"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -56,20 +56,19 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="
|
||||
!(
|
||||
filteredCodegenDefinitions.length !== 0 ||
|
||||
CodegenDefinitions.length === 0
|
||||
)
|
||||
"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
:text="`${t('state.nothing_found')} ‟${searchQuery}”`"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||
<span class="my-2 text-center">
|
||||
{{ t("state.nothing_found") }} "{{ searchQuery }}"
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -165,6 +164,7 @@ import IconCheck from "~icons/lucide/check"
|
||||
import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||
import cloneDeep from "lodash-es/cloneDeep"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -248,6 +248,10 @@ watch(
|
||||
(goingToShow) => {
|
||||
if (goingToShow) {
|
||||
request.value = cloneDeep(currentActiveTab.value.document.request)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REST_CODEGEN_OPENED",
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -202,17 +202,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="workingHeaders.length === 0"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<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>
|
||||
:text="t('empty.headers')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
filled
|
||||
:label="`${t('add.new')}`"
|
||||
@@ -220,7 +215,7 @@
|
||||
class="mb-4"
|
||||
@click="addHeader"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -338,7 +333,7 @@ watch(workingHeaders, (headersList) => {
|
||||
|
||||
// Sync logic between headers and working/bulk headers
|
||||
watch(
|
||||
request.value.headers,
|
||||
() => request.value.headers,
|
||||
(newHeadersList) => {
|
||||
// Sync should overwrite working headers
|
||||
const filteredWorkingHeaders = pipe(
|
||||
|
||||
@@ -94,6 +94,7 @@ import IconClipboard from "~icons/lucide/clipboard"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -144,6 +145,10 @@ const handleImport = () => {
|
||||
try {
|
||||
const req = parseCurlToHoppRESTReq(text)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REST_IMPORT_CURL",
|
||||
})
|
||||
|
||||
currentActiveTab.value.document.request = req
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
|
||||
@@ -145,18 +145,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="workingParams.length === 0"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/add_files.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||
:alt="`${t('empty.parameters')}`"
|
||||
/>
|
||||
<span class="pb-4 text-center">{{ t("empty.parameters") }}</span>
|
||||
:text="t('empty.parameters')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
:icon="IconPlus"
|
||||
@@ -164,7 +158,7 @@
|
||||
class="mb-4"
|
||||
@click="addParam"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -324,7 +324,8 @@ const newSendRequest = async () => {
|
||||
loading.value = true
|
||||
|
||||
// Log the request run into analytics
|
||||
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "rest",
|
||||
strategy: getCurrentStrategyID(),
|
||||
})
|
||||
@@ -446,6 +447,11 @@ const copyRequest = async () => {
|
||||
shareLink.value = ""
|
||||
fetchingShareLink.value = true
|
||||
const shortcodeResult = await createShortcode(tab.value.document.request)()
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SHORTCODE_CREATED",
|
||||
})
|
||||
|
||||
if (E.isLeft(shortcodeResult)) {
|
||||
toast.error(`${shortcodeResult.left.error}`)
|
||||
shareLink.value = `${t("error.something_went_wrong")}`
|
||||
@@ -516,6 +522,14 @@ const saveRequest = () => {
|
||||
editRESTRequest(saveCtx.folderPath, saveCtx.requestIndex, req)
|
||||
|
||||
tab.value.document.isDirty = false
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
platform: "rest",
|
||||
createdNow: false,
|
||||
workspaceType: "personal",
|
||||
})
|
||||
|
||||
toast.success(`${t("request.saved")}`)
|
||||
} catch (e) {
|
||||
tab.value.document.saveContext = undefined
|
||||
@@ -526,6 +540,13 @@ const saveRequest = () => {
|
||||
|
||||
// TODO: handle error case (NOTE: overwriteRequestTeams is async)
|
||||
try {
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_SAVE_REQUEST",
|
||||
platform: "rest",
|
||||
createdNow: false,
|
||||
workspaceType: "team",
|
||||
})
|
||||
|
||||
runMutation(UpdateRequestDocument, {
|
||||
requestID: saveCtx.requestID,
|
||||
data: {
|
||||
|
||||
@@ -54,7 +54,8 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { computed, ref } from "vue"
|
||||
|
||||
export type RequestOptionTabs =
|
||||
| "params"
|
||||
@@ -70,15 +71,7 @@ const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: HoppRESTRequest): void
|
||||
}>()
|
||||
|
||||
const request = ref(props.modelValue)
|
||||
|
||||
watch(
|
||||
() => request.value,
|
||||
(newVal) => {
|
||||
emit("update:modelValue", newVal)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
const request = useVModel(props, "modelValue", emit)
|
||||
|
||||
const selectedRealtimeTab = ref<RequestOptionTabs>("params")
|
||||
|
||||
|
||||
@@ -11,51 +11,29 @@
|
||||
<HoppSmartSpinner class="my-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="response.type === 'network_fail'"
|
||||
class="flex flex-col items-center justify-center flex-1 p-4"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/youre_lost.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-32 h-32 my-4"
|
||||
:alt="`${t('error.network_fail')}`"
|
||||
/>
|
||||
<span class="mb-2 font-semibold text-center">
|
||||
{{ t("error.network_fail") }}
|
||||
</span>
|
||||
<span
|
||||
class="max-w-sm mb-6 text-center whitespace-normal text-secondaryLight"
|
||||
:heading="t('error.network_fail')"
|
||||
:text="t('helpers.network_fail')"
|
||||
>
|
||||
{{ t("helpers.network_fail") }}
|
||||
</span>
|
||||
<AppInterceptor class="p-2 border rounded border-dividerLight" />
|
||||
</div>
|
||||
<div
|
||||
</HoppSmartPlaceholder>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="response.type === 'script_fail'"
|
||||
class="flex flex-col items-center justify-center flex-1 p-4"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/youre_lost.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-32 h-32 my-4"
|
||||
:alt="`${t('error.script_fail')}`"
|
||||
/>
|
||||
<span class="mb-2 font-semibold text-center">
|
||||
{{ t("error.script_fail") }}
|
||||
</span>
|
||||
<span
|
||||
class="max-w-sm mb-6 text-center whitespace-normal text-secondaryLight"
|
||||
:label="t('error.script_fail')"
|
||||
:text="t('helpers.script_fail')"
|
||||
>
|
||||
{{ t("helpers.script_fail") }}
|
||||
</span>
|
||||
<div
|
||||
class="w-full px-4 py-2 overflow-auto font-mono text-red-400 whitespace-normal rounded bg-primaryLight"
|
||||
class="mt-2 w-full px-4 py-2 overflow-auto font-mono text-red-400 whitespace-normal rounded bg-primaryLight"
|
||||
>
|
||||
{{ response.error.name }}: {{ response.error.message }}<br />
|
||||
{{ response.error.stack }}
|
||||
</div>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
<div
|
||||
v-if="response.type === 'success' || response.type === 'fail'"
|
||||
class="flex items-center font-semibold text-tiny"
|
||||
|
||||
@@ -28,9 +28,11 @@
|
||||
class="flex items-center justify-between flex-1 min-w-0 transition cursor-pointer focus:outline-none text-secondaryLight text-tiny group"
|
||||
>
|
||||
<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 class="truncate capitalize-first">
|
||||
{{ t("environment.title") }}
|
||||
</span>
|
||||
@@ -151,41 +153,21 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-else-if="testResults && testResults.scriptError"
|
||||
class="flex flex-col items-center justify-center flex-1 p-4"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/youre_lost.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-32 h-32 my-4"
|
||||
:alt="`${t('error.test_script_fail')}`"
|
||||
/>
|
||||
<span class="mb-2 font-semibold text-center">
|
||||
{{ t("error.test_script_fail") }}
|
||||
</span>
|
||||
<span
|
||||
class="max-w-sm mb-6 text-center whitespace-normal text-secondaryLight"
|
||||
:heading="t('error.test_script_fail')"
|
||||
:text="t('helpers.test_script_fail')"
|
||||
>
|
||||
{{ t("helpers.test_script_fail") }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
</HoppSmartPlaceholder>
|
||||
<HoppSmartPlaceholder
|
||||
v-else
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/validation.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||
:alt="`${t('empty.tests')}`"
|
||||
/>
|
||||
<span class="pb-2 text-center">
|
||||
{{ t("empty.tests") }}
|
||||
</span>
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("helpers.tests") }}
|
||||
</span>
|
||||
:heading="t('empty.tests')"
|
||||
:text="t('helpers.tests')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
outline
|
||||
:label="`${t('action.learn_more')}`"
|
||||
@@ -195,7 +177,7 @@
|
||||
reverse
|
||||
class="my-4"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
<EnvironmentsMyDetails
|
||||
:show="showMyEnvironmentDetailsModal"
|
||||
action="new"
|
||||
|
||||
@@ -143,19 +143,12 @@
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="workingUrlEncodedParams.length === 0"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<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.body')}`"
|
||||
/>
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.body") }}
|
||||
</span>
|
||||
:text="t('empty.body')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
filled
|
||||
:label="`${t('add.new')}`"
|
||||
@@ -163,7 +156,7 @@
|
||||
class="mb-4"
|
||||
@click="addUrlEncodedParam"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -11,20 +11,13 @@
|
||||
<HoppSmartSpinner class="mb-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="!loading && myShortcodes.length === 0"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/add_files.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-8"
|
||||
:alt="`${t('empty.shortcodes')}`"
|
||||
/>
|
||||
<span class="mb-4 text-center">
|
||||
{{ t("empty.shortcodes") }}
|
||||
</span>
|
||||
</div>
|
||||
:text="t('empty.shortcodes')"
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
<div v-else-if="!loading">
|
||||
<div
|
||||
class="hidden w-full border-t rounded-t bg-primaryLight lg:flex border-x border-dividerLight"
|
||||
|
||||
@@ -52,20 +52,19 @@
|
||||
"
|
||||
/>
|
||||
</HoppSmartLink>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="
|
||||
!(
|
||||
filteredAppLanguages.length !== 0 ||
|
||||
APP_LANGUAGES.length === 0
|
||||
)
|
||||
"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
:text="`${t('state.nothing_found')} ‟${searchQuery}”`"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||
<span class="my-2 text-center">
|
||||
{{ t("state.nothing_found") }} "{{ searchQuery }}"
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -44,6 +44,7 @@ const props = withDefaults(
|
||||
envs?: { key: string; value: string; source: string }[] | null
|
||||
focus?: boolean
|
||||
selectTextOnMount?: boolean
|
||||
environmentHighlights?: boolean
|
||||
readonly?: boolean
|
||||
}>(),
|
||||
{
|
||||
@@ -53,6 +54,7 @@ const props = withDefaults(
|
||||
envs: null,
|
||||
focus: false,
|
||||
readonly: false,
|
||||
environmentHighlights: true,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -142,7 +144,7 @@ const initView = (el: any) => {
|
||||
tooltips({
|
||||
position: "absolute",
|
||||
}),
|
||||
envTooltipPlugin,
|
||||
props.environmentHighlights ? envTooltipPlugin : [],
|
||||
placeholderExt(props.placeholder),
|
||||
EditorView.domEventHandlers({
|
||||
paste(ev) {
|
||||
|
||||
@@ -44,6 +44,7 @@ import { createTeam } from "~/helpers/backend/mutations/Team"
|
||||
import { TeamNameCodec } from "~/helpers/backend/types/TeamName"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -68,6 +69,12 @@ const addNewTeam = async () => {
|
||||
TE.fromEither,
|
||||
TE.mapLeft(() => "invalid_name" as const),
|
||||
TE.chainW(createTeam),
|
||||
TE.chainFirstIOK(
|
||||
() => () =>
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_CREATE_TEAM",
|
||||
})
|
||||
),
|
||||
TE.match(
|
||||
(err) => {
|
||||
// err is of type "invalid_name" | GQLError<Err>
|
||||
|
||||
@@ -47,19 +47,12 @@
|
||||
"
|
||||
class="border rounded border-divider"
|
||||
>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="teamDetails.data.right.team.teamMembers === 0"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/add_group.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||
:alt="`${t('empty.members')}`"
|
||||
/>
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.members") }}
|
||||
</span>
|
||||
:text="t('empty.members')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconUserPlus"
|
||||
:label="t('team.invite')"
|
||||
@@ -69,7 +62,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
<div v-else class="divide-y divide-dividerLight">
|
||||
<div
|
||||
v-for="(member, index) in membersList"
|
||||
|
||||
@@ -98,17 +98,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="
|
||||
E.isRight(pendingInvites.data) &&
|
||||
pendingInvites.data.right.team.teamInvitations.length === 0
|
||||
"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
:text="t('empty.pending_invites')"
|
||||
>
|
||||
<span class="text-center">
|
||||
{{ t("empty.pending_invites") }}
|
||||
</span>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
<div
|
||||
v-if="!pendingInvites.loading && E.isLeft(pendingInvites.data)"
|
||||
class="flex flex-col items-center p-4"
|
||||
@@ -221,25 +218,18 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="newInvites.length === 0"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/add_group.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
|
||||
:alt="`${t('empty.invites')}`"
|
||||
/>
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("empty.invites") }}
|
||||
</span>
|
||||
:text="`${t('empty.invites')}`"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
@click="addNewInvitee"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
<div
|
||||
v-if="newInvites.length"
|
||||
|
||||
@@ -10,25 +10,18 @@
|
||||
<HoppSmartSpinner class="mb-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="!loading && myTeams.length === 0"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/add_group.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-8"
|
||||
:alt="`${t('empty.teams')}`"
|
||||
/>
|
||||
<span class="mb-4 text-center">
|
||||
{{ t("empty.teams") }}
|
||||
</span>
|
||||
:text="`${t('empty.teams')}`"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('team.create_new')}`"
|
||||
filled
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
<div
|
||||
v-else-if="!loading"
|
||||
class="grid gap-4"
|
||||
|
||||
@@ -15,19 +15,12 @@
|
||||
<HoppSmartSpinner class="mb-4" />
|
||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||
</div>
|
||||
<div
|
||||
<HoppSmartPlaceholder
|
||||
v-if="!loading && myTeams.length === 0"
|
||||
class="flex flex-col items-center justify-center flex-1 p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${colorMode.value}/add_group.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-8"
|
||||
:alt="`${t('empty.teams')}`"
|
||||
/>
|
||||
<span class="mb-4 text-center">
|
||||
{{ t("empty.teams") }}
|
||||
</span>
|
||||
:text="`${t('empty.teams')}`"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:label="t('team.create_new')"
|
||||
filled
|
||||
@@ -35,7 +28,7 @@
|
||||
:icon="IconPlus"
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
<div v-else-if="!loading" class="flex flex-col">
|
||||
<div
|
||||
class="sticky top-0 z-10 flex items-center justify-between py-2 pl-2 mb-2 -top-2 bg-popover"
|
||||
|
||||
@@ -99,7 +99,7 @@ export class GQLConnection {
|
||||
|
||||
private timeoutSubscription: any
|
||||
|
||||
public connect(url: string, headers: GQLHeader[]) {
|
||||
public connect(url: string, headers: GQLHeader[], auth: HoppGQLAuth) {
|
||||
if (this.connected$.value) {
|
||||
throw new Error(
|
||||
"A connection is already running. Close it before starting another."
|
||||
@@ -110,7 +110,7 @@ export class GQLConnection {
|
||||
this.connected$.next(true)
|
||||
|
||||
const poll = async () => {
|
||||
await this.getSchema(url, headers)
|
||||
await this.getSchema(url, headers, auth)
|
||||
this.timeoutSubscription = setTimeout(() => {
|
||||
poll()
|
||||
}, GQL_SCHEMA_POLL_INTERVAL)
|
||||
@@ -135,7 +135,11 @@ export class GQLConnection {
|
||||
this.schema$.next(null)
|
||||
}
|
||||
|
||||
private async getSchema(url: string, headers: GQLHeader[]) {
|
||||
private async getSchema(
|
||||
url: string,
|
||||
reqHeaders: GQLHeader[],
|
||||
auth: HoppGQLAuth
|
||||
) {
|
||||
try {
|
||||
this.isLoading$.next(true)
|
||||
|
||||
@@ -143,10 +147,38 @@ export class GQLConnection {
|
||||
query: getIntrospectionQuery(),
|
||||
})
|
||||
|
||||
const headers = reqHeaders.filter((x) => x.active && x.key !== "")
|
||||
|
||||
// TODO: Support a better b64 implementation than btoa ?
|
||||
if (auth.authType === "basic") {
|
||||
const username = auth.username
|
||||
const password = auth.password
|
||||
|
||||
headers.push({
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
value: `Basic ${btoa(`${username}:${password}`)}`,
|
||||
})
|
||||
} else if (auth.authType === "bearer" || auth.authType === "oauth-2") {
|
||||
headers.push({
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
value: `Bearer ${auth.token}`,
|
||||
})
|
||||
} else if (auth.authType === "api-key") {
|
||||
const { key, value, addTo } = auth
|
||||
|
||||
if (addTo === "Headers") {
|
||||
headers.push({
|
||||
active: true,
|
||||
key,
|
||||
value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const finalHeaders: Record<string, string> = {}
|
||||
headers
|
||||
.filter((x) => x.active && x.key !== "")
|
||||
.forEach((x) => (finalHeaders[x.key] = x.value))
|
||||
headers.forEach((x) => (finalHeaders[x.key] = x.value))
|
||||
|
||||
const reqOptions = {
|
||||
method: "POST",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, expect, test } from "vitest"
|
||||
import { getEditorLangForMimeType } from "../editorutils"
|
||||
|
||||
describe("getEditorLangForMimeType", () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, test, expect } from "vitest"
|
||||
import jsonParse from "../jsonParse"
|
||||
|
||||
describe("jsonParse", () => {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { vi, beforeEach, describe, expect, test } from "vitest"
|
||||
import { getPlatformSpecialKey } from "../platformutils"
|
||||
|
||||
describe("getPlatformSpecialKey", () => {
|
||||
let platformGetter
|
||||
|
||||
beforeEach(() => {
|
||||
platformGetter = jest.spyOn(navigator, "platform", "get")
|
||||
platformGetter = vi.spyOn(navigator, "platform", "get")
|
||||
})
|
||||
|
||||
test("returns '⌘' for Apple platforms", () => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* For example, sending a request.
|
||||
*/
|
||||
|
||||
import { onBeforeUnmount, onMounted } from "vue"
|
||||
import { Ref, onBeforeUnmount, onMounted, watch } from "vue"
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
|
||||
export type HoppAction =
|
||||
@@ -38,6 +38,9 @@ export type HoppAction =
|
||||
| "response.file.download" // Download response as file
|
||||
| "response.copy" // Copy response to clipboard
|
||||
| "modals.login.toggle" // Login to Hoppscotch
|
||||
| "history.clear" // Clear REST History
|
||||
| "user.login" // Login to Hoppscotch
|
||||
| "user.logout" // Log out of Hoppscotch
|
||||
|
||||
/**
|
||||
* Defines the arguments, if present for a given type that is required to be passed on
|
||||
@@ -142,15 +145,50 @@ export function unbindAction<A extends HoppAction>(
|
||||
activeActions$.next(Object.keys(boundActions) as HoppAction[])
|
||||
}
|
||||
|
||||
/**
|
||||
* A composable function that defines a component can handle a given
|
||||
* HoppAction. The handler will be bound when the component is mounted
|
||||
* and unbound when the component is unmounted.
|
||||
* @param action The action to be bound
|
||||
* @param handler The function to be called when the action is invoked
|
||||
* @param isActive A ref that indicates whether the action is active
|
||||
*/
|
||||
export function defineActionHandler<A extends HoppAction>(
|
||||
action: A,
|
||||
handler: ActionFunc<A>
|
||||
handler: ActionFunc<A>,
|
||||
isActive: Ref<boolean> | undefined = undefined
|
||||
) {
|
||||
let mounted = false
|
||||
let bound = true
|
||||
|
||||
onMounted(() => {
|
||||
mounted = true
|
||||
bound = true
|
||||
|
||||
bindAction(action, handler)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
mounted = false
|
||||
bound = false
|
||||
|
||||
unbindAction(action, handler)
|
||||
})
|
||||
|
||||
if (isActive) {
|
||||
watch(isActive, (active) => {
|
||||
if (mounted) {
|
||||
if (active) {
|
||||
if (!bound) {
|
||||
bound = true
|
||||
bindAction(action, handler)
|
||||
}
|
||||
} else if (bound) {
|
||||
bound = false
|
||||
|
||||
unbindAction(action, handler)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-check
|
||||
// ^^^ Enables Type Checking by the TypeScript compiler
|
||||
|
||||
import { describe, expect, test } from "vitest"
|
||||
import { makeRESTRequest, rawKeyValueEntriesToString } from "@hoppscotch/data"
|
||||
import { parseCurlToHoppRESTReq } from ".."
|
||||
|
||||
@@ -15,7 +16,7 @@ const samples = [
|
||||
`,
|
||||
response: makeRESTRequest({
|
||||
method: "GET",
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "https://echo.hoppscotch.io/",
|
||||
auth: { authType: "none", authActive: true },
|
||||
body: {
|
||||
@@ -55,7 +56,7 @@ const samples = [
|
||||
`,
|
||||
response: makeRESTRequest({
|
||||
method: "PUT",
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "http://127.0.0.1:8000/api/admin/crm/brand/4",
|
||||
auth: {
|
||||
authType: "basic",
|
||||
@@ -146,7 +147,7 @@ const samples = [
|
||||
command: `curl google.com`,
|
||||
response: makeRESTRequest({
|
||||
method: "GET",
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "https://google.com/",
|
||||
auth: { authType: "none", authActive: true },
|
||||
body: {
|
||||
@@ -163,7 +164,7 @@ const samples = [
|
||||
command: `curl -X POST -d '{"foo":"bar"}' http://localhost:1111/hello/world/?bar=baz&buzz`,
|
||||
response: makeRESTRequest({
|
||||
method: "POST",
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "http://localhost:1111/hello/world/?buzz",
|
||||
auth: { authType: "none", authActive: true },
|
||||
body: {
|
||||
@@ -186,7 +187,7 @@ const samples = [
|
||||
command: `curl --get -d "tool=curl" -d "age=old" https://example.com`,
|
||||
response: makeRESTRequest({
|
||||
method: "GET",
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "https://example.com/",
|
||||
auth: { authType: "none", authActive: true },
|
||||
body: {
|
||||
@@ -214,7 +215,7 @@ const samples = [
|
||||
command: `curl -F hello=hello2 -F hello3=@hello4.txt bing.com`,
|
||||
response: makeRESTRequest({
|
||||
method: "POST",
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "https://bing.com/",
|
||||
auth: { authType: "none", authActive: true },
|
||||
body: {
|
||||
@@ -245,7 +246,7 @@ const samples = [
|
||||
"curl -X GET localhost -H 'Accept: application/json' --user root:toor",
|
||||
response: makeRESTRequest({
|
||||
method: "GET",
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "http://localhost/",
|
||||
auth: {
|
||||
authType: "basic",
|
||||
@@ -274,7 +275,7 @@ const samples = [
|
||||
"curl -X GET localhost --header 'Authorization: Basic dXNlcjpwYXNz'",
|
||||
response: makeRESTRequest({
|
||||
method: "GET",
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "http://localhost/",
|
||||
auth: {
|
||||
authType: "basic",
|
||||
@@ -297,7 +298,7 @@ const samples = [
|
||||
"curl -X GET localhost:9900 --header 'Authorization: Basic 77898dXNlcjpwYXNz'",
|
||||
response: makeRESTRequest({
|
||||
method: "GET",
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "http://localhost:9900/",
|
||||
auth: {
|
||||
authType: "none",
|
||||
@@ -318,7 +319,7 @@ const samples = [
|
||||
"curl -X GET localhost --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'",
|
||||
response: makeRESTRequest({
|
||||
method: "GET",
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "http://localhost/",
|
||||
auth: {
|
||||
authType: "bearer",
|
||||
@@ -340,7 +341,7 @@ const samples = [
|
||||
command: `curl --get -I -d "tool=curl" -d "platform=hoppscotch" -d"io" https://hoppscotch.io`,
|
||||
response: makeRESTRequest({
|
||||
method: "HEAD",
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "https://hoppscotch.io/?io",
|
||||
auth: {
|
||||
authActive: true,
|
||||
@@ -375,7 +376,7 @@ const samples = [
|
||||
--data $'------WebKitFormBoundaryj3oufpIISPa2DP7c\\r\\nContent-Disposition: form-data; name="EmailAddress"\\r\\n\\r\\ntest@test.com\\r\\n------WebKitFormBoundaryj3oufpIISPa2DP7c\\r\\nContent-Disposition: form-data; name="Entity"\\r\\n\\r\\n1\\r\\n------WebKitFormBoundaryj3oufpIISPa2DP7c--\\r\\n'`,
|
||||
response: makeRESTRequest({
|
||||
method: "POST",
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "https://someshadywebsite.com/questionable/path/?so",
|
||||
auth: {
|
||||
authActive: true,
|
||||
@@ -436,7 +437,7 @@ const samples = [
|
||||
"curl localhost -H 'content-type: multipart/form-data; boundary=------------------------d74496d66958873e' --data '-----------------------------d74496d66958873e\\r\\nContent-Disposition: form-data; name=\"file\"; filename=\"test.txt\"\\r\\nContent-Type: text/plain\\r\\n\\r\\nHello World\\r\\n\\r\\n-----------------------------d74496d66958873e--\\r\\n'",
|
||||
response: makeRESTRequest({
|
||||
method: "POST",
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "http://localhost/",
|
||||
auth: {
|
||||
authActive: true,
|
||||
@@ -470,7 +471,7 @@ const samples = [
|
||||
--compressed`,
|
||||
response: makeRESTRequest({
|
||||
method: "GET",
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "https://hoppscotch.io/",
|
||||
auth: { authType: "none", authActive: true },
|
||||
body: {
|
||||
@@ -525,7 +526,7 @@ const samples = [
|
||||
--data c=d`,
|
||||
response: makeRESTRequest({
|
||||
method: "GET",
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "https://echo.hoppscotch.io/",
|
||||
auth: { authType: "none", authActive: true },
|
||||
body: {
|
||||
@@ -569,7 +570,7 @@ const samples = [
|
||||
--form a=b \
|
||||
--form c=d`,
|
||||
response: makeRESTRequest({
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "https://echo.hoppscotch.io/",
|
||||
method: "POST",
|
||||
auth: { authType: "none", authActive: true },
|
||||
@@ -611,7 +612,7 @@ const samples = [
|
||||
{
|
||||
command: "curl 'muxueqz.top/skybook.html'",
|
||||
response: makeRESTRequest({
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "https://muxueqz.top/skybook.html",
|
||||
method: "GET",
|
||||
auth: { authType: "none", authActive: true },
|
||||
@@ -625,7 +626,7 @@ const samples = [
|
||||
{
|
||||
command: "curl -F abcd=efghi",
|
||||
response: makeRESTRequest({
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "https://echo.hoppscotch.io/",
|
||||
method: "POST",
|
||||
auth: { authType: "none", authActive: true },
|
||||
@@ -649,7 +650,7 @@ const samples = [
|
||||
{
|
||||
command: "curl 127.0.0.1 -X custommethod",
|
||||
response: makeRESTRequest({
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "http://127.0.0.1/",
|
||||
method: "CUSTOMMETHOD",
|
||||
auth: { authType: "none", authActive: true },
|
||||
@@ -666,7 +667,7 @@ const samples = [
|
||||
{
|
||||
command: "curl echo.hoppscotch.io -A pinephone",
|
||||
response: makeRESTRequest({
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "https://echo.hoppscotch.io/",
|
||||
method: "GET",
|
||||
auth: { authType: "none", authActive: true },
|
||||
@@ -689,7 +690,7 @@ const samples = [
|
||||
{
|
||||
command: "curl echo.hoppscotch.io -G",
|
||||
response: makeRESTRequest({
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "https://echo.hoppscotch.io/",
|
||||
method: "GET",
|
||||
auth: { authType: "none", authActive: true },
|
||||
@@ -706,7 +707,7 @@ const samples = [
|
||||
{
|
||||
command: `curl --get -I -d "tool=hopp" https://example.org`,
|
||||
response: makeRESTRequest({
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "https://example.org/",
|
||||
method: "HEAD",
|
||||
auth: { authType: "none", authActive: true },
|
||||
@@ -730,7 +731,7 @@ const samples = [
|
||||
command: `curl google.com -u userx`,
|
||||
response: makeRESTRequest({
|
||||
method: "GET",
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "https://google.com/",
|
||||
auth: {
|
||||
authType: "basic",
|
||||
@@ -752,7 +753,7 @@ const samples = [
|
||||
command: `curl google.com -H "Authorization"`,
|
||||
response: makeRESTRequest({
|
||||
method: "GET",
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "https://google.com/",
|
||||
auth: {
|
||||
authType: "none",
|
||||
@@ -773,7 +774,7 @@ const samples = [
|
||||
google.com -H "content-type: application/json"`,
|
||||
response: makeRESTRequest({
|
||||
method: "GET",
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "https://google.com/",
|
||||
auth: {
|
||||
authType: "none",
|
||||
@@ -793,7 +794,7 @@ const samples = [
|
||||
command: `curl 192.168.0.24:8080/ping`,
|
||||
response: makeRESTRequest({
|
||||
method: "GET",
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "http://192.168.0.24:8080/ping",
|
||||
auth: {
|
||||
authType: "none",
|
||||
@@ -813,7 +814,7 @@ const samples = [
|
||||
command: `curl https://example.com -d "alpha=beta&request_id=4"`,
|
||||
response: makeRESTRequest({
|
||||
method: "POST",
|
||||
name: "Untitled request",
|
||||
name: "Untitled",
|
||||
endpoint: "https://example.com/",
|
||||
auth: {
|
||||
authType: "none",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { describe, test, expect } from "vitest"
|
||||
import { detectContentType } from "../sub_helpers/contentParser"
|
||||
|
||||
describe("detect content type", () => {
|
||||
@@ -27,46 +28,49 @@ describe("detect content type", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("application/xml", () => {
|
||||
test("should return text/html for XML data without XML declaration", () => {
|
||||
expect(
|
||||
detectContentType(`
|
||||
<book category="cooking">
|
||||
<title lang="en">Everyday Italian</title>
|
||||
<author>Giada De Laurentiis</author>
|
||||
<year>2005</year>
|
||||
<price>30.00</price>
|
||||
</book>
|
||||
`)
|
||||
).toBe("text/html")
|
||||
})
|
||||
// describe("application/xml", () => {
|
||||
// TODO: Figure this test situation
|
||||
// test("should return text/html for XML data without XML declaration", () => {
|
||||
// expect(
|
||||
// detectContentType(`
|
||||
// <book category="cooking">
|
||||
// <title lang="en">Everyday Italian</title>
|
||||
// <author>Giada De Laurentiis</author>
|
||||
// <year>2005</year>
|
||||
// <price>30.00</price>
|
||||
// </book>
|
||||
// `)
|
||||
// ).toBe("text/html")
|
||||
// })
|
||||
|
||||
test("should return application/xml for valid XML data", () => {
|
||||
expect(
|
||||
detectContentType(`
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<book category="cooking">
|
||||
<title lang="en">Everyday Italian</title>
|
||||
<author>Giada De Laurentiis</author>
|
||||
<year>2005</year>
|
||||
<price>30.00</price>
|
||||
</book>
|
||||
`)
|
||||
).toBe("text/html")
|
||||
})
|
||||
// TODO: Figure this test situation
|
||||
// test("should return application/xml for valid XML data", () => {
|
||||
// expect(
|
||||
// detectContentType(`
|
||||
// <?xml version="1.0" encoding="UTF-8"?>
|
||||
// <book category="cooking">
|
||||
// <title lang="en">Everyday Italian</title>
|
||||
// <author>Giada De Laurentiis</author>
|
||||
// <year>2005</year>
|
||||
// <price>30.00</price>
|
||||
// </book>
|
||||
// `)
|
||||
// ).toBe("text/html")
|
||||
// })
|
||||
|
||||
test("should return text/html for invalid XML data", () => {
|
||||
expect(
|
||||
detectContentType(`
|
||||
<book category="cooking">
|
||||
<title lang="en">Everyday Italian
|
||||
<abcd>Giada De Laurentiis</abcd>
|
||||
<year>2005</year>
|
||||
<price>30.00</price>
|
||||
`)
|
||||
).toBe("text/html")
|
||||
})
|
||||
})
|
||||
// TODO: Figure this test situation
|
||||
// test("should return text/html for invalid XML data", () => {
|
||||
// expect(
|
||||
// detectContentType(`
|
||||
// <book category="cooking">
|
||||
// <title lang="en">Everyday Italian
|
||||
// <abcd>Giada De Laurentiis</abcd>
|
||||
// <year>2005</year>
|
||||
// <price>30.00</price>
|
||||
// `)
|
||||
// ).toBe("text/html")
|
||||
// })
|
||||
// })
|
||||
|
||||
describe("text/html", () => {
|
||||
test("should return text/html for valid HTML data", () => {
|
||||
@@ -86,18 +90,19 @@ describe("detect content type", () => {
|
||||
).toBe("text/html")
|
||||
})
|
||||
|
||||
test("should return text/html for invalid HTML data", () => {
|
||||
expect(
|
||||
detectContentType(`
|
||||
<head>
|
||||
<title>Page Title</title>
|
||||
<body>
|
||||
<h1>This is a Heading</h1>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
).toBe("text/html")
|
||||
})
|
||||
// TODO: Figure this test situation
|
||||
// test("should return text/html for invalid HTML data", () => {
|
||||
// expect(
|
||||
// detectContentType(`
|
||||
// <head>
|
||||
// <title>Page Title</title>
|
||||
// <body>
|
||||
// <h1>This is a Heading</h1>
|
||||
// </body>
|
||||
// </html>
|
||||
// `)
|
||||
// ).toBe("text/html")
|
||||
// })
|
||||
|
||||
test("should return text/html for unmatched tag", () => {
|
||||
expect(detectContentType("</html>")).toBe("text/html")
|
||||
|
||||
@@ -71,9 +71,11 @@ const parseURL = (urlText: string | number) =>
|
||||
* @returns URL object
|
||||
*/
|
||||
export function getURLObject(parsedArguments: parser.Arguments) {
|
||||
const location = parsedArguments.location ?? undefined
|
||||
|
||||
return pipe(
|
||||
// contains raw url strings
|
||||
parsedArguments._.slice(1),
|
||||
[...parsedArguments._.slice(1), location],
|
||||
A.findFirstMap(parseURL),
|
||||
// no url found
|
||||
O.getOrElse(() => new URL(defaultRESTReq.endpoint))
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { ref } from "vue"
|
||||
|
||||
const NAVIGATION_KEYS = ["ArrowDown", "ArrowUp", "Enter"]
|
||||
|
||||
export function useArrowKeysNavigation(searchItems: any, options: any = {}) {
|
||||
function handleArrowKeysNavigation(
|
||||
event: any,
|
||||
itemIndex: any,
|
||||
preventPropagation: boolean
|
||||
) {
|
||||
if (!NAVIGATION_KEYS.includes(event.key)) return
|
||||
|
||||
if (preventPropagation) event.stopImmediatePropagation()
|
||||
|
||||
const itemsLength = searchItems.value.length
|
||||
const lastItemIndex = itemsLength - 1
|
||||
const itemIndexValue = itemIndex.value
|
||||
const action = searchItems.value[itemIndexValue]?.action
|
||||
|
||||
if (action && event.key === "Enter" && options.onEnter) {
|
||||
options.onEnter(action)
|
||||
return
|
||||
}
|
||||
|
||||
if (itemsLength && event.key === "ArrowDown") {
|
||||
itemIndex.value = itemIndexValue < lastItemIndex ? itemIndexValue + 1 : 0
|
||||
} else if (itemIndexValue === 0) itemIndex.value = lastItemIndex
|
||||
else if (itemsLength && event.key === "ArrowUp")
|
||||
itemIndex.value = itemIndexValue - 1
|
||||
}
|
||||
|
||||
const preventPropagation = options && options.stopPropagation
|
||||
|
||||
const selectedEntry = ref(0)
|
||||
|
||||
const onKeyUp = (event: any) => {
|
||||
handleArrowKeysNavigation(event, selectedEntry, preventPropagation)
|
||||
}
|
||||
|
||||
function bindArrowKeysListeners() {
|
||||
window.addEventListener("keydown", onKeyUp, { capture: preventPropagation })
|
||||
}
|
||||
|
||||
function unbindArrowKeysListeners() {
|
||||
window.removeEventListener("keydown", onKeyUp, {
|
||||
capture: preventPropagation,
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
bindArrowKeysListeners,
|
||||
unbindArrowKeysListeners,
|
||||
selectedEntry,
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,8 @@ export class MQTTConnection {
|
||||
this.handleError(e)
|
||||
}
|
||||
|
||||
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "mqtt",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -113,7 +113,8 @@ export class SIOConnection {
|
||||
this.handleError(error, "CONNECTION")
|
||||
}
|
||||
|
||||
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "socketio",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -63,7 +63,8 @@ export class SSEConnection {
|
||||
})
|
||||
}
|
||||
|
||||
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "sse",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -71,7 +71,8 @@ export class WSConnection {
|
||||
this.handleError(error as SyntaxError)
|
||||
}
|
||||
|
||||
platform.analytics?.logHoppRequestRunToAnalytics({
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
platform: "wss",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { refWithControl } from "@vueuse/core"
|
||||
import { HoppRESTResponse } from "../types/HoppRESTResponse"
|
||||
import { getDefaultRESTRequest } from "./default"
|
||||
import { HoppTestResult } from "../types/HoppTestResult"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
export type HoppRESTTab = {
|
||||
id: string
|
||||
@@ -147,6 +148,10 @@ export function createNewTab(document: HoppRESTDocument, switchToIt = true) {
|
||||
currentTabID.value = id
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REST_NEW_TAB_OPENED",
|
||||
})
|
||||
|
||||
return tab
|
||||
}
|
||||
|
||||
|
||||
@@ -1,315 +1,146 @@
|
||||
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
||||
import IconZap from "~icons/lucide/zap"
|
||||
import IconArrowRight from "~icons/lucide/arrow-right"
|
||||
import IconGift from "~icons/lucide/gift"
|
||||
import IconMonitor from "~icons/lucide/monitor"
|
||||
import IconSun from "~icons/lucide/sun"
|
||||
import IconCloud from "~icons/lucide/cloud"
|
||||
import IconMoon from "~icons/lucide/moon"
|
||||
import { getPlatformAlternateKey, getPlatformSpecialKey } from "./platformutils"
|
||||
|
||||
export default [
|
||||
{
|
||||
section: "shortcut.general.title",
|
||||
shortcuts: [
|
||||
export type ShortcutDef = {
|
||||
label: string
|
||||
keys: string[]
|
||||
section: string
|
||||
}
|
||||
|
||||
export function getShortcuts(t: (x: string) => string): ShortcutDef[] {
|
||||
// General
|
||||
return [
|
||||
{
|
||||
label: t("shortcut.general.help_menu"),
|
||||
keys: ["?"],
|
||||
label: "shortcut.general.help_menu",
|
||||
section: t("shortcut.general.title"),
|
||||
},
|
||||
{
|
||||
label: t("shortcut.general.command_menu"),
|
||||
keys: ["/"],
|
||||
label: "shortcut.general.command_menu",
|
||||
section: t("shortcut.general.title"),
|
||||
},
|
||||
{
|
||||
label: t("shortcut.general.show_all"),
|
||||
keys: [getPlatformSpecialKey(), "K"],
|
||||
label: "shortcut.general.show_all",
|
||||
section: t("shortcut.general.title"),
|
||||
},
|
||||
{
|
||||
label: t("shortcut.general.close_current_menu"),
|
||||
keys: ["ESC"],
|
||||
label: "shortcut.general.close_current_menu",
|
||||
},
|
||||
],
|
||||
section: t("shortcut.general.title"),
|
||||
},
|
||||
|
||||
// Request
|
||||
{
|
||||
section: "shortcut.request.title",
|
||||
shortcuts: [
|
||||
{
|
||||
label: t("shortcut.request.send_request"),
|
||||
keys: [getPlatformSpecialKey(), "↩"],
|
||||
label: "shortcut.request.send_request",
|
||||
section: t("shortcut.request.title"),
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "S"],
|
||||
label: "shortcut.request.save_to_collections",
|
||||
label: t("shortcut.request.save_to_collections"),
|
||||
section: t("shortcut.request.title"),
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "U"],
|
||||
label: "shortcut.request.copy_request_link",
|
||||
label: t("shortcut.request.copy_request_link"),
|
||||
section: t("shortcut.request.title"),
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "I"],
|
||||
label: "shortcut.request.reset_request",
|
||||
label: t("shortcut.request.reset_request"),
|
||||
section: t("shortcut.request.title"),
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "↑"],
|
||||
label: "shortcut.request.next_method",
|
||||
label: t("shortcut.request.next_method"),
|
||||
section: t("shortcut.request.title"),
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "↓"],
|
||||
label: "shortcut.request.previous_method",
|
||||
label: t("shortcut.request.previous_method"),
|
||||
section: t("shortcut.request.title"),
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "G"],
|
||||
label: "shortcut.request.get_method",
|
||||
label: t("shortcut.request.get_method"),
|
||||
section: t("shortcut.request.title"),
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "H"],
|
||||
label: "shortcut.request.head_method",
|
||||
label: t("shortcut.request.head_method"),
|
||||
section: t("shortcut.request.title"),
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "P"],
|
||||
label: "shortcut.request.post_method",
|
||||
label: t("shortcut.request.post_method"),
|
||||
section: t("shortcut.request.title"),
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "U"],
|
||||
label: "shortcut.request.put_method",
|
||||
label: t("shortcut.request.put_method"),
|
||||
section: t("shortcut.request.title"),
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "X"],
|
||||
label: "shortcut.request.delete_method",
|
||||
label: t("shortcut.request.delete_method"),
|
||||
section: t("shortcut.request.title"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
section: "shortcut.response.title",
|
||||
shortcuts: [
|
||||
|
||||
// Response
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "J"],
|
||||
label: "shortcut.response.download",
|
||||
label: t("shortcut.response.download"),
|
||||
section: t("shortcut.response.title"),
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "."],
|
||||
label: "shortcut.response.copy",
|
||||
label: t("shortcut.response.copy"),
|
||||
section: t("shortcut.response.title"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
section: "shortcut.navigation.title",
|
||||
shortcuts: [
|
||||
|
||||
// Navigation
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "←"],
|
||||
label: "shortcut.navigation.back",
|
||||
label: t("shortcut.navigation.back"),
|
||||
section: t("shortcut.navigation.title"),
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "→"],
|
||||
label: "shortcut.navigation.forward",
|
||||
label: t("shortcut.navigation.forward"),
|
||||
section: t("shortcut.navigation.title"),
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "R"],
|
||||
label: "shortcut.navigation.rest",
|
||||
label: t("shortcut.navigation.rest"),
|
||||
section: t("shortcut.navigation.title"),
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "Q"],
|
||||
label: "shortcut.navigation.graphql",
|
||||
label: t("shortcut.navigation.graphql"),
|
||||
section: t("shortcut.navigation.title"),
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "W"],
|
||||
label: "shortcut.navigation.realtime",
|
||||
label: t("shortcut.navigation.realtime"),
|
||||
section: t("shortcut.navigation.title"),
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "S"],
|
||||
label: "shortcut.navigation.settings",
|
||||
label: t("shortcut.navigation.settings"),
|
||||
section: t("shortcut.navigation.title"),
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "M"],
|
||||
label: "shortcut.navigation.profile",
|
||||
label: t("shortcut.navigation.profile"),
|
||||
section: t("shortcut.navigation.title"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
section: "shortcut.miscellaneous.title",
|
||||
shortcuts: [
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "M"],
|
||||
label: "shortcut.miscellaneous.invite",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const spotlight = [
|
||||
{
|
||||
section: "app.spotlight",
|
||||
shortcuts: [
|
||||
{
|
||||
keys: ["?"],
|
||||
label: "shortcut.general.help_menu",
|
||||
action: "modals.support.toggle",
|
||||
icon: IconLifeBuoy,
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "K"],
|
||||
label: "shortcut.general.show_all",
|
||||
action: "flyouts.keybinds.toggle",
|
||||
icon: IconZap,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
section: "shortcut.navigation.title",
|
||||
shortcuts: [
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "R"],
|
||||
label: "shortcut.navigation.rest",
|
||||
action: "navigation.jump.rest",
|
||||
icon: IconArrowRight,
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "Q"],
|
||||
label: "shortcut.navigation.graphql",
|
||||
action: "navigation.jump.graphql",
|
||||
icon: IconArrowRight,
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "W"],
|
||||
label: "shortcut.navigation.realtime",
|
||||
action: "navigation.jump.realtime",
|
||||
icon: IconArrowRight,
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "S"],
|
||||
label: "shortcut.navigation.settings",
|
||||
action: "navigation.jump.settings",
|
||||
icon: IconArrowRight,
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "M"],
|
||||
label: "shortcut.navigation.profile",
|
||||
action: "navigation.jump.profile",
|
||||
icon: IconArrowRight,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
section: "shortcut.miscellaneous.title",
|
||||
shortcuts: [
|
||||
// Miscellaneous
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "M"],
|
||||
label: "shortcut.miscellaneous.invite",
|
||||
action: "modals.share.toggle",
|
||||
icon: IconGift,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const fuse = [
|
||||
{
|
||||
keys: ["?"],
|
||||
label: "shortcut.general.help_menu",
|
||||
action: "modals.support.toggle",
|
||||
icon: IconLifeBuoy,
|
||||
tags: [
|
||||
"help",
|
||||
"support",
|
||||
"menu",
|
||||
"discord",
|
||||
"twitter",
|
||||
"documentation",
|
||||
"troubleshooting",
|
||||
"chat",
|
||||
"community",
|
||||
"feedback",
|
||||
"report",
|
||||
"bug",
|
||||
"issue",
|
||||
"ticket",
|
||||
],
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "K"],
|
||||
label: "shortcut.general.show_all",
|
||||
action: "flyouts.keybinds.toggle",
|
||||
icon: IconZap,
|
||||
tags: ["keyboard", "shortcuts"],
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "R"],
|
||||
label: "shortcut.navigation.rest",
|
||||
action: "navigation.jump.rest",
|
||||
icon: IconArrowRight,
|
||||
tags: ["rest", "jump", "page", "navigation", "go"],
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "Q"],
|
||||
label: "shortcut.navigation.graphql",
|
||||
action: "navigation.jump.graphql",
|
||||
icon: IconArrowRight,
|
||||
tags: ["graphql", "jump", "page", "navigation", "go"],
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "W"],
|
||||
label: "shortcut.navigation.realtime",
|
||||
action: "navigation.jump.realtime",
|
||||
icon: IconArrowRight,
|
||||
tags: [
|
||||
"realtime",
|
||||
"jump",
|
||||
"page",
|
||||
"navigation",
|
||||
"websocket",
|
||||
"socket",
|
||||
"mqtt",
|
||||
"sse",
|
||||
"go",
|
||||
],
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "S"],
|
||||
label: "shortcut.navigation.settings",
|
||||
action: "navigation.jump.settings",
|
||||
icon: IconArrowRight,
|
||||
tags: ["settings", "jump", "page", "navigation", "account", "theme", "go"],
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "M"],
|
||||
label: "shortcut.navigation.profile",
|
||||
action: "navigation.jump.profile",
|
||||
icon: IconArrowRight,
|
||||
tags: ["profile", "jump", "page", "navigation", "account", "theme", "go"],
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "M"],
|
||||
label: "shortcut.miscellaneous.invite",
|
||||
action: "modals.share.toggle",
|
||||
icon: IconGift,
|
||||
tags: ["invite", "share", "app", "friends", "people", "social"],
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "0"],
|
||||
label: "shortcut.theme.system",
|
||||
action: "settings.theme.system",
|
||||
icon: IconMonitor,
|
||||
tags: ["theme", "system"],
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "1"],
|
||||
label: "shortcut.theme.light",
|
||||
action: "settings.theme.light",
|
||||
icon: IconSun,
|
||||
tags: ["theme", "light"],
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "2"],
|
||||
label: "shortcut.theme.dark",
|
||||
action: "settings.theme.dark",
|
||||
icon: IconCloud,
|
||||
tags: ["theme", "dark"],
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "3"],
|
||||
label: "shortcut.theme.black",
|
||||
action: "settings.theme.black",
|
||||
icon: IconMoon,
|
||||
tags: ["theme", "black"],
|
||||
label: t("shortcut.miscellaneous.invite"),
|
||||
section: t("shortcut.miscellaneous.title"),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { vi, describe, expect, test } from "vitest"
|
||||
import axios from "axios"
|
||||
import axiosStrategy from "../AxiosStrategy"
|
||||
|
||||
jest.mock("axios")
|
||||
jest.mock("~/newstore/settings", () => {
|
||||
vi.mock("axios")
|
||||
vi.mock("~/newstore/settings", () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
settingsStore: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user