Compare commits
8 Commits
fix/genera
...
2023.4.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e869d49e16 | ||
|
|
6496bea846 | ||
|
|
39842559b5 | ||
|
|
51efb35aa6 | ||
|
|
9402bb9285 | ||
|
|
82b6e08d68 | ||
|
|
25177bd635 | ||
|
|
6928eb7992 |
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*/**/node_modules
|
||||||
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@@ -2,9 +2,9 @@ name: Node.js CI
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, staging]
|
branches: [main, staging, "release/**"]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, staging]
|
branches: [main, staging, "release/**"]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ services:
|
|||||||
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
|
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
|
||||||
- PORT=3000
|
- PORT=3000
|
||||||
volumes:
|
volumes:
|
||||||
- ./packages/hoppscotch-backend/:/usr/src/app
|
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
||||||
|
# - ./packages/hoppscotch-backend/:/usr/src/app
|
||||||
- /usr/src/app/node_modules/
|
- /usr/src/app/node_modules/
|
||||||
depends_on:
|
depends_on:
|
||||||
- hoppscotch-db
|
hoppscotch-db:
|
||||||
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- "3170:3000"
|
- "3170:3000"
|
||||||
|
|
||||||
@@ -60,12 +62,20 @@ services:
|
|||||||
# you are using an external postgres instance
|
# you are using an external postgres instance
|
||||||
# This will be exposed at port 5432
|
# This will be exposed at port 5432
|
||||||
hoppscotch-db:
|
hoppscotch-db:
|
||||||
image: postgres
|
image: postgres:15
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
|
user: postgres
|
||||||
environment:
|
environment:
|
||||||
|
# The default user defined by the docker image
|
||||||
|
POSTGRES_USER: postgres
|
||||||
# NOTE: Please UPDATE THIS PASSWORD!
|
# NOTE: Please UPDATE THIS PASSWORD!
|
||||||
POSTGRES_PASSWORD: testpass
|
POSTGRES_PASSWORD: testpass
|
||||||
POSTGRES_DB: hoppscotch
|
POSTGRES_DB: hoppscotch
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "sh -c 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
24
packages/dioc/.gitignore
vendored
24
packages/dioc/.gitignore
vendored
@@ -1,24 +0,0 @@
|
|||||||
# 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?
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
# 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
2
packages/dioc/index.d.ts
vendored
@@ -1,2 +0,0 @@
|
|||||||
export { default } from "./dist/main.d.ts"
|
|
||||||
export * from "./dist/main.d.ts"
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from "./container"
|
|
||||||
export * from "./service"
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,262 +0,0 @@
|
|||||||
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([])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
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")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
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
2
packages/dioc/testing.d.ts
vendored
@@ -1,2 +0,0 @@
|
|||||||
export { default } from "./dist/testing.d.ts"
|
|
||||||
export * from "./dist/testing.d.ts"
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"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"]
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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'],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { defineConfig } from "vitest/config"
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
test: {
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
2
packages/dioc/vue.d.ts
vendored
2
packages/dioc/vue.d.ts
vendored
@@ -1,2 +0,0 @@
|
|||||||
export { default } from "./dist/vue.d.ts"
|
|
||||||
export * from "./dist/vue.d.ts"
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hoppscotch-backend",
|
"name": "hoppscotch-backend",
|
||||||
"version": "2023.4.7",
|
"version": "2023.4.8",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -306,8 +306,8 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
||||||
...teamEnvironment,
|
|
||||||
id: 'newid',
|
id: 'newid',
|
||||||
|
...teamEnvironment,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||||
@@ -337,8 +337,8 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
mockPrisma.teamEnvironment.create.mockResolvedValueOnce({
|
||||||
...teamEnvironment,
|
|
||||||
id: 'newid',
|
id: 'newid',
|
||||||
|
...teamEnvironment,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { TeamService } from 'src/team/team.service';
|
|||||||
* This guard only allows user to execute the resolver
|
* This guard only allows user to execute the resolver
|
||||||
* 1. If user is invitee, allow
|
* 1. If user is invitee, allow
|
||||||
* 2. Or else, if user is team member, allow
|
* 2. Or else, if user is team member, allow
|
||||||
*
|
*
|
||||||
* TLDR: Allow if user is invitee or team member
|
* TLDR: Allow if user is invitee or team member
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ module.exports = {
|
|||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
node: true,
|
node: true,
|
||||||
|
jest: true,
|
||||||
},
|
},
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
sourceType: "module",
|
sourceType: "module",
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"choose_file": "Choose a file",
|
"choose_file": "Choose a file",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
"clear_history": "Clear All History",
|
|
||||||
"clear_all": "Clear all",
|
"clear_all": "Clear all",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"connect": "Connect",
|
"connect": "Connect",
|
||||||
@@ -174,7 +173,6 @@
|
|||||||
"folder": "Folder is empty",
|
"folder": "Folder is empty",
|
||||||
"headers": "This request does not have any headers",
|
"headers": "This request does not have any headers",
|
||||||
"history": "History is empty",
|
"history": "History is empty",
|
||||||
"history_suggestions": "History does not have any matching entries",
|
|
||||||
"invites": "Invite list is empty",
|
"invites": "Invite list is empty",
|
||||||
"members": "Team is empty",
|
"members": "Team is empty",
|
||||||
"parameters": "This request does not have any parameters",
|
"parameters": "This request does not have any parameters",
|
||||||
@@ -584,11 +582,6 @@
|
|||||||
"log": "Log",
|
"log": "Log",
|
||||||
"url": "URL"
|
"url": "URL"
|
||||||
},
|
},
|
||||||
"spotlight": {
|
|
||||||
"section": {
|
|
||||||
"user": "User"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sse": {
|
"sse": {
|
||||||
"event_type": "Event type",
|
"event_type": "Event type",
|
||||||
"log": "Log",
|
"log": "Log",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"edit": "編輯",
|
"edit": "編輯",
|
||||||
"filter": "篩選回應",
|
"filter": "篩選回應",
|
||||||
"go_back": "返回",
|
"go_back": "返回",
|
||||||
"go_forward": "Go forward",
|
"go_forward": "向前",
|
||||||
"group_by": "分組方式",
|
"group_by": "分組方式",
|
||||||
"label": "標籤",
|
"label": "標籤",
|
||||||
"learn_more": "瞭解更多",
|
"learn_more": "瞭解更多",
|
||||||
@@ -117,37 +117,37 @@
|
|||||||
"username": "使用者名稱"
|
"username": "使用者名稱"
|
||||||
},
|
},
|
||||||
"collection": {
|
"collection": {
|
||||||
"created": "組合已建立",
|
"created": "集合已建立",
|
||||||
"different_parent": "Cannot reorder collection with different parent",
|
"different_parent": "無法為父集合不同的集合重新排序",
|
||||||
"edit": "編輯組合",
|
"edit": "編輯集合",
|
||||||
"invalid_name": "請提供有效的組合名稱",
|
"invalid_name": "請提供有效的集合名稱",
|
||||||
"invalid_root_move": "Collection already in the root",
|
"invalid_root_move": "集合已在根目錄",
|
||||||
"moved": "Moved Successfully",
|
"moved": "移動成功",
|
||||||
"my_collections": "我的組合",
|
"my_collections": "我的集合",
|
||||||
"name": "我的新組合",
|
"name": "我的新集合",
|
||||||
"name_length_insufficient": "組合名稱至少要有 3 個字元。",
|
"name_length_insufficient": "集合名稱至少要有 3 個字元。",
|
||||||
"new": "建立組合",
|
"new": "建立集合",
|
||||||
"order_changed": "Collection Order Updated",
|
"order_changed": "集合順序已更新",
|
||||||
"renamed": "組合已重新命名",
|
"renamed": "集合已重新命名",
|
||||||
"request_in_use": "請求正在使用中",
|
"request_in_use": "請求正在使用中",
|
||||||
"save_as": "另存為",
|
"save_as": "另存為",
|
||||||
"select": "選擇一個組合",
|
"select": "選擇一個集合",
|
||||||
"select_location": "選擇位置",
|
"select_location": "選擇位置",
|
||||||
"select_team": "選擇一個團隊",
|
"select_team": "選擇一個團隊",
|
||||||
"team_collections": "團隊組合"
|
"team_collections": "團隊集合"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"exit_team": "您確定要離開此團隊嗎?",
|
"exit_team": "您確定要離開此團隊嗎?",
|
||||||
"logout": "您確定要登出嗎?",
|
"logout": "您確定要登出嗎?",
|
||||||
"remove_collection": "您確定要永久刪除該組合嗎?",
|
"remove_collection": "您確定要永久刪除該集合嗎?",
|
||||||
"remove_environment": "您確定要永久刪除該環境嗎?",
|
"remove_environment": "您確定要永久刪除該環境嗎?",
|
||||||
"remove_folder": "您確定要永久刪除該資料夾嗎?",
|
"remove_folder": "您確定要永久刪除該資料夾嗎?",
|
||||||
"remove_history": "您確定要永久刪除全部歷史記錄嗎?",
|
"remove_history": "您確定要永久刪除全部歷史記錄嗎?",
|
||||||
"remove_request": "您確定要永久刪除該請求嗎?",
|
"remove_request": "您確定要永久刪除該請求嗎?",
|
||||||
"remove_team": "您確定要刪除該團隊嗎?",
|
"remove_team": "您確定要刪除該團隊嗎?",
|
||||||
"remove_telemetry": "您確定要退出遙測服務嗎?",
|
"remove_telemetry": "您確定要退出遙測服務嗎?",
|
||||||
"request_change": "您確定要捨棄當前請求嗎?未儲存的變更將遺失。",
|
"request_change": "您確定要捨棄目前的請求嗎?未儲存的變更將遺失。",
|
||||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
"save_unsaved_tab": "您要儲存在此分頁做出的改動嗎?",
|
||||||
"sync": "您想從雲端恢復您的工作區嗎?這將丟棄您的本地進度。"
|
"sync": "您想從雲端恢復您的工作區嗎?這將丟棄您的本地進度。"
|
||||||
},
|
},
|
||||||
"count": {
|
"count": {
|
||||||
@@ -160,13 +160,13 @@
|
|||||||
},
|
},
|
||||||
"documentation": {
|
"documentation": {
|
||||||
"generate": "產生文件",
|
"generate": "產生文件",
|
||||||
"generate_message": "匯入 Hoppscotch 組合以隨時隨地產生 API 文件。"
|
"generate_message": "匯入 Hoppscotch 集合以隨時隨地產生 API 文件。"
|
||||||
},
|
},
|
||||||
"empty": {
|
"empty": {
|
||||||
"authorization": "該請求沒有使用任何授權",
|
"authorization": "該請求沒有使用任何授權",
|
||||||
"body": "該請求沒有任何請求主體",
|
"body": "該請求沒有任何請求主體",
|
||||||
"collection": "組合為空",
|
"collection": "集合為空",
|
||||||
"collections": "組合為空",
|
"collections": "集合為空",
|
||||||
"documentation": "連線到 GraphQL 端點以檢視文件",
|
"documentation": "連線到 GraphQL 端點以檢視文件",
|
||||||
"endpoint": "端點不能留空",
|
"endpoint": "端點不能留空",
|
||||||
"environments": "環境為空",
|
"environments": "環境為空",
|
||||||
@@ -209,7 +209,7 @@
|
|||||||
"browser_support_sse": "此瀏覽器似乎不支援 SSE。",
|
"browser_support_sse": "此瀏覽器似乎不支援 SSE。",
|
||||||
"check_console_details": "檢查控制台日誌以獲悉詳情",
|
"check_console_details": "檢查控制台日誌以獲悉詳情",
|
||||||
"curl_invalid_format": "cURL 格式不正確",
|
"curl_invalid_format": "cURL 格式不正確",
|
||||||
"danger_zone": "Danger zone",
|
"danger_zone": "危險地帶",
|
||||||
"delete_account": "您的帳號目前為這些團隊的擁有者:",
|
"delete_account": "您的帳號目前為這些團隊的擁有者:",
|
||||||
"delete_account_description": "您在刪除帳號前必須先將您自己從團隊中移除、轉移擁有權,或是刪除團隊。",
|
"delete_account_description": "您在刪除帳號前必須先將您自己從團隊中移除、轉移擁有權,或是刪除團隊。",
|
||||||
"empty_req_name": "空請求名稱",
|
"empty_req_name": "空請求名稱",
|
||||||
@@ -277,38 +277,38 @@
|
|||||||
"tests": "編寫測試指令碼以自動除錯。"
|
"tests": "編寫測試指令碼以自動除錯。"
|
||||||
},
|
},
|
||||||
"hide": {
|
"hide": {
|
||||||
"collection": "隱藏組合面板",
|
"collection": "隱藏集合面板",
|
||||||
"more": "隱藏更多",
|
"more": "隱藏更多",
|
||||||
"preview": "隱藏預覽",
|
"preview": "隱藏預覽",
|
||||||
"sidebar": "隱藏側邊欄"
|
"sidebar": "隱藏側邊欄"
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
"collections": "匯入組合",
|
"collections": "匯入集合",
|
||||||
"curl": "匯入 cURL",
|
"curl": "匯入 cURL",
|
||||||
"failed": "匯入失敗",
|
"failed": "匯入失敗",
|
||||||
"from_gist": "從 Gist 匯入",
|
"from_gist": "從 Gist 匯入",
|
||||||
"from_gist_description": "從 Gist 網址匯入",
|
"from_gist_description": "從 Gist 網址匯入",
|
||||||
"from_insomnia": "從 Insomnia 匯入",
|
"from_insomnia": "從 Insomnia 匯入",
|
||||||
"from_insomnia_description": "從 Insomnia 組合匯入",
|
"from_insomnia_description": "從 Insomnia 集合匯入",
|
||||||
"from_json": "從 Hoppscotch 匯入",
|
"from_json": "從 Hoppscotch 匯入",
|
||||||
"from_json_description": "從 Hoppscotch 組合檔匯入",
|
"from_json_description": "從 Hoppscotch 集合檔匯入",
|
||||||
"from_my_collections": "從我的組合匯入",
|
"from_my_collections": "從我的集合匯入",
|
||||||
"from_my_collections_description": "從我的組合檔匯入",
|
"from_my_collections_description": "從我的集合檔匯入",
|
||||||
"from_openapi": "從 OpenAPI 匯入",
|
"from_openapi": "從 OpenAPI 匯入",
|
||||||
"from_openapi_description": "從 OpenAPI 規格檔 (YML/JSON) 匯入",
|
"from_openapi_description": "從 OpenAPI 規格檔 (YML/JSON) 匯入",
|
||||||
"from_postman": "從 Postman 匯入",
|
"from_postman": "從 Postman 匯入",
|
||||||
"from_postman_description": "從 Postman 組合匯入",
|
"from_postman_description": "從 Postman 集合匯入",
|
||||||
"from_url": "從網址匯入",
|
"from_url": "從網址匯入",
|
||||||
"gist_url": "輸入 Gist 網址",
|
"gist_url": "輸入 Gist 網址",
|
||||||
"import_from_url_invalid_fetch": "無法從網址取得資料",
|
"import_from_url_invalid_fetch": "無法從網址取得資料",
|
||||||
"import_from_url_invalid_file_format": "匯入組合時發生錯誤",
|
"import_from_url_invalid_file_format": "匯入集合時發生錯誤",
|
||||||
"import_from_url_invalid_type": "不支援此類型。可接受的值為 'hoppscotch'、'openapi'、'postman'、'insomnia'",
|
"import_from_url_invalid_type": "不支援此類型。可接受的值為 'hoppscotch'、'openapi'、'postman'、'insomnia'",
|
||||||
"import_from_url_success": "已匯入組合",
|
"import_from_url_success": "已匯入集合",
|
||||||
"json_description": "從 Hoppscotch 組合 JSON 檔匯入組合",
|
"json_description": "從 Hoppscotch 集合 JSON 檔匯入集合",
|
||||||
"title": "匯入"
|
"title": "匯入"
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
"collapse_collection": "隱藏或顯示組合",
|
"collapse_collection": "隱藏或顯示集合",
|
||||||
"collapse_sidebar": "隱藏或顯示側邊欄",
|
"collapse_sidebar": "隱藏或顯示側邊欄",
|
||||||
"column": "垂直版面",
|
"column": "垂直版面",
|
||||||
"name": "配置",
|
"name": "配置",
|
||||||
@@ -316,8 +316,8 @@
|
|||||||
"zen_mode": "專注模式"
|
"zen_mode": "專注模式"
|
||||||
},
|
},
|
||||||
"modal": {
|
"modal": {
|
||||||
"close_unsaved_tab": "You have unsaved changes",
|
"close_unsaved_tab": "您有未儲存的改動",
|
||||||
"collections": "組合",
|
"collections": "集合",
|
||||||
"confirm": "確認",
|
"confirm": "確認",
|
||||||
"edit_request": "編輯請求",
|
"edit_request": "編輯請求",
|
||||||
"import_export": "匯入/匯出"
|
"import_export": "匯入/匯出"
|
||||||
@@ -374,9 +374,9 @@
|
|||||||
"email_verification_mail": "已將驗證信寄送至您的電子郵件地址。請點擊信中連結以驗證您的電子郵件地址。",
|
"email_verification_mail": "已將驗證信寄送至您的電子郵件地址。請點擊信中連結以驗證您的電子郵件地址。",
|
||||||
"no_permission": "您沒有權限執行此操作。",
|
"no_permission": "您沒有權限執行此操作。",
|
||||||
"owner": "擁有者",
|
"owner": "擁有者",
|
||||||
"owner_description": "擁有者可以新增、編輯和刪除請求、組合和團隊成員。",
|
"owner_description": "擁有者可以新增、編輯和刪除請求、集合和團隊成員。",
|
||||||
"roles": "角色",
|
"roles": "角色",
|
||||||
"roles_description": "角色用來控制對共用組合的存取權。",
|
"roles_description": "角色用來控制對共用集合的存取權。",
|
||||||
"updated": "已更新個人檔案",
|
"updated": "已更新個人檔案",
|
||||||
"viewer": "檢視者",
|
"viewer": "檢視者",
|
||||||
"viewer_description": "檢視者只能檢視和使用請求。"
|
"viewer_description": "檢視者只能檢視和使用請求。"
|
||||||
@@ -396,8 +396,8 @@
|
|||||||
"text": "文字"
|
"text": "文字"
|
||||||
},
|
},
|
||||||
"copy_link": "複製連結",
|
"copy_link": "複製連結",
|
||||||
"different_collection": "Cannot reorder requests from different collections",
|
"different_collection": "無法重新排列來自不同集合的請求",
|
||||||
"duplicated": "Request duplicated",
|
"duplicated": "已複製請求",
|
||||||
"duration": "持續時間",
|
"duration": "持續時間",
|
||||||
"enter_curl": "輸入 cURL",
|
"enter_curl": "輸入 cURL",
|
||||||
"generate_code": "產生程式碼",
|
"generate_code": "產生程式碼",
|
||||||
@@ -405,10 +405,10 @@
|
|||||||
"header_list": "請求標頭列表",
|
"header_list": "請求標頭列表",
|
||||||
"invalid_name": "請提供請求名稱",
|
"invalid_name": "請提供請求名稱",
|
||||||
"method": "方法",
|
"method": "方法",
|
||||||
"moved": "Request moved",
|
"moved": "已移動請求",
|
||||||
"name": "請求名稱",
|
"name": "請求名稱",
|
||||||
"new": "新請求",
|
"new": "新請求",
|
||||||
"order_changed": "Request Order Updated",
|
"order_changed": "已更新請求順序",
|
||||||
"override": "覆寫",
|
"override": "覆寫",
|
||||||
"override_help": "在標頭設置 <kbd>Content-Type</kbd>",
|
"override_help": "在標頭設置 <kbd>Content-Type</kbd>",
|
||||||
"overriden": "已覆寫",
|
"overriden": "已覆寫",
|
||||||
@@ -432,7 +432,7 @@
|
|||||||
"view_my_links": "檢視我的連結"
|
"view_my_links": "檢視我的連結"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"audio": "Audio",
|
"audio": "音訊",
|
||||||
"body": "回應本體",
|
"body": "回應本體",
|
||||||
"filter_response_body": "篩選 JSON 回應本體 (使用 JSONPath 語法)",
|
"filter_response_body": "篩選 JSON 回應本體 (使用 JSONPath 語法)",
|
||||||
"headers": "回應標頭",
|
"headers": "回應標頭",
|
||||||
@@ -446,7 +446,7 @@
|
|||||||
"status": "狀態",
|
"status": "狀態",
|
||||||
"time": "時間",
|
"time": "時間",
|
||||||
"title": "回應",
|
"title": "回應",
|
||||||
"video": "Video",
|
"video": "視訊",
|
||||||
"waiting_for_connection": "等待連線",
|
"waiting_for_connection": "等待連線",
|
||||||
"xml": "XML"
|
"xml": "XML"
|
||||||
},
|
},
|
||||||
@@ -494,7 +494,7 @@
|
|||||||
"short_codes_description": "我們為您打造的快捷碼。",
|
"short_codes_description": "我們為您打造的快捷碼。",
|
||||||
"sidebar_on_left": "左側邊欄",
|
"sidebar_on_left": "左側邊欄",
|
||||||
"sync": "同步",
|
"sync": "同步",
|
||||||
"sync_collections": "組合",
|
"sync_collections": "集合",
|
||||||
"sync_description": "這些設定會同步到雲端。",
|
"sync_description": "這些設定會同步到雲端。",
|
||||||
"sync_environments": "環境",
|
"sync_environments": "環境",
|
||||||
"sync_history": "歷史",
|
"sync_history": "歷史",
|
||||||
@@ -551,7 +551,7 @@
|
|||||||
"previous_method": "選擇上一個方法",
|
"previous_method": "選擇上一個方法",
|
||||||
"put_method": "選擇 PUT 方法",
|
"put_method": "選擇 PUT 方法",
|
||||||
"reset_request": "重置請求",
|
"reset_request": "重置請求",
|
||||||
"save_to_collections": "儲存到組合",
|
"save_to_collections": "儲存到集合",
|
||||||
"send_request": "傳送請求",
|
"send_request": "傳送請求",
|
||||||
"title": "請求"
|
"title": "請求"
|
||||||
},
|
},
|
||||||
@@ -570,7 +570,7 @@
|
|||||||
},
|
},
|
||||||
"show": {
|
"show": {
|
||||||
"code": "顯示程式碼",
|
"code": "顯示程式碼",
|
||||||
"collection": "顯示組合面板",
|
"collection": "顯示集合面板",
|
||||||
"more": "顯示更多",
|
"more": "顯示更多",
|
||||||
"sidebar": "顯示側邊欄"
|
"sidebar": "顯示側邊欄"
|
||||||
},
|
},
|
||||||
@@ -639,9 +639,9 @@
|
|||||||
"tab": {
|
"tab": {
|
||||||
"authorization": "授權",
|
"authorization": "授權",
|
||||||
"body": "請求本體",
|
"body": "請求本體",
|
||||||
"collections": "組合",
|
"collections": "集合",
|
||||||
"documentation": "幫助文件",
|
"documentation": "幫助文件",
|
||||||
"environments": "Environments",
|
"environments": "環境",
|
||||||
"headers": "請求標頭",
|
"headers": "請求標頭",
|
||||||
"history": "歷史記錄",
|
"history": "歷史記錄",
|
||||||
"mqtt": "MQTT",
|
"mqtt": "MQTT",
|
||||||
@@ -666,7 +666,7 @@
|
|||||||
"email_do_not_match": "電子信箱與您的帳號資料不一致。請聯絡您的團隊擁有者。",
|
"email_do_not_match": "電子信箱與您的帳號資料不一致。請聯絡您的團隊擁有者。",
|
||||||
"exit": "退出團隊",
|
"exit": "退出團隊",
|
||||||
"exit_disabled": "團隊擁有者無法退出團隊",
|
"exit_disabled": "團隊擁有者無法退出團隊",
|
||||||
"invalid_coll_id": "Invalid collection ID",
|
"invalid_coll_id": "集合 ID 無效",
|
||||||
"invalid_email_format": "電子信箱格式無效",
|
"invalid_email_format": "電子信箱格式無效",
|
||||||
"invalid_id": "團隊 ID 無效。請聯絡您的團隊擁有者。",
|
"invalid_id": "團隊 ID 無效。請聯絡您的團隊擁有者。",
|
||||||
"invalid_invite_link": "邀請連結無效",
|
"invalid_invite_link": "邀請連結無效",
|
||||||
@@ -690,21 +690,21 @@
|
|||||||
"member_removed": "使用者已移除",
|
"member_removed": "使用者已移除",
|
||||||
"member_role_updated": "使用者角色已更新",
|
"member_role_updated": "使用者角色已更新",
|
||||||
"members": "成員",
|
"members": "成員",
|
||||||
"more_members": "+{count} more",
|
"more_members": "還有 {count} 位",
|
||||||
"name_length_insufficient": "團隊名稱至少為 6 個字元",
|
"name_length_insufficient": "團隊名稱至少為 6 個字元",
|
||||||
"name_updated": "團隊名稱已更新",
|
"name_updated": "團隊名稱已更新",
|
||||||
"new": "新團隊",
|
"new": "新團隊",
|
||||||
"new_created": "已建立新團隊",
|
"new_created": "已建立新團隊",
|
||||||
"new_name": "我的新團隊",
|
"new_name": "我的新團隊",
|
||||||
"no_access": "您沒有編輯組合的許可權",
|
"no_access": "您沒有編輯集合的許可權",
|
||||||
"no_invite_found": "未找到邀請。請聯絡您的團隊擁有者。",
|
"no_invite_found": "未找到邀請。請聯絡您的團隊擁有者。",
|
||||||
"no_request_found": "Request not found.",
|
"no_request_found": "找不到請求。",
|
||||||
"not_found": "找不到團隊。請聯絡您的團隊擁有者。",
|
"not_found": "找不到團隊。請聯絡您的團隊擁有者。",
|
||||||
"not_valid_viewer": "您不是一個有效的檢視者。請聯絡您的團隊擁有者。",
|
"not_valid_viewer": "您不是一個有效的檢視者。請聯絡您的團隊擁有者。",
|
||||||
"parent_coll_move": "Cannot move collection to a child collection",
|
"parent_coll_move": "無法將集合移動至子集合",
|
||||||
"pending_invites": "待定邀請",
|
"pending_invites": "待定邀請",
|
||||||
"permissions": "許可權",
|
"permissions": "許可權",
|
||||||
"same_target_destination": "Same target and destination",
|
"same_target_destination": "目標和目的地相同",
|
||||||
"saved": "團隊已儲存",
|
"saved": "團隊已儲存",
|
||||||
"select_a_team": "選擇團隊",
|
"select_a_team": "選擇團隊",
|
||||||
"title": "團隊",
|
"title": "團隊",
|
||||||
@@ -734,9 +734,9 @@
|
|||||||
"url": "網址"
|
"url": "網址"
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"change": "Change workspace",
|
"change": "切換工作區",
|
||||||
"personal": "My Workspace",
|
"personal": "我的工作區",
|
||||||
"team": "Team Workspace",
|
"team": "團隊工作區",
|
||||||
"title": "Workspaces"
|
"title": "工作區"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "@hoppscotch/common",
|
"name": "@hoppscotch/common",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2023.4.7",
|
"version": "2023.4.8",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||||
"test": "vitest --run",
|
|
||||||
"test:watch": "vitest",
|
|
||||||
"dev:vite": "vite",
|
"dev:vite": "vite",
|
||||||
"dev:gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml --watch dotenv_config_path=\"../../.env\"",
|
"dev:gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml --watch dotenv_config_path=\"../../.env\"",
|
||||||
"lint": "eslint src --ext .ts,.js,.vue --ignore-path .gitignore .",
|
"lint": "eslint src --ext .ts,.js,.vue --ignore-path .gitignore .",
|
||||||
@@ -15,7 +13,6 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml dotenv_config_path=\"../../.env\"",
|
"gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml dotenv_config_path=\"../../.env\"",
|
||||||
"postinstall": "pnpm run gql-codegen",
|
"postinstall": "pnpm run gql-codegen",
|
||||||
"do-test": "pnpm run test",
|
|
||||||
"do-lint": "pnpm run prod-lint",
|
"do-lint": "pnpm run prod-lint",
|
||||||
"do-typecheck": "pnpm run lint",
|
"do-typecheck": "pnpm run lint",
|
||||||
"do-lintfix": "pnpm run lintfix"
|
"do-lintfix": "pnpm run lintfix"
|
||||||
@@ -46,12 +43,11 @@
|
|||||||
"@urql/exchange-auth": "^0.1.7",
|
"@urql/exchange-auth": "^0.1.7",
|
||||||
"@urql/exchange-graphcache": "^4.4.3",
|
"@urql/exchange-graphcache": "^4.4.3",
|
||||||
"@vitejs/plugin-legacy": "^2.3.0",
|
"@vitejs/plugin-legacy": "^2.3.0",
|
||||||
"@vueuse/core": "^8.9.4",
|
"@vueuse/core": "^8.7.5",
|
||||||
"@vueuse/head": "^0.7.9",
|
"@vueuse/head": "^0.7.9",
|
||||||
"acorn-walk": "^8.2.0",
|
"acorn-walk": "^8.2.0",
|
||||||
"axios": "^0.21.4",
|
"axios": "^0.21.4",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"dioc": "workspace:^",
|
|
||||||
"esprima": "^4.0.1",
|
"esprima": "^4.0.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"fp-ts": "^2.12.1",
|
"fp-ts": "^2.12.1",
|
||||||
@@ -67,7 +63,6 @@
|
|||||||
"jsonpath-plus": "^7.0.0",
|
"jsonpath-plus": "^7.0.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lossless-json": "^2.0.8",
|
"lossless-json": "^2.0.8",
|
||||||
"minisearch": "^6.1.0",
|
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"paho-mqtt": "^1.1.0",
|
"paho-mqtt": "^1.1.0",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
@@ -113,7 +108,6 @@
|
|||||||
"@graphql-typed-document-node/core": "^3.1.1",
|
"@graphql-typed-document-node/core": "^3.1.1",
|
||||||
"@iconify-json/lucide": "^1.1.40",
|
"@iconify-json/lucide": "^1.1.40",
|
||||||
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
||||||
"@relmify/jest-fp-ts": "^2.1.1",
|
|
||||||
"@rushstack/eslint-patch": "^1.1.4",
|
"@rushstack/eslint-patch": "^1.1.4",
|
||||||
"@types/js-yaml": "^4.0.5",
|
"@types/js-yaml": "^4.0.5",
|
||||||
"@types/lodash-es": "^4.17.6",
|
"@types/lodash-es": "^4.17.6",
|
||||||
@@ -148,11 +142,10 @@
|
|||||||
"vite-plugin-html-config": "^1.0.10",
|
"vite-plugin-html-config": "^1.0.10",
|
||||||
"vite-plugin-inspect": "^0.7.4",
|
"vite-plugin-inspect": "^0.7.4",
|
||||||
"vite-plugin-pages": "^0.26.0",
|
"vite-plugin-pages": "^0.26.0",
|
||||||
"vite-plugin-pages-sitemap": "^1.4.5",
|
"vite-plugin-pages-sitemap": "^1.4.0",
|
||||||
"vite-plugin-pwa": "^0.13.1",
|
"vite-plugin-pwa": "^0.13.1",
|
||||||
"vite-plugin-vue-layouts": "^0.7.0",
|
"vite-plugin-vue-layouts": "^0.7.0",
|
||||||
"vite-plugin-windicss": "^1.8.8",
|
"vite-plugin-windicss": "^1.8.8",
|
||||||
"vitest": "^0.32.2",
|
|
||||||
"vue-tsc": "^0.38.2",
|
"vue-tsc": "^0.38.2",
|
||||||
"windicss": "^3.5.6"
|
"windicss": "^3.5.6"
|
||||||
}
|
}
|
||||||
|
|||||||
42
packages/hoppscotch-common/src/components.d.ts
vendored
42
packages/hoppscotch-common/src/components.d.ts
vendored
@@ -11,21 +11,20 @@ declare module '@vue/runtime-core' {
|
|||||||
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
|
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
|
||||||
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
|
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
|
||||||
AppFooter: typeof import('./components/app/Footer.vue')['default']
|
AppFooter: typeof import('./components/app/Footer.vue')['default']
|
||||||
|
AppFuse: typeof import('./components/app/Fuse.vue')['default']
|
||||||
AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default']
|
AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default']
|
||||||
AppHeader: typeof import('./components/app/Header.vue')['default']
|
AppHeader: typeof import('./components/app/Header.vue')['default']
|
||||||
AppInterceptor: typeof import('./components/app/Interceptor.vue')['default']
|
AppInterceptor: typeof import('./components/app/Interceptor.vue')['default']
|
||||||
AppLogo: typeof import('./components/app/Logo.vue')['default']
|
AppLogo: typeof import('./components/app/Logo.vue')['default']
|
||||||
AppOptions: typeof import('./components/app/Options.vue')['default']
|
AppOptions: typeof import('./components/app/Options.vue')['default']
|
||||||
AppPaneLayout: typeof import('./components/app/PaneLayout.vue')['default']
|
AppPaneLayout: typeof import('./components/app/PaneLayout.vue')['default']
|
||||||
|
AppPowerSearch: typeof import('./components/app/PowerSearch.vue')['default']
|
||||||
|
AppPowerSearchEntry: typeof import('./components/app/PowerSearchEntry.vue')['default']
|
||||||
AppShare: typeof import('./components/app/Share.vue')['default']
|
AppShare: typeof import('./components/app/Share.vue')['default']
|
||||||
AppShortcuts: typeof import('./components/app/Shortcuts.vue')['default']
|
AppShortcuts: typeof import('./components/app/Shortcuts.vue')['default']
|
||||||
AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
|
AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
|
||||||
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
|
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
|
||||||
AppSidenav: typeof import('./components/app/Sidenav.vue')['default']
|
AppSidenav: typeof import('./components/app/Sidenav.vue')['default']
|
||||||
AppSpotlight: typeof import('./components/app/spotlight/index.vue')['default']
|
|
||||||
AppSpotlightEntry: typeof import('./components/app/spotlight/Entry.vue')['default']
|
|
||||||
AppSpotlightEntryGQLHistory: typeof import('./components/app/spotlight/entry/GQLHistory.vue')['default']
|
|
||||||
AppSpotlightEntryRESTHistory: typeof import('./components/app/spotlight/entry/RESTHistory.vue')['default']
|
|
||||||
AppSupport: typeof import('./components/app/Support.vue')['default']
|
AppSupport: typeof import('./components/app/Support.vue')['default']
|
||||||
ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default']
|
ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default']
|
||||||
ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default']
|
ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default']
|
||||||
@@ -75,27 +74,6 @@ declare module '@vue/runtime-core' {
|
|||||||
History: typeof import('./components/history/index.vue')['default']
|
History: typeof import('./components/history/index.vue')['default']
|
||||||
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
|
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
|
||||||
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
|
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
|
||||||
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
|
||||||
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
|
||||||
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
|
||||||
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
|
|
||||||
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
|
|
||||||
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
|
||||||
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
|
|
||||||
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
|
|
||||||
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
|
||||||
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']
|
|
||||||
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
|
||||||
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
|
||||||
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
|
|
||||||
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
|
|
||||||
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
|
|
||||||
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
|
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
|
||||||
HttpAuthorizationApiKey: typeof import('./components/http/authorization/ApiKey.vue')['default']
|
HttpAuthorizationApiKey: typeof import('./components/http/authorization/ApiKey.vue')['default']
|
||||||
HttpAuthorizationBasic: typeof import('./components/http/authorization/Basic.vue')['default']
|
HttpAuthorizationBasic: typeof import('./components/http/authorization/Basic.vue')['default']
|
||||||
@@ -121,19 +99,6 @@ declare module '@vue/runtime-core' {
|
|||||||
HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
|
HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
|
||||||
HttpTests: typeof import('./components/http/Tests.vue')['default']
|
HttpTests: typeof import('./components/http/Tests.vue')['default']
|
||||||
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
|
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
|
||||||
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
|
||||||
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
|
||||||
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
|
||||||
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
|
||||||
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
|
||||||
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']
|
|
||||||
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
|
|
||||||
IconLucideInfo: typeof import('~icons/lucide/info')['default']
|
|
||||||
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
|
||||||
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
|
||||||
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
|
||||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
|
||||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
|
||||||
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
||||||
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
||||||
LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
|
LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
|
||||||
@@ -169,7 +134,6 @@ declare module '@vue/runtime-core' {
|
|||||||
SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default']
|
SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default']
|
||||||
SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.vue')['default']
|
SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.vue')['default']
|
||||||
SmartPicture: typeof import('./../../hoppscotch-ui/src/components/smart/Picture.vue')['default']
|
SmartPicture: typeof import('./../../hoppscotch-ui/src/components/smart/Picture.vue')['default']
|
||||||
SmartPlaceholder: typeof import('./../../hoppscotch-ui/src/components/smart/Placeholder.vue')['default']
|
|
||||||
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
|
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
|
||||||
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
||||||
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
||||||
|
|||||||
@@ -152,7 +152,7 @@
|
|||||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||||
:title="`${t(
|
:title="`${t(
|
||||||
'app.shortcuts'
|
'app.shortcuts'
|
||||||
)} <kbd>${getSpecialKey()}</kbd><kbd>/</kbd>`"
|
)} <kbd>${getSpecialKey()}</kbd><kbd>K</kbd>`"
|
||||||
:icon="IconZap"
|
:icon="IconZap"
|
||||||
@click="invokeAction('flyouts.keybinds.toggle')"
|
@click="invokeAction('flyouts.keybinds.toggle')"
|
||||||
/>
|
/>
|
||||||
|
|||||||
70
packages/hoppscotch-common/src/components/app/Fuse.vue
Normal file
70
packages/hoppscotch-common/src/components/app/Fuse.vue
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<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>
|
||||||
@@ -20,9 +20,7 @@
|
|||||||
<div class="inline-flex items-center space-x-2">
|
<div class="inline-flex items-center space-x-2">
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||||
:title="`${t(
|
:title="`${t('app.search')} <kbd>/</kbd>`"
|
||||||
'app.search'
|
|
||||||
)} <kbd>${getPlatformSpecialKey()}</kbd> <kbd>K</kbd>`"
|
|
||||||
:icon="IconSearch"
|
:icon="IconSearch"
|
||||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
@click="invokeAction('modals.search.toggle')"
|
@click="invokeAction('modals.search.toggle')"
|
||||||
@@ -244,12 +242,11 @@ import { pwaDefferedPrompt, installPWA } from "@modules/pwa"
|
|||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { defineActionHandler, invokeAction } from "@helpers/actions"
|
import { invokeAction } from "@helpers/actions"
|
||||||
import { workspaceStatus$, updateWorkspaceTeamName } from "~/newstore/workspace"
|
import { workspaceStatus$, updateWorkspaceTeamName } from "~/newstore/workspace"
|
||||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||||
import { onLoggedIn } from "~/composables/auth"
|
import { onLoggedIn } from "~/composables/auth"
|
||||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -377,12 +374,4 @@ const profile = ref<any | null>(null)
|
|||||||
const settings = ref<any | null>(null)
|
const settings = ref<any | null>(null)
|
||||||
const logout = ref<any | null>(null)
|
const logout = ref<any | null>(null)
|
||||||
const accountActions = ref<any | null>(null)
|
const accountActions = ref<any | null>(null)
|
||||||
|
|
||||||
defineActionHandler(
|
|
||||||
"user.login",
|
|
||||||
() => {
|
|
||||||
invokeAction("modals.login.toggle")
|
|
||||||
},
|
|
||||||
computed(() => !currentUser.value)
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
122
packages/hoppscotch-common/src/components/app/PowerSearch.vue
Normal file
122
packages/hoppscotch-common/src/components/app/PowerSearch.vue
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<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,14 +14,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col divide-y divide-dividerLight">
|
<div v-if="filterText" class="flex flex-col divide-y divide-dividerLight">
|
||||||
<HoppSmartPlaceholder v-if="isEmpty(shortcutsResults)">
|
|
||||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
|
||||||
</HoppSmartPlaceholder>
|
|
||||||
<details
|
<details
|
||||||
v-for="(sectionResults, sectionTitle) in shortcutsResults"
|
v-for="(map, mapIndex) in searchResults"
|
||||||
v-else
|
:key="`map-${mapIndex}`"
|
||||||
:key="`section-${sectionTitle}`"
|
|
||||||
class="flex flex-col"
|
class="flex flex-col"
|
||||||
open
|
open
|
||||||
>
|
>
|
||||||
@@ -32,28 +28,63 @@
|
|||||||
<span
|
<span
|
||||||
class="font-semibold truncate capitalize-first text-secondaryDark"
|
class="font-semibold truncate capitalize-first text-secondaryDark"
|
||||||
>
|
>
|
||||||
{{ sectionTitle }}
|
{{ t(map.item.section) }}
|
||||||
</span>
|
</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="flex flex-col px-6 pb-4 space-y-2">
|
<div class="flex flex-col px-6 pb-4 space-y-2">
|
||||||
<AppShortcutsEntry
|
<AppShortcutsEntry
|
||||||
v-for="(shortcut, index) in sectionResults"
|
v-for="(shortcut, index) in map.item.shortcuts"
|
||||||
:key="`shortcut-${index}`"
|
:key="`shortcut-${index}`"
|
||||||
:shortcut="shortcut"
|
:shortcut="shortcut"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
<div
|
||||||
|
v-if="searchResults.length === 0"
|
||||||
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
|
>
|
||||||
|
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||||
|
<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">
|
||||||
|
<details
|
||||||
|
v-for="(map, mapIndex) in mappings"
|
||||||
|
: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.section) }}
|
||||||
|
</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}`"
|
||||||
|
:shortcut="shortcut"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartSlideOver>
|
</HoppSmartSlideOver>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeMount, ref } from "vue"
|
import { computed, ref } from "vue"
|
||||||
import { ShortcutDef, getShortcuts } from "~/helpers/shortcuts"
|
import Fuse from "fuse.js"
|
||||||
import MiniSearch from "minisearch"
|
import mappings from "~/helpers/shortcuts"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { groupBy, isEmpty } from "lodash-es"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -61,33 +92,15 @@ defineProps<{
|
|||||||
show: boolean
|
show: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const minisearch = new MiniSearch({
|
const options = {
|
||||||
fields: ["label", "keys", "section"],
|
keys: ["shortcuts.label"],
|
||||||
idField: "label",
|
}
|
||||||
storeFields: ["label", "keys", "section"],
|
|
||||||
searchOptions: {
|
|
||||||
fuzzy: true,
|
|
||||||
prefix: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const shortcuts = getShortcuts(t)
|
const fuse = new Fuse(mappings, options)
|
||||||
|
|
||||||
onBeforeMount(() => {
|
|
||||||
minisearch.addAllAsync(shortcuts)
|
|
||||||
})
|
|
||||||
|
|
||||||
const filterText = ref("")
|
const filterText = ref("")
|
||||||
|
|
||||||
const shortcutsResults = computed(() => {
|
const searchResults = computed(() => fuse.search(filterText.value))
|
||||||
// If there are no search text, return all the shortcuts
|
|
||||||
const results =
|
|
||||||
filterText.value.length > 0
|
|
||||||
? minisearch.search(filterText.value)
|
|
||||||
: shortcuts
|
|
||||||
|
|
||||||
return groupBy(results, "section") as Record<string, ShortcutDef[]>
|
|
||||||
})
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "close"): void
|
(e: "close"): void
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center py-1">
|
<div class="flex items-center py-1">
|
||||||
<span class="flex flex-1 mr-4">
|
<span class="flex flex-1 mr-4">
|
||||||
{{ shortcut.label }}
|
{{ t(shortcut.label) }}
|
||||||
</span>
|
</span>
|
||||||
<kbd
|
<kbd
|
||||||
v-for="(key, index) in shortcut.keys"
|
v-for="(key, index) in shortcut.keys"
|
||||||
@@ -14,9 +14,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ShortcutDef } from "~/helpers/shortcuts"
|
import { useI18n } from "@composables/i18n"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
shortcut: ShortcutDef
|
shortcut: {
|
||||||
|
label: string
|
||||||
|
keys: string[]
|
||||||
|
}
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
<template>
|
|
||||||
<button
|
|
||||||
ref="el"
|
|
||||||
class="flex items-center flex-1 px-6 py-4 font-medium space-x-4 transition cursor-pointer relative search-entry focus:outline-none"
|
|
||||||
:class="{ 'active bg-primaryLight text-secondaryDark': active }"
|
|
||||||
tabindex="-1"
|
|
||||||
@click="emit('action')"
|
|
||||||
@keydown.enter="emit('action')"
|
|
||||||
>
|
|
||||||
<component
|
|
||||||
:is="entry.icon"
|
|
||||||
class="opacity-50 svg-icons"
|
|
||||||
:class="{ 'opacity-100': active }"
|
|
||||||
/>
|
|
||||||
<template
|
|
||||||
v-if="entry.text.type === 'text' && typeof entry.text.text === 'string'"
|
|
||||||
>
|
|
||||||
<span class="block truncate">
|
|
||||||
{{ entry.text.text }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
v-else-if="entry.text.type === 'text' && Array.isArray(entry.text.text)"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
v-for="(labelPart, labelPartIndex) in entry.text.text"
|
|
||||||
:key="`label-${labelPart}-${labelPartIndex}`"
|
|
||||||
>
|
|
||||||
<span class="block truncate">
|
|
||||||
{{ labelPart }}
|
|
||||||
</span>
|
|
||||||
<icon-lucide-chevron-right
|
|
||||||
v-if="labelPartIndex < entry.text.text.length - 1"
|
|
||||||
class="flex flex-shrink-0"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="entry.text.type === 'custom'">
|
|
||||||
<span class="block truncate">
|
|
||||||
<component
|
|
||||||
:is="entry.text.component"
|
|
||||||
v-bind="entry.text.componentProps"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<span v-if="formattedShortcutKeys" class="block truncate">
|
|
||||||
<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 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 after:bg-accentLight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span class="flex flex-1 items-center space-x-2">
|
|
||||||
<span class="block truncate">
|
|
||||||
{{ dateTimeText }}
|
|
||||||
</span>
|
|
||||||
<icon-lucide-chevron-right class="flex flex-shrink-0" />
|
|
||||||
<span class="block truncate">
|
|
||||||
{{ historyEntry.request.url }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="font-semibold truncate text-tiny flex flex-shrink-0 border border-dividerDark rounded-md px-1"
|
|
||||||
>
|
|
||||||
{{ historyEntry.request.query.split("\n")[0] }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed } from "vue"
|
|
||||||
import { shortDateTime } from "~/helpers/utils/date"
|
|
||||||
import { GQLHistoryEntry } from "~/newstore/history"
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
historyEntry: GQLHistoryEntry
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const dateTimeText = computed(() =>
|
|
||||||
shortDateTime(props.historyEntry.updatedOn!)
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span class="flex flex-1 items-center space-x-2">
|
|
||||||
<span class="block truncate">
|
|
||||||
{{ dateTimeText }}
|
|
||||||
</span>
|
|
||||||
<icon-lucide-chevron-right class="flex flex-shrink-0" />
|
|
||||||
<span
|
|
||||||
class="font-semibold truncate text-tiny flex flex-shrink-0 border border-dividerDark rounded-md px-1"
|
|
||||||
:class="entryStatus.className"
|
|
||||||
>
|
|
||||||
{{ historyEntry.request.method }}
|
|
||||||
</span>
|
|
||||||
<span class="block truncate">
|
|
||||||
{{ 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>
|
|
||||||
@@ -1,238 +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-divider">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<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 px-6 py-5"
|
|
||||||
/>
|
|
||||||
<HoppSmartSpinner v-if="searchSession?.loading" class="mr-6" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="searchSession && search.length > 0"
|
|
||||||
class="flex flex-col flex-1 overflow-y-auto border-b border-divider divide-y divide-dividerLight"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="([sectionID, sectionResult], sectionIndex) in scoredResults"
|
|
||||||
:key="`section-${sectionID}`"
|
|
||||||
class="flex flex-col"
|
|
||||||
>
|
|
||||||
<h5
|
|
||||||
class="px-6 py-2 bg-primaryContrast z-10 text-secondaryLight sticky top-0"
|
|
||||||
>
|
|
||||||
{{ 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>
|
|
||||||
<HoppSmartPlaceholder
|
|
||||||
v-if="search.length > 0 && scoredResults.length === 0"
|
|
||||||
:text="`${t('state.nothing_found')} ‟${search}”`"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
|
||||||
</template>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
:label="t('action.clear')"
|
|
||||||
outline
|
|
||||||
@click="search = ''"
|
|
||||||
/>
|
|
||||||
</HoppSmartPlaceholder>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex flex-shrink-0 text-tiny text-secondaryLight p-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="mx-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>
|
|
||||||
</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,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onEnter = () => {
|
|
||||||
// If no entries, do nothing
|
|
||||||
if (scoredResults.value.length === 0) return
|
|
||||||
|
|
||||||
const [sectionIndex, entryIndex] = selectedEntry.value
|
|
||||||
const [sectionID, section] = scoredResults.value[sectionIndex]
|
|
||||||
const result = section.results[entryIndex]
|
|
||||||
|
|
||||||
runAction(sectionID, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
onEnter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.show,
|
|
||||||
(show) => {
|
|
||||||
if (show) {
|
|
||||||
window.addEventListener("keydown", handleKeyPress)
|
|
||||||
} else {
|
|
||||||
window.removeEventListener("keydown", handleKeyPress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return { selectedEntry }
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -243,33 +243,49 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #emptyNode="{ node }">
|
<template #emptyNode="{ node }">
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="filterText.length !== 0 && filteredCollections.length === 0"
|
v-if="filterText.length !== 0 && filteredCollections.length === 0"
|
||||||
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
<span class="my-2 text-center">
|
||||||
</template>
|
{{ t("state.nothing_found") }} "{{ filterText }}"
|
||||||
</HoppSmartPlaceholder>
|
</span>
|
||||||
<HoppSmartPlaceholder
|
</div>
|
||||||
v-else-if="node === null"
|
<div v-else-if="node === null">
|
||||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
<div
|
||||||
:alt="`${t('empty.collections')}`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:text="t('empty.collections')"
|
>
|
||||||
>
|
<img
|
||||||
<HoppButtonSecondary
|
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||||
:label="t('add.new')"
|
loading="lazy"
|
||||||
filled
|
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
|
||||||
outline
|
:alt="`${t('empty.collections')}`"
|
||||||
@click="emit('display-modal-add')"
|
/>
|
||||||
/>
|
<span class="pb-4 text-center">
|
||||||
</HoppSmartPlaceholder>
|
{{ t("empty.collections") }}
|
||||||
<HoppSmartPlaceholder
|
</span>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="t('add.new')"
|
||||||
|
filled
|
||||||
|
outline
|
||||||
|
@click="emit('display-modal-add')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
v-else-if="node.data.type === 'collections'"
|
v-else-if="node.data.type === 'collections'"
|
||||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.collections')}`"
|
|
||||||
:text="t('empty.collections')"
|
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
|
||||||
|
:alt="`${t('empty.collection')}`"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">
|
||||||
|
{{ t("empty.collection") }}
|
||||||
|
</span>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="t('add.new')"
|
:label="t('add.new')"
|
||||||
filled
|
filled
|
||||||
@@ -282,14 +298,21 @@
|
|||||||
})
|
})
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-else-if="node.data.type === 'folders'"
|
v-else-if="node.data.type === 'folders'"
|
||||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.folder')}`"
|
|
||||||
:text="t('empty.folder')"
|
|
||||||
>
|
>
|
||||||
</HoppSmartPlaceholder>
|
<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>
|
||||||
</template>
|
</template>
|
||||||
</SmartTree>
|
</SmartTree>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -262,53 +262,67 @@
|
|||||||
</template>
|
</template>
|
||||||
<template #emptyNode="{ node }">
|
<template #emptyNode="{ node }">
|
||||||
<div v-if="node === null">
|
<div v-if="node === null">
|
||||||
<div @drop="(e) => e.stopPropagation()">
|
<div
|
||||||
<HoppSmartPlaceholder
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
|
@drop="(e) => e.stopPropagation()"
|
||||||
|
>
|
||||||
|
<img
|
||||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||||
:alt="`${t('empty.collections')}`"
|
loading="lazy"
|
||||||
:text="t('empty.collections')"
|
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
|
||||||
>
|
:alt="`${t('empty.collection')}`"
|
||||||
<HoppButtonSecondary
|
/>
|
||||||
v-if="hasNoTeamAccess"
|
<span class="pb-4 text-center">
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
{{ t("empty.collections") }}
|
||||||
disabled
|
</span>
|
||||||
filled
|
<HoppButtonSecondary
|
||||||
outline
|
v-if="hasNoTeamAccess"
|
||||||
:title="t('team.no_access')"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:label="t('action.new')"
|
disabled
|
||||||
/>
|
filled
|
||||||
<HoppButtonSecondary
|
outline
|
||||||
v-else
|
:title="t('team.no_access')"
|
||||||
:icon="IconPlus"
|
:label="t('action.new')"
|
||||||
:label="t('action.new')"
|
/>
|
||||||
filled
|
<HoppButtonSecondary
|
||||||
outline
|
v-else
|
||||||
@click="emit('display-modal-add')"
|
:icon="IconPlus"
|
||||||
/>
|
:label="t('action.new')"
|
||||||
</HoppSmartPlaceholder>
|
filled
|
||||||
|
outline
|
||||||
|
@click="emit('display-modal-add')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="node.data.type === 'collections'"
|
v-else-if="node.data.type === 'collections'"
|
||||||
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
@drop="(e) => e.stopPropagation()"
|
@drop="(e) => e.stopPropagation()"
|
||||||
>
|
>
|
||||||
<HoppSmartPlaceholder
|
<img
|
||||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||||
:alt="`${t('empty.collections')}`"
|
loading="lazy"
|
||||||
:text="t('empty.collections')"
|
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
|
||||||
>
|
:alt="`${t('empty.collection')}`"
|
||||||
</HoppSmartPlaceholder>
|
/>
|
||||||
|
<span class="pb-4 text-center">
|
||||||
|
{{ t("empty.collections") }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="node.data.type === 'folders'"
|
v-else-if="node.data.type === 'folders'"
|
||||||
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
@drop="(e) => e.stopPropagation()"
|
@drop="(e) => e.stopPropagation()"
|
||||||
>
|
>
|
||||||
<HoppSmartPlaceholder
|
<img
|
||||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
|
||||||
:alt="`${t('empty.folder')}`"
|
:alt="`${t('empty.folder')}`"
|
||||||
:text="t('empty.folder')"
|
/>
|
||||||
>
|
<span class="text-center">
|
||||||
</HoppSmartPlaceholder>
|
{{ t("empty.folder") }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</SmartTree>
|
</SmartTree>
|
||||||
|
|||||||
@@ -171,14 +171,21 @@
|
|||||||
@duplicate-request="$emit('duplicate-request', $event)"
|
@duplicate-request="$emit('duplicate-request', $event)"
|
||||||
@select="$emit('select', $event)"
|
@select="$emit('select', $event)"
|
||||||
/>
|
/>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
collection.folders.length === 0 && collection.requests.length === 0
|
collection.folders.length === 0 && collection.requests.length === 0
|
||||||
"
|
"
|
||||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.collection')}`"
|
|
||||||
:text="t('empty.collection')"
|
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-4"
|
||||||
|
:alt="`${t('empty.collection')}`"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">
|
||||||
|
{{ t("empty.collection") }}
|
||||||
|
</span>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="t('add.new')"
|
:label="t('add.new')"
|
||||||
filled
|
filled
|
||||||
@@ -189,7 +196,7 @@
|
|||||||
})
|
})
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartConfirmModal
|
<HoppSmartConfirmModal
|
||||||
|
|||||||
@@ -160,19 +160,25 @@
|
|||||||
@duplicate-request="emit('duplicate-request', $event)"
|
@duplicate-request="emit('duplicate-request', $event)"
|
||||||
@select="emit('select', $event)"
|
@select="emit('select', $event)"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
<HoppSmartPlaceholder
|
|
||||||
v-if="
|
v-if="
|
||||||
folder.folders &&
|
folder.folders &&
|
||||||
folder.folders.length === 0 &&
|
folder.folders.length === 0 &&
|
||||||
folder.requests &&
|
folder.requests &&
|
||||||
folder.requests.length === 0
|
folder.requests.length === 0
|
||||||
"
|
"
|
||||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.folder')}`"
|
|
||||||
:text="t('empty.folder')"
|
|
||||||
>
|
>
|
||||||
</HoppSmartPlaceholder>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartConfirmModal
|
<HoppSmartConfirmModal
|
||||||
|
|||||||
@@ -60,27 +60,35 @@
|
|||||||
@select="$emit('select', $event)"
|
@select="$emit('select', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="collections.length === 0"
|
v-if="collections.length === 0"
|
||||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.collections')}`"
|
|
||||||
:text="t('empty.collections')"
|
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||||
|
:alt="t('empty.collections')"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">
|
||||||
|
{{ t("empty.collections") }}
|
||||||
|
</span>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="t('add.new')"
|
:label="t('add.new')"
|
||||||
filled
|
filled
|
||||||
outline
|
outline
|
||||||
@click="displayModalAdd(true)"
|
@click="displayModalAdd(true)"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="!(filteredCollections.length !== 0 || collections.length === 0)"
|
v-if="!(filteredCollections.length !== 0 || collections.length === 0)"
|
||||||
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
<span class="my-2 text-center">
|
||||||
</template>
|
{{ t("state.nothing_found") }} "{{ filterText }}"
|
||||||
</HoppSmartPlaceholder>
|
</span>
|
||||||
|
</div>
|
||||||
<CollectionsGraphqlAdd
|
<CollectionsGraphqlAdd
|
||||||
:show="showModalAdd"
|
:show="showModalAdd"
|
||||||
@hide-modal="displayModalAdd(false)"
|
@hide-modal="displayModalAdd(false)"
|
||||||
|
|||||||
@@ -70,13 +70,20 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="myEnvironments.length === 0"
|
v-if="myEnvironments.length === 0"
|
||||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
class="flex flex-col items-center justify-center text-secondaryLight"
|
||||||
:alt="`${t('empty.environments')}`"
|
|
||||||
:text="t('empty.environments')"
|
|
||||||
>
|
>
|
||||||
</HoppSmartPlaceholder>
|
<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>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
:id="'team-environments'"
|
:id="'team-environments'"
|
||||||
@@ -112,14 +119,20 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
<HoppSmartPlaceholder
|
|
||||||
v-if="teamEnvironmentList.length === 0"
|
v-if="teamEnvironmentList.length === 0"
|
||||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
class="flex flex-col items-center justify-center text-secondaryLight"
|
||||||
:alt="`${t('empty.environments')}`"
|
|
||||||
:text="t('empty.environments')"
|
|
||||||
>
|
>
|
||||||
</HoppSmartPlaceholder>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!teamListLoading && teamAdapterError"
|
v-if="!teamListLoading && teamAdapterError"
|
||||||
|
|||||||
@@ -79,19 +79,26 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="vars.length === 0"
|
v-if="vars.length === 0"
|
||||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.environments')}`"
|
|
||||||
:text="t('empty.environments')"
|
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||||
|
:alt="`${t('empty.environments')}`"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">
|
||||||
|
{{ t("empty.environments") }}
|
||||||
|
</span>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="`${t('add.new')}`"
|
:label="`${t('add.new')}`"
|
||||||
filled
|
filled
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
@click="addEnvironmentVariable"
|
@click="addEnvironmentVariable"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -32,12 +32,19 @@
|
|||||||
:environment="environment"
|
:environment="environment"
|
||||||
@edit-environment="editEnvironment(index)"
|
@edit-environment="editEnvironment(index)"
|
||||||
/>
|
/>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="environments.length === 0"
|
v-if="environments.length === 0"
|
||||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.environments')}`"
|
|
||||||
:text="t('empty.environments')"
|
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||||
|
:alt="`${t('empty.environments')}`"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">
|
||||||
|
{{ t("empty.environments") }}
|
||||||
|
</span>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="`${t('add.new')}`"
|
:label="`${t('add.new')}`"
|
||||||
filled
|
filled
|
||||||
@@ -45,7 +52,7 @@
|
|||||||
class="mb-4"
|
class="mb-4"
|
||||||
@click="displayModalAdd(true)"
|
@click="displayModalAdd(true)"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
<EnvironmentsMyDetails
|
<EnvironmentsMyDetails
|
||||||
:show="showModalDetails"
|
:show="showModalDetails"
|
||||||
:action="action"
|
:action="action"
|
||||||
|
|||||||
@@ -83,12 +83,19 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="vars.length === 0"
|
v-if="vars.length === 0"
|
||||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.environments')}`"
|
|
||||||
:text="t('empty.environments')"
|
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||||
|
:alt="`${t('empty.environments')}`"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">
|
||||||
|
{{ t("empty.environments") }}
|
||||||
|
</span>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-if="isViewer"
|
v-if="isViewer"
|
||||||
disabled
|
disabled
|
||||||
@@ -103,7 +110,7 @@
|
|||||||
class="mb-4"
|
class="mb-4"
|
||||||
@click="addEnvironmentVariable"
|
@click="addEnvironmentVariable"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -43,12 +43,19 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="!loading && teamEnvironments.length === 0 && !adapterError"
|
v-if="!loading && teamEnvironments.length === 0 && !adapterError"
|
||||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.environments')}`"
|
|
||||||
:text="t('empty.environments')"
|
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||||
|
:alt="`${t('empty.environments')}`"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">
|
||||||
|
{{ t("empty.environments") }}
|
||||||
|
</span>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-if="team === undefined || team.myRole === 'VIEWER'"
|
v-if="team === undefined || team.myRole === 'VIEWER'"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -67,7 +74,7 @@
|
|||||||
class="mb-4"
|
class="mb-4"
|
||||||
@click="displayModalAdd(true)"
|
@click="displayModalAdd(true)"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
<div v-else-if="!loading">
|
<div v-else-if="!loading">
|
||||||
<EnvironmentsTeamsEnvironment
|
<EnvironmentsTeamsEnvironment
|
||||||
v-for="(environment, index) in JSON.parse(
|
v-for="(environment, index) in JSON.parse(
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex" @click="openLogoutModal()">
|
<div class="flex" @click="OpenLogoutModal()">
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
ref="logoutItem"
|
ref="logoutItem"
|
||||||
:icon="IconLogOut"
|
:icon="IconLogOut"
|
||||||
:label="`${t('auth.logout')}`"
|
:label="`${t('auth.logout')}`"
|
||||||
:outline="outline"
|
:outline="outline"
|
||||||
:shortcut="shortcut"
|
:shortcut="shortcut"
|
||||||
@click="openLogoutModal()"
|
@click="OpenLogoutModal()"
|
||||||
/>
|
/>
|
||||||
<HoppSmartConfirmModal
|
<HoppSmartConfirmModal
|
||||||
:show="confirmLogout"
|
:show="confirmLogout"
|
||||||
@@ -23,7 +23,6 @@ import IconLogOut from "~icons/lucide/log-out"
|
|||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
outline: {
|
outline: {
|
||||||
@@ -56,12 +55,8 @@ const logout = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const openLogoutModal = () => {
|
const OpenLogoutModal = () => {
|
||||||
emit("confirm-logout")
|
emit("confirm-logout")
|
||||||
confirmLogout.value = true
|
confirmLogout.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
defineActionHandler("user.logout", () => {
|
|
||||||
openLogoutModal()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -114,12 +114,19 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="authType === 'none'"
|
v-if="authType === 'none'"
|
||||||
:src="`/images/states/${colorMode.value}/login.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.authorization')}`"
|
|
||||||
:text="t('empty.authorization')"
|
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/login.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||||
|
:alt="`${t('empty.authorization')}`"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">
|
||||||
|
{{ t("empty.authorization") }}
|
||||||
|
</span>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
outline
|
outline
|
||||||
:label="t('app.documentation')"
|
:label="t('app.documentation')"
|
||||||
@@ -129,7 +136,7 @@
|
|||||||
reverse
|
reverse
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
<div v-else class="flex flex-1 border-b border-dividerLight">
|
<div v-else class="flex flex-1 border-b border-dividerLight">
|
||||||
<div class="w-2/3 border-r border-dividerLight">
|
<div class="w-2/3 border-r border-dividerLight">
|
||||||
<div v-if="authType === 'basic'">
|
<div v-if="authType === 'basic'">
|
||||||
|
|||||||
@@ -289,12 +289,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="workingHeaders.length === 0"
|
v-if="workingHeaders.length === 0"
|
||||||
:src="`/images/states/${colorMode.value}/add_category.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.headers')}`"
|
|
||||||
:text="t('empty.headers')"
|
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/add_category.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||||
|
:alt="`${t('empty.headers')}`"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">
|
||||||
|
{{ t("empty.headers") }}
|
||||||
|
</span>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="`${t('add.new')}`"
|
:label="`${t('add.new')}`"
|
||||||
filled
|
filled
|
||||||
@@ -302,7 +309,7 @@
|
|||||||
class="mb-4"
|
class="mb-4"
|
||||||
@click="addHeader"
|
@click="addHeader"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
|
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col flex-1 overflow-auto whitespace-nowrap">
|
<div class="flex flex-col flex-1 overflow-auto whitespace-nowrap">
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="responseString === 'loading'"
|
v-if="responseString === 'loading'"
|
||||||
:text="t('state.loading')"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<HoppSmartSpinner class="my-4" />
|
||||||
<HoppSmartSpinner class="my-4" />
|
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||||
</template>
|
</div>
|
||||||
</HoppSmartPlaceholder>
|
|
||||||
<div v-else-if="responseString" class="flex flex-col flex-1">
|
<div v-else-if="responseString" class="flex flex-col flex-1">
|
||||||
<div
|
<div
|
||||||
class="sticky top-0 z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight"
|
class="sticky top-0 z-10 flex items-center justify-between flex-shrink-0 pl-4 overflow-x-auto border-b bg-primary border-dividerLight"
|
||||||
|
|||||||
@@ -24,18 +24,25 @@
|
|||||||
:icon="IconBookOpen"
|
:icon="IconBookOpen"
|
||||||
:label="`${t('tab.documentation')}`"
|
:label="`${t('tab.documentation')}`"
|
||||||
>
|
>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
queryFields.length === 0 &&
|
queryFields.length === 0 &&
|
||||||
mutationFields.length === 0 &&
|
mutationFields.length === 0 &&
|
||||||
subscriptionFields.length === 0 &&
|
subscriptionFields.length === 0 &&
|
||||||
graphqlTypes.length === 0
|
graphqlTypes.length === 0
|
||||||
"
|
"
|
||||||
:src="`/images/states/${colorMode.value}/add_comment.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.documentation')}`"
|
|
||||||
:text="t('empty.documentation')"
|
|
||||||
>
|
>
|
||||||
</HoppSmartPlaceholder>
|
<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>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div
|
<div
|
||||||
class="sticky top-0 z-10 flex flex-shrink-0 overflow-x-auto bg-primary"
|
class="sticky top-0 z-10 flex flex-shrink-0 overflow-x-auto bg-primary"
|
||||||
@@ -165,13 +172,20 @@
|
|||||||
ref="schemaEditor"
|
ref="schemaEditor"
|
||||||
class="flex flex-col flex-1"
|
class="flex flex-col flex-1"
|
||||||
></div>
|
></div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-else
|
v-else
|
||||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.schema')}`"
|
|
||||||
:text="t('empty.schema')"
|
|
||||||
>
|
>
|
||||||
</HoppSmartPlaceholder>
|
<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>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
</HoppSmartTabs>
|
</HoppSmartTabs>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -108,23 +108,31 @@
|
|||||||
/>
|
/>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="history.length === 0"
|
v-if="history.length === 0"
|
||||||
:src="`/images/states/${colorMode.value}/history.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.history')}`"
|
|
||||||
:text="t('empty.history')"
|
|
||||||
>
|
>
|
||||||
</HoppSmartPlaceholder>
|
<img
|
||||||
<HoppSmartPlaceholder
|
: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
|
||||||
v-else-if="
|
v-else-if="
|
||||||
Object.keys(filteredHistoryGroups).length === 0 ||
|
Object.keys(filteredHistoryGroups).length === 0 ||
|
||||||
filteredHistory.length === 0
|
filteredHistory.length === 0
|
||||||
"
|
"
|
||||||
:text="`${t('state.nothing_found')} ‟${filterText || filterSelection}”`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
<span class="mt-2 mb-4 text-center">
|
||||||
</template>
|
{{ t("state.nothing_found") }} "{{ filterText || filterSelection }}"
|
||||||
|
</span>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="t('action.clear')"
|
:label="t('action.clear')"
|
||||||
outline
|
outline
|
||||||
@@ -135,7 +143,7 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
<HoppSmartConfirmModal
|
<HoppSmartConfirmModal
|
||||||
:show="confirmRemove"
|
:show="confirmRemove"
|
||||||
:title="`${t('confirm.remove_history')}`"
|
:title="`${t('confirm.remove_history')}`"
|
||||||
@@ -176,7 +184,6 @@ import {
|
|||||||
import HistoryRestCard from "./rest/Card.vue"
|
import HistoryRestCard from "./rest/Card.vue"
|
||||||
import HistoryGraphqlCard from "./graphql/Card.vue"
|
import HistoryGraphqlCard from "./graphql/Card.vue"
|
||||||
import { createNewTab } from "~/helpers/rest/tab"
|
import { createNewTab } from "~/helpers/rest/tab"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
|
||||||
|
|
||||||
type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry
|
type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry
|
||||||
|
|
||||||
@@ -330,8 +337,4 @@ const toggleStar = (entry: HistoryEntry) => {
|
|||||||
toggleRESTHistoryEntryStar(entry as RESTHistoryEntry)
|
toggleRESTHistoryEntryStar(entry as RESTHistoryEntry)
|
||||||
else toggleGraphqlHistoryEntryStar(entry as GQLHistoryEntry)
|
else toggleGraphqlHistoryEntryStar(entry as GQLHistoryEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
defineActionHandler("history.clear", () => {
|
|
||||||
confirmRemove.value = true
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -113,12 +113,17 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="auth.authType === 'none'"
|
v-if="auth.authType === 'none'"
|
||||||
:src="`/images/states/${colorMode.value}/login.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.authorization')}`"
|
|
||||||
:text="t('empty.authorization')"
|
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/login.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||||
|
:alt="`${t('empty.authorization')}`"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">{{ t("empty.authorization") }}</span>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
outline
|
outline
|
||||||
:label="t('app.documentation')"
|
:label="t('app.documentation')"
|
||||||
@@ -128,7 +133,7 @@
|
|||||||
reverse
|
reverse
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
<div v-else class="flex flex-1 border-b border-dividerLight">
|
<div v-else class="flex flex-1 border-b border-dividerLight">
|
||||||
<div class="w-2/3 border-r border-dividerLight">
|
<div class="w-2/3 border-r border-dividerLight">
|
||||||
<div v-if="auth.authType === 'basic'">
|
<div v-if="auth.authType === 'basic'">
|
||||||
|
|||||||
@@ -102,12 +102,17 @@
|
|||||||
v-model="body"
|
v-model="body"
|
||||||
/>
|
/>
|
||||||
<HttpRawBody v-else-if="body.contentType !== null" v-model="body" />
|
<HttpRawBody v-else-if="body.contentType !== null" v-model="body" />
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="body.contentType == null"
|
v-if="body.contentType == null"
|
||||||
:src="`/images/states/${colorMode.value}/upload_single_file.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.body')}`"
|
|
||||||
:text="t('empty.body')"
|
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/upload_single_file.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||||
|
:alt="`${t('empty.body')}`"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">{{ t("empty.body") }}</span>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
outline
|
outline
|
||||||
:label="`${t('app.documentation')}`"
|
:label="`${t('app.documentation')}`"
|
||||||
@@ -117,7 +122,7 @@
|
|||||||
reverse
|
reverse
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -152,12 +152,17 @@
|
|||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
|
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="workingParams.length === 0"
|
v-if="workingParams.length === 0"
|
||||||
:src="`/images/states/${colorMode.value}/upload_single_file.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.body')}`"
|
|
||||||
:text="t('empty.body')"
|
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/upload_single_file.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||||
|
:alt="`${t('empty.body')}`"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">{{ t("empty.body") }}</span>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="`${t('add.new')}`"
|
:label="`${t('add.new')}`"
|
||||||
filled
|
filled
|
||||||
@@ -165,7 +170,7 @@
|
|||||||
class="mb-4"
|
class="mb-4"
|
||||||
@click="addBodyParam"
|
@click="addBodyParam"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -56,19 +56,20 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
!(
|
!(
|
||||||
filteredCodegenDefinitions.length !== 0 ||
|
filteredCodegenDefinitions.length !== 0 ||
|
||||||
CodegenDefinitions.length === 0
|
CodegenDefinitions.length === 0
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
:text="`${t('state.nothing_found')} ‟${searchQuery}”`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
<span class="my-2 text-center">
|
||||||
</template>
|
{{ t("state.nothing_found") }} "{{ searchQuery }}"
|
||||||
</HoppSmartPlaceholder>
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -202,12 +202,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="workingHeaders.length === 0"
|
v-if="workingHeaders.length === 0"
|
||||||
:src="`/images/states/${colorMode.value}/add_category.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.headers')}`"
|
|
||||||
:text="t('empty.headers')"
|
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/add_category.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||||
|
:alt="`${t('empty.headers')}`"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">{{ t("empty.headers") }}</span>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
filled
|
filled
|
||||||
:label="`${t('add.new')}`"
|
:label="`${t('add.new')}`"
|
||||||
@@ -215,7 +220,7 @@
|
|||||||
class="mb-4"
|
class="mb-4"
|
||||||
@click="addHeader"
|
@click="addHeader"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -145,12 +145,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
<HoppSmartPlaceholder
|
|
||||||
|
<div
|
||||||
v-if="workingParams.length === 0"
|
v-if="workingParams.length === 0"
|
||||||
:src="`/images/states/${colorMode.value}/add_files.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.parameters')}`"
|
|
||||||
:text="t('empty.parameters')"
|
|
||||||
>
|
>
|
||||||
|
<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>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="`${t('add.new')}`"
|
:label="`${t('add.new')}`"
|
||||||
:icon="IconPlus"
|
:icon="IconPlus"
|
||||||
@@ -158,7 +164,7 @@
|
|||||||
class="mb-4"
|
class="mb-4"
|
||||||
@click="addParam"
|
@click="addParam"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="sticky top-0 z-20 flex-none flex-shrink-0 p-4 sm:flex sm:flex-shrink-0 sm:space-x-2 bg-primary"
|
class="sticky top-0 z-20 flex-none flex-shrink-0 p-4 overflow-x-auto sm:flex sm:flex-shrink-0 sm:space-x-2 bg-primary"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex flex-1 border rounded min-w-52 border-divider whitespace-nowrap"
|
class="flex flex-1 border rounded min-w-52 border-divider whitespace-nowrap"
|
||||||
@@ -47,14 +47,13 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex flex-1 transition border-l rounded-r border-divider bg-primaryLight whitespace-nowrap"
|
class="flex flex-1 overflow-auto transition border-l rounded-r border-divider bg-primaryLight whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="tab.document.request.endpoint"
|
v-model="tab.document.request.endpoint"
|
||||||
:placeholder="`${t('request.url')}`"
|
:placeholder="`${t('request.url')}`"
|
||||||
:auto-complete-source="userHistories"
|
@enter="newSendRequest()"
|
||||||
@paste="onPasteUrl($event)"
|
@paste="onPasteUrl($event)"
|
||||||
@enter="newSendRequest"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,7 +228,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useSetting } from "@composables/settings"
|
import { useSetting } from "@composables/settings"
|
||||||
import { useReadonlyStream, useStreamSubscriber } from "@composables/stream"
|
import { useStreamSubscriber } from "@composables/stream"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { refAutoReset, useVModel } from "@vueuse/core"
|
import { refAutoReset, useVModel } from "@vueuse/core"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
@@ -260,7 +259,6 @@ import IconSave from "~icons/lucide/save"
|
|||||||
import IconShare2 from "~icons/lucide/share-2"
|
import IconShare2 from "~icons/lucide/share-2"
|
||||||
import { HoppRESTTab } from "~/helpers/rest/tab"
|
import { HoppRESTTab } from "~/helpers/rest/tab"
|
||||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history"
|
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { getCurrentStrategyID } from "~/helpers/network"
|
import { getCurrentStrategyID } from "~/helpers/network"
|
||||||
|
|
||||||
@@ -315,12 +313,6 @@ const clearAll = ref<any | null>(null)
|
|||||||
const copyRequestAction = ref<any | null>(null)
|
const copyRequestAction = ref<any | null>(null)
|
||||||
const saveRequestAction = ref<any | null>(null)
|
const saveRequestAction = ref<any | null>(null)
|
||||||
|
|
||||||
const history = useReadonlyStream<RESTHistoryEntry[]>(restHistory$, [])
|
|
||||||
|
|
||||||
const userHistories = computed(() => {
|
|
||||||
return history.value.map((history) => history.request.endpoint).slice(0, 10)
|
|
||||||
})
|
|
||||||
|
|
||||||
const newSendRequest = async () => {
|
const newSendRequest = async () => {
|
||||||
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
|
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
|
||||||
toast.error(`${t("empty.endpoint")}`)
|
toast.error(`${t("empty.endpoint")}`)
|
||||||
|
|||||||
@@ -11,29 +11,51 @@
|
|||||||
<HoppSmartSpinner class="my-4" />
|
<HoppSmartSpinner class="my-4" />
|
||||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="response.type === 'network_fail'"
|
v-if="response.type === 'network_fail'"
|
||||||
:src="`/images/states/${colorMode.value}/youre_lost.svg`"
|
class="flex flex-col items-center justify-center flex-1 p-4"
|
||||||
:alt="`${t('error.network_fail')}`"
|
|
||||||
:heading="t('error.network_fail')"
|
|
||||||
:text="t('helpers.network_fail')"
|
|
||||||
>
|
>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{{ t("helpers.network_fail") }}
|
||||||
|
</span>
|
||||||
<AppInterceptor class="p-2 border rounded border-dividerLight" />
|
<AppInterceptor class="p-2 border rounded border-dividerLight" />
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="response.type === 'script_fail'"
|
v-if="response.type === 'script_fail'"
|
||||||
:src="`/images/states/${colorMode.value}/youre_lost.svg`"
|
class="flex flex-col items-center justify-center flex-1 p-4"
|
||||||
:alt="`${t('error.script_fail')}`"
|
|
||||||
:label="t('error.script_fail')"
|
|
||||||
:text="t('helpers.script_fail')"
|
|
||||||
>
|
>
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
{{ t("helpers.script_fail") }}
|
||||||
|
</span>
|
||||||
<div
|
<div
|
||||||
class="mt-2 w-full px-4 py-2 overflow-auto font-mono text-red-400 whitespace-normal rounded bg-primaryLight"
|
class="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.name }}: {{ response.error.message }}<br />
|
||||||
{{ response.error.stack }}
|
{{ response.error.stack }}
|
||||||
</div>
|
</div>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="response.type === 'success' || response.type === 'fail'"
|
v-if="response.type === 'success' || response.type === 'fail'"
|
||||||
class="flex items-center font-semibold text-tiny"
|
class="flex items-center font-semibold text-tiny"
|
||||||
|
|||||||
@@ -153,21 +153,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-else-if="testResults && testResults.scriptError"
|
v-else-if="testResults && testResults.scriptError"
|
||||||
:src="`/images/states/${colorMode.value}/youre_lost.svg`"
|
class="flex flex-col items-center justify-center flex-1 p-4"
|
||||||
:alt="`${t('error.test_script_fail')}`"
|
|
||||||
:heading="t('error.test_script_fail')"
|
|
||||||
:text="t('helpers.test_script_fail')"
|
|
||||||
>
|
>
|
||||||
</HoppSmartPlaceholder>
|
<img
|
||||||
<HoppSmartPlaceholder
|
: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"
|
||||||
|
>
|
||||||
|
{{ t("helpers.test_script_fail") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
v-else
|
v-else
|
||||||
:src="`/images/states/${colorMode.value}/validation.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.tests')}`"
|
|
||||||
:heading="t('empty.tests')"
|
|
||||||
:text="t('helpers.tests')"
|
|
||||||
>
|
>
|
||||||
|
<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>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
outline
|
outline
|
||||||
:label="`${t('action.learn_more')}`"
|
:label="`${t('action.learn_more')}`"
|
||||||
@@ -177,7 +197,7 @@
|
|||||||
reverse
|
reverse
|
||||||
class="my-4"
|
class="my-4"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
<EnvironmentsMyDetails
|
<EnvironmentsMyDetails
|
||||||
:show="showMyEnvironmentDetailsModal"
|
:show="showMyEnvironmentDetailsModal"
|
||||||
action="new"
|
action="new"
|
||||||
|
|||||||
@@ -143,12 +143,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="workingUrlEncodedParams.length === 0"
|
v-if="workingUrlEncodedParams.length === 0"
|
||||||
:src="`/images/states/${colorMode.value}/add_category.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.body')}`"
|
|
||||||
:text="t('empty.body')"
|
|
||||||
>
|
>
|
||||||
|
<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>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
filled
|
filled
|
||||||
:label="`${t('add.new')}`"
|
:label="`${t('add.new')}`"
|
||||||
@@ -156,7 +163,7 @@
|
|||||||
class="mb-4"
|
class="mb-4"
|
||||||
@click="addUrlEncodedParam"
|
@click="addUrlEncodedParam"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -11,13 +11,20 @@
|
|||||||
<HoppSmartSpinner class="mb-4" />
|
<HoppSmartSpinner class="mb-4" />
|
||||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="!loading && myShortcodes.length === 0"
|
v-if="!loading && myShortcodes.length === 0"
|
||||||
:src="`/images/states/${colorMode.value}/add_files.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.shortcodes')}`"
|
|
||||||
:text="t('empty.shortcodes')"
|
|
||||||
>
|
>
|
||||||
</HoppSmartPlaceholder>
|
<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>
|
||||||
<div v-else-if="!loading">
|
<div v-else-if="!loading">
|
||||||
<div
|
<div
|
||||||
class="hidden w-full border-t rounded-t bg-primaryLight lg:flex border-x border-dividerLight"
|
class="hidden w-full border-t rounded-t bg-primaryLight lg:flex border-x border-dividerLight"
|
||||||
|
|||||||
@@ -52,19 +52,20 @@
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</HoppSmartLink>
|
</HoppSmartLink>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
!(
|
!(
|
||||||
filteredAppLanguages.length !== 0 ||
|
filteredAppLanguages.length !== 0 ||
|
||||||
APP_LANGUAGES.length === 0
|
APP_LANGUAGES.length === 0
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
:text="`${t('state.nothing_found')} ‟${searchQuery}”`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
<span class="my-2 text-center">
|
||||||
</template>
|
{{ t("state.nothing_found") }} "{{ searchQuery }}"
|
||||||
</HoppSmartPlaceholder>
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,44 +1,19 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="autocomplete-wrapper">
|
<div
|
||||||
<div class="absolute inset-0 flex flex-1 overflow-x-auto">
|
class="relative flex items-center flex-1 flex-shrink-0 py-4 overflow-auto whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<div class="absolute inset-0 flex flex-1">
|
||||||
<div
|
<div
|
||||||
ref="editor"
|
ref="editor"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
class="flex flex-1"
|
class="flex flex-1"
|
||||||
:class="styles"
|
:class="styles"
|
||||||
|
@keydown.enter.prevent="emit('enter', $event)"
|
||||||
|
@keyup="emit('keyup', $event)"
|
||||||
@click="emit('click', $event)"
|
@click="emit('click', $event)"
|
||||||
@keydown="handleKeystroke"
|
@keydown="emit('keydown', $event)"
|
||||||
@focusin="showSuggestionPopover = true"
|
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
|
||||||
v-if="showSuggestionPopover && autoCompleteSource"
|
|
||||||
ref="suggestionsMenu"
|
|
||||||
class="suggestions"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
v-for="(suggestion, index) in suggestions"
|
|
||||||
:key="`suggestion-${index}`"
|
|
||||||
:class="{ active: currentSuggestionIndex === index }"
|
|
||||||
@click="updateModelValue(suggestion)"
|
|
||||||
>
|
|
||||||
<span class="truncate py-0.5">
|
|
||||||
{{ suggestion }}
|
|
||||||
</span>
|
|
||||||
<div
|
|
||||||
v-if="currentSuggestionIndex === index"
|
|
||||||
class="hidden md:flex text-secondary items-center"
|
|
||||||
>
|
|
||||||
<kbd class="shortcut-key">TAB</kbd>
|
|
||||||
<span class="ml-2 truncate">to select</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li v-if="suggestions.length === 0" class="pointer-events-none">
|
|
||||||
<span class="truncate py-0.5">
|
|
||||||
{{ t("empty.history_suggestions") }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -60,8 +35,6 @@ import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironme
|
|||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
|
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { useI18n } from "~/composables/i18n"
|
|
||||||
import { onClickOutside } from "@vueuse/core"
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -73,7 +46,6 @@ const props = withDefaults(
|
|||||||
selectTextOnMount?: boolean
|
selectTextOnMount?: boolean
|
||||||
environmentHighlights?: boolean
|
environmentHighlights?: boolean
|
||||||
readonly?: boolean
|
readonly?: boolean
|
||||||
autoCompleteSource?: string[]
|
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
modelValue: "",
|
modelValue: "",
|
||||||
@@ -83,7 +55,6 @@ const props = withDefaults(
|
|||||||
focus: false,
|
focus: false,
|
||||||
readonly: false,
|
readonly: false,
|
||||||
environmentHighlights: true,
|
environmentHighlights: true,
|
||||||
autoCompleteSource: undefined,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -97,160 +68,12 @@ const emit = defineEmits<{
|
|||||||
(e: "click", ev: any): void
|
(e: "click", ev: any): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const t = useI18n()
|
|
||||||
|
|
||||||
const cachedValue = ref(props.modelValue)
|
const cachedValue = ref(props.modelValue)
|
||||||
|
|
||||||
const view = ref<EditorView>()
|
const view = ref<EditorView>()
|
||||||
|
|
||||||
const editor = ref<any | null>(null)
|
const editor = ref<any | null>(null)
|
||||||
|
|
||||||
const currentSuggestionIndex = ref(-1)
|
|
||||||
const showSuggestionPopover = ref(false)
|
|
||||||
|
|
||||||
const suggestionsMenu = ref<any | null>(null)
|
|
||||||
|
|
||||||
onClickOutside(suggestionsMenu, () => {
|
|
||||||
showSuggestionPopover.value = false
|
|
||||||
})
|
|
||||||
|
|
||||||
//filter autocompleteSource with unique values
|
|
||||||
const uniqueAutoCompleteSource = computed(() => {
|
|
||||||
if (props.autoCompleteSource) {
|
|
||||||
return [...new Set(props.autoCompleteSource)]
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const suggestions = computed(() => {
|
|
||||||
if (
|
|
||||||
props.modelValue &&
|
|
||||||
props.modelValue.length > 0 &&
|
|
||||||
uniqueAutoCompleteSource.value &&
|
|
||||||
uniqueAutoCompleteSource.value.length > 0
|
|
||||||
) {
|
|
||||||
return uniqueAutoCompleteSource.value.filter((suggestion) =>
|
|
||||||
suggestion.toLowerCase().includes(props.modelValue.toLowerCase())
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return uniqueAutoCompleteSource.value ?? []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateModelValue = (value: string) => {
|
|
||||||
emit("update:modelValue", value)
|
|
||||||
emit("change", value)
|
|
||||||
showSuggestionPopover.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleKeystroke = (ev: KeyboardEvent) => {
|
|
||||||
if (["ArrowDown", "ArrowUp", "Enter", "Tab", "Escape"].includes(ev.key)) {
|
|
||||||
ev.preventDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
showSuggestionPopover.value = true
|
|
||||||
|
|
||||||
if (
|
|
||||||
["Enter", "Tab"].includes(ev.key) &&
|
|
||||||
suggestions.value.length > 0 &&
|
|
||||||
currentSuggestionIndex.value > -1
|
|
||||||
) {
|
|
||||||
updateModelValue(suggestions.value[currentSuggestionIndex.value])
|
|
||||||
currentSuggestionIndex.value = -1
|
|
||||||
|
|
||||||
//used to set codemirror cursor at the end of the line after selecting a suggestion
|
|
||||||
nextTick(() => {
|
|
||||||
view.value?.dispatch({
|
|
||||||
selection: EditorSelection.create([
|
|
||||||
EditorSelection.range(
|
|
||||||
props.modelValue.length,
|
|
||||||
props.modelValue.length
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ev.key === "ArrowDown") {
|
|
||||||
scrollActiveElIntoView()
|
|
||||||
|
|
||||||
currentSuggestionIndex.value =
|
|
||||||
currentSuggestionIndex.value < suggestions.value.length - 1
|
|
||||||
? currentSuggestionIndex.value + 1
|
|
||||||
: suggestions.value.length - 1
|
|
||||||
|
|
||||||
emit("keydown", ev)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ev.key === "ArrowUp") {
|
|
||||||
scrollActiveElIntoView()
|
|
||||||
|
|
||||||
currentSuggestionIndex.value =
|
|
||||||
currentSuggestionIndex.value - 1 >= 0
|
|
||||||
? currentSuggestionIndex.value - 1
|
|
||||||
: 0
|
|
||||||
|
|
||||||
emit("keyup", ev)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ev.key === "Enter") {
|
|
||||||
emit("enter", ev)
|
|
||||||
showSuggestionPopover.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ev.key === "Escape") {
|
|
||||||
showSuggestionPopover.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// used to scroll to the first suggestion when left arrow is pressed
|
|
||||||
if (ev.key === "ArrowLeft") {
|
|
||||||
if (suggestions.value.length > 0) {
|
|
||||||
currentSuggestionIndex.value = 0
|
|
||||||
nextTick(() => {
|
|
||||||
scrollActiveElIntoView()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// used to scroll to the last suggestion when right arrow is pressed
|
|
||||||
if (ev.key === "ArrowRight") {
|
|
||||||
if (suggestions.value.length > 0) {
|
|
||||||
currentSuggestionIndex.value = suggestions.value.length - 1
|
|
||||||
nextTick(() => {
|
|
||||||
scrollActiveElIntoView()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// reset currentSuggestionIndex showSuggestionPopover is false
|
|
||||||
watch(
|
|
||||||
() => showSuggestionPopover.value,
|
|
||||||
(newVal) => {
|
|
||||||
if (!newVal) {
|
|
||||||
currentSuggestionIndex.value = -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to scroll the active suggestion into view
|
|
||||||
*/
|
|
||||||
const scrollActiveElIntoView = () => {
|
|
||||||
const suggestionsMenuEl = suggestionsMenu.value
|
|
||||||
if (suggestionsMenuEl) {
|
|
||||||
const activeSuggestionEl = suggestionsMenuEl.querySelector(".active")
|
|
||||||
if (activeSuggestionEl) {
|
|
||||||
activeSuggestionEl.scrollIntoView({
|
|
||||||
behavior: "smooth",
|
|
||||||
block: "center",
|
|
||||||
inline: "start",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.modelValue,
|
() => props.modelValue,
|
||||||
(newVal) => {
|
(newVal) => {
|
||||||
@@ -413,49 +236,3 @@ watch(editor, () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.autocomplete-wrapper {
|
|
||||||
@apply relative;
|
|
||||||
@apply flex;
|
|
||||||
@apply flex-1;
|
|
||||||
@apply flex-shrink-0;
|
|
||||||
@apply whitespace-nowrap;
|
|
||||||
|
|
||||||
.suggestions {
|
|
||||||
@apply absolute;
|
|
||||||
@apply bg-popover;
|
|
||||||
@apply z-50;
|
|
||||||
@apply shadow-lg;
|
|
||||||
@apply max-h-46;
|
|
||||||
@apply border-b border-x border-divider;
|
|
||||||
@apply overflow-y-auto;
|
|
||||||
@apply -left-[1px];
|
|
||||||
@apply right-0;
|
|
||||||
|
|
||||||
top: calc(100% + 1px);
|
|
||||||
border-radius: 0 0 8px 8px;
|
|
||||||
|
|
||||||
li {
|
|
||||||
@apply flex;
|
|
||||||
@apply items-center;
|
|
||||||
@apply justify-between;
|
|
||||||
@apply w-full;
|
|
||||||
@apply py-2 px-4;
|
|
||||||
@apply text-secondary;
|
|
||||||
@apply cursor-pointer;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-radius: 0 0 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&.active {
|
|
||||||
@apply bg-primaryDark;
|
|
||||||
@apply text-secondaryDark;
|
|
||||||
@apply cursor-pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -47,12 +47,19 @@
|
|||||||
"
|
"
|
||||||
class="border rounded border-divider"
|
class="border rounded border-divider"
|
||||||
>
|
>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="teamDetails.data.right.team.teamMembers === 0"
|
v-if="teamDetails.data.right.team.teamMembers === 0"
|
||||||
:src="`/images/states/${colorMode.value}/add_group.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.members')}`"
|
|
||||||
:text="t('empty.members')"
|
|
||||||
>
|
>
|
||||||
|
<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>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:icon="IconUserPlus"
|
:icon="IconUserPlus"
|
||||||
:label="t('team.invite')"
|
:label="t('team.invite')"
|
||||||
@@ -62,7 +69,7 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
<div v-else class="divide-y divide-dividerLight">
|
<div v-else class="divide-y divide-dividerLight">
|
||||||
<div
|
<div
|
||||||
v-for="(member, index) in membersList"
|
v-for="(member, index) in membersList"
|
||||||
|
|||||||
@@ -98,14 +98,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="
|
v-if="
|
||||||
E.isRight(pendingInvites.data) &&
|
E.isRight(pendingInvites.data) &&
|
||||||
pendingInvites.data.right.team.teamInvitations.length === 0
|
pendingInvites.data.right.team.teamInvitations.length === 0
|
||||||
"
|
"
|
||||||
:text="t('empty.pending_invites')"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
>
|
>
|
||||||
</HoppSmartPlaceholder>
|
<span class="text-center">
|
||||||
|
{{ t("empty.pending_invites") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!pendingInvites.loading && E.isLeft(pendingInvites.data)"
|
v-if="!pendingInvites.loading && E.isLeft(pendingInvites.data)"
|
||||||
class="flex flex-col items-center p-4"
|
class="flex flex-col items-center p-4"
|
||||||
@@ -218,18 +221,25 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="newInvites.length === 0"
|
v-if="newInvites.length === 0"
|
||||||
:src="`/images/states/${colorMode.value}/add_group.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.invites')}`"
|
|
||||||
:text="`${t('empty.invites')}`"
|
|
||||||
>
|
>
|
||||||
|
<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>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="t('add.new')"
|
:label="t('add.new')"
|
||||||
filled
|
filled
|
||||||
@click="addNewInvitee"
|
@click="addNewInvitee"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="newInvites.length"
|
v-if="newInvites.length"
|
||||||
|
|||||||
@@ -10,18 +10,25 @@
|
|||||||
<HoppSmartSpinner class="mb-4" />
|
<HoppSmartSpinner class="mb-4" />
|
||||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="!loading && myTeams.length === 0"
|
v-if="!loading && myTeams.length === 0"
|
||||||
:src="`/images/states/${colorMode.value}/add_group.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.teams')}`"
|
|
||||||
:text="`${t('empty.teams')}`"
|
|
||||||
>
|
>
|
||||||
|
<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>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="`${t('team.create_new')}`"
|
:label="`${t('team.create_new')}`"
|
||||||
filled
|
filled
|
||||||
@click="displayModalAdd(true)"
|
@click="displayModalAdd(true)"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="!loading"
|
v-else-if="!loading"
|
||||||
class="grid gap-4"
|
class="grid gap-4"
|
||||||
|
|||||||
@@ -15,12 +15,19 @@
|
|||||||
<HoppSmartSpinner class="mb-4" />
|
<HoppSmartSpinner class="mb-4" />
|
||||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="!loading && myTeams.length === 0"
|
v-if="!loading && myTeams.length === 0"
|
||||||
:src="`/images/states/${colorMode.value}/add_group.svg`"
|
class="flex flex-col items-center justify-center flex-1 p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.teams')}`"
|
|
||||||
:text="`${t('empty.teams')}`"
|
|
||||||
>
|
>
|
||||||
|
<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>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="t('team.create_new')"
|
:label="t('team.create_new')"
|
||||||
filled
|
filled
|
||||||
@@ -28,7 +35,7 @@
|
|||||||
:icon="IconPlus"
|
:icon="IconPlus"
|
||||||
@click="displayModalAdd(true)"
|
@click="displayModalAdd(true)"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
<div v-else-if="!loading" class="flex flex-col">
|
<div v-else-if="!loading" class="flex flex-col">
|
||||||
<div
|
<div
|
||||||
class="sticky top-0 z-10 flex items-center justify-between py-2 pl-2 mb-2 -top-2 bg-popover"
|
class="sticky top-0 z-10 flex items-center justify-between py-2 pl-2 mb-2 -top-2 bg-popover"
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { describe, expect, test } from "vitest"
|
|
||||||
import { getEditorLangForMimeType } from "../editorutils"
|
import { getEditorLangForMimeType } from "../editorutils"
|
||||||
|
|
||||||
describe("getEditorLangForMimeType", () => {
|
describe("getEditorLangForMimeType", () => {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { describe, test, expect } from "vitest"
|
|
||||||
import jsonParse from "../jsonParse"
|
import jsonParse from "../jsonParse"
|
||||||
|
|
||||||
describe("jsonParse", () => {
|
describe("jsonParse", () => {
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { vi, beforeEach, describe, expect, test } from "vitest"
|
|
||||||
import { getPlatformSpecialKey } from "../platformutils"
|
import { getPlatformSpecialKey } from "../platformutils"
|
||||||
|
|
||||||
describe("getPlatformSpecialKey", () => {
|
describe("getPlatformSpecialKey", () => {
|
||||||
let platformGetter
|
let platformGetter
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
platformGetter = vi.spyOn(navigator, "platform", "get")
|
platformGetter = jest.spyOn(navigator, "platform", "get")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns '⌘' for Apple platforms", () => {
|
test("returns '⌘' for Apple platforms", () => {
|
||||||
|
|||||||
@@ -2,10 +2,8 @@
|
|||||||
* For example, sending a request.
|
* For example, sending a request.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Ref, onBeforeUnmount, onMounted, watch } from "vue"
|
import { onBeforeUnmount, onMounted } from "vue"
|
||||||
import { BehaviorSubject } from "rxjs"
|
import { BehaviorSubject } from "rxjs"
|
||||||
import { HoppRESTDocument } from "./rest/document"
|
|
||||||
import { HoppGQLRequest } from "@hoppscotch/data"
|
|
||||||
|
|
||||||
export type HoppAction =
|
export type HoppAction =
|
||||||
| "request.send-cancel" // Send/Cancel a Hoppscotch Request
|
| "request.send-cancel" // Send/Cancel a Hoppscotch Request
|
||||||
@@ -24,6 +22,8 @@ export type HoppAction =
|
|||||||
| "modals.search.toggle" // Shows the search modal
|
| "modals.search.toggle" // Shows the search modal
|
||||||
| "modals.support.toggle" // Shows the support modal
|
| "modals.support.toggle" // Shows the support modal
|
||||||
| "modals.share.toggle" // Shows the share modal
|
| "modals.share.toggle" // Shows the share modal
|
||||||
|
| "modals.my.environment.edit" // Edit current personal environment
|
||||||
|
| "modals.team.environment.edit" // Edit current team environment
|
||||||
| "navigation.jump.rest" // Jump to REST page
|
| "navigation.jump.rest" // Jump to REST page
|
||||||
| "navigation.jump.graphql" // Jump to GraphQL page
|
| "navigation.jump.graphql" // Jump to GraphQL page
|
||||||
| "navigation.jump.realtime" // Jump to realtime page
|
| "navigation.jump.realtime" // Jump to realtime page
|
||||||
@@ -38,9 +38,6 @@ export type HoppAction =
|
|||||||
| "response.file.download" // Download response as file
|
| "response.file.download" // Download response as file
|
||||||
| "response.copy" // Copy response to clipboard
|
| "response.copy" // Copy response to clipboard
|
||||||
| "modals.login.toggle" // Login to Hoppscotch
|
| "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
|
* Defines the arguments, if present for a given type that is required to be passed on
|
||||||
@@ -53,7 +50,7 @@ export type HoppAction =
|
|||||||
* NOTE: We can't enforce type checks to make sure the key is Action, you
|
* NOTE: We can't enforce type checks to make sure the key is Action, you
|
||||||
* will know if you got something wrong if there is a type error in this file
|
* will know if you got something wrong if there is a type error in this file
|
||||||
*/
|
*/
|
||||||
type HoppActionArgsMap = {
|
type HoppActionArgs = {
|
||||||
"modals.my.environment.edit": {
|
"modals.my.environment.edit": {
|
||||||
envName: string
|
envName: string
|
||||||
variableName: string
|
variableName: string
|
||||||
@@ -62,18 +59,12 @@ type HoppActionArgsMap = {
|
|||||||
envName: string
|
envName: string
|
||||||
variableName: string
|
variableName: string
|
||||||
}
|
}
|
||||||
"rest.request.open": {
|
|
||||||
doc: HoppRESTDocument
|
|
||||||
}
|
|
||||||
"gql.request.open": {
|
|
||||||
request: HoppGQLRequest
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HoppActions which require arguments for their invocation
|
* HoppActions which require arguments for their invocation
|
||||||
*/
|
*/
|
||||||
export type HoppActionWithArgs = keyof HoppActionArgsMap
|
type HoppActionWithArgs = keyof HoppActionArgs
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HoppActions which do not require arguments for their invocation
|
* HoppActions which do not require arguments for their invocation
|
||||||
@@ -83,27 +74,27 @@ export type HoppActionWithNoArgs = Exclude<HoppAction, HoppActionWithArgs>
|
|||||||
/**
|
/**
|
||||||
* Resolves the argument type for a given HoppAction
|
* Resolves the argument type for a given HoppAction
|
||||||
*/
|
*/
|
||||||
type ArgOfHoppAction<A extends HoppAction | HoppActionWithArgs> =
|
type ArgOfHoppAction<A extends HoppAction> = A extends HoppActionWithArgs
|
||||||
A extends HoppActionWithArgs ? HoppActionArgsMap[A] : undefined
|
? HoppActionArgs[A]
|
||||||
|
: undefined
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the action function for a given HoppAction, used by action handler function defs
|
* Resolves the action function for a given HoppAction, used by action handler function defs
|
||||||
*/
|
*/
|
||||||
type ActionFunc<A extends HoppAction | HoppActionWithArgs> =
|
type ActionFunc<A extends HoppAction> = A extends HoppActionWithArgs
|
||||||
A extends HoppActionWithArgs ? (arg: ArgOfHoppAction<A>) => void : () => void
|
? (arg: ArgOfHoppAction<A>) => void
|
||||||
|
: () => void
|
||||||
|
|
||||||
type BoundActionList = {
|
type BoundActionList = {
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
[A in HoppAction | HoppActionWithArgs]?: Array<ActionFunc<A>>
|
[A in HoppAction]?: Array<ActionFunc<A>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const boundActions: BoundActionList = {}
|
const boundActions: BoundActionList = {}
|
||||||
|
|
||||||
export const activeActions$ = new BehaviorSubject<
|
export const activeActions$ = new BehaviorSubject<HoppAction[]>([])
|
||||||
(HoppAction | HoppActionWithArgs)[]
|
|
||||||
>([])
|
|
||||||
|
|
||||||
export function bindAction<A extends HoppAction | HoppActionWithArgs>(
|
export function bindAction<A extends HoppAction>(
|
||||||
action: A,
|
action: A,
|
||||||
handler: ActionFunc<A>
|
handler: ActionFunc<A>
|
||||||
) {
|
) {
|
||||||
@@ -119,7 +110,7 @@ export function bindAction<A extends HoppAction | HoppActionWithArgs>(
|
|||||||
|
|
||||||
type InvokeActionFunc = {
|
type InvokeActionFunc = {
|
||||||
(action: HoppActionWithNoArgs, args?: undefined): void
|
(action: HoppActionWithNoArgs, args?: undefined): void
|
||||||
<A extends HoppActionWithArgs>(action: A, args: HoppActionArgsMap[A]): void
|
<A extends HoppActionWithArgs>(action: A, args: ArgOfHoppAction<A>): void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -128,16 +119,14 @@ type InvokeActionFunc = {
|
|||||||
* @param action The action to fire
|
* @param action The action to fire
|
||||||
* @param args The argument passed to the action handler. Optional if action has no args required
|
* @param args The argument passed to the action handler. Optional if action has no args required
|
||||||
*/
|
*/
|
||||||
export const invokeAction: InvokeActionFunc = <
|
export const invokeAction: InvokeActionFunc = <A extends HoppAction>(
|
||||||
A extends HoppAction | HoppActionWithArgs
|
|
||||||
>(
|
|
||||||
action: A,
|
action: A,
|
||||||
args: ArgOfHoppAction<A>
|
args: ArgOfHoppAction<A>
|
||||||
) => {
|
) => {
|
||||||
boundActions[action]?.forEach((handler) => handler(args! as any))
|
boundActions[action]?.forEach((handler) => handler(args!))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unbindAction<A extends HoppAction | HoppActionWithArgs>(
|
export function unbindAction<A extends HoppAction>(
|
||||||
action: A,
|
action: A,
|
||||||
handler: ActionFunc<A>
|
handler: ActionFunc<A>
|
||||||
) {
|
) {
|
||||||
@@ -153,57 +142,15 @@ export function unbindAction<A extends HoppAction | HoppActionWithArgs>(
|
|||||||
activeActions$.next(Object.keys(boundActions) as HoppAction[])
|
activeActions$.next(Object.keys(boundActions) as HoppAction[])
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export function defineActionHandler<A extends 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 | HoppActionWithArgs>(
|
|
||||||
action: A,
|
action: A,
|
||||||
handler: ActionFunc<A>,
|
handler: ActionFunc<A>
|
||||||
isActive: Ref<boolean> | undefined = undefined
|
|
||||||
) {
|
) {
|
||||||
let mounted = false
|
|
||||||
let bound = false
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
mounted = true
|
bindAction(action, handler)
|
||||||
|
|
||||||
// Only bind if isActive is undefined or true
|
|
||||||
if (isActive === undefined || isActive.value === true) {
|
|
||||||
bound = true
|
|
||||||
bindAction(action, handler)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
mounted = false
|
|
||||||
bound = false
|
|
||||||
|
|
||||||
unbindAction(action, handler)
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
// ^^^ Enables Type Checking by the TypeScript compiler
|
// ^^^ Enables Type Checking by the TypeScript compiler
|
||||||
|
|
||||||
import { describe, expect, test } from "vitest"
|
|
||||||
import { makeRESTRequest, rawKeyValueEntriesToString } from "@hoppscotch/data"
|
import { makeRESTRequest, rawKeyValueEntriesToString } from "@hoppscotch/data"
|
||||||
import { parseCurlToHoppRESTReq } from ".."
|
import { parseCurlToHoppRESTReq } from ".."
|
||||||
|
|
||||||
@@ -16,7 +15,7 @@ const samples = [
|
|||||||
`,
|
`,
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "https://echo.hoppscotch.io/",
|
endpoint: "https://echo.hoppscotch.io/",
|
||||||
auth: { authType: "none", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
body: {
|
body: {
|
||||||
@@ -56,7 +55,7 @@ const samples = [
|
|||||||
`,
|
`,
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "http://127.0.0.1:8000/api/admin/crm/brand/4",
|
endpoint: "http://127.0.0.1:8000/api/admin/crm/brand/4",
|
||||||
auth: {
|
auth: {
|
||||||
authType: "basic",
|
authType: "basic",
|
||||||
@@ -147,7 +146,7 @@ const samples = [
|
|||||||
command: `curl google.com`,
|
command: `curl google.com`,
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "https://google.com/",
|
endpoint: "https://google.com/",
|
||||||
auth: { authType: "none", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
body: {
|
body: {
|
||||||
@@ -164,7 +163,7 @@ const samples = [
|
|||||||
command: `curl -X POST -d '{"foo":"bar"}' http://localhost:1111/hello/world/?bar=baz&buzz`,
|
command: `curl -X POST -d '{"foo":"bar"}' http://localhost:1111/hello/world/?bar=baz&buzz`,
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "http://localhost:1111/hello/world/?buzz",
|
endpoint: "http://localhost:1111/hello/world/?buzz",
|
||||||
auth: { authType: "none", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
body: {
|
body: {
|
||||||
@@ -187,7 +186,7 @@ const samples = [
|
|||||||
command: `curl --get -d "tool=curl" -d "age=old" https://example.com`,
|
command: `curl --get -d "tool=curl" -d "age=old" https://example.com`,
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "https://example.com/",
|
endpoint: "https://example.com/",
|
||||||
auth: { authType: "none", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
body: {
|
body: {
|
||||||
@@ -215,7 +214,7 @@ const samples = [
|
|||||||
command: `curl -F hello=hello2 -F hello3=@hello4.txt bing.com`,
|
command: `curl -F hello=hello2 -F hello3=@hello4.txt bing.com`,
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "https://bing.com/",
|
endpoint: "https://bing.com/",
|
||||||
auth: { authType: "none", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
body: {
|
body: {
|
||||||
@@ -246,7 +245,7 @@ const samples = [
|
|||||||
"curl -X GET localhost -H 'Accept: application/json' --user root:toor",
|
"curl -X GET localhost -H 'Accept: application/json' --user root:toor",
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "http://localhost/",
|
endpoint: "http://localhost/",
|
||||||
auth: {
|
auth: {
|
||||||
authType: "basic",
|
authType: "basic",
|
||||||
@@ -275,7 +274,7 @@ const samples = [
|
|||||||
"curl -X GET localhost --header 'Authorization: Basic dXNlcjpwYXNz'",
|
"curl -X GET localhost --header 'Authorization: Basic dXNlcjpwYXNz'",
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "http://localhost/",
|
endpoint: "http://localhost/",
|
||||||
auth: {
|
auth: {
|
||||||
authType: "basic",
|
authType: "basic",
|
||||||
@@ -298,7 +297,7 @@ const samples = [
|
|||||||
"curl -X GET localhost:9900 --header 'Authorization: Basic 77898dXNlcjpwYXNz'",
|
"curl -X GET localhost:9900 --header 'Authorization: Basic 77898dXNlcjpwYXNz'",
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "http://localhost:9900/",
|
endpoint: "http://localhost:9900/",
|
||||||
auth: {
|
auth: {
|
||||||
authType: "none",
|
authType: "none",
|
||||||
@@ -319,7 +318,7 @@ const samples = [
|
|||||||
"curl -X GET localhost --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'",
|
"curl -X GET localhost --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'",
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "http://localhost/",
|
endpoint: "http://localhost/",
|
||||||
auth: {
|
auth: {
|
||||||
authType: "bearer",
|
authType: "bearer",
|
||||||
@@ -341,7 +340,7 @@ const samples = [
|
|||||||
command: `curl --get -I -d "tool=curl" -d "platform=hoppscotch" -d"io" https://hoppscotch.io`,
|
command: `curl --get -I -d "tool=curl" -d "platform=hoppscotch" -d"io" https://hoppscotch.io`,
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
method: "HEAD",
|
method: "HEAD",
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "https://hoppscotch.io/?io",
|
endpoint: "https://hoppscotch.io/?io",
|
||||||
auth: {
|
auth: {
|
||||||
authActive: true,
|
authActive: true,
|
||||||
@@ -376,7 +375,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'`,
|
--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({
|
response: makeRESTRequest({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "https://someshadywebsite.com/questionable/path/?so",
|
endpoint: "https://someshadywebsite.com/questionable/path/?so",
|
||||||
auth: {
|
auth: {
|
||||||
authActive: true,
|
authActive: true,
|
||||||
@@ -437,7 +436,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'",
|
"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({
|
response: makeRESTRequest({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "http://localhost/",
|
endpoint: "http://localhost/",
|
||||||
auth: {
|
auth: {
|
||||||
authActive: true,
|
authActive: true,
|
||||||
@@ -471,7 +470,7 @@ const samples = [
|
|||||||
--compressed`,
|
--compressed`,
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "https://hoppscotch.io/",
|
endpoint: "https://hoppscotch.io/",
|
||||||
auth: { authType: "none", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
body: {
|
body: {
|
||||||
@@ -526,7 +525,7 @@ const samples = [
|
|||||||
--data c=d`,
|
--data c=d`,
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "https://echo.hoppscotch.io/",
|
endpoint: "https://echo.hoppscotch.io/",
|
||||||
auth: { authType: "none", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
body: {
|
body: {
|
||||||
@@ -570,7 +569,7 @@ const samples = [
|
|||||||
--form a=b \
|
--form a=b \
|
||||||
--form c=d`,
|
--form c=d`,
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "https://echo.hoppscotch.io/",
|
endpoint: "https://echo.hoppscotch.io/",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
auth: { authType: "none", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
@@ -612,7 +611,7 @@ const samples = [
|
|||||||
{
|
{
|
||||||
command: "curl 'muxueqz.top/skybook.html'",
|
command: "curl 'muxueqz.top/skybook.html'",
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "https://muxueqz.top/skybook.html",
|
endpoint: "https://muxueqz.top/skybook.html",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
auth: { authType: "none", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
@@ -626,7 +625,7 @@ const samples = [
|
|||||||
{
|
{
|
||||||
command: "curl -F abcd=efghi",
|
command: "curl -F abcd=efghi",
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "https://echo.hoppscotch.io/",
|
endpoint: "https://echo.hoppscotch.io/",
|
||||||
method: "POST",
|
method: "POST",
|
||||||
auth: { authType: "none", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
@@ -650,7 +649,7 @@ const samples = [
|
|||||||
{
|
{
|
||||||
command: "curl 127.0.0.1 -X custommethod",
|
command: "curl 127.0.0.1 -X custommethod",
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "http://127.0.0.1/",
|
endpoint: "http://127.0.0.1/",
|
||||||
method: "CUSTOMMETHOD",
|
method: "CUSTOMMETHOD",
|
||||||
auth: { authType: "none", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
@@ -667,7 +666,7 @@ const samples = [
|
|||||||
{
|
{
|
||||||
command: "curl echo.hoppscotch.io -A pinephone",
|
command: "curl echo.hoppscotch.io -A pinephone",
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "https://echo.hoppscotch.io/",
|
endpoint: "https://echo.hoppscotch.io/",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
auth: { authType: "none", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
@@ -690,7 +689,7 @@ const samples = [
|
|||||||
{
|
{
|
||||||
command: "curl echo.hoppscotch.io -G",
|
command: "curl echo.hoppscotch.io -G",
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "https://echo.hoppscotch.io/",
|
endpoint: "https://echo.hoppscotch.io/",
|
||||||
method: "GET",
|
method: "GET",
|
||||||
auth: { authType: "none", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
@@ -707,7 +706,7 @@ const samples = [
|
|||||||
{
|
{
|
||||||
command: `curl --get -I -d "tool=hopp" https://example.org`,
|
command: `curl --get -I -d "tool=hopp" https://example.org`,
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "https://example.org/",
|
endpoint: "https://example.org/",
|
||||||
method: "HEAD",
|
method: "HEAD",
|
||||||
auth: { authType: "none", authActive: true },
|
auth: { authType: "none", authActive: true },
|
||||||
@@ -731,7 +730,7 @@ const samples = [
|
|||||||
command: `curl google.com -u userx`,
|
command: `curl google.com -u userx`,
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "https://google.com/",
|
endpoint: "https://google.com/",
|
||||||
auth: {
|
auth: {
|
||||||
authType: "basic",
|
authType: "basic",
|
||||||
@@ -753,7 +752,7 @@ const samples = [
|
|||||||
command: `curl google.com -H "Authorization"`,
|
command: `curl google.com -H "Authorization"`,
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "https://google.com/",
|
endpoint: "https://google.com/",
|
||||||
auth: {
|
auth: {
|
||||||
authType: "none",
|
authType: "none",
|
||||||
@@ -774,7 +773,7 @@ const samples = [
|
|||||||
google.com -H "content-type: application/json"`,
|
google.com -H "content-type: application/json"`,
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "https://google.com/",
|
endpoint: "https://google.com/",
|
||||||
auth: {
|
auth: {
|
||||||
authType: "none",
|
authType: "none",
|
||||||
@@ -794,7 +793,7 @@ const samples = [
|
|||||||
command: `curl 192.168.0.24:8080/ping`,
|
command: `curl 192.168.0.24:8080/ping`,
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "http://192.168.0.24:8080/ping",
|
endpoint: "http://192.168.0.24:8080/ping",
|
||||||
auth: {
|
auth: {
|
||||||
authType: "none",
|
authType: "none",
|
||||||
@@ -814,7 +813,7 @@ const samples = [
|
|||||||
command: `curl https://example.com -d "alpha=beta&request_id=4"`,
|
command: `curl https://example.com -d "alpha=beta&request_id=4"`,
|
||||||
response: makeRESTRequest({
|
response: makeRESTRequest({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
name: "Untitled",
|
name: "Untitled request",
|
||||||
endpoint: "https://example.com/",
|
endpoint: "https://example.com/",
|
||||||
auth: {
|
auth: {
|
||||||
authType: "none",
|
authType: "none",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { describe, test, expect } from "vitest"
|
|
||||||
import { detectContentType } from "../sub_helpers/contentParser"
|
import { detectContentType } from "../sub_helpers/contentParser"
|
||||||
|
|
||||||
describe("detect content type", () => {
|
describe("detect content type", () => {
|
||||||
@@ -28,49 +27,46 @@ describe("detect content type", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// describe("application/xml", () => {
|
describe("application/xml", () => {
|
||||||
// TODO: Figure this test situation
|
test("should return text/html for XML data without XML declaration", () => {
|
||||||
// test("should return text/html for XML data without XML declaration", () => {
|
expect(
|
||||||
// expect(
|
detectContentType(`
|
||||||
// detectContentType(`
|
<book category="cooking">
|
||||||
// <book category="cooking">
|
<title lang="en">Everyday Italian</title>
|
||||||
// <title lang="en">Everyday Italian</title>
|
<author>Giada De Laurentiis</author>
|
||||||
// <author>Giada De Laurentiis</author>
|
<year>2005</year>
|
||||||
// <year>2005</year>
|
<price>30.00</price>
|
||||||
// <price>30.00</price>
|
</book>
|
||||||
// </book>
|
`)
|
||||||
// `)
|
).toBe("text/html")
|
||||||
// ).toBe("text/html")
|
})
|
||||||
// })
|
|
||||||
|
|
||||||
// TODO: Figure this test situation
|
test("should return application/xml for valid XML data", () => {
|
||||||
// test("should return application/xml for valid XML data", () => {
|
expect(
|
||||||
// expect(
|
detectContentType(`
|
||||||
// detectContentType(`
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
// <?xml version="1.0" encoding="UTF-8"?>
|
<book category="cooking">
|
||||||
// <book category="cooking">
|
<title lang="en">Everyday Italian</title>
|
||||||
// <title lang="en">Everyday Italian</title>
|
<author>Giada De Laurentiis</author>
|
||||||
// <author>Giada De Laurentiis</author>
|
<year>2005</year>
|
||||||
// <year>2005</year>
|
<price>30.00</price>
|
||||||
// <price>30.00</price>
|
</book>
|
||||||
// </book>
|
`)
|
||||||
// `)
|
).toBe("text/html")
|
||||||
// ).toBe("text/html")
|
})
|
||||||
// })
|
|
||||||
|
|
||||||
// TODO: Figure this test situation
|
test("should return text/html for invalid XML data", () => {
|
||||||
// test("should return text/html for invalid XML data", () => {
|
expect(
|
||||||
// expect(
|
detectContentType(`
|
||||||
// detectContentType(`
|
<book category="cooking">
|
||||||
// <book category="cooking">
|
<title lang="en">Everyday Italian
|
||||||
// <title lang="en">Everyday Italian
|
<abcd>Giada De Laurentiis</abcd>
|
||||||
// <abcd>Giada De Laurentiis</abcd>
|
<year>2005</year>
|
||||||
// <year>2005</year>
|
<price>30.00</price>
|
||||||
// <price>30.00</price>
|
`)
|
||||||
// `)
|
).toBe("text/html")
|
||||||
// ).toBe("text/html")
|
})
|
||||||
// })
|
})
|
||||||
// })
|
|
||||||
|
|
||||||
describe("text/html", () => {
|
describe("text/html", () => {
|
||||||
test("should return text/html for valid HTML data", () => {
|
test("should return text/html for valid HTML data", () => {
|
||||||
@@ -90,19 +86,18 @@ describe("detect content type", () => {
|
|||||||
).toBe("text/html")
|
).toBe("text/html")
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO: Figure this test situation
|
test("should return text/html for invalid HTML data", () => {
|
||||||
// test("should return text/html for invalid HTML data", () => {
|
expect(
|
||||||
// expect(
|
detectContentType(`
|
||||||
// detectContentType(`
|
<head>
|
||||||
// <head>
|
<title>Page Title</title>
|
||||||
// <title>Page Title</title>
|
<body>
|
||||||
// <body>
|
<h1>This is a Heading</h1>
|
||||||
// <h1>This is a Heading</h1>
|
</body>
|
||||||
// </body>
|
</html>
|
||||||
// </html>
|
`)
|
||||||
// `)
|
).toBe("text/html")
|
||||||
// ).toBe("text/html")
|
})
|
||||||
// })
|
|
||||||
|
|
||||||
test("should return text/html for unmatched tag", () => {
|
test("should return text/html for unmatched tag", () => {
|
||||||
expect(detectContentType("</html>")).toBe("text/html")
|
expect(detectContentType("</html>")).toBe("text/html")
|
||||||
|
|||||||
@@ -14,7 +14,13 @@ let keybindingsEnabled = true
|
|||||||
* Alt is also regarded as macOS OPTION (⌥) key
|
* Alt is also regarded as macOS OPTION (⌥) key
|
||||||
* Ctrl is also regarded as macOS COMMAND (⌘) key (NOTE: this differs from HTML Keyboard spec where COMMAND is Meta key!)
|
* Ctrl is also regarded as macOS COMMAND (⌘) key (NOTE: this differs from HTML Keyboard spec where COMMAND is Meta key!)
|
||||||
*/
|
*/
|
||||||
type ModifierKeys = "ctrl" | "alt" | "ctrl-shift" | "alt-shift"
|
type ModifierKeys =
|
||||||
|
| "ctrl"
|
||||||
|
| "alt"
|
||||||
|
| "ctrl-shift"
|
||||||
|
| "alt-shift"
|
||||||
|
| "ctrl-alt"
|
||||||
|
| "ctrl-alt-shift"
|
||||||
|
|
||||||
/* eslint-disable prettier/prettier */
|
/* eslint-disable prettier/prettier */
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
@@ -48,8 +54,8 @@ export const bindings: {
|
|||||||
"alt-p": "request.method.post",
|
"alt-p": "request.method.post",
|
||||||
"alt-u": "request.method.put",
|
"alt-u": "request.method.put",
|
||||||
"alt-x": "request.method.delete",
|
"alt-x": "request.method.delete",
|
||||||
"ctrl-k": "modals.search.toggle",
|
"ctrl-k": "flyouts.keybinds.toggle",
|
||||||
"ctrl-/": "flyouts.keybinds.toggle",
|
"/": "modals.search.toggle",
|
||||||
"?": "modals.support.toggle",
|
"?": "modals.support.toggle",
|
||||||
"ctrl-m": "modals.share.toggle",
|
"ctrl-m": "modals.share.toggle",
|
||||||
"alt-r": "navigation.jump.rest",
|
"alt-r": "navigation.jump.rest",
|
||||||
@@ -143,18 +149,19 @@ function getPressedKey(ev: KeyboardEvent): Key | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getActiveModifier(ev: KeyboardEvent): ModifierKeys | null {
|
function getActiveModifier(ev: KeyboardEvent): ModifierKeys | null {
|
||||||
const isShiftKey = ev.shiftKey
|
const modifierKeys = {
|
||||||
|
ctrl: isAppleDevice() ? ev.metaKey : ev.ctrlKey,
|
||||||
|
alt: ev.altKey,
|
||||||
|
shift: ev.shiftKey,
|
||||||
|
}
|
||||||
|
|
||||||
// We only allow one modifier key to be pressed (for now)
|
// active modifier: ctrl | alt | ctrl-alt | ctrl-shift | ctrl-alt-shift | alt-shift
|
||||||
// Control key (+ Command) gets priority and if Alt is also pressed, it is ignored
|
// modiferKeys object's keys are sorted to match the above order
|
||||||
if (isAppleDevice() && ev.metaKey) return isShiftKey ? "ctrl-shift" : "ctrl"
|
const activeModifier = Object.keys(modifierKeys)
|
||||||
else if (!isAppleDevice() && ev.ctrlKey)
|
.filter((key) => modifierKeys[key as keyof typeof modifierKeys])
|
||||||
return isShiftKey ? "ctrl-shift" : "ctrl"
|
.join("-")
|
||||||
|
|
||||||
// Test for Alt key
|
return activeModifier === "" ? null : (activeModifier as ModifierKeys)
|
||||||
if (ev.altKey) return isShiftKey ? "alt-shift" : "alt"
|
|
||||||
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,146 +1,315 @@
|
|||||||
|
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"
|
import { getPlatformAlternateKey, getPlatformSpecialKey } from "./platformutils"
|
||||||
|
|
||||||
export type ShortcutDef = {
|
export default [
|
||||||
label: string
|
{
|
||||||
keys: string[]
|
section: "shortcut.general.title",
|
||||||
section: string
|
shortcuts: [
|
||||||
}
|
{
|
||||||
|
keys: ["?"],
|
||||||
|
label: "shortcut.general.help_menu",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: ["/"],
|
||||||
|
label: "shortcut.general.command_menu",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [getPlatformSpecialKey(), "K"],
|
||||||
|
label: "shortcut.general.show_all",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: ["ESC"],
|
||||||
|
label: "shortcut.general.close_current_menu",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
section: "shortcut.request.title",
|
||||||
|
shortcuts: [
|
||||||
|
{
|
||||||
|
keys: [getPlatformSpecialKey(), "↩"],
|
||||||
|
label: "shortcut.request.send_request",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [getPlatformSpecialKey(), "S"],
|
||||||
|
label: "shortcut.request.save_to_collections",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [getPlatformSpecialKey(), "U"],
|
||||||
|
label: "shortcut.request.copy_request_link",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [getPlatformSpecialKey(), "I"],
|
||||||
|
label: "shortcut.request.reset_request",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [getPlatformAlternateKey(), "↑"],
|
||||||
|
label: "shortcut.request.next_method",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [getPlatformAlternateKey(), "↓"],
|
||||||
|
label: "shortcut.request.previous_method",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [getPlatformAlternateKey(), "G"],
|
||||||
|
label: "shortcut.request.get_method",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [getPlatformAlternateKey(), "H"],
|
||||||
|
label: "shortcut.request.head_method",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [getPlatformAlternateKey(), "P"],
|
||||||
|
label: "shortcut.request.post_method",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [getPlatformAlternateKey(), "U"],
|
||||||
|
label: "shortcut.request.put_method",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [getPlatformAlternateKey(), "X"],
|
||||||
|
label: "shortcut.request.delete_method",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
section: "shortcut.response.title",
|
||||||
|
shortcuts: [
|
||||||
|
{
|
||||||
|
keys: [getPlatformSpecialKey(), "J"],
|
||||||
|
label: "shortcut.response.download",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [getPlatformSpecialKey(), "."],
|
||||||
|
label: "shortcut.response.copy",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
section: "shortcut.navigation.title",
|
||||||
|
shortcuts: [
|
||||||
|
{
|
||||||
|
keys: [getPlatformSpecialKey(), "←"],
|
||||||
|
label: "shortcut.navigation.back",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [getPlatformSpecialKey(), "→"],
|
||||||
|
label: "shortcut.navigation.forward",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [getPlatformAlternateKey(), "R"],
|
||||||
|
label: "shortcut.navigation.rest",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [getPlatformAlternateKey(), "Q"],
|
||||||
|
label: "shortcut.navigation.graphql",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [getPlatformAlternateKey(), "W"],
|
||||||
|
label: "shortcut.navigation.realtime",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [getPlatformAlternateKey(), "S"],
|
||||||
|
label: "shortcut.navigation.settings",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keys: [getPlatformAlternateKey(), "M"],
|
||||||
|
label: "shortcut.navigation.profile",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
section: "shortcut.miscellaneous.title",
|
||||||
|
shortcuts: [
|
||||||
|
{
|
||||||
|
keys: [getPlatformSpecialKey(), "M"],
|
||||||
|
label: "shortcut.miscellaneous.invite",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
export function getShortcuts(t: (x: string) => string): ShortcutDef[] {
|
export const spotlight = [
|
||||||
// General
|
{
|
||||||
return [
|
section: "app.spotlight",
|
||||||
{
|
shortcuts: [
|
||||||
label: t("shortcut.general.help_menu"),
|
{
|
||||||
keys: ["?"],
|
keys: ["?"],
|
||||||
section: t("shortcut.general.title"),
|
label: "shortcut.general.help_menu",
|
||||||
},
|
action: "modals.support.toggle",
|
||||||
{
|
icon: IconLifeBuoy,
|
||||||
label: t("shortcut.general.command_menu"),
|
},
|
||||||
keys: ["/"],
|
{
|
||||||
section: t("shortcut.general.title"),
|
keys: [getPlatformSpecialKey(), "K"],
|
||||||
},
|
label: "shortcut.general.show_all",
|
||||||
{
|
action: "flyouts.keybinds.toggle",
|
||||||
label: t("shortcut.general.show_all"),
|
icon: IconZap,
|
||||||
keys: [getPlatformSpecialKey(), "K"],
|
},
|
||||||
section: t("shortcut.general.title"),
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("shortcut.general.close_current_menu"),
|
section: "shortcut.navigation.title",
|
||||||
keys: ["ESC"],
|
shortcuts: [
|
||||||
section: t("shortcut.general.title"),
|
{
|
||||||
},
|
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: [
|
||||||
|
{
|
||||||
|
keys: [getPlatformSpecialKey(), "M"],
|
||||||
|
label: "shortcut.miscellaneous.invite",
|
||||||
|
action: "modals.share.toggle",
|
||||||
|
icon: IconGift,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
// Request
|
export const fuse = [
|
||||||
{
|
{
|
||||||
label: t("shortcut.request.send_request"),
|
keys: ["?"],
|
||||||
keys: [getPlatformSpecialKey(), "↩"],
|
label: "shortcut.general.help_menu",
|
||||||
section: t("shortcut.request.title"),
|
action: "modals.support.toggle",
|
||||||
},
|
icon: IconLifeBuoy,
|
||||||
{
|
tags: [
|
||||||
keys: [getPlatformSpecialKey(), "S"],
|
"help",
|
||||||
label: t("shortcut.request.save_to_collections"),
|
"support",
|
||||||
section: t("shortcut.request.title"),
|
"menu",
|
||||||
},
|
"discord",
|
||||||
{
|
"twitter",
|
||||||
keys: [getPlatformSpecialKey(), "U"],
|
"documentation",
|
||||||
label: t("shortcut.request.copy_request_link"),
|
"troubleshooting",
|
||||||
section: t("shortcut.request.title"),
|
"chat",
|
||||||
},
|
"community",
|
||||||
{
|
"feedback",
|
||||||
keys: [getPlatformSpecialKey(), "I"],
|
"report",
|
||||||
label: t("shortcut.request.reset_request"),
|
"bug",
|
||||||
section: t("shortcut.request.title"),
|
"issue",
|
||||||
},
|
"ticket",
|
||||||
{
|
],
|
||||||
keys: [getPlatformAlternateKey(), "↑"],
|
},
|
||||||
label: t("shortcut.request.next_method"),
|
{
|
||||||
section: t("shortcut.request.title"),
|
keys: [getPlatformSpecialKey(), "K"],
|
||||||
},
|
label: "shortcut.general.show_all",
|
||||||
{
|
action: "flyouts.keybinds.toggle",
|
||||||
keys: [getPlatformAlternateKey(), "↓"],
|
icon: IconZap,
|
||||||
label: t("shortcut.request.previous_method"),
|
tags: ["keyboard", "shortcuts"],
|
||||||
section: t("shortcut.request.title"),
|
},
|
||||||
},
|
{
|
||||||
{
|
keys: [getPlatformAlternateKey(), "R"],
|
||||||
keys: [getPlatformAlternateKey(), "G"],
|
label: "shortcut.navigation.rest",
|
||||||
label: t("shortcut.request.get_method"),
|
action: "navigation.jump.rest",
|
||||||
section: t("shortcut.request.title"),
|
icon: IconArrowRight,
|
||||||
},
|
tags: ["rest", "jump", "page", "navigation", "go"],
|
||||||
{
|
},
|
||||||
keys: [getPlatformAlternateKey(), "H"],
|
{
|
||||||
label: t("shortcut.request.head_method"),
|
keys: [getPlatformAlternateKey(), "Q"],
|
||||||
section: t("shortcut.request.title"),
|
label: "shortcut.navigation.graphql",
|
||||||
},
|
action: "navigation.jump.graphql",
|
||||||
{
|
icon: IconArrowRight,
|
||||||
keys: [getPlatformAlternateKey(), "P"],
|
tags: ["graphql", "jump", "page", "navigation", "go"],
|
||||||
label: t("shortcut.request.post_method"),
|
},
|
||||||
section: t("shortcut.request.title"),
|
{
|
||||||
},
|
keys: [getPlatformAlternateKey(), "W"],
|
||||||
{
|
label: "shortcut.navigation.realtime",
|
||||||
keys: [getPlatformAlternateKey(), "U"],
|
action: "navigation.jump.realtime",
|
||||||
label: t("shortcut.request.put_method"),
|
icon: IconArrowRight,
|
||||||
section: t("shortcut.request.title"),
|
tags: [
|
||||||
},
|
"realtime",
|
||||||
{
|
"jump",
|
||||||
keys: [getPlatformAlternateKey(), "X"],
|
"page",
|
||||||
label: t("shortcut.request.delete_method"),
|
"navigation",
|
||||||
section: t("shortcut.request.title"),
|
"websocket",
|
||||||
},
|
"socket",
|
||||||
|
"mqtt",
|
||||||
// Response
|
"sse",
|
||||||
{
|
"go",
|
||||||
keys: [getPlatformSpecialKey(), "J"],
|
],
|
||||||
label: t("shortcut.response.download"),
|
},
|
||||||
section: t("shortcut.response.title"),
|
{
|
||||||
},
|
keys: [getPlatformAlternateKey(), "S"],
|
||||||
{
|
label: "shortcut.navigation.settings",
|
||||||
keys: [getPlatformSpecialKey(), "."],
|
action: "navigation.jump.settings",
|
||||||
label: t("shortcut.response.copy"),
|
icon: IconArrowRight,
|
||||||
section: t("shortcut.response.title"),
|
tags: ["settings", "jump", "page", "navigation", "account", "theme", "go"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
// Navigation
|
keys: [getPlatformAlternateKey(), "M"],
|
||||||
{
|
label: "shortcut.navigation.profile",
|
||||||
keys: [getPlatformSpecialKey(), "←"],
|
action: "navigation.jump.profile",
|
||||||
label: t("shortcut.navigation.back"),
|
icon: IconArrowRight,
|
||||||
section: t("shortcut.navigation.title"),
|
tags: ["profile", "jump", "page", "navigation", "account", "theme", "go"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
keys: [getPlatformSpecialKey(), "→"],
|
keys: [getPlatformSpecialKey(), "M"],
|
||||||
label: t("shortcut.navigation.forward"),
|
label: "shortcut.miscellaneous.invite",
|
||||||
section: t("shortcut.navigation.title"),
|
action: "modals.share.toggle",
|
||||||
},
|
icon: IconGift,
|
||||||
{
|
tags: ["invite", "share", "app", "friends", "people", "social"],
|
||||||
keys: [getPlatformAlternateKey(), "R"],
|
},
|
||||||
label: t("shortcut.navigation.rest"),
|
{
|
||||||
section: t("shortcut.navigation.title"),
|
keys: [getPlatformAlternateKey(), "0"],
|
||||||
},
|
label: "shortcut.theme.system",
|
||||||
{
|
action: "settings.theme.system",
|
||||||
keys: [getPlatformAlternateKey(), "Q"],
|
icon: IconMonitor,
|
||||||
label: t("shortcut.navigation.graphql"),
|
tags: ["theme", "system"],
|
||||||
section: t("shortcut.navigation.title"),
|
},
|
||||||
},
|
{
|
||||||
{
|
keys: [getPlatformAlternateKey(), "1"],
|
||||||
keys: [getPlatformAlternateKey(), "W"],
|
label: "shortcut.theme.light",
|
||||||
label: t("shortcut.navigation.realtime"),
|
action: "settings.theme.light",
|
||||||
section: t("shortcut.navigation.title"),
|
icon: IconSun,
|
||||||
},
|
tags: ["theme", "light"],
|
||||||
{
|
},
|
||||||
keys: [getPlatformAlternateKey(), "S"],
|
{
|
||||||
label: t("shortcut.navigation.settings"),
|
keys: [getPlatformAlternateKey(), "2"],
|
||||||
section: t("shortcut.navigation.title"),
|
label: "shortcut.theme.dark",
|
||||||
},
|
action: "settings.theme.dark",
|
||||||
{
|
icon: IconCloud,
|
||||||
keys: [getPlatformAlternateKey(), "M"],
|
tags: ["theme", "dark"],
|
||||||
label: t("shortcut.navigation.profile"),
|
},
|
||||||
section: t("shortcut.navigation.title"),
|
{
|
||||||
},
|
keys: [getPlatformAlternateKey(), "3"],
|
||||||
|
label: "shortcut.theme.black",
|
||||||
// Miscellaneous
|
action: "settings.theme.black",
|
||||||
{
|
icon: IconMoon,
|
||||||
keys: [getPlatformSpecialKey(), "M"],
|
tags: ["theme", "black"],
|
||||||
label: t("shortcut.miscellaneous.invite"),
|
},
|
||||||
section: t("shortcut.miscellaneous.title"),
|
]
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { vi, describe, expect, test } from "vitest"
|
|
||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import axiosStrategy from "../AxiosStrategy"
|
import axiosStrategy from "../AxiosStrategy"
|
||||||
|
|
||||||
vi.mock("axios")
|
jest.mock("axios")
|
||||||
vi.mock("~/newstore/settings", () => {
|
jest.mock("~/newstore/settings", () => {
|
||||||
return {
|
return {
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
settingsStore: {
|
settingsStore: {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { describe, test, expect, vi } from "vitest"
|
|
||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import axiosStrategy from "../AxiosStrategy"
|
import axiosStrategy from "../AxiosStrategy"
|
||||||
|
|
||||||
vi.mock("../../utils/b64", () => ({
|
jest.mock("../../utils/b64", () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
decodeB64StringToArrayBuffer: vi.fn((data) => `${data}-converted`),
|
decodeB64StringToArrayBuffer: jest.fn((data) => `${data}-converted`),
|
||||||
}))
|
}))
|
||||||
vi.mock("~/newstore/settings", () => {
|
jest.mock("~/newstore/settings", () => {
|
||||||
return {
|
return {
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
settingsStore: {
|
settingsStore: {
|
||||||
@@ -23,7 +22,7 @@ describe("axiosStrategy", () => {
|
|||||||
test("sends POST request to proxy if proxy is enabled", async () => {
|
test("sends POST request to proxy if proxy is enabled", async () => {
|
||||||
let passedURL
|
let passedURL
|
||||||
|
|
||||||
vi.spyOn(axios, "post").mockImplementation((url) => {
|
jest.spyOn(axios, "post").mockImplementation((url) => {
|
||||||
passedURL = url
|
passedURL = url
|
||||||
return Promise.resolve({ data: { success: true, isBinary: false } })
|
return Promise.resolve({ data: { success: true, isBinary: false } })
|
||||||
})
|
})
|
||||||
@@ -42,7 +41,7 @@ describe("axiosStrategy", () => {
|
|||||||
|
|
||||||
let passedFields
|
let passedFields
|
||||||
|
|
||||||
vi.spyOn(axios, "post").mockImplementation((_url, req) => {
|
jest.spyOn(axios, "post").mockImplementation((_url, req) => {
|
||||||
passedFields = req
|
passedFields = req
|
||||||
return Promise.resolve({ data: { success: true, isBinary: false } })
|
return Promise.resolve({ data: { success: true, isBinary: false } })
|
||||||
})
|
})
|
||||||
@@ -55,7 +54,7 @@ describe("axiosStrategy", () => {
|
|||||||
test("passes wantsBinary field", async () => {
|
test("passes wantsBinary field", async () => {
|
||||||
let passedFields
|
let passedFields
|
||||||
|
|
||||||
vi.spyOn(axios, "post").mockImplementation((_url, req) => {
|
jest.spyOn(axios, "post").mockImplementation((_url, req) => {
|
||||||
passedFields = req
|
passedFields = req
|
||||||
return Promise.resolve({ data: { success: true, isBinary: false } })
|
return Promise.resolve({ data: { success: true, isBinary: false } })
|
||||||
})
|
})
|
||||||
@@ -66,7 +65,7 @@ describe("axiosStrategy", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("checks for proxy response success field and throws error message for non-success", async () => {
|
test("checks for proxy response success field and throws error message for non-success", async () => {
|
||||||
vi.spyOn(axios, "post").mockResolvedValue({
|
jest.spyOn(axios, "post").mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
success: false,
|
success: false,
|
||||||
data: {
|
data: {
|
||||||
@@ -79,7 +78,7 @@ describe("axiosStrategy", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("checks for proxy response success field and throws error 'Proxy Error' for non-success", async () => {
|
test("checks for proxy response success field and throws error 'Proxy Error' for non-success", async () => {
|
||||||
vi.spyOn(axios, "post").mockResolvedValue({
|
jest.spyOn(axios, "post").mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
success: false,
|
success: false,
|
||||||
data: {},
|
data: {},
|
||||||
@@ -90,7 +89,7 @@ describe("axiosStrategy", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("checks for proxy response success and doesn't left for success", async () => {
|
test("checks for proxy response success and doesn't left for success", async () => {
|
||||||
vi.spyOn(axios, "post").mockResolvedValue({
|
jest.spyOn(axios, "post").mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
success: true,
|
success: true,
|
||||||
data: {},
|
data: {},
|
||||||
@@ -101,7 +100,7 @@ describe("axiosStrategy", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("checks isBinary response field and right with the converted value if so", async () => {
|
test("checks isBinary response field and right with the converted value if so", async () => {
|
||||||
vi.spyOn(axios, "post").mockResolvedValue({
|
jest.spyOn(axios, "post").mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
success: true,
|
success: true,
|
||||||
isBinary: true,
|
isBinary: true,
|
||||||
@@ -115,7 +114,7 @@ describe("axiosStrategy", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("checks isBinary response field and right with the actual value if not so", async () => {
|
test("checks isBinary response field and right with the actual value if not so", async () => {
|
||||||
vi.spyOn(axios, "post").mockResolvedValue({
|
jest.spyOn(axios, "post").mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
success: true,
|
success: true,
|
||||||
isBinary: false,
|
isBinary: false,
|
||||||
@@ -129,15 +128,15 @@ describe("axiosStrategy", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("cancel errors are returned a left with the string 'cancellation'", async () => {
|
test("cancel errors are returned a left with the string 'cancellation'", async () => {
|
||||||
vi.spyOn(axios, "post").mockRejectedValue("errr")
|
jest.spyOn(axios, "post").mockRejectedValue("errr")
|
||||||
vi.spyOn(axios, "isCancel").mockReturnValueOnce(true)
|
jest.spyOn(axios, "isCancel").mockReturnValueOnce(true)
|
||||||
|
|
||||||
expect(await axiosStrategy({})()).toEqualLeft("cancellation")
|
expect(await axiosStrategy({})()).toEqualLeft("cancellation")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("non-cancellation errors return a left", async () => {
|
test("non-cancellation errors return a left", async () => {
|
||||||
vi.spyOn(axios, "post").mockRejectedValue("errr")
|
jest.spyOn(axios, "post").mockRejectedValue("errr")
|
||||||
vi.spyOn(axios, "isCancel").mockReturnValueOnce(false)
|
jest.spyOn(axios, "isCancel").mockReturnValueOnce(false)
|
||||||
|
|
||||||
expect(await axiosStrategy({})()).toEqualLeft("errr")
|
expect(await axiosStrategy({})()).toEqualLeft("errr")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { vi, describe, expect, test, beforeEach } from "vitest"
|
|
||||||
import extensionStrategy, {
|
import extensionStrategy, {
|
||||||
hasExtensionInstalled,
|
hasExtensionInstalled,
|
||||||
hasChromeExtensionInstalled,
|
hasChromeExtensionInstalled,
|
||||||
@@ -6,12 +5,12 @@ import extensionStrategy, {
|
|||||||
cancelRunningExtensionRequest,
|
cancelRunningExtensionRequest,
|
||||||
} from "../ExtensionStrategy"
|
} from "../ExtensionStrategy"
|
||||||
|
|
||||||
vi.mock("../../utils/b64", () => ({
|
jest.mock("../../utils/b64", () => ({
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
decodeB64StringToArrayBuffer: vi.fn((data) => `${data}-converted`),
|
decodeB64StringToArrayBuffer: jest.fn((data) => `${data}-converted`),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock("~/newstore/settings", () => {
|
jest.mock("~/newstore/settings", () => {
|
||||||
return {
|
return {
|
||||||
__esModule: true,
|
__esModule: true,
|
||||||
settingsStore: {
|
settingsStore: {
|
||||||
@@ -40,32 +39,32 @@ describe("hasExtensionInstalled", () => {
|
|||||||
describe("hasChromeExtensionInstalled", () => {
|
describe("hasChromeExtensionInstalled", () => {
|
||||||
test("returns true if extension is hooked and browser is chrome", () => {
|
test("returns true if extension is hooked and browser is chrome", () => {
|
||||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
|
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
|
||||||
vi.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome")
|
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome")
|
||||||
vi.spyOn(navigator, "vendor", "get").mockReturnValue("Google")
|
jest.spyOn(navigator, "vendor", "get").mockReturnValue("Google")
|
||||||
|
|
||||||
expect(hasChromeExtensionInstalled()).toEqual(true)
|
expect(hasChromeExtensionInstalled()).toEqual(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns false if extension is hooked and browser is not chrome", () => {
|
test("returns false if extension is hooked and browser is not chrome", () => {
|
||||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
|
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
|
||||||
vi.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox")
|
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox")
|
||||||
vi.spyOn(navigator, "vendor", "get").mockReturnValue("Google")
|
jest.spyOn(navigator, "vendor", "get").mockReturnValue("Google")
|
||||||
|
|
||||||
expect(hasChromeExtensionInstalled()).toEqual(false)
|
expect(hasChromeExtensionInstalled()).toEqual(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns false if extension not installed and browser is chrome", () => {
|
test("returns false if extension not installed and browser is chrome", () => {
|
||||||
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
|
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
|
||||||
vi.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome")
|
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome")
|
||||||
vi.spyOn(navigator, "vendor", "get").mockReturnValue("Google")
|
jest.spyOn(navigator, "vendor", "get").mockReturnValue("Google")
|
||||||
|
|
||||||
expect(hasChromeExtensionInstalled()).toEqual(false)
|
expect(hasChromeExtensionInstalled()).toEqual(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns false if extension not installed and browser is not chrome", () => {
|
test("returns false if extension not installed and browser is not chrome", () => {
|
||||||
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
|
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
|
||||||
vi.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox")
|
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox")
|
||||||
vi.spyOn(navigator, "vendor", "get").mockReturnValue("Google")
|
jest.spyOn(navigator, "vendor", "get").mockReturnValue("Google")
|
||||||
|
|
||||||
expect(hasChromeExtensionInstalled()).toEqual(false)
|
expect(hasChromeExtensionInstalled()).toEqual(false)
|
||||||
})
|
})
|
||||||
@@ -74,35 +73,35 @@ describe("hasChromeExtensionInstalled", () => {
|
|||||||
describe("hasFirefoxExtensionInstalled", () => {
|
describe("hasFirefoxExtensionInstalled", () => {
|
||||||
test("returns true if extension is hooked and browser is firefox", () => {
|
test("returns true if extension is hooked and browser is firefox", () => {
|
||||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
|
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
|
||||||
vi.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox")
|
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox")
|
||||||
|
|
||||||
expect(hasFirefoxExtensionInstalled()).toEqual(true)
|
expect(hasFirefoxExtensionInstalled()).toEqual(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns false if extension is hooked and browser is not firefox", () => {
|
test("returns false if extension is hooked and browser is not firefox", () => {
|
||||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
|
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
|
||||||
vi.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome")
|
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome")
|
||||||
|
|
||||||
expect(hasFirefoxExtensionInstalled()).toEqual(false)
|
expect(hasFirefoxExtensionInstalled()).toEqual(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns false if extension not installed and browser is firefox", () => {
|
test("returns false if extension not installed and browser is firefox", () => {
|
||||||
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
|
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
|
||||||
vi.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox")
|
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox")
|
||||||
|
|
||||||
expect(hasFirefoxExtensionInstalled()).toEqual(false)
|
expect(hasFirefoxExtensionInstalled()).toEqual(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns false if extension not installed and browser is not firefox", () => {
|
test("returns false if extension not installed and browser is not firefox", () => {
|
||||||
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
|
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
|
||||||
vi.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome")
|
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome")
|
||||||
|
|
||||||
expect(hasFirefoxExtensionInstalled()).toEqual(false)
|
expect(hasFirefoxExtensionInstalled()).toEqual(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("cancelRunningExtensionRequest", () => {
|
describe("cancelRunningExtensionRequest", () => {
|
||||||
const cancelFunc = vi.fn()
|
const cancelFunc = jest.fn()
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cancelFunc.mockClear()
|
cancelFunc.mockClear()
|
||||||
@@ -110,7 +109,7 @@ describe("cancelRunningExtensionRequest", () => {
|
|||||||
|
|
||||||
test("cancels request if extension installed and function present in hook", () => {
|
test("cancels request if extension installed and function present in hook", () => {
|
||||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {
|
global.__POSTWOMAN_EXTENSION_HOOK__ = {
|
||||||
cancelRequest: cancelFunc,
|
cancelRunningRequest: cancelFunc,
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelRunningExtensionRequest()
|
cancelRunningExtensionRequest()
|
||||||
@@ -126,7 +125,7 @@ describe("cancelRunningExtensionRequest", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("extensionStrategy", () => {
|
describe("extensionStrategy", () => {
|
||||||
const sendReqFunc = vi.fn()
|
const sendReqFunc = jest.fn()
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sendReqFunc.mockClear()
|
sendReqFunc.mockClear()
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { describe, expect, test } from "vitest"
|
import { TextDecoder } from "util"
|
||||||
import { decodeB64StringToArrayBuffer } from "../b64"
|
import { decodeB64StringToArrayBuffer } from "../b64"
|
||||||
|
|
||||||
describe("decodeB64StringToArrayBuffer", () => {
|
describe("decodeB64StringToArrayBuffer", () => {
|
||||||
test("decodes content correctly", () => {
|
test("decodes content correctly", () => {
|
||||||
|
const decoder = new TextDecoder("utf-8")
|
||||||
expect(
|
expect(
|
||||||
decodeB64StringToArrayBuffer("aG9wcHNjb3RjaCBpcyBhd2Vzb21lIQ==")
|
decoder.decode(
|
||||||
).toEqual(Buffer.from("hoppscotch is awesome!", "utf-8").buffer)
|
decodeB64StringToArrayBuffer("aG9wcHNjb3RjaCBpcyBhd2Vzb21lIQ==")
|
||||||
|
)
|
||||||
|
).toMatch("hoppscotch is awesome!")
|
||||||
})
|
})
|
||||||
|
|
||||||
// TODO : More tests for binary data ?
|
// TODO : More tests for binary data ?
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { describe, expect, test } from "vitest"
|
|
||||||
import { isJSONContentType } from "../contenttypes"
|
import { isJSONContentType } from "../contenttypes"
|
||||||
|
|
||||||
describe("isJSONContentType", () => {
|
describe("isJSONContentType", () => {
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { vi, describe, test, expect } from "vitest"
|
|
||||||
import debounce from "../debounce"
|
import debounce from "../debounce"
|
||||||
|
|
||||||
describe("debounce", () => {
|
describe("debounce", () => {
|
||||||
test("doesn't call function right after calling", () => {
|
test("doesn't call function right after calling", () => {
|
||||||
const fn = vi.fn()
|
const fn = jest.fn()
|
||||||
|
|
||||||
const debFunc = debounce(fn, 100)
|
const debFunc = debounce(fn, 100)
|
||||||
debFunc()
|
debFunc()
|
||||||
@@ -12,27 +11,27 @@ describe("debounce", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("calls the function after the given timeout", () => {
|
test("calls the function after the given timeout", () => {
|
||||||
const fn = vi.fn()
|
const fn = jest.fn()
|
||||||
|
|
||||||
vi.useFakeTimers()
|
jest.useFakeTimers()
|
||||||
|
|
||||||
const debFunc = debounce(fn, 100)
|
const debFunc = debounce(fn, 100)
|
||||||
debFunc()
|
debFunc()
|
||||||
|
|
||||||
vi.runAllTimers()
|
jest.runAllTimers()
|
||||||
|
|
||||||
expect(fn).toHaveBeenCalled()
|
expect(fn).toHaveBeenCalled()
|
||||||
// expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 100)
|
// expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 100)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("calls the function only one time within the timeframe", () => {
|
test("calls the function only one time within the timeframe", () => {
|
||||||
const fn = vi.fn()
|
const fn = jest.fn()
|
||||||
|
|
||||||
const debFunc = debounce(fn, 1000)
|
const debFunc = debounce(fn, 1000)
|
||||||
|
|
||||||
for (let i = 0; i < 100; i++) debFunc()
|
for (let i = 0; i < 100; i++) debFunc()
|
||||||
|
|
||||||
vi.runAllTimers()
|
jest.runAllTimers()
|
||||||
|
|
||||||
expect(fn).toHaveBeenCalledTimes(1)
|
expect(fn).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { describe, expect, test } from "vitest"
|
|
||||||
import { parseUrlAndPath } from "../uri"
|
import { parseUrlAndPath } from "../uri"
|
||||||
|
|
||||||
describe("parseUrlAndPath", () => {
|
describe("parseUrlAndPath", () => {
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { describe, test, expect } from "vitest"
|
|
||||||
import { wsValid, httpValid, socketioValid } from "../valid"
|
import { wsValid, httpValid, socketioValid } from "../valid"
|
||||||
|
|
||||||
describe("wsValid", () => {
|
describe("wsValid", () => {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
</Pane>
|
</Pane>
|
||||||
</Splitpanes>
|
</Splitpanes>
|
||||||
<AppActionHandler />
|
<AppActionHandler />
|
||||||
<AppSpotlight :show="showSearch" @hide-modal="showSearch = false" />
|
<AppPowerSearch :show="showSearch" @hide-modal="showSearch = false" />
|
||||||
<AppSupport
|
<AppSupport
|
||||||
v-if="mdAndLarger"
|
v-if="mdAndLarger"
|
||||||
:show="showSupport"
|
:show="showSupport"
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
import { HoppModule } from "."
|
|
||||||
import { Container, Service } from "dioc"
|
|
||||||
import { diocPlugin } from "dioc/vue"
|
|
||||||
import { DebugService } from "~/services/debug.service"
|
|
||||||
|
|
||||||
const serviceContainer = new Container()
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
serviceContainer.bind(DebugService)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a service from the app service container. You can use this function
|
|
||||||
* to get a service if you have no access to the container or if you are not
|
|
||||||
* in a component (if you are, you can use `useService`) or if you are not in a
|
|
||||||
* service.
|
|
||||||
* @param service The class of the service to get
|
|
||||||
* @returns The service instance
|
|
||||||
*
|
|
||||||
* @deprecated This is a temporary escape hatch for legacy code to access
|
|
||||||
* services. Please use `useService` if within components or try to convert your
|
|
||||||
* legacy subsystem into a service if possible.
|
|
||||||
*/
|
|
||||||
export function getService<T extends typeof Service<any> & { ID: string }>(
|
|
||||||
service: T
|
|
||||||
): InstanceType<T> {
|
|
||||||
return serviceContainer.bind(service)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default <HoppModule>{
|
|
||||||
onVueAppInit(app) {
|
|
||||||
// TODO: look into this
|
|
||||||
// @ts-expect-error Something weird with Vue versions
|
|
||||||
app.use(diocPlugin, {
|
|
||||||
container: serviceContainer,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -115,14 +115,6 @@ export const changeAppLanguage = async (locale: string) => {
|
|||||||
setLocalConfig("locale", locale)
|
setLocalConfig("locale", locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the i18n instance
|
|
||||||
*/
|
|
||||||
export function getI18n() {
|
|
||||||
// @ts-expect-error Something weird with the i18n errors
|
|
||||||
return i18nInstance!.global.t
|
|
||||||
}
|
|
||||||
|
|
||||||
export default <HoppModule>{
|
export default <HoppModule>{
|
||||||
onVueAppInit(app) {
|
onVueAppInit(app) {
|
||||||
const i18n = createI18n(<I18nOptions>{
|
const i18n = createI18n(<I18nOptions>{
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { vi, describe, expect, test } from "vitest"
|
|
||||||
import { BehaviorSubject, Subject } from "rxjs"
|
import { BehaviorSubject, Subject } from "rxjs"
|
||||||
import { isEqual } from "lodash-es"
|
import { isEqual } from "lodash-es"
|
||||||
import DispatchingStore from "~/newstore/DispatchingStore"
|
import DispatchingStore from "~/newstore/DispatchingStore"
|
||||||
@@ -53,8 +52,8 @@ describe("DispatchingStore", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("only correct dispatcher method is ran", () => {
|
test("only correct dispatcher method is ran", () => {
|
||||||
const dispatchFn = vi.fn().mockReturnValue({})
|
const dispatchFn = jest.fn().mockReturnValue({})
|
||||||
const dontCallDispatchFn = vi.fn().mockReturnValue({})
|
const dontCallDispatchFn = jest.fn().mockReturnValue({})
|
||||||
|
|
||||||
const store = new DispatchingStore(
|
const store = new DispatchingStore(
|
||||||
{},
|
{},
|
||||||
@@ -77,7 +76,7 @@ describe("DispatchingStore", () => {
|
|||||||
const testInitValue = { name: "bob" }
|
const testInitValue = { name: "bob" }
|
||||||
const testPayload = { name: "alice" }
|
const testPayload = { name: "alice" }
|
||||||
|
|
||||||
const testDispatchFn = vi.fn().mockReturnValue({})
|
const testDispatchFn = jest.fn().mockReturnValue({})
|
||||||
|
|
||||||
const store = new DispatchingStore(testInitValue, {
|
const store = new DispatchingStore(testInitValue, {
|
||||||
testDispatcher: testDispatchFn,
|
testDispatcher: testDispatchFn,
|
||||||
@@ -95,7 +94,7 @@ describe("DispatchingStore", () => {
|
|||||||
const testInitValue = { name: "bob" }
|
const testInitValue = { name: "bob" }
|
||||||
const testDispatchReturnVal = { name: "alice" }
|
const testDispatchReturnVal = { name: "alice" }
|
||||||
|
|
||||||
const testDispatchFn = vi.fn().mockReturnValue(testDispatchReturnVal)
|
const testDispatchFn = jest.fn().mockReturnValue(testDispatchReturnVal)
|
||||||
|
|
||||||
const store = new DispatchingStore(testInitValue, {
|
const store = new DispatchingStore(testInitValue, {
|
||||||
testDispatcher: testDispatchFn,
|
testDispatcher: testDispatchFn,
|
||||||
@@ -113,7 +112,7 @@ describe("DispatchingStore", () => {
|
|||||||
const testInitValue = { name: "bob" }
|
const testInitValue = { name: "bob" }
|
||||||
const testDispatchReturnVal = { age: 25 }
|
const testDispatchReturnVal = { age: 25 }
|
||||||
|
|
||||||
const testDispatchFn = vi.fn().mockReturnValue(testDispatchReturnVal)
|
const testDispatchFn = jest.fn().mockReturnValue(testDispatchReturnVal)
|
||||||
|
|
||||||
const store = new DispatchingStore(testInitValue, {
|
const store = new DispatchingStore(testInitValue, {
|
||||||
testDispatcher: testDispatchFn,
|
testDispatcher: testDispatchFn,
|
||||||
@@ -130,51 +129,49 @@ describe("DispatchingStore", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test("emits the current store value to the new subscribers", () =>
|
test("emits the current store value to the new subscribers", (done) => {
|
||||||
new Promise((resolve) => {
|
const testInitValue = { name: "bob" }
|
||||||
const testInitValue = { name: "bob" }
|
|
||||||
|
|
||||||
const testDispatchFn = vi.fn().mockReturnValue({})
|
const testDispatchFn = jest.fn().mockReturnValue({})
|
||||||
|
|
||||||
const store = new DispatchingStore(testInitValue, {
|
const store = new DispatchingStore(testInitValue, {
|
||||||
testDispatcher: testDispatchFn,
|
testDispatcher: testDispatchFn,
|
||||||
})
|
})
|
||||||
|
|
||||||
store.subject$.subscribe((value) => {
|
store.subject$.subscribe((value) => {
|
||||||
if (value === testInitValue) {
|
if (value === testInitValue) {
|
||||||
resolve()
|
done()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}))
|
})
|
||||||
|
|
||||||
test("emits the dispatched store value to the subscribers", () =>
|
test("emits the dispatched store value to the subscribers", (done) => {
|
||||||
new Promise((resolve) => {
|
const testInitValue = { name: "bob" }
|
||||||
const testInitValue = { name: "bob" }
|
const testDispatchReturnVal = { age: 25 }
|
||||||
const testDispatchReturnVal = { age: 25 }
|
|
||||||
|
|
||||||
const testDispatchFn = vi.fn().mockReturnValue(testDispatchReturnVal)
|
const testDispatchFn = jest.fn().mockReturnValue(testDispatchReturnVal)
|
||||||
|
|
||||||
const store = new DispatchingStore(testInitValue, {
|
const store = new DispatchingStore(testInitValue, {
|
||||||
testDispatcher: testDispatchFn,
|
testDispatcher: testDispatchFn,
|
||||||
})
|
})
|
||||||
|
|
||||||
store.subject$.subscribe((value) => {
|
store.subject$.subscribe((value) => {
|
||||||
if (isEqual(value, { name: "bob", age: 25 })) {
|
if (isEqual(value, { name: "bob", age: 25 })) {
|
||||||
resolve()
|
done()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
dispatcher: "testDispatcher",
|
dispatcher: "testDispatcher",
|
||||||
payload: {},
|
payload: {},
|
||||||
})
|
})
|
||||||
}))
|
})
|
||||||
|
|
||||||
test("dispatching emits the new dispatch requests to the subscribers", () => {
|
test("dispatching emits the new dispatch requests to the subscribers", () => {
|
||||||
const testInitValue = { name: "bob" }
|
const testInitValue = { name: "bob" }
|
||||||
const testPayload = { age: 25 }
|
const testPayload = { age: 25 }
|
||||||
|
|
||||||
const testDispatchFn = vi.fn().mockReturnValue({})
|
const testDispatchFn = jest.fn().mockReturnValue({})
|
||||||
|
|
||||||
const store = new DispatchingStore(testInitValue, {
|
const store = new DispatchingStore(testInitValue, {
|
||||||
testDispatcher: testDispatchFn,
|
testDispatcher: testDispatchFn,
|
||||||
|
|||||||
@@ -17,10 +17,7 @@
|
|||||||
import { usePageHead } from "@composables/head"
|
import { usePageHead } from "@composables/head"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { GQLConnection } from "@helpers/GQLConnection"
|
import { GQLConnection } from "@helpers/GQLConnection"
|
||||||
import { cloneDeep } from "lodash-es"
|
|
||||||
import { computed, onBeforeUnmount } from "vue"
|
import { computed, onBeforeUnmount } from "vue"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
|
||||||
import { getGQLSession, setGQLSession } from "~/newstore/GQLSession"
|
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -35,14 +32,4 @@ onBeforeUnmount(() => {
|
|||||||
gqlConn.disconnect()
|
gqlConn.disconnect()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
defineActionHandler("gql.request.open", ({ request }) => {
|
|
||||||
const session = getGQLSession()
|
|
||||||
|
|
||||||
setGQLSession({
|
|
||||||
request: cloneDeep(request),
|
|
||||||
schema: session.schema,
|
|
||||||
response: session.response,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ import {
|
|||||||
updateTabOrdering,
|
updateTabOrdering,
|
||||||
} from "~/helpers/rest/tab"
|
} from "~/helpers/rest/tab"
|
||||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||||
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
import { invokeAction } from "~/helpers/actions"
|
||||||
import { onLoggedIn } from "~/composables/auth"
|
import { onLoggedIn } from "~/composables/auth"
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import {
|
import {
|
||||||
@@ -368,8 +368,4 @@ function oAuthURL() {
|
|||||||
setupTabStateSync()
|
setupTabStateSync()
|
||||||
bindRequestToURLParams()
|
bindRequestToURLParams()
|
||||||
oAuthURL()
|
oAuthURL()
|
||||||
|
|
||||||
defineActionHandler("rest.request.open", ({ doc }) => {
|
|
||||||
createNewTab(doc)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,18 +8,25 @@
|
|||||||
>
|
>
|
||||||
<HoppSmartSpinner class="mb-4" />
|
<HoppSmartSpinner class="mb-4" />
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-else-if="currentUser === null"
|
v-else-if="currentUser === null"
|
||||||
:src="`/images/states/${colorMode.value}/login.svg`"
|
class="flex flex-col items-center justify-center"
|
||||||
:alt="`${t('empty.profile')}`"
|
|
||||||
:text="`${t('empty.profile')}`"
|
|
||||||
>
|
>
|
||||||
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/login.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-24 h-24 my-4"
|
||||||
|
:alt="`${t('empty.parameters')}`"
|
||||||
|
/>
|
||||||
|
<p class="pb-4 text-center text-secondaryLight">
|
||||||
|
{{ t("empty.profile") }}
|
||||||
|
</p>
|
||||||
<HoppButtonPrimary
|
<HoppButtonPrimary
|
||||||
:label="t('auth.login')"
|
:label="t('auth.login')"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
@click="invokeAction('modals.login.toggle')"
|
@click="invokeAction('modals.login.toggle')"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
<div v-else class="space-y-8">
|
<div v-else class="space-y-8">
|
||||||
<div
|
<div
|
||||||
class="h-24 rounded bg-primaryLight -mb-11 md:h-32"
|
class="h-24 rounded bg-primaryLight -mb-11 md:h-32"
|
||||||
|
|||||||
@@ -136,19 +136,28 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
|
||||||
|
<div
|
||||||
v-if="topics.length === 0"
|
v-if="topics.length === 0"
|
||||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.subscription')}`"
|
|
||||||
:text="`${t('empty.subscription')}`"
|
|
||||||
>
|
>
|
||||||
|
<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.subscription')}`"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">
|
||||||
|
{{ t("empty.subscription") }}
|
||||||
|
</span>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="t('mqtt.new')"
|
:label="t('mqtt.new')"
|
||||||
filled
|
filled
|
||||||
outline
|
outline
|
||||||
@click="showSubscriptionModal(true)"
|
@click="showSubscriptionModal(true)"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div
|
<div
|
||||||
v-for="(topic, index) in topics"
|
v-for="(topic, index) in topics"
|
||||||
|
|||||||
@@ -190,12 +190,19 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="authType === 'None'"
|
v-if="authType === 'None'"
|
||||||
:src="`/images/states/${colorMode.value}/login.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('socketio.connection_not_authorized')}`"
|
|
||||||
:text="`${t('socketio.connection_not_authorized')}`"
|
|
||||||
>
|
>
|
||||||
|
<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("socketio.connection_not_authorized") }}
|
||||||
|
</span>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
outline
|
outline
|
||||||
:label="t('app.documentation')"
|
:label="t('app.documentation')"
|
||||||
@@ -205,7 +212,7 @@
|
|||||||
reverse
|
reverse
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="authType === 'Bearer'"
|
v-if="authType === 'Bearer'"
|
||||||
class="flex flex-1 border-b border-dividerLight"
|
class="flex flex-1 border-b border-dividerLight"
|
||||||
|
|||||||
@@ -159,13 +159,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</draggable>
|
</draggable>
|
||||||
<HoppSmartPlaceholder
|
<div
|
||||||
v-if="protocols.length === 0"
|
v-if="protocols.length === 0"
|
||||||
:src="`/images/states/${colorMode.value}/add_category.svg`"
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
:alt="`${t('empty.protocols')}`"
|
|
||||||
:text="`${t('empty.protocols')}`"
|
|
||||||
>
|
>
|
||||||
</HoppSmartPlaceholder>
|
<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.protocols')}`"
|
||||||
|
/>
|
||||||
|
<span class="mb-4 text-center">
|
||||||
|
{{ t("empty.protocols") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
</HoppSmartTabs>
|
</HoppSmartTabs>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
import { Service } from "dioc"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This service provice debug utilities for the application and is
|
|
||||||
* supposed to be used only in development.
|
|
||||||
*
|
|
||||||
* This service logs events from the container and also events
|
|
||||||
* from all the services that are bound to the container.
|
|
||||||
*
|
|
||||||
* This service injects couple of utilities into the global scope:
|
|
||||||
* - `_getService(id: string): Service | undefined` - Returns the service instance with the given ID or undefined.
|
|
||||||
* - `_getBoundServiceIDs(): string[]` - Returns the IDs of all the bound services.
|
|
||||||
*/
|
|
||||||
export class DebugService extends Service {
|
|
||||||
public static readonly ID = "DEBUG_SERVICE"
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super()
|
|
||||||
|
|
||||||
console.log("DebugService is initialized...")
|
|
||||||
|
|
||||||
const container = this.getContainer()
|
|
||||||
|
|
||||||
// Log container events
|
|
||||||
container.getEventStream().subscribe((event) => {
|
|
||||||
if (event.type === "SERVICE_BIND") {
|
|
||||||
console.log(
|
|
||||||
"[CONTAINER] Service Bind:",
|
|
||||||
event.bounderID ?? "<CONTAINER>",
|
|
||||||
"->",
|
|
||||||
event.boundeeID
|
|
||||||
)
|
|
||||||
} else if (event.type === "SERVICE_INIT") {
|
|
||||||
console.log("[CONTAINER] Service Init:", event.serviceID)
|
|
||||||
|
|
||||||
// Subscribe to event stream of the newly initialized service
|
|
||||||
const service = container.getBoundServiceWithID(event.serviceID)
|
|
||||||
|
|
||||||
service?.getEventStream().subscribe((ev: any) => {
|
|
||||||
console.log(`[${event.serviceID}] Event:`, ev)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Subscribe to event stream of all already bound services (if any)
|
|
||||||
for (const [id, service] of container.getBoundServices()) {
|
|
||||||
service.getEventStream().subscribe((event: any) => {
|
|
||||||
console.log(`[${id}]`, event)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inject debug utilities into the global scope
|
|
||||||
;(window as any)._getService = this.getService.bind(this)
|
|
||||||
;(window as any)._getBoundServiceIDs = this.getBoundServiceIDs.bind(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private getBoundServiceIDs() {
|
|
||||||
return Array.from(this.getContainer().getBoundServices()).map(([id]) => id)
|
|
||||||
}
|
|
||||||
|
|
||||||
private getService(id: string) {
|
|
||||||
return this.getContainer().getBoundServiceWithID(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user