Compare commits
101 Commits
HPS-OSS-1
...
fix/genera
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32f577850b | ||
|
|
31fd6567b7 | ||
|
|
8300f9a0a2 | ||
|
|
525ba77739 | ||
|
|
6bc748a267 | ||
|
|
5230d2d3b8 | ||
|
|
c3531c9d8b | ||
|
|
b29c04c28d | ||
|
|
b2af353941 | ||
|
|
9dbce74f5e | ||
|
|
db1cf5cc08 | ||
|
|
09360abf81 | ||
|
|
355bd62b8d | ||
|
|
5650de1183 | ||
|
|
2ee8614b93 | ||
|
|
5632334c9a | ||
|
|
780dd8a713 | ||
|
|
7db3c6d290 | ||
|
|
c765270dfe | ||
|
|
03f667c21d | ||
|
|
f79f3078dc | ||
|
|
6e29a2f6d4 | ||
|
|
6304fd50c3 | ||
|
|
2ec29c47ad | ||
|
|
399a238bf4 | ||
|
|
b20ab72298 | ||
|
|
f723e6496a | ||
|
|
8c0aff8863 | ||
|
|
64c5077506 | ||
|
|
2afc87847d | ||
|
|
878ec833ce | ||
|
|
039de8015f | ||
|
|
f67b366b90 | ||
|
|
6f35574d68 | ||
|
|
77e8a36ab0 | ||
|
|
d7cc9f5dbc | ||
|
|
fc3e3aeaec | ||
|
|
4ba135f3b9 | ||
|
|
24894e05dc | ||
|
|
e2b668bee2 | ||
|
|
f112c46bb4 | ||
|
|
331d482b22 | ||
|
|
b07243f131 | ||
|
|
84b0c30d64 | ||
|
|
e3dd9e99a1 | ||
|
|
e3091cb6db | ||
|
|
270f796683 | ||
|
|
24c6bce02d | ||
|
|
2db567589f | ||
|
|
1fe83ebdc8 | ||
|
|
8320d4f222 | ||
|
|
e76c1bc64c | ||
|
|
1f3f8464ea | ||
|
|
81a7e23a12 | ||
|
|
e75391cdf1 | ||
|
|
a213c0c26c | ||
|
|
15424903ed | ||
|
|
1cce117b0a | ||
|
|
abc7b4b6f3 | ||
|
|
05e32ef9e4 | ||
|
|
f0a1fc319c | ||
|
|
385cabc6aa | ||
|
|
397b26a9f3 | ||
|
|
9a40058329 | ||
|
|
7ec2380ed5 | ||
|
|
3d4825305d | ||
|
|
26e564288b | ||
|
|
385a587cfd | ||
|
|
215df02783 | ||
|
|
7c7ed68b20 | ||
|
|
c910a0314a | ||
|
|
ddaec1b9ac | ||
|
|
9dbdef9286 | ||
|
|
e77eef1532 | ||
|
|
1fe0b8861d | ||
|
|
aeb9172144 | ||
|
|
1b413e2f47 | ||
|
|
d6c8400116 | ||
|
|
4a0205e622 | ||
|
|
c2520006ac | ||
|
|
99817fd8bd | ||
|
|
3f35fedd9d | ||
|
|
b7c2d13992 | ||
|
|
a6426587fb | ||
|
|
5f68356278 | ||
|
|
08f61e7408 | ||
|
|
9beda15f00 | ||
|
|
09d1663f81 | ||
|
|
f43b6e7cff | ||
|
|
6581eb4fd1 | ||
|
|
caedfe5c1e | ||
|
|
f6a234aaf9 | ||
|
|
8450fb6596 | ||
|
|
41fa3b5a8c | ||
|
|
522de45a62 | ||
|
|
4acc4b2dda | ||
|
|
c1f4855daf | ||
|
|
3506e96cfd | ||
|
|
b42a94ed77 | ||
|
|
80da790a3c | ||
|
|
d6c706d0f9 |
@@ -31,6 +31,7 @@ MICROSOFT_CLIENT_ID="************************************************"
|
||||
MICROSOFT_CLIENT_SECRET="************************************************"
|
||||
MICROSOFT_CALLBACK_URL="http://localhost:3170/v1/auth/microsoft/callback"
|
||||
MICROSOFT_SCOPE="user.read"
|
||||
MICROSOFT_TENANT="common"
|
||||
|
||||
# Mailer config
|
||||
MAILER_SMTP_URL="smtps://user@domain.com:pass@smtp.domain.com"
|
||||
@@ -51,7 +52,7 @@ VITE_ADMIN_URL=http://localhost:3100
|
||||
|
||||
# Backend URLs
|
||||
VITE_BACKEND_GQL_URL=http://localhost:3170/graphql
|
||||
VITE_BACKEND_WS_URL=wss://localhost:3170/graphql
|
||||
VITE_BACKEND_WS_URL=ws://localhost:3170/graphql
|
||||
VITE_BACKEND_API_URL=http://localhost:3170/v1
|
||||
|
||||
# Terms Of Service And Privacy Policy Links (Optional)
|
||||
|
||||
42
.github/workflows/ui.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Deploy to Netlify (ui)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
# run this workflow only if an update is made to the ui package
|
||||
paths:
|
||||
- "packages/hoppscotch-ui/**"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup environment
|
||||
run: mv .env.example .env
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v2.2.4
|
||||
with:
|
||||
version: 8
|
||||
run_install: true
|
||||
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: pnpm
|
||||
|
||||
- name: Build site
|
||||
run: pnpm run generate-ui
|
||||
|
||||
# Deploy the ui site with netlify-cli
|
||||
- name: Deploy to Netlify (ui)
|
||||
run: npx netlify-cli deploy --dir=packages/hoppscotch-ui/.histoire/dist --prod
|
||||
env:
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_UI_SITE_ID }}
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
@@ -11,7 +11,7 @@
|
||||
"dev": "pnpm -r do-dev",
|
||||
"gen-gql": "cross-env GQL_SCHEMA_EMIT_LOCATION='../../../gql-gen/backend-schema.gql' pnpm -r generate-gql-sdl",
|
||||
"generate": "pnpm -r do-build-prod",
|
||||
"start": "http-server packages/hoppscotch-web/dist -p 3000",
|
||||
"start": "http-server packages/hoppscotch-selfhost-web/dist -p 3000",
|
||||
"lint": "pnpm -r do-lint",
|
||||
"typecheck": "pnpm -r do-typecheck",
|
||||
"lintfix": "pnpm -r do-lintfix",
|
||||
|
||||
24
packages/dioc/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
141
packages/dioc/README.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# dioc
|
||||
|
||||
A small and lightweight dependency injection / inversion of control system.
|
||||
|
||||
### About
|
||||
|
||||
`dioc` is a really simple **DI/IOC** system where you write services (which are singletons per container) that can depend on each other and emit events that can be listened upon.
|
||||
|
||||
### Demo
|
||||
|
||||
```ts
|
||||
import { Service, Container } from "dioc"
|
||||
|
||||
// Here is a simple service, which you can define by extending the Service class
|
||||
// and providing an ID static field (of type string)
|
||||
export class PersistenceService extends Service {
|
||||
// This should be unique for each container
|
||||
public static ID = "PERSISTENCE_SERVICE"
|
||||
|
||||
public read(key: string): string | undefined {
|
||||
// ...
|
||||
}
|
||||
|
||||
public write(key: string, value: string) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
type TodoServiceEvent =
|
||||
| { type: "TODO_CREATED"; index: number }
|
||||
| { type: "TODO_DELETED"; index: number }
|
||||
|
||||
// Services have a built in event system
|
||||
// Define the generic argument to say what are the possible emitted values
|
||||
export class TodoService extends Service<TodoServiceEvent> {
|
||||
public static ID = "TODO_SERVICE"
|
||||
|
||||
// Inject persistence service into this service
|
||||
private readonly persistence = this.bind(PersistenceService)
|
||||
|
||||
public todos = []
|
||||
|
||||
// Service constructors cannot have arguments
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.todos = JSON.parse(this.persistence.read("todos") ?? "[]")
|
||||
}
|
||||
|
||||
public addTodo(text: string) {
|
||||
// ...
|
||||
|
||||
// You can access services via the bound fields
|
||||
this.persistence.write("todos", JSON.stringify(this.todos))
|
||||
|
||||
// This is how you emit an event
|
||||
this.emit({
|
||||
type: "TODO_CREATED",
|
||||
index,
|
||||
})
|
||||
}
|
||||
|
||||
public removeTodo(index: number) {
|
||||
// ...
|
||||
|
||||
this.emit({
|
||||
type: "TODO_DELETED",
|
||||
index,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Services need a container to run in
|
||||
const container = new Container()
|
||||
|
||||
// You can initialize and get services using Container#bind
|
||||
// It will automatically initialize the service (and its dependencies)
|
||||
const todoService = container.bind(TodoService) // Returns an instance of TodoService
|
||||
```
|
||||
|
||||
### Demo (Unit Test)
|
||||
|
||||
`dioc/testing` contains `TestContainer` which lets you bind mocked services to the container.
|
||||
|
||||
```ts
|
||||
import { TestContainer } from "dioc/testing"
|
||||
import { TodoService, PersistenceService } from "./demo.ts" // The above demo code snippet
|
||||
import { describe, it, expect, vi } from "vitest"
|
||||
|
||||
describe("TodoService", () => {
|
||||
it("addTodo writes to persistence", () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
const writeFn = vi.fn()
|
||||
|
||||
// The first parameter is the service to mock and the second parameter
|
||||
// is the mocked service fields and functions
|
||||
container.bindMock(PersistenceService, {
|
||||
read: () => undefined, // Not really important for this test
|
||||
write: writeFn,
|
||||
})
|
||||
|
||||
// the peristence service bind in TodoService will now use the
|
||||
// above defined mocked implementation
|
||||
const todoService = container.bind(TodoService)
|
||||
|
||||
todoService.addTodo("sup")
|
||||
|
||||
expect(writeFn).toHaveBeenCalledOnce()
|
||||
expect(writeFn).toHaveBeenCalledWith("todos", JSON.stringify(["sup"]))
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Demo (Vue)
|
||||
|
||||
`dioc/vue` contains a Vue Plugin and a `useService` composable that allows Vue components to use the defined services.
|
||||
|
||||
In the app entry point:
|
||||
|
||||
```ts
|
||||
import { createApp } from "vue"
|
||||
import { diocPlugin } from "dioc/vue"
|
||||
|
||||
const app = createApp()
|
||||
|
||||
app.use(diocPlugin, {
|
||||
container: new Container(), // You can pass in the container you want to provide to the components here
|
||||
})
|
||||
```
|
||||
|
||||
In your Vue components:
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { TodoService } from "./demo.ts" // The above demo
|
||||
import { useService } from "dioc/vue"
|
||||
|
||||
const todoService = useService(TodoService) // Returns an instance of the TodoService class
|
||||
</script>
|
||||
```
|
||||
2
packages/dioc/index.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from "./dist/main.d.ts"
|
||||
export * from "./dist/main.d.ts"
|
||||
147
packages/dioc/lib/container.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { Service } from "./service"
|
||||
import { Observable, Subject } from 'rxjs'
|
||||
|
||||
/**
|
||||
* Stores the current container instance in the current operating context.
|
||||
*
|
||||
* NOTE: This should not be used outside of dioc library code
|
||||
*/
|
||||
export let currentContainer: Container | null = null
|
||||
|
||||
/**
|
||||
* The events emitted by the container
|
||||
*
|
||||
* `SERVICE_BIND` - emitted when a service is bound to the container directly or as a dependency to another service
|
||||
* `SERVICE_INIT` - emitted when a service is initialized
|
||||
*/
|
||||
export type ContainerEvent =
|
||||
| {
|
||||
type: 'SERVICE_BIND';
|
||||
|
||||
/** The Service ID of the service being bounded (the dependency) */
|
||||
boundeeID: string;
|
||||
|
||||
/**
|
||||
* The Service ID of the bounder that is binding the boundee (the dependent)
|
||||
*
|
||||
* NOTE: This will be undefined if the service is bound directly to the container
|
||||
*/
|
||||
bounderID: string | undefined
|
||||
}
|
||||
| {
|
||||
type: 'SERVICE_INIT';
|
||||
|
||||
/** The Service ID of the service being initialized */
|
||||
serviceID: string
|
||||
}
|
||||
|
||||
/**
|
||||
* The dependency injection container, allows for services to be initialized and maintains the dependency trees.
|
||||
*/
|
||||
export class Container {
|
||||
/** Used during the `bind` operation to detect circular dependencies */
|
||||
private bindStack: string[] = []
|
||||
|
||||
/** The map of bound services to their IDs */
|
||||
protected boundMap = new Map<string, Service<unknown>>()
|
||||
|
||||
/** The RxJS observable representing the event stream */
|
||||
protected event$ = new Subject<ContainerEvent>()
|
||||
|
||||
/**
|
||||
* Returns whether a container has the given service bound
|
||||
* @param service The service to check for
|
||||
*/
|
||||
public hasBound<
|
||||
T extends typeof Service<any> & { ID: string }
|
||||
>(service: T): boolean {
|
||||
return this.boundMap.has(service.ID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the service bound to the container with the given ID or if not found, undefined.
|
||||
*
|
||||
* NOTE: This is an advanced method and should not be used as much as possible.
|
||||
*
|
||||
* @param serviceID The ID of the service to get
|
||||
*/
|
||||
public getBoundServiceWithID(serviceID: string): Service<unknown> | undefined {
|
||||
return this.boundMap.get(serviceID)
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds a service to the container. This is equivalent to marking a service as a dependency.
|
||||
* @param service The class reference of a service to bind
|
||||
* @param bounder The class reference of the service that is binding the service (if bound directly to the container, this should be undefined)
|
||||
*/
|
||||
public bind<T extends typeof Service<any> & { ID: string }>(
|
||||
service: T,
|
||||
bounder: ((typeof Service<T>) & { ID: string }) | undefined = undefined
|
||||
): InstanceType<T> {
|
||||
// We need to store the current container in a variable so that we can restore it after the bind operation
|
||||
const oldCurrentContainer = currentContainer;
|
||||
currentContainer = this;
|
||||
|
||||
// If the service is already bound, return the existing instance
|
||||
if (this.hasBound(service)) {
|
||||
this.event$.next({
|
||||
type: 'SERVICE_BIND',
|
||||
boundeeID: service.ID,
|
||||
bounderID: bounder?.ID // Return the bounder ID if it is defined, else assume its the container
|
||||
})
|
||||
|
||||
return this.boundMap.get(service.ID) as InstanceType<T> // Casted as InstanceType<T> because service IDs and types are expected to match
|
||||
}
|
||||
|
||||
// Detect circular dependency and throw error
|
||||
if (this.bindStack.findIndex((serviceID) => serviceID === service.ID) !== -1) {
|
||||
const circularServices = `${this.bindStack.join(' -> ')} -> ${service.ID}`
|
||||
|
||||
throw new Error(`Circular dependency detected.\nChain: ${circularServices}`)
|
||||
}
|
||||
|
||||
// Push the service ID onto the bind stack to detect circular dependencies
|
||||
this.bindStack.push(service.ID)
|
||||
|
||||
// Initialize the service and emit events
|
||||
|
||||
// NOTE: We need to cast the service to any as TypeScript thinks that the service is abstract
|
||||
const instance: Service<any> = new (service as any)()
|
||||
|
||||
this.boundMap.set(service.ID, instance)
|
||||
|
||||
this.bindStack.pop()
|
||||
|
||||
this.event$.next({
|
||||
type: 'SERVICE_INIT',
|
||||
serviceID: service.ID,
|
||||
})
|
||||
|
||||
this.event$.next({
|
||||
type: 'SERVICE_BIND',
|
||||
boundeeID: service.ID,
|
||||
bounderID: bounder?.ID
|
||||
})
|
||||
|
||||
|
||||
// Restore the current container
|
||||
currentContainer = oldCurrentContainer;
|
||||
|
||||
// We expect the return type to match the service definition
|
||||
return instance as InstanceType<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an iterator of the currently bound service IDs and their instances
|
||||
*/
|
||||
public getBoundServices(): IterableIterator<[string, Service<any>]> {
|
||||
return this.boundMap.entries()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the public container event stream
|
||||
*/
|
||||
public getEventStream(): Observable<ContainerEvent> {
|
||||
return this.event$.asObservable()
|
||||
}
|
||||
}
|
||||
2
packages/dioc/lib/main.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./container"
|
||||
export * from "./service"
|
||||
65
packages/dioc/lib/service.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Observable, Subject } from 'rxjs'
|
||||
import { Container, currentContainer } from './container'
|
||||
|
||||
/**
|
||||
* A Dioc service that can bound to a container and can bind dependency services.
|
||||
*
|
||||
* NOTE: Services cannot have a constructor that takes arguments.
|
||||
*
|
||||
* @template EventDef The type of events that can be emitted by the service. These will be accessible by event streams
|
||||
*/
|
||||
export abstract class Service<EventDef = {}> {
|
||||
|
||||
/**
|
||||
* The internal event stream of the service
|
||||
*/
|
||||
private event$ = new Subject<EventDef>()
|
||||
|
||||
/** The container the service is bound to */
|
||||
#container: Container
|
||||
|
||||
constructor() {
|
||||
if (!currentContainer) {
|
||||
throw new Error(
|
||||
`Tried to initialize service with no container (ID: ${ (this.constructor as any).ID })`
|
||||
)
|
||||
}
|
||||
|
||||
this.#container = currentContainer
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds a dependency service into this service.
|
||||
* @param service The class reference of the service to bind
|
||||
*/
|
||||
protected bind<T extends typeof Service<any> & { ID: string }>(service: T): InstanceType<T> {
|
||||
if (!currentContainer) {
|
||||
throw new Error('No currentContainer defined.')
|
||||
}
|
||||
|
||||
return currentContainer.bind(service, this.constructor as typeof Service<any> & { ID: string })
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the container the service is bound to
|
||||
*/
|
||||
protected getContainer(): Container {
|
||||
return this.#container
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits an event on the service's event stream
|
||||
* @param event The event to emit
|
||||
*/
|
||||
protected emit(event: EventDef) {
|
||||
this.event$.next(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the event stream of the service
|
||||
*/
|
||||
public getEventStream(): Observable<EventDef> {
|
||||
|
||||
return this.event$.asObservable()
|
||||
}
|
||||
}
|
||||
33
packages/dioc/lib/testing.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Container, Service } from "./main";
|
||||
|
||||
/**
|
||||
* A container that can be used for writing tests, contains additional methods
|
||||
* for binding suitable for writing tests. (see `bindMock`).
|
||||
*/
|
||||
export class TestContainer extends Container {
|
||||
|
||||
/**
|
||||
* Binds a mock service to the container.
|
||||
*
|
||||
* @param service
|
||||
* @param mock
|
||||
*/
|
||||
public bindMock<
|
||||
T extends typeof Service<any> & { ID: string },
|
||||
U extends Partial<InstanceType<T>>
|
||||
>(service: T, mock: U): U {
|
||||
if (this.boundMap.has(service.ID)) {
|
||||
throw new Error(`Service '${service.ID}' already bound to container. Did you already call bindMock on this ?`)
|
||||
}
|
||||
|
||||
this.boundMap.set(service.ID, mock as any)
|
||||
|
||||
this.event$.next({
|
||||
type: "SERVICE_BIND",
|
||||
boundeeID: service.ID,
|
||||
bounderID: undefined,
|
||||
})
|
||||
|
||||
return mock
|
||||
}
|
||||
}
|
||||
34
packages/dioc/lib/vue.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Plugin, inject } from "vue"
|
||||
import { Container } from "./container"
|
||||
import { Service } from "./service"
|
||||
|
||||
const VUE_CONTAINER_KEY = Symbol()
|
||||
|
||||
// TODO: Some Vue version issue with plugin generics is breaking type checking
|
||||
/**
|
||||
* The Vue Dioc Plugin, this allows the composables to work and access the container
|
||||
*
|
||||
* NOTE: Make sure you add `vue` as dependency to be able to use this plugin (duh)
|
||||
*/
|
||||
export const diocPlugin: Plugin = {
|
||||
install(app, { container }) {
|
||||
app.provide(VUE_CONTAINER_KEY, container)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A composable that binds a service to a Vue Component
|
||||
*
|
||||
* @param service The class reference of the service to bind
|
||||
*/
|
||||
export function useService<
|
||||
T extends typeof Service<any> & { ID: string }
|
||||
>(service: T): InstanceType<T> {
|
||||
const container = inject(VUE_CONTAINER_KEY) as Container | undefined | null
|
||||
|
||||
if (!container) {
|
||||
throw new Error("Container not found, did you forget to install the dioc plugin?")
|
||||
}
|
||||
|
||||
return container.bind(service)
|
||||
}
|
||||
54
packages/dioc/package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "dioc",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist",
|
||||
"index.d.ts"
|
||||
],
|
||||
"main": "./dist/counter.umd.cjs",
|
||||
"module": "./dist/counter.js",
|
||||
"types": "./index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/main.d.ts",
|
||||
"require": "./dist/index.cjs",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./vue": {
|
||||
"types": "./dist/vue.d.ts",
|
||||
"require": "./dist/vue.cjs",
|
||||
"import": "./dist/vue.js"
|
||||
},
|
||||
"./testing": {
|
||||
"types": "./dist/testing.d.ts",
|
||||
"require": "./dist/testing.cjs",
|
||||
"import": "./dist/testing.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build && tsc --emitDeclarationOnly",
|
||||
"prepare": "pnpm run build",
|
||||
"test": "vitest run",
|
||||
"do-test": "pnpm run test",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^4.0.4",
|
||||
"vitest": "^0.29.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.2.25"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"vue": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
262
packages/dioc/test/container.spec.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
import { it, expect, describe, vi } from "vitest"
|
||||
import { Service } from "../lib/service"
|
||||
import { Container, currentContainer, ContainerEvent } from "../lib/container"
|
||||
|
||||
class TestServiceA extends Service {
|
||||
public static ID = "TestServiceA"
|
||||
}
|
||||
|
||||
class TestServiceB extends Service {
|
||||
public static ID = "TestServiceB"
|
||||
|
||||
// Marked public to allow for testing
|
||||
public readonly serviceA = this.bind(TestServiceA)
|
||||
}
|
||||
|
||||
describe("Container", () => {
|
||||
describe("getBoundServiceWithID", () => {
|
||||
it("returns the service instance if it is bound to the container", () => {
|
||||
const container = new Container()
|
||||
|
||||
const service = container.bind(TestServiceA)
|
||||
|
||||
expect(container.getBoundServiceWithID(TestServiceA.ID)).toBe(service)
|
||||
})
|
||||
|
||||
it("returns undefined if the service is not bound to the container", () => {
|
||||
const container = new Container()
|
||||
|
||||
expect(container.getBoundServiceWithID(TestServiceA.ID)).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("bind", () => {
|
||||
it("correctly binds the service to it", () => {
|
||||
const container = new Container()
|
||||
|
||||
const service = container.bind(TestServiceA)
|
||||
|
||||
// @ts-expect-error getContainer is defined as a protected property, but we are leveraging it here to check
|
||||
expect(service.getContainer()).toBe(container)
|
||||
})
|
||||
|
||||
it("after bind, the current container is set back to its previous value", () => {
|
||||
const originalValue = currentContainer
|
||||
|
||||
const container = new Container()
|
||||
container.bind(TestServiceA)
|
||||
|
||||
expect(currentContainer).toBe(originalValue)
|
||||
})
|
||||
|
||||
it("dependent services are registered in the same container", () => {
|
||||
const container = new Container()
|
||||
|
||||
const serviceB = container.bind(TestServiceB)
|
||||
|
||||
// @ts-expect-error getContainer is defined as a protected property, but we are leveraging it here to check
|
||||
expect(serviceB.serviceA.getContainer()).toBe(container)
|
||||
})
|
||||
|
||||
it("binding an already initialized service returns the initialized instance (services are singletons)", () => {
|
||||
const container = new Container()
|
||||
|
||||
const serviceA = container.bind(TestServiceA)
|
||||
const serviceA2 = container.bind(TestServiceA)
|
||||
|
||||
expect(serviceA).toBe(serviceA2)
|
||||
})
|
||||
|
||||
it("binding a service which is a dependency of another service returns the same instance created from the dependency resolution (services are singletons)", () => {
|
||||
const container = new Container()
|
||||
|
||||
const serviceB = container.bind(TestServiceB)
|
||||
const serviceA = container.bind(TestServiceA)
|
||||
|
||||
expect(serviceB.serviceA).toBe(serviceA)
|
||||
})
|
||||
|
||||
it("binding an initialized service as a dependency returns the same instance", () => {
|
||||
const container = new Container()
|
||||
|
||||
const serviceA = container.bind(TestServiceA)
|
||||
const serviceB = container.bind(TestServiceB)
|
||||
|
||||
expect(serviceB.serviceA).toBe(serviceA)
|
||||
})
|
||||
|
||||
it("container emits an init event when an uninitialized service is initialized via bind and event only called once", () => {
|
||||
const container = new Container()
|
||||
|
||||
const serviceFunc = vi.fn<
|
||||
[ContainerEvent & { type: "SERVICE_INIT" }],
|
||||
void
|
||||
>()
|
||||
|
||||
container.getEventStream().subscribe((ev) => {
|
||||
if (ev.type === "SERVICE_INIT") {
|
||||
serviceFunc(ev)
|
||||
}
|
||||
})
|
||||
|
||||
const instance = container.bind(TestServiceA)
|
||||
|
||||
expect(serviceFunc).toHaveBeenCalledOnce()
|
||||
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
|
||||
type: "SERVICE_INIT",
|
||||
serviceID: TestServiceA.ID,
|
||||
})
|
||||
})
|
||||
|
||||
it("the bind event emitted has an undefined bounderID when the service is bound directly to the container", () => {
|
||||
const container = new Container()
|
||||
|
||||
const serviceFunc = vi.fn<
|
||||
[ContainerEvent & { type: "SERVICE_BIND" }],
|
||||
void
|
||||
>()
|
||||
|
||||
container.getEventStream().subscribe((ev) => {
|
||||
if (ev.type === "SERVICE_BIND") {
|
||||
serviceFunc(ev)
|
||||
}
|
||||
})
|
||||
|
||||
container.bind(TestServiceA)
|
||||
|
||||
expect(serviceFunc).toHaveBeenCalledOnce()
|
||||
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
|
||||
type: "SERVICE_BIND",
|
||||
boundeeID: TestServiceA.ID,
|
||||
bounderID: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it("the bind event emitted has the correct bounderID when the service is bound to another service", () => {
|
||||
const container = new Container()
|
||||
|
||||
const serviceFunc = vi.fn<
|
||||
[ContainerEvent & { type: "SERVICE_BIND" }],
|
||||
void
|
||||
>()
|
||||
|
||||
container.getEventStream().subscribe((ev) => {
|
||||
// We only care about the bind event of TestServiceA
|
||||
if (ev.type === "SERVICE_BIND" && ev.boundeeID === TestServiceA.ID) {
|
||||
serviceFunc(ev)
|
||||
}
|
||||
})
|
||||
|
||||
container.bind(TestServiceB)
|
||||
|
||||
expect(serviceFunc).toHaveBeenCalledOnce()
|
||||
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
|
||||
type: "SERVICE_BIND",
|
||||
boundeeID: TestServiceA.ID,
|
||||
bounderID: TestServiceB.ID,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("hasBound", () => {
|
||||
it("returns true if the given service is bound to the container", () => {
|
||||
const container = new Container()
|
||||
|
||||
container.bind(TestServiceA)
|
||||
|
||||
expect(container.hasBound(TestServiceA)).toEqual(true)
|
||||
})
|
||||
|
||||
it("returns false if the given service is not bound to the container", () => {
|
||||
const container = new Container()
|
||||
|
||||
expect(container.hasBound(TestServiceA)).toEqual(false)
|
||||
})
|
||||
|
||||
it("returns true when the service is bound because it is a dependency of another service", () => {
|
||||
const container = new Container()
|
||||
|
||||
container.bind(TestServiceB)
|
||||
|
||||
expect(container.hasBound(TestServiceA)).toEqual(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getEventStream", () => {
|
||||
it("returns an observable which emits events correctly when services are initialized", () => {
|
||||
const container = new Container()
|
||||
|
||||
const serviceFunc = vi.fn<
|
||||
[ContainerEvent & { type: "SERVICE_INIT" }],
|
||||
void
|
||||
>()
|
||||
|
||||
container.getEventStream().subscribe((ev) => {
|
||||
if (ev.type === "SERVICE_INIT") {
|
||||
serviceFunc(ev)
|
||||
}
|
||||
})
|
||||
|
||||
container.bind(TestServiceB)
|
||||
|
||||
expect(serviceFunc).toHaveBeenCalledTimes(2)
|
||||
expect(serviceFunc).toHaveBeenNthCalledWith(1, <ContainerEvent>{
|
||||
type: "SERVICE_INIT",
|
||||
serviceID: TestServiceA.ID,
|
||||
})
|
||||
expect(serviceFunc).toHaveBeenNthCalledWith(2, <ContainerEvent>{
|
||||
type: "SERVICE_INIT",
|
||||
serviceID: TestServiceB.ID,
|
||||
})
|
||||
})
|
||||
|
||||
it("returns an observable which emits events correctly when services are bound", () => {
|
||||
const container = new Container()
|
||||
|
||||
const serviceFunc = vi.fn<
|
||||
[ContainerEvent & { type: "SERVICE_BIND" }],
|
||||
void
|
||||
>()
|
||||
|
||||
container.getEventStream().subscribe((ev) => {
|
||||
if (ev.type === "SERVICE_BIND") {
|
||||
serviceFunc(ev)
|
||||
}
|
||||
})
|
||||
|
||||
container.bind(TestServiceB)
|
||||
|
||||
expect(serviceFunc).toHaveBeenCalledTimes(2)
|
||||
expect(serviceFunc).toHaveBeenNthCalledWith(1, <ContainerEvent>{
|
||||
type: "SERVICE_BIND",
|
||||
boundeeID: TestServiceA.ID,
|
||||
bounderID: TestServiceB.ID,
|
||||
})
|
||||
expect(serviceFunc).toHaveBeenNthCalledWith(2, <ContainerEvent>{
|
||||
type: "SERVICE_BIND",
|
||||
boundeeID: TestServiceB.ID,
|
||||
bounderID: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("getBoundServices", () => {
|
||||
it("returns an iterator over all services bound to the container in the format [service id, service instance]", () => {
|
||||
const container = new Container()
|
||||
|
||||
const instanceB = container.bind(TestServiceB)
|
||||
const instanceA = instanceB.serviceA
|
||||
|
||||
expect(Array.from(container.getBoundServices())).toEqual([
|
||||
[TestServiceA.ID, instanceA],
|
||||
[TestServiceB.ID, instanceB],
|
||||
])
|
||||
})
|
||||
|
||||
it("returns an empty iterator if no services are bound", () => {
|
||||
const container = new Container()
|
||||
|
||||
expect(Array.from(container.getBoundServices())).toEqual([])
|
||||
})
|
||||
})
|
||||
})
|
||||
66
packages/dioc/test/service.spec.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { Service, Container } from "../lib/main"
|
||||
|
||||
class TestServiceA extends Service {
|
||||
public static ID = "TestServiceA"
|
||||
}
|
||||
|
||||
class TestServiceB extends Service<"test"> {
|
||||
public static ID = "TestServiceB"
|
||||
|
||||
// Marked public to allow for testing
|
||||
public readonly serviceA = this.bind(TestServiceA)
|
||||
|
||||
public emitTestEvent() {
|
||||
this.emit("test")
|
||||
}
|
||||
}
|
||||
|
||||
describe("Service", () => {
|
||||
describe("constructor", () => {
|
||||
it("throws an error if the service is initialized without a container", () => {
|
||||
expect(() => new TestServiceA()).toThrowError(
|
||||
"Tried to initialize service with no container (ID: TestServiceA)"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("bind", () => {
|
||||
it("correctly binds the dependency service using the container", () => {
|
||||
const container = new Container()
|
||||
|
||||
const serviceA = container.bind(TestServiceA)
|
||||
|
||||
const serviceB = container.bind(TestServiceB)
|
||||
expect(serviceB.serviceA).toBe(serviceA)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getContainer", () => {
|
||||
it("returns the container the service is bound to", () => {
|
||||
const container = new Container()
|
||||
|
||||
const serviceA = container.bind(TestServiceA)
|
||||
|
||||
// @ts-expect-error getContainer is a protected member, we are just using it to help with testing
|
||||
expect(serviceA.getContainer()).toBe(container)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getEventStream", () => {
|
||||
it("returns the valid event stream of the service", () => {
|
||||
const container = new Container()
|
||||
|
||||
const serviceB = container.bind(TestServiceB)
|
||||
|
||||
const serviceFunc = vi.fn()
|
||||
|
||||
serviceB.getEventStream().subscribe(serviceFunc)
|
||||
|
||||
serviceB.emitTestEvent()
|
||||
|
||||
expect(serviceFunc).toHaveBeenCalledOnce()
|
||||
expect(serviceFunc).toHaveBeenCalledWith("test")
|
||||
})
|
||||
})
|
||||
})
|
||||
92
packages/dioc/test/test-container.spec.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { TestContainer } from "../lib/testing"
|
||||
import { Service } from "../lib/service"
|
||||
import { ContainerEvent } from "../lib/container"
|
||||
|
||||
class TestServiceA extends Service {
|
||||
public static ID = "TestServiceA"
|
||||
|
||||
public test() {
|
||||
return "real"
|
||||
}
|
||||
}
|
||||
|
||||
class TestServiceB extends Service {
|
||||
public static ID = "TestServiceB"
|
||||
|
||||
// declared public to help with testing
|
||||
public readonly serviceA = this.bind(TestServiceA)
|
||||
|
||||
public test() {
|
||||
return this.serviceA.test()
|
||||
}
|
||||
}
|
||||
|
||||
describe("TestContainer", () => {
|
||||
describe("bindMock", () => {
|
||||
it("returns the fake service defined", () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
const fakeService = {
|
||||
test: () => "fake",
|
||||
}
|
||||
|
||||
const result = container.bindMock(TestServiceA, fakeService)
|
||||
|
||||
expect(result).toBe(fakeService)
|
||||
})
|
||||
|
||||
it("new services bound to the container get the mock service", () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
const fakeServiceA = {
|
||||
test: () => "fake",
|
||||
}
|
||||
|
||||
container.bindMock(TestServiceA, fakeServiceA)
|
||||
|
||||
const serviceB = container.bind(TestServiceB)
|
||||
|
||||
expect(serviceB.serviceA).toBe(fakeServiceA)
|
||||
})
|
||||
|
||||
it("container emits SERVICE_BIND event", () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
const fakeServiceA = {
|
||||
test: () => "fake",
|
||||
}
|
||||
|
||||
const serviceFunc = vi.fn<[ContainerEvent, void]>()
|
||||
|
||||
container.getEventStream().subscribe((ev) => {
|
||||
serviceFunc(ev)
|
||||
})
|
||||
|
||||
container.bindMock(TestServiceA, fakeServiceA)
|
||||
|
||||
expect(serviceFunc).toHaveBeenCalledOnce()
|
||||
expect(serviceFunc).toHaveBeenCalledWith(<ContainerEvent>{
|
||||
type: "SERVICE_BIND",
|
||||
boundeeID: TestServiceA.ID,
|
||||
bounderID: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it("throws if service already bound", () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
const fakeServiceA = {
|
||||
test: () => "fake",
|
||||
}
|
||||
|
||||
container.bindMock(TestServiceA, fakeServiceA)
|
||||
|
||||
expect(() => {
|
||||
container.bindMock(TestServiceA, fakeServiceA)
|
||||
}).toThrowError(
|
||||
"Service 'TestServiceA' already bound to container. Did you already call bindMock on this ?"
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
2
packages/dioc/testing.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from "./dist/testing.d.ts"
|
||||
export * from "./dist/testing.d.ts"
|
||||
21
packages/dioc/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["lib"]
|
||||
}
|
||||
16
packages/dioc/vite.config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: {
|
||||
index: './lib/main.ts',
|
||||
vue: './lib/vue.ts',
|
||||
testing: './lib/testing.ts',
|
||||
},
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['vue'],
|
||||
}
|
||||
},
|
||||
})
|
||||
7
packages/dioc/vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from "vitest/config"
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
|
||||
}
|
||||
})
|
||||
2
packages/dioc/vue.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from "./dist/vue.d.ts"
|
||||
export * from "./dist/vue.d.ts"
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hoppscotch-backend",
|
||||
"version": "2023.4.0",
|
||||
"version": "2023.4.7",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -181,7 +181,7 @@ export class AdminService {
|
||||
* @returns an array team invitations
|
||||
*/
|
||||
async pendingInvitationCountInTeam(teamID: string) {
|
||||
const invitations = await this.teamInvitationService.getAllTeamInvitations(
|
||||
const invitations = await this.teamInvitationService.getTeamInvitations(
|
||||
teamID,
|
||||
);
|
||||
|
||||
@@ -236,11 +236,11 @@ export class AdminService {
|
||||
const user = await this.userService.findUserByEmail(userEmail);
|
||||
if (O.isNone(user)) return E.left(USER_NOT_FOUND);
|
||||
|
||||
const isUserAlreadyMember = await this.teamService.getTeamMemberTE(
|
||||
const teamMember = await this.teamService.getTeamMemberTE(
|
||||
teamID,
|
||||
user.value.uid,
|
||||
)();
|
||||
if (E.left(isUserAlreadyMember)) {
|
||||
if (E.isLeft(teamMember)) {
|
||||
const addedUser = await this.teamService.addMemberToTeamWithEmail(
|
||||
teamID,
|
||||
userEmail,
|
||||
@@ -248,6 +248,18 @@ export class AdminService {
|
||||
);
|
||||
if (E.isLeft(addedUser)) return E.left(addedUser.left);
|
||||
|
||||
const userInvitation =
|
||||
await this.teamInvitationService.getTeamInviteByEmailAndTeamID(
|
||||
userEmail,
|
||||
teamID,
|
||||
);
|
||||
|
||||
if (E.isRight(userInvitation)) {
|
||||
await this.teamInvitationService.revokeInvitation(
|
||||
userInvitation.right.id,
|
||||
);
|
||||
}
|
||||
|
||||
return E.right(addedUser.right);
|
||||
}
|
||||
|
||||
|
||||
@@ -228,11 +228,11 @@ export class AuthService {
|
||||
url = process.env.VITE_BASE_URL;
|
||||
}
|
||||
|
||||
await this.mailerService.sendAuthEmail(email, {
|
||||
await this.mailerService.sendEmail(email, {
|
||||
template: 'code-your-own',
|
||||
variables: {
|
||||
inviteeEmail: email,
|
||||
magicLink: `${url}/magic-link?token=${generatedTokens.token}`,
|
||||
magicLink: `${url}/enter?token=${generatedTokens.token}`,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ export const authCookieHandler = (
|
||||
});
|
||||
|
||||
if (!redirect) {
|
||||
res.status(HttpStatus.OK).send();
|
||||
return res.status(HttpStatus.OK).send();
|
||||
}
|
||||
|
||||
// check to see if redirectUrl is a whitelisted url
|
||||
@@ -72,7 +72,7 @@ export const authCookieHandler = (
|
||||
// if it is not redirect by default to REDIRECT_URL
|
||||
redirectUrl = process.env.REDIRECT_URL;
|
||||
|
||||
res.status(HttpStatus.OK).redirect(redirectUrl);
|
||||
return res.status(HttpStatus.OK).redirect(redirectUrl);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,7 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy) {
|
||||
clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
|
||||
callbackURL: process.env.MICROSOFT_CALLBACK_URL,
|
||||
scope: [process.env.MICROSOFT_SCOPE],
|
||||
passReqToCallback: true,
|
||||
tenant: process.env.MICROSOFT_TENANT,
|
||||
store: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ export const AUTH_FAIL = 'auth/fail';
|
||||
export const JSON_INVALID = 'json_invalid';
|
||||
|
||||
/**
|
||||
* Tried to delete an user data document from fb firestore but failed.
|
||||
* Tried to delete a user data document from fb firestore but failed.
|
||||
* (FirebaseService)
|
||||
*/
|
||||
export const USER_FB_DOCUMENT_DELETION_FAILED =
|
||||
@@ -231,7 +231,7 @@ export const TEAM_COLL_INVALID_JSON = 'team_coll/invalid_json';
|
||||
export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const;
|
||||
|
||||
/**
|
||||
* Tried to perform action on a request that doesn't accept their member role level
|
||||
* Tried to perform an action on a request that doesn't accept their member role level
|
||||
* (GqlRequestTeamMemberGuard)
|
||||
*/
|
||||
export const TEAM_REQ_NOT_REQUIRED_ROLE = 'team_req/not_required_role';
|
||||
@@ -262,7 +262,7 @@ export const TEAM_REQ_REORDERING_FAILED = 'team_req/reordering_failed' as const;
|
||||
export const SENDER_EMAIL_INVALID = 'mailer/sender_email_invalid' as const;
|
||||
|
||||
/**
|
||||
* Tried to perform action on a request when the user is not even member of the team
|
||||
* Tried to perform an action on a request when the user is not even a member of the team
|
||||
* (GqlRequestTeamMemberGuard, GqlCollectionTeamMemberGuard)
|
||||
*/
|
||||
export const TEAM_REQ_NOT_MEMBER = 'team_req/not_member';
|
||||
@@ -307,11 +307,18 @@ export const SHORTCODE_INVALID_JSON = 'shortcode/invalid_json' as const;
|
||||
export const SHORTCODE_ALREADY_EXISTS = 'shortcode/already_exists' as const;
|
||||
|
||||
/**
|
||||
* Invalid or non-existent TEAM ENVIRONMMENT ID
|
||||
* Invalid or non-existent TEAM ENVIRONMENT ID
|
||||
* (TeamEnvironmentsService)
|
||||
*/
|
||||
export const TEAM_ENVIRONMENT_NOT_FOUND = 'team_environment/not_found' as const;
|
||||
|
||||
/**
|
||||
* Invalid TEAM ENVIRONMENT name
|
||||
* (TeamEnvironmentsService)
|
||||
*/
|
||||
export const TEAM_ENVIRONMENT_SHORT_NAME =
|
||||
'team_environment/short_name' as const;
|
||||
|
||||
/**
|
||||
* The user is not a member of the team of the given environment
|
||||
* (GqlTeamEnvTeamGuard)
|
||||
@@ -340,7 +347,7 @@ export const USER_SETTINGS_NULL_SETTINGS =
|
||||
'user_settings/null_settings' as const;
|
||||
|
||||
/*
|
||||
* Global environment doesnt exists for the user
|
||||
* Global environment doesn't exist for the user
|
||||
* (UserEnvironmentsService)
|
||||
*/
|
||||
export const USER_ENVIRONMENT_GLOBAL_ENV_DOES_NOT_EXISTS =
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
UserMagicLinkMailDescription,
|
||||
} from './MailDescriptions';
|
||||
import { throwErr } from 'src/utils';
|
||||
import * as TE from 'fp-ts/TaskEither';
|
||||
import { EMAIL_FAILED } from 'src/errors';
|
||||
import { MailerService as NestMailerService } from '@nestjs-modules/mailer';
|
||||
|
||||
@@ -35,33 +34,14 @@ export class MailerService {
|
||||
|
||||
/**
|
||||
* Sends an email to the given email address given a mail description
|
||||
* @param to The email address to be sent to (NOTE: this is not validated)
|
||||
* @param to Receiver's email id
|
||||
* @param mailDesc Definition of what email to be sent
|
||||
* @returns Response if email was send successfully or not
|
||||
*/
|
||||
sendMail(
|
||||
async sendEmail(
|
||||
to: string,
|
||||
mailDesc: MailDescription | UserMagicLinkMailDescription,
|
||||
) {
|
||||
return TE.tryCatch(
|
||||
async () => {
|
||||
await this.nestMailerService.sendMail({
|
||||
to,
|
||||
template: mailDesc.template,
|
||||
subject: this.resolveSubjectForMailDesc(mailDesc),
|
||||
context: mailDesc.variables,
|
||||
});
|
||||
},
|
||||
() => EMAIL_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param to Receiver's email id
|
||||
* @param mailDesc Details of email to be sent for Magic-Link auth
|
||||
* @returns Response if email was send successfully or not
|
||||
*/
|
||||
async sendAuthEmail(to: string, mailDesc: UserMagicLinkMailDescription) {
|
||||
try {
|
||||
await this.nestMailerService.sendMail({
|
||||
to,
|
||||
|
||||
@@ -9,7 +9,6 @@ import { emitGQLSchemaFile } from './gql-schema';
|
||||
async function bootstrap() {
|
||||
console.log(`Running in production: ${process.env.PRODUCTION}`);
|
||||
console.log(`Port: ${process.env.PORT}`);
|
||||
console.log(`Database: ${process.env.DATABASE_URL}`);
|
||||
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import * as TE from 'fp-ts/TaskEither';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import * as S from 'fp-ts/string';
|
||||
import { pipe } from 'fp-ts/function';
|
||||
import {
|
||||
getAnnotatedRequiredRoles,
|
||||
getGqlArg,
|
||||
getUserFromGQLContext,
|
||||
throwErr,
|
||||
} from 'src/utils';
|
||||
import { TeamEnvironmentsService } from './team-environments.service';
|
||||
import {
|
||||
BUG_AUTH_NO_USER_CTX,
|
||||
@@ -19,6 +9,10 @@ import {
|
||||
TEAM_ENVIRONMENT_NOT_FOUND,
|
||||
} from 'src/errors';
|
||||
import { TeamService } from 'src/team/team.service';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { TeamMemberRole } from '@prisma/client';
|
||||
import { throwErr } from 'src/utils';
|
||||
|
||||
/**
|
||||
* A guard which checks whether the caller of a GQL Operation
|
||||
@@ -33,50 +27,31 @@ export class GqlTeamEnvTeamGuard implements CanActivate {
|
||||
private readonly teamService: TeamService,
|
||||
) {}
|
||||
|
||||
canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
return pipe(
|
||||
TE.Do,
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const requireRoles = this.reflector.get<TeamMemberRole[]>(
|
||||
'requiresTeamRole',
|
||||
context.getHandler(),
|
||||
);
|
||||
if (!requireRoles) throw new Error(BUG_TEAM_ENV_GUARD_NO_REQUIRE_ROLES);
|
||||
|
||||
TE.bindW('requiredRoles', () =>
|
||||
pipe(
|
||||
getAnnotatedRequiredRoles(this.reflector, context),
|
||||
TE.fromOption(() => BUG_TEAM_ENV_GUARD_NO_REQUIRE_ROLES),
|
||||
),
|
||||
),
|
||||
const gqlExecCtx = GqlExecutionContext.create(context);
|
||||
|
||||
TE.bindW('user', () =>
|
||||
pipe(
|
||||
getUserFromGQLContext(context),
|
||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
||||
),
|
||||
),
|
||||
const { user } = gqlExecCtx.getContext().req;
|
||||
if (user == undefined) throw new Error(BUG_AUTH_NO_USER_CTX);
|
||||
|
||||
TE.bindW('envID', () =>
|
||||
pipe(
|
||||
getGqlArg('id', context),
|
||||
O.fromPredicate(S.isString),
|
||||
TE.fromOption(() => BUG_TEAM_ENV_GUARD_NO_ENV_ID),
|
||||
),
|
||||
),
|
||||
const { id } = gqlExecCtx.getArgs<{ id: string }>();
|
||||
if (!id) throwErr(BUG_TEAM_ENV_GUARD_NO_ENV_ID);
|
||||
|
||||
TE.bindW('membership', ({ envID, user }) =>
|
||||
pipe(
|
||||
this.teamEnvironmentService.getTeamEnvironment(envID),
|
||||
TE.fromTaskOption(() => TEAM_ENVIRONMENT_NOT_FOUND),
|
||||
TE.chainW((env) =>
|
||||
pipe(
|
||||
this.teamService.getTeamMemberTE(env.teamID, user.uid),
|
||||
TE.mapLeft(() => TEAM_ENVIRONMENT_NOT_TEAM_MEMBER),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const teamEnvironment =
|
||||
await this.teamEnvironmentService.getTeamEnvironment(id);
|
||||
if (E.isLeft(teamEnvironment)) throwErr(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||
|
||||
TE.map(({ membership, requiredRoles }) =>
|
||||
requiredRoles.includes(membership.role),
|
||||
),
|
||||
const member = await this.teamService.getTeamMember(
|
||||
teamEnvironment.right.teamID,
|
||||
user.uid,
|
||||
);
|
||||
if (!member) throwErr(TEAM_ENVIRONMENT_NOT_TEAM_MEMBER);
|
||||
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
return requireRoles.includes(member.role);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ArgsType, Field, ID } from '@nestjs/graphql';
|
||||
|
||||
@ArgsType()
|
||||
export class CreateTeamEnvironmentArgs {
|
||||
@Field({
|
||||
name: 'name',
|
||||
description: 'Name of the Team Environment',
|
||||
})
|
||||
name: string;
|
||||
|
||||
@Field(() => ID, {
|
||||
name: 'teamID',
|
||||
description: 'ID of the Team',
|
||||
})
|
||||
teamID: string;
|
||||
|
||||
@Field({
|
||||
name: 'variables',
|
||||
description: 'JSON string of the variables object',
|
||||
})
|
||||
variables: string;
|
||||
}
|
||||
|
||||
@ArgsType()
|
||||
export class UpdateTeamEnvironmentArgs {
|
||||
@Field(() => ID, {
|
||||
name: 'id',
|
||||
description: 'ID of the Team Environment',
|
||||
})
|
||||
id: string;
|
||||
@Field({
|
||||
name: 'name',
|
||||
description: 'Name of the Team Environment',
|
||||
})
|
||||
name: string;
|
||||
@Field({
|
||||
name: 'variables',
|
||||
description: 'JSON string of the variables object',
|
||||
})
|
||||
variables: string;
|
||||
}
|
||||
@@ -13,6 +13,11 @@ import { throwErr } from 'src/utils';
|
||||
import { GqlTeamEnvTeamGuard } from './gql-team-env-team.guard';
|
||||
import { TeamEnvironment } from './team-environments.model';
|
||||
import { TeamEnvironmentsService } from './team-environments.service';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import {
|
||||
CreateTeamEnvironmentArgs,
|
||||
UpdateTeamEnvironmentArgs,
|
||||
} from './input-type.args';
|
||||
|
||||
@UseGuards(GqlThrottlerGuard)
|
||||
@Resolver(() => 'TeamEnvironment')
|
||||
@@ -29,29 +34,18 @@ export class TeamEnvironmentsResolver {
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||
createTeamEnvironment(
|
||||
@Args({
|
||||
name: 'name',
|
||||
description: 'Name of the Team Environment',
|
||||
})
|
||||
name: string,
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
description: 'ID of the Team',
|
||||
type: () => ID,
|
||||
})
|
||||
teamID: string,
|
||||
@Args({
|
||||
name: 'variables',
|
||||
description: 'JSON string of the variables object',
|
||||
})
|
||||
variables: string,
|
||||
async createTeamEnvironment(
|
||||
@Args() args: CreateTeamEnvironmentArgs,
|
||||
): Promise<TeamEnvironment> {
|
||||
return this.teamEnvironmentsService.createTeamEnvironment(
|
||||
name,
|
||||
teamID,
|
||||
variables,
|
||||
)();
|
||||
const teamEnvironment =
|
||||
await this.teamEnvironmentsService.createTeamEnvironment(
|
||||
args.name,
|
||||
args.teamID,
|
||||
args.variables,
|
||||
);
|
||||
|
||||
if (E.isLeft(teamEnvironment)) throwErr(teamEnvironment.left);
|
||||
return teamEnvironment.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
@@ -59,7 +53,7 @@ export class TeamEnvironmentsResolver {
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||
deleteTeamEnvironment(
|
||||
async deleteTeamEnvironment(
|
||||
@Args({
|
||||
name: 'id',
|
||||
description: 'ID of the Team Environment',
|
||||
@@ -67,10 +61,12 @@ export class TeamEnvironmentsResolver {
|
||||
})
|
||||
id: string,
|
||||
): Promise<boolean> {
|
||||
return pipe(
|
||||
this.teamEnvironmentsService.deleteTeamEnvironment(id),
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
const isDeleted = await this.teamEnvironmentsService.deleteTeamEnvironment(
|
||||
id,
|
||||
);
|
||||
|
||||
if (E.isLeft(isDeleted)) throwErr(isDeleted.left);
|
||||
return isDeleted.right;
|
||||
}
|
||||
|
||||
@Mutation(() => TeamEnvironment, {
|
||||
@@ -79,28 +75,19 @@ export class TeamEnvironmentsResolver {
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||
updateTeamEnvironment(
|
||||
@Args({
|
||||
name: 'id',
|
||||
description: 'ID of the Team Environment',
|
||||
type: () => ID,
|
||||
})
|
||||
id: string,
|
||||
@Args({
|
||||
name: 'name',
|
||||
description: 'Name of the Team Environment',
|
||||
})
|
||||
name: string,
|
||||
@Args({
|
||||
name: 'variables',
|
||||
description: 'JSON string of the variables object',
|
||||
})
|
||||
variables: string,
|
||||
async updateTeamEnvironment(
|
||||
@Args()
|
||||
args: UpdateTeamEnvironmentArgs,
|
||||
): Promise<TeamEnvironment> {
|
||||
return pipe(
|
||||
this.teamEnvironmentsService.updateTeamEnvironment(id, name, variables),
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
const updatedTeamEnvironment =
|
||||
await this.teamEnvironmentsService.updateTeamEnvironment(
|
||||
args.id,
|
||||
args.name,
|
||||
args.variables,
|
||||
);
|
||||
|
||||
if (E.isLeft(updatedTeamEnvironment)) throwErr(updatedTeamEnvironment.left);
|
||||
return updatedTeamEnvironment.right;
|
||||
}
|
||||
|
||||
@Mutation(() => TeamEnvironment, {
|
||||
@@ -108,7 +95,7 @@ export class TeamEnvironmentsResolver {
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||
deleteAllVariablesFromTeamEnvironment(
|
||||
async deleteAllVariablesFromTeamEnvironment(
|
||||
@Args({
|
||||
name: 'id',
|
||||
description: 'ID of the Team Environment',
|
||||
@@ -116,10 +103,13 @@ export class TeamEnvironmentsResolver {
|
||||
})
|
||||
id: string,
|
||||
): Promise<TeamEnvironment> {
|
||||
return pipe(
|
||||
this.teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(id),
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
const teamEnvironment =
|
||||
await this.teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||
id,
|
||||
);
|
||||
|
||||
if (E.isLeft(teamEnvironment)) throwErr(teamEnvironment.left);
|
||||
return teamEnvironment.right;
|
||||
}
|
||||
|
||||
@Mutation(() => TeamEnvironment, {
|
||||
@@ -127,7 +117,7 @@ export class TeamEnvironmentsResolver {
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamEnvTeamGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||
createDuplicateEnvironment(
|
||||
async createDuplicateEnvironment(
|
||||
@Args({
|
||||
name: 'id',
|
||||
description: 'ID of the Team Environment',
|
||||
@@ -135,10 +125,12 @@ export class TeamEnvironmentsResolver {
|
||||
})
|
||||
id: string,
|
||||
): Promise<TeamEnvironment> {
|
||||
return pipe(
|
||||
this.teamEnvironmentsService.createDuplicateEnvironment(id),
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
const res = await this.teamEnvironmentsService.createDuplicateEnvironment(
|
||||
id,
|
||||
);
|
||||
|
||||
if (E.isLeft(res)) throwErr(res.left);
|
||||
return res.right;
|
||||
}
|
||||
|
||||
/* Subscriptions */
|
||||
|
||||
@@ -2,7 +2,11 @@ import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { TeamEnvironment } from './team-environments.model';
|
||||
import { TeamEnvironmentsService } from './team-environments.service';
|
||||
import { TEAM_ENVIRONMENT_NOT_FOUND } from 'src/errors';
|
||||
import {
|
||||
JSON_INVALID,
|
||||
TEAM_ENVIRONMENT_NOT_FOUND,
|
||||
TEAM_ENVIRONMENT_SHORT_NAME,
|
||||
} from 'src/errors';
|
||||
|
||||
const mockPrisma = mockDeep<PrismaService>();
|
||||
|
||||
@@ -31,125 +35,81 @@ beforeEach(() => {
|
||||
|
||||
describe('TeamEnvironmentsService', () => {
|
||||
describe('getTeamEnvironment', () => {
|
||||
test('queries the db with the id', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
|
||||
|
||||
await teamEnvironmentsService.getTeamEnvironment('123')();
|
||||
|
||||
expect(mockPrisma.teamEnvironment.findFirst).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: {
|
||||
id: '123',
|
||||
},
|
||||
}),
|
||||
test('should successfully return a TeamEnvironment with valid ID', async () => {
|
||||
mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce(
|
||||
teamEnvironment,
|
||||
);
|
||||
});
|
||||
|
||||
test('requests prisma to reject the query promise if not found', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
|
||||
|
||||
await teamEnvironmentsService.getTeamEnvironment('123')();
|
||||
|
||||
expect(mockPrisma.teamEnvironment.findFirst).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rejectOnNotFound: true,
|
||||
}),
|
||||
const result = await teamEnvironmentsService.getTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
);
|
||||
expect(result).toEqualRight(teamEnvironment);
|
||||
});
|
||||
|
||||
test('should return a Some of the correct environment if exists', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValue(teamEnvironment);
|
||||
test('should throw TEAM_ENVIRONMENT_NOT_FOUND with invalid ID', async () => {
|
||||
mockPrisma.teamEnvironment.findFirstOrThrow.mockRejectedValueOnce(
|
||||
'RejectOnNotFound',
|
||||
);
|
||||
|
||||
const result = await teamEnvironmentsService.getTeamEnvironment('123')();
|
||||
|
||||
expect(result).toEqualSome(teamEnvironment);
|
||||
});
|
||||
|
||||
test('should return a None if the environment does not exist', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
|
||||
|
||||
const result = await teamEnvironmentsService.getTeamEnvironment('123')();
|
||||
|
||||
expect(result).toBeNone();
|
||||
const result = await teamEnvironmentsService.getTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
);
|
||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTeamEnvironment', () => {
|
||||
test('should create and return a new team environment given a valid name,variable and team ID', async () => {
|
||||
test('should successfully create and return a new team environment given valid inputs', async () => {
|
||||
mockPrisma.teamEnvironment.create.mockResolvedValue(teamEnvironment);
|
||||
|
||||
const result = await teamEnvironmentsService.createTeamEnvironment(
|
||||
teamEnvironment.name,
|
||||
teamEnvironment.teamID,
|
||||
JSON.stringify(teamEnvironment.variables),
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqual(<TeamEnvironment>{
|
||||
id: teamEnvironment.id,
|
||||
name: teamEnvironment.name,
|
||||
teamID: teamEnvironment.teamID,
|
||||
expect(result).toEqualRight({
|
||||
...teamEnvironment,
|
||||
variables: JSON.stringify(teamEnvironment.variables),
|
||||
});
|
||||
});
|
||||
|
||||
test('should reject if given team ID is invalid', async () => {
|
||||
mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
|
||||
test('should throw TEAM_ENVIRONMENT_SHORT_NAME if input TeamEnvironment name is invalid', async () => {
|
||||
const result = await teamEnvironmentsService.createTeamEnvironment(
|
||||
'12',
|
||||
teamEnvironment.teamID,
|
||||
JSON.stringify(teamEnvironment.variables),
|
||||
);
|
||||
|
||||
await expect(
|
||||
teamEnvironmentsService.createTeamEnvironment(
|
||||
teamEnvironment.name,
|
||||
'invalidteamid',
|
||||
JSON.stringify(teamEnvironment.variables),
|
||||
),
|
||||
).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
test('should reject if provided team environment name is not a string', async () => {
|
||||
mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
|
||||
|
||||
await expect(
|
||||
teamEnvironmentsService.createTeamEnvironment(
|
||||
null as any,
|
||||
teamEnvironment.teamID,
|
||||
JSON.stringify(teamEnvironment.variables),
|
||||
),
|
||||
).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
test('should reject if provided variable is not a string', async () => {
|
||||
mockPrisma.teamEnvironment.create.mockRejectedValue(null as any);
|
||||
|
||||
await expect(
|
||||
teamEnvironmentsService.createTeamEnvironment(
|
||||
teamEnvironment.name,
|
||||
teamEnvironment.teamID,
|
||||
null as any,
|
||||
),
|
||||
).rejects.toBeDefined();
|
||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_SHORT_NAME);
|
||||
});
|
||||
|
||||
test('should send pubsub message to "team_environment/<teamID>/created" if team environment is created successfully', async () => {
|
||||
mockPrisma.teamEnvironment.create.mockResolvedValueOnce(teamEnvironment);
|
||||
mockPrisma.teamEnvironment.create.mockResolvedValue(teamEnvironment);
|
||||
|
||||
const result = await teamEnvironmentsService.createTeamEnvironment(
|
||||
teamEnvironment.name,
|
||||
teamEnvironment.teamID,
|
||||
JSON.stringify(teamEnvironment.variables),
|
||||
)();
|
||||
);
|
||||
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_environment/${teamEnvironment.teamID}/created`,
|
||||
result,
|
||||
{
|
||||
...teamEnvironment,
|
||||
variables: JSON.stringify(teamEnvironment.variables),
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteTeamEnvironment', () => {
|
||||
test('should resolve to true given a valid team environment ID', async () => {
|
||||
test('should successfully delete a TeamEnvironment with a valid ID', async () => {
|
||||
mockPrisma.teamEnvironment.delete.mockResolvedValueOnce(teamEnvironment);
|
||||
|
||||
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualRight(true);
|
||||
});
|
||||
@@ -159,7 +119,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
|
||||
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
||||
'invalidid',
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||
});
|
||||
@@ -169,7 +129,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
|
||||
const result = await teamEnvironmentsService.deleteTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
)();
|
||||
);
|
||||
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_environment/${teamEnvironment.teamID}/deleted`,
|
||||
@@ -182,7 +142,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
});
|
||||
|
||||
describe('updateVariablesInTeamEnvironment', () => {
|
||||
test('should add new variable to a team environment', async () => {
|
||||
test('should successfully add new variable to a team environment', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||
...teamEnvironment,
|
||||
variables: [{ key: 'value' }],
|
||||
@@ -192,7 +152,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
teamEnvironment.id,
|
||||
teamEnvironment.name,
|
||||
JSON.stringify([{ key: 'value' }]),
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualRight(<TeamEnvironment>{
|
||||
...teamEnvironment,
|
||||
@@ -200,7 +160,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should add new variable to already existing list of variables in a team environment', async () => {
|
||||
test('should successfully add new variable to already existing list of variables in a team environment', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||
...teamEnvironment,
|
||||
variables: [{ key: 'value' }, { key_2: 'value_2' }],
|
||||
@@ -210,7 +170,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
teamEnvironment.id,
|
||||
teamEnvironment.name,
|
||||
JSON.stringify([{ key: 'value' }, { key_2: 'value_2' }]),
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualRight(<TeamEnvironment>{
|
||||
...teamEnvironment,
|
||||
@@ -218,7 +178,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should edit existing variables in a team environment', async () => {
|
||||
test('should successfully edit existing variables in a team environment', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||
...teamEnvironment,
|
||||
variables: [{ key: '1234' }],
|
||||
@@ -228,7 +188,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
teamEnvironment.id,
|
||||
teamEnvironment.name,
|
||||
JSON.stringify([{ key: '1234' }]),
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualRight(<TeamEnvironment>{
|
||||
...teamEnvironment,
|
||||
@@ -236,22 +196,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should delete existing variable in a team environment', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment);
|
||||
|
||||
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
teamEnvironment.name,
|
||||
JSON.stringify([{}]),
|
||||
)();
|
||||
|
||||
expect(result).toEqualRight(<TeamEnvironment>{
|
||||
...teamEnvironment,
|
||||
variables: JSON.stringify([{}]),
|
||||
});
|
||||
});
|
||||
|
||||
test('should edit name of an existing team environment', async () => {
|
||||
test('should successfully edit name of an existing team environment', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce({
|
||||
...teamEnvironment,
|
||||
variables: [{ key: '123' }],
|
||||
@@ -261,7 +206,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
teamEnvironment.id,
|
||||
teamEnvironment.name,
|
||||
JSON.stringify([{ key: '123' }]),
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualRight(<TeamEnvironment>{
|
||||
...teamEnvironment,
|
||||
@@ -269,14 +214,24 @@ describe('TeamEnvironmentsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||
test('should throw TEAM_ENVIRONMENT_SHORT_NAME if input TeamEnvironment name is invalid', async () => {
|
||||
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
'12',
|
||||
JSON.stringify([{ key: 'value' }]),
|
||||
);
|
||||
|
||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_SHORT_NAME);
|
||||
});
|
||||
|
||||
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound');
|
||||
|
||||
const result = await teamEnvironmentsService.updateTeamEnvironment(
|
||||
'invalidid',
|
||||
teamEnvironment.name,
|
||||
JSON.stringify(teamEnvironment.variables),
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||
});
|
||||
@@ -288,7 +243,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
teamEnvironment.id,
|
||||
teamEnvironment.name,
|
||||
JSON.stringify([{ key: 'value' }]),
|
||||
)();
|
||||
);
|
||||
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_environment/${teamEnvironment.teamID}/updated`,
|
||||
@@ -301,13 +256,13 @@ describe('TeamEnvironmentsService', () => {
|
||||
});
|
||||
|
||||
describe('deleteAllVariablesFromTeamEnvironment', () => {
|
||||
test('should delete all variables in a team environment', async () => {
|
||||
test('should successfully delete all variables in a team environment', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockResolvedValueOnce(teamEnvironment);
|
||||
|
||||
const result =
|
||||
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualRight(<TeamEnvironment>{
|
||||
...teamEnvironment,
|
||||
@@ -315,13 +270,13 @@ describe('TeamEnvironmentsService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||
mockPrisma.teamEnvironment.update.mockRejectedValue('RecordNotFound');
|
||||
|
||||
const result =
|
||||
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||
'invalidid',
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||
});
|
||||
@@ -332,7 +287,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
const result =
|
||||
await teamEnvironmentsService.deleteAllVariablesFromTeamEnvironment(
|
||||
teamEnvironment.id,
|
||||
)();
|
||||
);
|
||||
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_environment/${teamEnvironment.teamID}/updated`,
|
||||
@@ -345,7 +300,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
});
|
||||
|
||||
describe('createDuplicateEnvironment', () => {
|
||||
test('should duplicate an existing team environment', async () => {
|
||||
test('should successfully duplicate an existing team environment', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
|
||||
teamEnvironment,
|
||||
);
|
||||
@@ -357,21 +312,21 @@ describe('TeamEnvironmentsService', () => {
|
||||
|
||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||
teamEnvironment.id,
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualRight(<TeamEnvironment>{
|
||||
...teamEnvironment,
|
||||
id: 'newid',
|
||||
...teamEnvironment,
|
||||
variables: JSON.stringify(teamEnvironment.variables),
|
||||
});
|
||||
});
|
||||
|
||||
test('should reject to TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
|
||||
|
||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||
teamEnvironment.id,
|
||||
)();
|
||||
);
|
||||
|
||||
expect(result).toEqualLeft(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||
});
|
||||
@@ -388,13 +343,13 @@ describe('TeamEnvironmentsService', () => {
|
||||
|
||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||
teamEnvironment.id,
|
||||
)();
|
||||
);
|
||||
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`team_environment/${teamEnvironment.teamID}/created`,
|
||||
{
|
||||
...teamEnvironment,
|
||||
id: 'newid',
|
||||
...teamEnvironment,
|
||||
variables: JSON.stringify([{}]),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { pipe } from 'fp-ts/function';
|
||||
import * as T from 'fp-ts/Task';
|
||||
import * as TO from 'fp-ts/TaskOption';
|
||||
import * as TE from 'fp-ts/TaskEither';
|
||||
import * as A from 'fp-ts/Array';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { TeamEnvironment as DBTeamEnvironment, Prisma } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { TeamEnvironment } from './team-environments.model';
|
||||
import { TEAM_ENVIRONMENT_NOT_FOUND } from 'src/errors';
|
||||
|
||||
import {
|
||||
TEAM_ENVIRONMENT_NOT_FOUND,
|
||||
TEAM_ENVIRONMENT_SHORT_NAME,
|
||||
} from 'src/errors';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { isValidLength } from 'src/utils';
|
||||
@Injectable()
|
||||
export class TeamEnvironmentsService {
|
||||
constructor(
|
||||
@@ -17,219 +16,218 @@ export class TeamEnvironmentsService {
|
||||
private readonly pubsub: PubSubService,
|
||||
) {}
|
||||
|
||||
getTeamEnvironment(id: string) {
|
||||
return TO.tryCatch(() =>
|
||||
this.prisma.teamEnvironment.findFirst({
|
||||
where: { id },
|
||||
TITLE_LENGTH = 3;
|
||||
|
||||
/**
|
||||
* TeamEnvironments are saved in the DB in the following way
|
||||
* [{ key: value }, { key: value },....]
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Typecast a database TeamEnvironment to a TeamEnvironment model
|
||||
* @param teamEnvironment database TeamEnvironment
|
||||
* @returns TeamEnvironment model
|
||||
*/
|
||||
private cast(teamEnvironment: DBTeamEnvironment): TeamEnvironment {
|
||||
return {
|
||||
id: teamEnvironment.id,
|
||||
name: teamEnvironment.name,
|
||||
teamID: teamEnvironment.teamID,
|
||||
variables: JSON.stringify(teamEnvironment.variables),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get details of a TeamEnvironment.
|
||||
*
|
||||
* @param id TeamEnvironment ID
|
||||
* @returns Either of a TeamEnvironment or error message
|
||||
*/
|
||||
async getTeamEnvironment(id: string) {
|
||||
try {
|
||||
const teamEnvironment =
|
||||
await this.prisma.teamEnvironment.findFirstOrThrow({
|
||||
where: { id },
|
||||
});
|
||||
return E.right(teamEnvironment);
|
||||
} catch (error) {
|
||||
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new TeamEnvironment.
|
||||
*
|
||||
* @param name name of new TeamEnvironment
|
||||
* @param teamID teamID of new TeamEnvironment
|
||||
* @param variables JSONified string of contents of new TeamEnvironment
|
||||
* @returns Either of a TeamEnvironment or error message
|
||||
*/
|
||||
async createTeamEnvironment(name: string, teamID: string, variables: string) {
|
||||
const isTitleValid = isValidLength(name, this.TITLE_LENGTH);
|
||||
if (!isTitleValid) return E.left(TEAM_ENVIRONMENT_SHORT_NAME);
|
||||
|
||||
const result = await this.prisma.teamEnvironment.create({
|
||||
data: {
|
||||
name: name,
|
||||
teamID: teamID,
|
||||
variables: JSON.parse(variables),
|
||||
},
|
||||
});
|
||||
|
||||
const createdTeamEnvironment = this.cast(result);
|
||||
|
||||
this.pubsub.publish(
|
||||
`team_environment/${createdTeamEnvironment.teamID}/created`,
|
||||
createdTeamEnvironment,
|
||||
);
|
||||
|
||||
return E.right(createdTeamEnvironment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a TeamEnvironment.
|
||||
*
|
||||
* @param id TeamEnvironment ID
|
||||
* @returns Either of boolean or error message
|
||||
*/
|
||||
async deleteTeamEnvironment(id: string) {
|
||||
try {
|
||||
const result = await this.prisma.teamEnvironment.delete({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
});
|
||||
|
||||
const deletedTeamEnvironment = this.cast(result);
|
||||
|
||||
this.pubsub.publish(
|
||||
`team_environment/${deletedTeamEnvironment.teamID}/deleted`,
|
||||
deletedTeamEnvironment,
|
||||
);
|
||||
|
||||
return E.right(true);
|
||||
} catch (error) {
|
||||
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a TeamEnvironment.
|
||||
*
|
||||
* @param id TeamEnvironment ID
|
||||
* @param name TeamEnvironment name
|
||||
* @param variables JSONified string of contents of new TeamEnvironment
|
||||
* @returns Either of a TeamEnvironment or error message
|
||||
*/
|
||||
async updateTeamEnvironment(id: string, name: string, variables: string) {
|
||||
try {
|
||||
const isTitleValid = isValidLength(name, this.TITLE_LENGTH);
|
||||
if (!isTitleValid) return E.left(TEAM_ENVIRONMENT_SHORT_NAME);
|
||||
|
||||
const result = await this.prisma.teamEnvironment.update({
|
||||
where: { id: id },
|
||||
data: {
|
||||
name,
|
||||
variables: JSON.parse(variables),
|
||||
},
|
||||
});
|
||||
|
||||
const updatedTeamEnvironment = this.cast(result);
|
||||
|
||||
this.pubsub.publish(
|
||||
`team_environment/${updatedTeamEnvironment.teamID}/updated`,
|
||||
updatedTeamEnvironment,
|
||||
);
|
||||
|
||||
return E.right(updatedTeamEnvironment);
|
||||
} catch (error) {
|
||||
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear contents of a TeamEnvironment.
|
||||
*
|
||||
* @param id TeamEnvironment ID
|
||||
* @returns Either of a TeamEnvironment or error message
|
||||
*/
|
||||
async deleteAllVariablesFromTeamEnvironment(id: string) {
|
||||
try {
|
||||
const result = await this.prisma.teamEnvironment.update({
|
||||
where: { id: id },
|
||||
data: {
|
||||
variables: [],
|
||||
},
|
||||
});
|
||||
|
||||
const teamEnvironment = this.cast(result);
|
||||
|
||||
this.pubsub.publish(
|
||||
`team_environment/${teamEnvironment.teamID}/updated`,
|
||||
teamEnvironment,
|
||||
);
|
||||
|
||||
return E.right(teamEnvironment);
|
||||
} catch (error) {
|
||||
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a duplicate of a existing TeamEnvironment.
|
||||
*
|
||||
* @param id TeamEnvironment ID
|
||||
* @returns Either of a TeamEnvironment or error message
|
||||
*/
|
||||
async createDuplicateEnvironment(id: string) {
|
||||
try {
|
||||
const environment = await this.prisma.teamEnvironment.findFirst({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
rejectOnNotFound: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const result = await this.prisma.teamEnvironment.create({
|
||||
data: {
|
||||
name: environment.name,
|
||||
teamID: environment.teamID,
|
||||
variables: environment.variables as Prisma.JsonArray,
|
||||
},
|
||||
});
|
||||
|
||||
const duplicatedTeamEnvironment = this.cast(result);
|
||||
|
||||
this.pubsub.publish(
|
||||
`team_environment/${duplicatedTeamEnvironment.teamID}/created`,
|
||||
duplicatedTeamEnvironment,
|
||||
);
|
||||
|
||||
return E.right(duplicatedTeamEnvironment);
|
||||
} catch (error) {
|
||||
return E.left(TEAM_ENVIRONMENT_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
createTeamEnvironment(name: string, teamID: string, variables: string) {
|
||||
return pipe(
|
||||
() =>
|
||||
this.prisma.teamEnvironment.create({
|
||||
data: {
|
||||
name: name,
|
||||
teamID: teamID,
|
||||
variables: JSON.parse(variables),
|
||||
},
|
||||
}),
|
||||
T.chainFirst(
|
||||
(environment) => () =>
|
||||
this.pubsub.publish(
|
||||
`team_environment/${environment.teamID}/created`,
|
||||
<TeamEnvironment>{
|
||||
id: environment.id,
|
||||
name: environment.name,
|
||||
teamID: environment.teamID,
|
||||
variables: JSON.stringify(environment.variables),
|
||||
},
|
||||
),
|
||||
),
|
||||
T.map((data) => {
|
||||
return <TeamEnvironment>{
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
teamID: data.teamID,
|
||||
variables: JSON.stringify(data.variables),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Fetch all TeamEnvironments of a team.
|
||||
*
|
||||
* @param teamID teamID of new TeamEnvironment
|
||||
* @returns List of TeamEnvironments
|
||||
*/
|
||||
async fetchAllTeamEnvironments(teamID: string) {
|
||||
const result = await this.prisma.teamEnvironment.findMany({
|
||||
where: {
|
||||
teamID: teamID,
|
||||
},
|
||||
});
|
||||
const teamEnvironments = result.map((item) => {
|
||||
return this.cast(item);
|
||||
});
|
||||
|
||||
deleteTeamEnvironment(id: string) {
|
||||
return pipe(
|
||||
TE.tryCatch(
|
||||
() =>
|
||||
this.prisma.teamEnvironment.delete({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
}),
|
||||
() => TEAM_ENVIRONMENT_NOT_FOUND,
|
||||
),
|
||||
TE.chainFirst((environment) =>
|
||||
TE.fromTask(() =>
|
||||
this.pubsub.publish(
|
||||
`team_environment/${environment.teamID}/deleted`,
|
||||
<TeamEnvironment>{
|
||||
id: environment.id,
|
||||
name: environment.name,
|
||||
teamID: environment.teamID,
|
||||
variables: JSON.stringify(environment.variables),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
TE.map((data) => true),
|
||||
);
|
||||
}
|
||||
|
||||
updateTeamEnvironment(id: string, name: string, variables: string) {
|
||||
return pipe(
|
||||
TE.tryCatch(
|
||||
() =>
|
||||
this.prisma.teamEnvironment.update({
|
||||
where: { id: id },
|
||||
data: {
|
||||
name,
|
||||
variables: JSON.parse(variables),
|
||||
},
|
||||
}),
|
||||
() => TEAM_ENVIRONMENT_NOT_FOUND,
|
||||
),
|
||||
TE.chainFirst((environment) =>
|
||||
TE.fromTask(() =>
|
||||
this.pubsub.publish(
|
||||
`team_environment/${environment.teamID}/updated`,
|
||||
<TeamEnvironment>{
|
||||
id: environment.id,
|
||||
name: environment.name,
|
||||
teamID: environment.teamID,
|
||||
variables: JSON.stringify(environment.variables),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
TE.map(
|
||||
(environment) =>
|
||||
<TeamEnvironment>{
|
||||
id: environment.id,
|
||||
name: environment.name,
|
||||
teamID: environment.teamID,
|
||||
variables: JSON.stringify(environment.variables),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
deleteAllVariablesFromTeamEnvironment(id: string) {
|
||||
return pipe(
|
||||
TE.tryCatch(
|
||||
() =>
|
||||
this.prisma.teamEnvironment.update({
|
||||
where: { id: id },
|
||||
data: {
|
||||
variables: [],
|
||||
},
|
||||
}),
|
||||
() => TEAM_ENVIRONMENT_NOT_FOUND,
|
||||
),
|
||||
TE.chainFirst((environment) =>
|
||||
TE.fromTask(() =>
|
||||
this.pubsub.publish(
|
||||
`team_environment/${environment.teamID}/updated`,
|
||||
<TeamEnvironment>{
|
||||
id: environment.id,
|
||||
name: environment.name,
|
||||
teamID: environment.teamID,
|
||||
variables: JSON.stringify(environment.variables),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
TE.map(
|
||||
(environment) =>
|
||||
<TeamEnvironment>{
|
||||
id: environment.id,
|
||||
name: environment.name,
|
||||
teamID: environment.teamID,
|
||||
variables: JSON.stringify(environment.variables),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
createDuplicateEnvironment(id: string) {
|
||||
return pipe(
|
||||
TE.tryCatch(
|
||||
() =>
|
||||
this.prisma.teamEnvironment.findFirst({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
rejectOnNotFound: true,
|
||||
}),
|
||||
() => TEAM_ENVIRONMENT_NOT_FOUND,
|
||||
),
|
||||
TE.chain((environment) =>
|
||||
TE.fromTask(() =>
|
||||
this.prisma.teamEnvironment.create({
|
||||
data: {
|
||||
name: environment.name,
|
||||
teamID: environment.teamID,
|
||||
variables: environment.variables as Prisma.JsonArray,
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
TE.chainFirst((environment) =>
|
||||
TE.fromTask(() =>
|
||||
this.pubsub.publish(
|
||||
`team_environment/${environment.teamID}/created`,
|
||||
<TeamEnvironment>{
|
||||
id: environment.id,
|
||||
name: environment.name,
|
||||
teamID: environment.teamID,
|
||||
variables: JSON.stringify(environment.variables),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
TE.map(
|
||||
(environment) =>
|
||||
<TeamEnvironment>{
|
||||
id: environment.id,
|
||||
name: environment.name,
|
||||
teamID: environment.teamID,
|
||||
variables: JSON.stringify(environment.variables),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
fetchAllTeamEnvironments(teamID: string) {
|
||||
return pipe(
|
||||
() =>
|
||||
this.prisma.teamEnvironment.findMany({
|
||||
where: {
|
||||
teamID: teamID,
|
||||
},
|
||||
}),
|
||||
T.map(
|
||||
A.map(
|
||||
(environment) =>
|
||||
<TeamEnvironment>{
|
||||
id: environment.id,
|
||||
name: environment.name,
|
||||
teamID: environment.teamID,
|
||||
variables: JSON.stringify(environment.variables),
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
return teamEnvironments;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,6 @@ export class TeamEnvsTeamResolver {
|
||||
description: 'Returns all Team Environments for the given Team',
|
||||
})
|
||||
teamEnvironments(@Parent() team: Team): Promise<TeamEnvironment[]> {
|
||||
return this.teamEnvironmentService.fetchAllTeamEnvironments(team.id)();
|
||||
return this.teamEnvironmentService.fetchAllTeamEnvironments(team.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ArgsType, Field, ID } from '@nestjs/graphql';
|
||||
import { TeamMemberRole } from 'src/team/team.model';
|
||||
|
||||
@ArgsType()
|
||||
export class CreateTeamInvitationArgs {
|
||||
@Field(() => ID, {
|
||||
name: 'teamID',
|
||||
description: 'ID of the Team ID to invite from',
|
||||
})
|
||||
teamID: string;
|
||||
|
||||
@Field({ name: 'inviteeEmail', description: 'Email of the user to invite' })
|
||||
inviteeEmail: string;
|
||||
|
||||
@Field(() => TeamMemberRole, {
|
||||
name: 'inviteeRole',
|
||||
description: 'Role to be given to the user',
|
||||
})
|
||||
inviteeRole: TeamMemberRole;
|
||||
}
|
||||
@@ -12,15 +12,10 @@ import { TeamInvitation } from './team-invitation.model';
|
||||
import { TeamInvitationService } from './team-invitation.service';
|
||||
import { pipe } from 'fp-ts/function';
|
||||
import * as TE from 'fp-ts/TaskEither';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import { Team, TeamMember, TeamMemberRole } from 'src/team/team.model';
|
||||
import { EmailCodec } from 'src/types/Email';
|
||||
import {
|
||||
INVALID_EMAIL,
|
||||
TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
||||
TEAM_INVITE_NO_INVITE_FOUND,
|
||||
USER_NOT_FOUND,
|
||||
} from 'src/errors';
|
||||
import { TEAM_INVITE_NO_INVITE_FOUND, USER_NOT_FOUND } from 'src/errors';
|
||||
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
||||
import { User } from 'src/user/user.model';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
@@ -36,6 +31,8 @@ import { UserService } from 'src/user/user.service';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||
import { SkipThrottle } from '@nestjs/throttler';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
import { CreateTeamInvitationArgs } from './input-type.args';
|
||||
|
||||
@UseGuards(GqlThrottlerGuard)
|
||||
@Resolver(() => TeamInvitation)
|
||||
@@ -79,8 +76,8 @@ export class TeamInvitationResolver {
|
||||
'Gets the Team Invitation with the given ID, or null if not exists',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, TeamInviteViewerGuard)
|
||||
teamInvitation(
|
||||
@GqlUser() user: User,
|
||||
async teamInvitation(
|
||||
@GqlUser() user: AuthUser,
|
||||
@Args({
|
||||
name: 'inviteID',
|
||||
description: 'ID of the Team Invitation to lookup',
|
||||
@@ -88,17 +85,11 @@ export class TeamInvitationResolver {
|
||||
})
|
||||
inviteID: string,
|
||||
): Promise<TeamInvitation> {
|
||||
return pipe(
|
||||
this.teamInvitationService.getInvitation(inviteID),
|
||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
||||
TE.chainW(
|
||||
TE.fromPredicate(
|
||||
(a) => a.inviteeEmail.toLowerCase() === user.email?.toLowerCase(),
|
||||
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
||||
),
|
||||
),
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
const teamInvitation = await this.teamInvitationService.getInvitation(
|
||||
inviteID,
|
||||
);
|
||||
if (O.isNone(teamInvitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
||||
return teamInvitation.value;
|
||||
}
|
||||
|
||||
@Mutation(() => TeamInvitation, {
|
||||
@@ -106,56 +97,19 @@ export class TeamInvitationResolver {
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlTeamMemberGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER)
|
||||
createTeamInvitation(
|
||||
@GqlUser()
|
||||
user: User,
|
||||
|
||||
@Args({
|
||||
name: 'teamID',
|
||||
description: 'ID of the Team ID to invite from',
|
||||
type: () => ID,
|
||||
})
|
||||
teamID: string,
|
||||
@Args({
|
||||
name: 'inviteeEmail',
|
||||
description: 'Email of the user to invite',
|
||||
})
|
||||
inviteeEmail: string,
|
||||
@Args({
|
||||
name: 'inviteeRole',
|
||||
type: () => TeamMemberRole,
|
||||
description: 'Role to be given to the user',
|
||||
})
|
||||
inviteeRole: TeamMemberRole,
|
||||
async createTeamInvitation(
|
||||
@GqlUser() user: AuthUser,
|
||||
@Args() args: CreateTeamInvitationArgs,
|
||||
): Promise<TeamInvitation> {
|
||||
return pipe(
|
||||
TE.Do,
|
||||
const teamInvitation = await this.teamInvitationService.createInvitation(
|
||||
user,
|
||||
args.teamID,
|
||||
args.inviteeEmail,
|
||||
args.inviteeRole,
|
||||
);
|
||||
|
||||
// Validate email
|
||||
TE.bindW('email', () =>
|
||||
pipe(
|
||||
EmailCodec.decode(inviteeEmail),
|
||||
TE.fromEither,
|
||||
TE.mapLeft(() => INVALID_EMAIL),
|
||||
),
|
||||
),
|
||||
|
||||
// Validate and get Team
|
||||
TE.bindW('team', () => this.teamService.getTeamWithIDTE(teamID)),
|
||||
|
||||
// Create team
|
||||
TE.chainW(({ email, team }) =>
|
||||
this.teamInvitationService.createInvitation(
|
||||
user,
|
||||
team,
|
||||
email,
|
||||
inviteeRole,
|
||||
),
|
||||
),
|
||||
|
||||
// If failed, throw err (so the message is passed) else return value
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
if (E.isLeft(teamInvitation)) throwErr(teamInvitation.left);
|
||||
return teamInvitation.right;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean, {
|
||||
@@ -163,7 +117,7 @@ export class TeamInvitationResolver {
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, TeamInviteTeamOwnerGuard)
|
||||
@RequiresTeamRole(TeamMemberRole.OWNER)
|
||||
revokeTeamInvitation(
|
||||
async revokeTeamInvitation(
|
||||
@Args({
|
||||
name: 'inviteID',
|
||||
type: () => ID,
|
||||
@@ -171,19 +125,19 @@ export class TeamInvitationResolver {
|
||||
})
|
||||
inviteID: string,
|
||||
): Promise<true> {
|
||||
return pipe(
|
||||
this.teamInvitationService.revokeInvitation(inviteID),
|
||||
TE.map(() => true as const),
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
const isRevoked = await this.teamInvitationService.revokeInvitation(
|
||||
inviteID,
|
||||
);
|
||||
if (E.isLeft(isRevoked)) throwErr(isRevoked.left);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Mutation(() => TeamMember, {
|
||||
description: 'Accept an Invitation',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, TeamInviteeGuard)
|
||||
acceptTeamInvitation(
|
||||
@GqlUser() user: User,
|
||||
async acceptTeamInvitation(
|
||||
@GqlUser() user: AuthUser,
|
||||
@Args({
|
||||
name: 'inviteID',
|
||||
type: () => ID,
|
||||
@@ -191,10 +145,12 @@ export class TeamInvitationResolver {
|
||||
})
|
||||
inviteID: string,
|
||||
): Promise<TeamMember> {
|
||||
return pipe(
|
||||
this.teamInvitationService.acceptInvitation(inviteID, user),
|
||||
TE.getOrElse(throwErr),
|
||||
)();
|
||||
const teamMember = await this.teamInvitationService.acceptInvitation(
|
||||
inviteID,
|
||||
user,
|
||||
);
|
||||
if (E.isLeft(teamMember)) throwErr(teamMember.left);
|
||||
return teamMember.right;
|
||||
}
|
||||
|
||||
// Subscriptions
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import * as T from 'fp-ts/Task';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import * as TO from 'fp-ts/TaskOption';
|
||||
import * as TE from 'fp-ts/TaskEither';
|
||||
import { pipe, flow, constVoid } from 'fp-ts/function';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { Team, TeamMemberRole } from 'src/team/team.model';
|
||||
import { Email } from 'src/types/Email';
|
||||
import { User } from 'src/user/user.model';
|
||||
import { TeamInvitation as DBTeamInvitation } from '@prisma/client';
|
||||
import { TeamMember, TeamMemberRole } from 'src/team/team.model';
|
||||
import { TeamService } from 'src/team/team.service';
|
||||
import {
|
||||
INVALID_EMAIL,
|
||||
TEAM_INVALID_ID,
|
||||
TEAM_INVITE_ALREADY_MEMBER,
|
||||
TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
||||
TEAM_INVITE_MEMBER_HAS_INVITE,
|
||||
TEAM_INVITE_NO_INVITE_FOUND,
|
||||
TEAM_MEMBER_NOT_FOUND,
|
||||
} from 'src/errors';
|
||||
import { TeamInvitation } from './team-invitation.model';
|
||||
import { MailerService } from 'src/mailer/mailer.service';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { validateEmail } from '../utils';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
|
||||
@Injectable()
|
||||
export class TeamInvitationService {
|
||||
@@ -29,245 +30,221 @@ export class TeamInvitationService {
|
||||
private readonly mailerService: MailerService,
|
||||
|
||||
private readonly pubsub: PubSubService,
|
||||
) {
|
||||
this.getInvitation = this.getInvitation.bind(this);
|
||||
}
|
||||
) {}
|
||||
|
||||
getInvitation(inviteID: string): TO.TaskOption<TeamInvitation> {
|
||||
return pipe(
|
||||
() =>
|
||||
this.prisma.teamInvitation.findUnique({
|
||||
where: {
|
||||
id: inviteID,
|
||||
},
|
||||
}),
|
||||
TO.fromTask,
|
||||
TO.chain(flow(O.fromNullable, TO.fromOption)),
|
||||
TO.map((x) => x as TeamInvitation),
|
||||
);
|
||||
}
|
||||
|
||||
getInvitationWithEmail(email: Email, team: Team) {
|
||||
return pipe(
|
||||
() =>
|
||||
this.prisma.teamInvitation.findUnique({
|
||||
where: {
|
||||
teamID_inviteeEmail: {
|
||||
inviteeEmail: email,
|
||||
teamID: team.id,
|
||||
},
|
||||
},
|
||||
}),
|
||||
TO.fromTask,
|
||||
TO.chain(flow(O.fromNullable, TO.fromOption)),
|
||||
);
|
||||
}
|
||||
|
||||
createInvitation(
|
||||
creator: User,
|
||||
team: Team,
|
||||
inviteeEmail: Email,
|
||||
inviteeRole: TeamMemberRole,
|
||||
) {
|
||||
return pipe(
|
||||
// Perform all validation checks
|
||||
TE.sequenceArray([
|
||||
// creator should be a TeamMember
|
||||
pipe(
|
||||
this.teamService.getTeamMemberTE(team.id, creator.uid),
|
||||
TE.map(constVoid),
|
||||
),
|
||||
|
||||
// Invitee should not be a team member
|
||||
pipe(
|
||||
async () => await this.userService.findUserByEmail(inviteeEmail),
|
||||
TO.foldW(
|
||||
() => TE.right(undefined), // If no user, short circuit to completion
|
||||
(user) =>
|
||||
pipe(
|
||||
// If user is found, check if team member
|
||||
this.teamService.getTeamMemberTE(team.id, user.uid),
|
||||
TE.foldW(
|
||||
() => TE.right(undefined), // Not team-member, this is good
|
||||
() => TE.left(TEAM_INVITE_ALREADY_MEMBER), // Is team member, not good
|
||||
),
|
||||
),
|
||||
),
|
||||
TE.map(constVoid),
|
||||
),
|
||||
|
||||
// Should not have an existing invite
|
||||
pipe(
|
||||
this.getInvitationWithEmail(inviteeEmail, team),
|
||||
TE.fromTaskOption(() => null),
|
||||
TE.swap,
|
||||
TE.map(constVoid),
|
||||
TE.mapLeft(() => TEAM_INVITE_MEMBER_HAS_INVITE),
|
||||
),
|
||||
]),
|
||||
|
||||
// Create the invitation
|
||||
TE.chainTaskK(
|
||||
() => () =>
|
||||
this.prisma.teamInvitation.create({
|
||||
data: {
|
||||
teamID: team.id,
|
||||
inviteeEmail,
|
||||
inviteeRole,
|
||||
creatorUid: creator.uid,
|
||||
},
|
||||
}),
|
||||
),
|
||||
|
||||
// Send email, this is a side effect
|
||||
TE.chainFirstTaskK((invitation) =>
|
||||
pipe(
|
||||
this.mailerService.sendMail(inviteeEmail, {
|
||||
template: 'team-invitation',
|
||||
variables: {
|
||||
invitee: creator.displayName ?? 'A Hoppscotch User',
|
||||
action_url: `https://hoppscotch.io/join-team?id=${invitation.id}`,
|
||||
invite_team_name: team.name,
|
||||
},
|
||||
}),
|
||||
|
||||
TE.getOrElseW(() => T.of(undefined)), // This value doesn't matter as we don't mind the return value (chainFirst) as long as the task completes
|
||||
),
|
||||
),
|
||||
|
||||
// Send PubSub topic
|
||||
TE.chainFirstTaskK((invitation) =>
|
||||
TE.fromTask(async () => {
|
||||
const inv: TeamInvitation = {
|
||||
id: invitation.id,
|
||||
teamID: invitation.teamID,
|
||||
creatorUid: invitation.creatorUid,
|
||||
inviteeEmail: invitation.inviteeEmail,
|
||||
inviteeRole: TeamMemberRole[invitation.inviteeRole],
|
||||
};
|
||||
|
||||
this.pubsub.publish(`team/${inv.teamID}/invite_added`, inv);
|
||||
}),
|
||||
),
|
||||
|
||||
// Map to model type
|
||||
TE.map((x) => x as TeamInvitation),
|
||||
);
|
||||
}
|
||||
|
||||
revokeInvitation(inviteID: string) {
|
||||
return pipe(
|
||||
// Make sure invite exists
|
||||
this.getInvitation(inviteID),
|
||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
||||
|
||||
// Delete team invitation
|
||||
TE.chainTaskK(
|
||||
() => () =>
|
||||
this.prisma.teamInvitation.delete({
|
||||
where: {
|
||||
id: inviteID,
|
||||
},
|
||||
}),
|
||||
),
|
||||
|
||||
// Emit Pubsub Event
|
||||
TE.chainFirst((invitation) =>
|
||||
TE.fromTask(() =>
|
||||
this.pubsub.publish(
|
||||
`team/${invitation.teamID}/invite_removed`,
|
||||
invitation.id,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// We are not returning anything
|
||||
TE.map(constVoid),
|
||||
);
|
||||
}
|
||||
|
||||
getAllInvitationsInTeam(team: Team) {
|
||||
return pipe(
|
||||
() =>
|
||||
this.prisma.teamInvitation.findMany({
|
||||
where: {
|
||||
teamID: team.id,
|
||||
},
|
||||
}),
|
||||
T.map((x) => x as TeamInvitation[]),
|
||||
);
|
||||
}
|
||||
|
||||
acceptInvitation(inviteID: string, acceptedBy: User) {
|
||||
return pipe(
|
||||
TE.Do,
|
||||
|
||||
// First get the invitation
|
||||
TE.bindW('invitation', () =>
|
||||
pipe(
|
||||
this.getInvitation(inviteID),
|
||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
||||
),
|
||||
),
|
||||
|
||||
// Validation checks
|
||||
TE.chainFirstW(({ invitation }) =>
|
||||
TE.sequenceArray([
|
||||
// Make sure the invited user is not part of the team
|
||||
pipe(
|
||||
this.teamService.getTeamMemberTE(invitation.teamID, acceptedBy.uid),
|
||||
TE.swap,
|
||||
TE.bimap(
|
||||
() => TEAM_INVITE_ALREADY_MEMBER,
|
||||
constVoid, // The return type is ignored
|
||||
),
|
||||
),
|
||||
|
||||
// Make sure the invited user and accepting user has the same email
|
||||
pipe(
|
||||
undefined,
|
||||
TE.fromPredicate(
|
||||
(a) => acceptedBy.email === invitation.inviteeEmail,
|
||||
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
|
||||
// Add the team member
|
||||
// TODO: Somehow bring subscriptions to this ?
|
||||
TE.bindW('teamMember', ({ invitation }) =>
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
() =>
|
||||
this.teamService.addMemberToTeam(
|
||||
invitation.teamID,
|
||||
acceptedBy.uid,
|
||||
invitation.inviteeRole,
|
||||
),
|
||||
() => TEAM_INVITE_ALREADY_MEMBER, // Can only fail if Team Member already exists, which we checked, but due to async lets assert that here too
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
TE.chainFirstW(({ invitation }) => this.revokeInvitation(invitation.id)),
|
||||
|
||||
TE.map(({ teamMember }) => teamMember),
|
||||
);
|
||||
/**
|
||||
* Cast a DBTeamInvitation to a TeamInvitation
|
||||
* @param dbTeamInvitation database TeamInvitation
|
||||
* @returns TeamInvitation model
|
||||
*/
|
||||
cast(dbTeamInvitation: DBTeamInvitation): TeamInvitation {
|
||||
return {
|
||||
...dbTeamInvitation,
|
||||
inviteeRole: TeamMemberRole[dbTeamInvitation.inviteeRole],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the count invitations for a given team.
|
||||
* @param teamID team id
|
||||
* @returns a count team invitations for a team
|
||||
* Get the team invite
|
||||
* @param inviteID invite id
|
||||
* @returns an Option of team invitation or none
|
||||
*/
|
||||
async getAllTeamInvitations(teamID: string) {
|
||||
const invitations = await this.prisma.teamInvitation.findMany({
|
||||
async getInvitation(inviteID: string) {
|
||||
try {
|
||||
const dbInvitation = await this.prisma.teamInvitation.findUniqueOrThrow({
|
||||
where: {
|
||||
id: inviteID,
|
||||
},
|
||||
});
|
||||
|
||||
return O.some(this.cast(dbInvitation));
|
||||
} catch (e) {
|
||||
return O.none;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the team invite for an invitee with email and teamID.
|
||||
* @param inviteeEmail invitee email
|
||||
* @param teamID team id
|
||||
* @returns an Either of team invitation for the invitee or error
|
||||
*/
|
||||
async getTeamInviteByEmailAndTeamID(inviteeEmail: string, teamID: string) {
|
||||
const isEmailValid = validateEmail(inviteeEmail);
|
||||
if (!isEmailValid) return E.left(INVALID_EMAIL);
|
||||
|
||||
try {
|
||||
const teamInvite = await this.prisma.teamInvitation.findUniqueOrThrow({
|
||||
where: {
|
||||
teamID_inviteeEmail: {
|
||||
inviteeEmail: inviteeEmail,
|
||||
teamID: teamID,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return E.right(teamInvite);
|
||||
} catch (e) {
|
||||
return E.left(TEAM_INVITE_NO_INVITE_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a team invitation
|
||||
* @param creator creator of the invitation
|
||||
* @param teamID team id
|
||||
* @param inviteeEmail invitee email
|
||||
* @param inviteeRole invitee role
|
||||
* @returns an Either of team invitation or error message
|
||||
*/
|
||||
async createInvitation(
|
||||
creator: AuthUser,
|
||||
teamID: string,
|
||||
inviteeEmail: string,
|
||||
inviteeRole: TeamMemberRole,
|
||||
) {
|
||||
// validate email
|
||||
const isEmailValid = validateEmail(inviteeEmail);
|
||||
if (!isEmailValid) return E.left(INVALID_EMAIL);
|
||||
|
||||
// team ID should valid
|
||||
const team = await this.teamService.getTeamWithID(teamID);
|
||||
if (!team) return E.left(TEAM_INVALID_ID);
|
||||
|
||||
// invitation creator should be a TeamMember
|
||||
const isTeamMember = await this.teamService.getTeamMember(
|
||||
team.id,
|
||||
creator.uid,
|
||||
);
|
||||
if (!isTeamMember) return E.left(TEAM_MEMBER_NOT_FOUND);
|
||||
|
||||
// Checking to see if the invitee is already part of the team or not
|
||||
const inviteeUser = await this.userService.findUserByEmail(inviteeEmail);
|
||||
if (O.isSome(inviteeUser)) {
|
||||
// invitee should not already a member
|
||||
const isTeamMember = await this.teamService.getTeamMember(
|
||||
team.id,
|
||||
inviteeUser.value.uid,
|
||||
);
|
||||
if (isTeamMember) return E.left(TEAM_INVITE_ALREADY_MEMBER);
|
||||
}
|
||||
|
||||
// check invitee already invited earlier or not
|
||||
const teamInvitation = await this.getTeamInviteByEmailAndTeamID(
|
||||
inviteeEmail,
|
||||
team.id,
|
||||
);
|
||||
if (E.isRight(teamInvitation)) return E.left(TEAM_INVITE_MEMBER_HAS_INVITE);
|
||||
|
||||
// create the invitation
|
||||
const dbInvitation = await this.prisma.teamInvitation.create({
|
||||
data: {
|
||||
teamID: team.id,
|
||||
inviteeEmail,
|
||||
inviteeRole,
|
||||
creatorUid: creator.uid,
|
||||
},
|
||||
});
|
||||
|
||||
await this.mailerService.sendEmail(inviteeEmail, {
|
||||
template: 'team-invitation',
|
||||
variables: {
|
||||
invitee: creator.displayName ?? 'A Hoppscotch User',
|
||||
action_url: `${process.env.VITE_BASE_URL}/join-team?id=${dbInvitation.id}`,
|
||||
invite_team_name: team.name,
|
||||
},
|
||||
});
|
||||
|
||||
const invitation = this.cast(dbInvitation);
|
||||
this.pubsub.publish(`team/${invitation.teamID}/invite_added`, invitation);
|
||||
|
||||
return E.right(invitation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke a team invitation
|
||||
* @param inviteID invite id
|
||||
* @returns an Either of true or error message
|
||||
*/
|
||||
async revokeInvitation(inviteID: string) {
|
||||
// check if the invite exists
|
||||
const invitation = await this.getInvitation(inviteID);
|
||||
if (O.isNone(invitation)) return E.left(TEAM_INVITE_NO_INVITE_FOUND);
|
||||
|
||||
// delete the invite
|
||||
await this.prisma.teamInvitation.delete({
|
||||
where: {
|
||||
id: inviteID,
|
||||
},
|
||||
});
|
||||
|
||||
this.pubsub.publish(
|
||||
`team/${invitation.value.teamID}/invite_removed`,
|
||||
invitation.value.id,
|
||||
);
|
||||
|
||||
return E.right(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept a team invitation
|
||||
* @param inviteID invite id
|
||||
* @param acceptedBy user who accepted the invitation
|
||||
* @returns an Either of team member or error message
|
||||
*/
|
||||
async acceptInvitation(inviteID: string, acceptedBy: AuthUser) {
|
||||
// check if the invite exists
|
||||
const invitation = await this.getInvitation(inviteID);
|
||||
if (O.isNone(invitation)) return E.left(TEAM_INVITE_NO_INVITE_FOUND);
|
||||
|
||||
// make sure the user is not already a member of the team
|
||||
const teamMemberInvitee = await this.teamService.getTeamMember(
|
||||
invitation.value.teamID,
|
||||
acceptedBy.uid,
|
||||
);
|
||||
if (teamMemberInvitee) return E.left(TEAM_INVITE_ALREADY_MEMBER);
|
||||
|
||||
// make sure the user is the same as the invitee
|
||||
if (
|
||||
acceptedBy.email.toLowerCase() !==
|
||||
invitation.value.inviteeEmail.toLowerCase()
|
||||
)
|
||||
return E.left(TEAM_INVITE_EMAIL_DO_NOT_MATCH);
|
||||
|
||||
// add the user to the team
|
||||
let teamMember: TeamMember;
|
||||
try {
|
||||
teamMember = await this.teamService.addMemberToTeam(
|
||||
invitation.value.teamID,
|
||||
acceptedBy.uid,
|
||||
invitation.value.inviteeRole,
|
||||
);
|
||||
} catch (e) {
|
||||
return E.left(TEAM_INVITE_ALREADY_MEMBER);
|
||||
}
|
||||
|
||||
// delete the invite
|
||||
await this.revokeInvitation(inviteID);
|
||||
|
||||
return E.right(teamMember);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all team invitations for a given team.
|
||||
* @param teamID team id
|
||||
* @returns array of team invitations for a team
|
||||
*/
|
||||
async getTeamInvitations(teamID: string) {
|
||||
const dbInvitations = await this.prisma.teamInvitation.findMany({
|
||||
where: {
|
||||
teamID: teamID,
|
||||
},
|
||||
});
|
||||
|
||||
const invitations: TeamInvitation[] = dbInvitations.map((dbInvitation) =>
|
||||
this.cast(dbInvitation),
|
||||
);
|
||||
|
||||
return invitations;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { pipe } from 'fp-ts/function';
|
||||
import { TeamService } from 'src/team/team.service';
|
||||
import { TeamInvitationService } from './team-invitation.service';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import * as T from 'fp-ts/Task';
|
||||
import * as TE from 'fp-ts/TaskEither';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
import {
|
||||
BUG_AUTH_NO_USER_CTX,
|
||||
BUG_TEAM_INVITE_NO_INVITE_ID,
|
||||
TEAM_INVITE_NO_INVITE_FOUND,
|
||||
TEAM_MEMBER_NOT_FOUND,
|
||||
TEAM_NOT_REQUIRED_ROLE,
|
||||
} from 'src/errors';
|
||||
import { User } from 'src/user/user.model';
|
||||
import { throwErr } from 'src/utils';
|
||||
import { TeamMemberRole } from 'src/team/team.model';
|
||||
|
||||
/**
|
||||
* This guard only allows team owner to execute the resolver
|
||||
*/
|
||||
@Injectable()
|
||||
export class TeamInviteTeamOwnerGuard implements CanActivate {
|
||||
constructor(
|
||||
@@ -24,48 +24,30 @@ export class TeamInviteTeamOwnerGuard implements CanActivate {
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
return pipe(
|
||||
TE.Do,
|
||||
// Get GQL context
|
||||
const gqlExecCtx = GqlExecutionContext.create(context);
|
||||
|
||||
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
|
||||
// Get user
|
||||
const { user } = gqlExecCtx.getContext().req;
|
||||
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
|
||||
|
||||
// Get the invite
|
||||
TE.bindW('invite', ({ gqlCtx }) =>
|
||||
pipe(
|
||||
O.fromNullable(gqlCtx.getArgs<{ inviteID?: string }>().inviteID),
|
||||
TE.fromOption(() => BUG_TEAM_INVITE_NO_INVITE_ID),
|
||||
TE.chainW((inviteID) =>
|
||||
pipe(
|
||||
this.teamInviteService.getInvitation(inviteID),
|
||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Get the invite
|
||||
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>();
|
||||
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID);
|
||||
|
||||
TE.bindW('user', ({ gqlCtx }) =>
|
||||
pipe(
|
||||
gqlCtx.getContext().req.user,
|
||||
O.fromNullable,
|
||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
||||
),
|
||||
),
|
||||
const invitation = await this.teamInviteService.getInvitation(inviteID);
|
||||
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
||||
|
||||
TE.bindW('userMember', ({ invite, user }) =>
|
||||
this.teamService.getTeamMemberTE(invite.teamID, user.uid),
|
||||
),
|
||||
// Fetch team member details of this user
|
||||
const teamMember = await this.teamService.getTeamMember(
|
||||
invitation.value.teamID,
|
||||
user.uid,
|
||||
);
|
||||
|
||||
TE.chainW(
|
||||
TE.fromPredicate(
|
||||
({ userMember }) => userMember.role === TeamMemberRole.OWNER,
|
||||
() => TEAM_NOT_REQUIRED_ROLE,
|
||||
),
|
||||
),
|
||||
if (!teamMember) throwErr(TEAM_MEMBER_NOT_FOUND);
|
||||
if (teamMember.role !== TeamMemberRole.OWNER)
|
||||
throwErr(TEAM_NOT_REQUIRED_ROLE);
|
||||
|
||||
TE.fold(
|
||||
(err) => throwErr(err),
|
||||
() => T.of(true),
|
||||
),
|
||||
)();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { TeamInvitationService } from './team-invitation.service';
|
||||
import { pipe, flow } from 'fp-ts/function';
|
||||
import * as TE from 'fp-ts/TaskEither';
|
||||
import * as T from 'fp-ts/Task';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
import {
|
||||
BUG_AUTH_NO_USER_CTX,
|
||||
BUG_TEAM_INVITE_NO_INVITE_ID,
|
||||
TEAM_INVITE_NOT_VALID_VIEWER,
|
||||
TEAM_INVITE_NO_INVITE_FOUND,
|
||||
TEAM_MEMBER_NOT_FOUND,
|
||||
} from 'src/errors';
|
||||
import { User } from 'src/user/user.model';
|
||||
import { throwErr } from 'src/utils';
|
||||
import { TeamService } from 'src/team/team.service';
|
||||
|
||||
/**
|
||||
* This guard only allows user to execute the resolver
|
||||
* 1. If user is invitee, allow
|
||||
* 2. Or else, if user is team member, allow
|
||||
*
|
||||
* TLDR: Allow if user is invitee or team member
|
||||
*/
|
||||
@Injectable()
|
||||
export class TeamInviteViewerGuard implements CanActivate {
|
||||
constructor(
|
||||
@@ -23,50 +26,32 @@ export class TeamInviteViewerGuard implements CanActivate {
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
return pipe(
|
||||
TE.Do,
|
||||
// Get GQL context
|
||||
const gqlExecCtx = GqlExecutionContext.create(context);
|
||||
|
||||
// Get GQL Context
|
||||
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
|
||||
// Get user
|
||||
const { user } = gqlExecCtx.getContext().req;
|
||||
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
|
||||
|
||||
// Get user
|
||||
TE.bindW('user', ({ gqlCtx }) =>
|
||||
pipe(
|
||||
O.fromNullable(gqlCtx.getContext<{ user?: User }>().user),
|
||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
||||
),
|
||||
),
|
||||
// Get the invite
|
||||
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>();
|
||||
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID);
|
||||
|
||||
// Get the invite
|
||||
TE.bindW('invite', ({ gqlCtx }) =>
|
||||
pipe(
|
||||
O.fromNullable(gqlCtx.getArgs<{ inviteID?: string }>().inviteID),
|
||||
TE.fromOption(() => BUG_TEAM_INVITE_NO_INVITE_ID),
|
||||
TE.chainW(
|
||||
flow(
|
||||
this.teamInviteService.getInvitation,
|
||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const invitation = await this.teamInviteService.getInvitation(inviteID);
|
||||
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
||||
|
||||
// Check if the user and the invite email match, else if we can resolver the user as a team member
|
||||
// any better solution ?
|
||||
TE.chainW(({ user, invite }) =>
|
||||
user.email?.toLowerCase() === invite.inviteeEmail.toLowerCase()
|
||||
? TE.of(true)
|
||||
: pipe(
|
||||
this.teamService.getTeamMemberTE(invite.teamID, user.uid),
|
||||
TE.map(() => true),
|
||||
),
|
||||
),
|
||||
// Check if the user and the invite email match, else if user is a team member
|
||||
if (
|
||||
user.email?.toLowerCase() !== invitation.value.inviteeEmail.toLowerCase()
|
||||
) {
|
||||
const teamMember = await this.teamService.getTeamMember(
|
||||
invitation.value.teamID,
|
||||
user.uid,
|
||||
);
|
||||
|
||||
TE.mapLeft((e) =>
|
||||
e === 'team/member_not_found' ? TEAM_INVITE_NOT_VALID_VIEWER : e,
|
||||
),
|
||||
if (!teamMember) throwErr(TEAM_MEMBER_NOT_FOUND);
|
||||
}
|
||||
|
||||
TE.fold(throwErr, () => T.of(true)),
|
||||
)();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { TeamInvitationService } from './team-invitation.service';
|
||||
import { pipe, flow } from 'fp-ts/function';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import * as T from 'fp-ts/Task';
|
||||
import * as TE from 'fp-ts/TaskEither';
|
||||
import { GqlExecutionContext } from '@nestjs/graphql';
|
||||
import { User } from 'src/user/user.model';
|
||||
import {
|
||||
BUG_AUTH_NO_USER_CTX,
|
||||
BUG_TEAM_INVITE_NO_INVITE_ID,
|
||||
@@ -24,44 +20,26 @@ export class TeamInviteeGuard implements CanActivate {
|
||||
constructor(private readonly teamInviteService: TeamInvitationService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
return pipe(
|
||||
TE.Do,
|
||||
// Get GQL Context
|
||||
const gqlExecCtx = GqlExecutionContext.create(context);
|
||||
|
||||
// Get execution context
|
||||
TE.bindW('gqlCtx', () => TE.of(GqlExecutionContext.create(context))),
|
||||
// Get user
|
||||
const { user } = gqlExecCtx.getContext().req;
|
||||
if (!user) throwErr(BUG_AUTH_NO_USER_CTX);
|
||||
|
||||
// Get user
|
||||
TE.bindW('user', ({ gqlCtx }) =>
|
||||
pipe(
|
||||
O.fromNullable(gqlCtx.getContext<{ user?: User }>().user),
|
||||
TE.fromOption(() => BUG_AUTH_NO_USER_CTX),
|
||||
),
|
||||
),
|
||||
// Get the invite
|
||||
const { inviteID } = gqlExecCtx.getArgs<{ inviteID: string }>();
|
||||
if (!inviteID) throwErr(BUG_TEAM_INVITE_NO_INVITE_ID);
|
||||
|
||||
// Get invite
|
||||
TE.bindW('invite', ({ gqlCtx }) =>
|
||||
pipe(
|
||||
O.fromNullable(gqlCtx.getArgs<{ inviteID?: string }>().inviteID),
|
||||
TE.fromOption(() => BUG_TEAM_INVITE_NO_INVITE_ID),
|
||||
TE.chainW(
|
||||
flow(
|
||||
this.teamInviteService.getInvitation,
|
||||
TE.fromTaskOption(() => TEAM_INVITE_NO_INVITE_FOUND),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const invitation = await this.teamInviteService.getInvitation(inviteID);
|
||||
if (O.isNone(invitation)) throwErr(TEAM_INVITE_NO_INVITE_FOUND);
|
||||
|
||||
// Check if the emails match
|
||||
TE.chainW(
|
||||
TE.fromPredicate(
|
||||
({ user, invite }) => user.email === invite.inviteeEmail,
|
||||
() => TEAM_INVITE_EMAIL_DO_NOT_MATCH,
|
||||
),
|
||||
),
|
||||
if (
|
||||
user.email.toLowerCase() !== invitation.value.inviteeEmail.toLowerCase()
|
||||
) {
|
||||
throwErr(TEAM_INVITE_EMAIL_DO_NOT_MATCH);
|
||||
}
|
||||
|
||||
// Fold it to a promise
|
||||
TE.fold(throwErr, () => T.of(true)),
|
||||
)();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@ export class TeamTeamInviteExtResolver {
|
||||
complexity: 10,
|
||||
})
|
||||
teamInvitations(@Parent() team: Team): Promise<TeamInvitation[]> {
|
||||
return this.teamInviteService.getAllInvitationsInTeam(team)();
|
||||
return this.teamInviteService.getTeamInvitations(team.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { HttpStatus } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
** Custom interface to handle errors specific to Auth module
|
||||
** Since its REST we need to return HTTP status code along with error message
|
||||
** Since its REST we need to return the HTTP status code along with the error message
|
||||
*/
|
||||
export type AuthError = {
|
||||
message: string;
|
||||
|
||||
@@ -360,13 +360,15 @@ describe('UserHistoryService', () => {
|
||||
});
|
||||
describe('removeRequestFromHistory', () => {
|
||||
test('Should resolve right and delete request from users history', async () => {
|
||||
const executedOn = new Date();
|
||||
|
||||
mockPrisma.userHistory.delete.mockResolvedValueOnce({
|
||||
userUid: 'abc',
|
||||
id: '1',
|
||||
request: [{}],
|
||||
responseMetadata: [{}],
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn: executedOn,
|
||||
isStarred: false,
|
||||
});
|
||||
|
||||
@@ -376,7 +378,7 @@ describe('UserHistoryService', () => {
|
||||
request: JSON.stringify([{}]),
|
||||
responseMetadata: JSON.stringify([{}]),
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn: executedOn,
|
||||
isStarred: false,
|
||||
};
|
||||
|
||||
@@ -384,7 +386,7 @@ describe('UserHistoryService', () => {
|
||||
await userHistoryService.removeRequestFromHistory('abc', '1'),
|
||||
).toEqualRight(userHistory);
|
||||
});
|
||||
test('Should resolve left and error out when req id is invalid ', async () => {
|
||||
test('Should resolve left and error out when req id is invalid', async () => {
|
||||
mockPrisma.userHistory.delete.mockResolvedValueOnce(null);
|
||||
|
||||
return expect(
|
||||
|
||||
@@ -6,7 +6,6 @@ module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
jest: true,
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
|
||||
1
packages/hoppscotch-common/assets/icons/star-off.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-star"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
|
||||
|
After Width: | Height: | Size: 337 B |
@@ -459,6 +459,14 @@ pre.ace_editor {
|
||||
@apply bg-dividerLight;
|
||||
}
|
||||
|
||||
.splitpanes--horizontal .splitpanes__pane {
|
||||
@apply transition-none;
|
||||
}
|
||||
|
||||
.splitpanes--vertical .splitpanes__pane {
|
||||
@apply transition-none;
|
||||
}
|
||||
|
||||
.cm-focused {
|
||||
@apply select-auto;
|
||||
@apply outline-none #{!important};
|
||||
|
||||
@@ -207,16 +207,19 @@
|
||||
:root.light {
|
||||
@include light-theme;
|
||||
@include light-editor-theme;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
:root.dark {
|
||||
@include dark-theme;
|
||||
@include dark-editor-theme;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root.black {
|
||||
@include black-theme;
|
||||
@include black-editor-theme;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-accent="blue"] {
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Reaksie liggaam",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Opskrifte",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Status",
|
||||
"time": "Tyd",
|
||||
"title": "Reaksie",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "wag vir verbinding",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "هيئة الاستجابة",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "الرؤوس",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "حالة",
|
||||
"time": "وقت",
|
||||
"title": "إجابة",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "في انتظار الاتصال",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "Visualitzar els meus enllaços"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Cos de resposta",
|
||||
"filter_response_body": "Filtrar el cos de la resposta JSON (utilitza la sintaxi JSONPath)",
|
||||
"headers": "Capçaleres",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Estat",
|
||||
"time": "Temps",
|
||||
"title": "Resposta",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "esperant la connexió",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"edit": "编辑",
|
||||
"filter": "过滤",
|
||||
"go_back": "返回",
|
||||
"go_forward": "Go forward",
|
||||
"go_forward": "前进",
|
||||
"group_by": "分组方式",
|
||||
"label": "标签",
|
||||
"learn_more": "了解更多",
|
||||
@@ -40,9 +40,9 @@
|
||||
"start": "开始",
|
||||
"starting": "正在开始",
|
||||
"stop": "停止",
|
||||
"to_close": "以关闭",
|
||||
"to_navigate": "以定位",
|
||||
"to_select": "以选择",
|
||||
"to_close": "关闭",
|
||||
"to_navigate": "定位",
|
||||
"to_select": "选择",
|
||||
"turn_off": "关闭",
|
||||
"turn_on": "开启",
|
||||
"undo": "撤消",
|
||||
@@ -118,16 +118,16 @@
|
||||
},
|
||||
"collection": {
|
||||
"created": "集合已创建",
|
||||
"different_parent": "Cannot reorder collection with different parent",
|
||||
"different_parent": "不能用不同的父类来重新排序集合",
|
||||
"edit": "编辑集合",
|
||||
"invalid_name": "请提供有效的集合名称",
|
||||
"invalid_root_move": "Collection already in the root",
|
||||
"moved": "Moved Successfully",
|
||||
"invalid_root_move": "该集合已经在根级了",
|
||||
"moved": "移动完成",
|
||||
"my_collections": "我的集合",
|
||||
"name": "我的新集合",
|
||||
"name_length_insufficient": "集合名字至少需要 3 个字符",
|
||||
"new": "新建集合",
|
||||
"order_changed": "Collection Order Updated",
|
||||
"order_changed": "集合顺序已更新",
|
||||
"renamed": "集合已更名",
|
||||
"request_in_use": "请求正在使用中",
|
||||
"save_as": "另存为",
|
||||
@@ -147,7 +147,7 @@
|
||||
"remove_team": "你确定要删除该团队吗?",
|
||||
"remove_telemetry": "你确定要退出遥测服务吗?",
|
||||
"request_change": "你确定你要放弃当前的请求,未保存的修改将被丢失。",
|
||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
||||
"save_unsaved_tab": "你想保存在此标签页中所作的修改吗?",
|
||||
"sync": "您确定要同步该工作区吗?"
|
||||
},
|
||||
"count": {
|
||||
@@ -177,7 +177,7 @@
|
||||
"members": "团队为空",
|
||||
"parameters": "该请求没有任何参数",
|
||||
"pending_invites": "此团队无待办邀请",
|
||||
"profile": "登录以查看你的个人档案",
|
||||
"profile": "登录以查看你的个人资料",
|
||||
"protocols": "协议为空",
|
||||
"schema": "连接至 GraphQL 端点",
|
||||
"shortcodes": "Shortcodes 为空",
|
||||
@@ -209,7 +209,7 @@
|
||||
"browser_support_sse": "该浏览器似乎不支持 SSE。",
|
||||
"check_console_details": "检查控制台日志以获悉详情",
|
||||
"curl_invalid_format": "cURL 格式不正确",
|
||||
"danger_zone": "Danger zone",
|
||||
"danger_zone": "危险区域",
|
||||
"delete_account": "您的帐号目前为这些团队的拥有者:",
|
||||
"delete_account_description": "您在删除帐号前必须先将您自己从团队中移除、转移拥有权,或是删除团队。",
|
||||
"empty_req_name": "空请求名称",
|
||||
@@ -219,7 +219,7 @@
|
||||
"incorrect_email": "电子邮箱错误",
|
||||
"invalid_link": "无效链接",
|
||||
"invalid_link_description": "你点击的链接无效或已过期。",
|
||||
"json_parsing_failed": "Invalid JSON",
|
||||
"json_parsing_failed": "不合法的 JSON",
|
||||
"json_prettify_invalid_body": "无法美化无效的请求头,处理 JSON 语法错误并重试",
|
||||
"network_error": "好像发生了网络错误,请重试。",
|
||||
"network_fail": "无法发送请求",
|
||||
@@ -316,14 +316,14 @@
|
||||
"zen_mode": "ZEN 模式"
|
||||
},
|
||||
"modal": {
|
||||
"close_unsaved_tab": "You have unsaved changes",
|
||||
"close_unsaved_tab": "有未保存的变更",
|
||||
"collections": "集合",
|
||||
"confirm": "确认",
|
||||
"edit_request": "编辑请求",
|
||||
"import_export": "导入/导出"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "您已经订阅了此主題。",
|
||||
"already_subscribed": "您已经订阅了此主题。",
|
||||
"clean_session": "清除会话",
|
||||
"clear_input": "清除输入",
|
||||
"clear_input_on_send": "发送后清除输入",
|
||||
@@ -355,7 +355,7 @@
|
||||
"navigation": {
|
||||
"doc": "文档",
|
||||
"graphql": "GraphQL",
|
||||
"profile": "个人档案",
|
||||
"profile": "个人资料",
|
||||
"realtime": "实时",
|
||||
"rest": "REST",
|
||||
"settings": "设置"
|
||||
@@ -377,7 +377,7 @@
|
||||
"owner_description": "所有者可以添加、编辑和删除请求、集合及团队成员。",
|
||||
"roles": "角色",
|
||||
"roles_description": "角色用以控制共享集合的访问权限。",
|
||||
"updated": "档案已更新",
|
||||
"updated": "已更新",
|
||||
"viewer": "查看者",
|
||||
"viewer_description": "查看者只可查看与使用请求。"
|
||||
},
|
||||
@@ -396,8 +396,8 @@
|
||||
"text": "文字"
|
||||
},
|
||||
"copy_link": "复制链接",
|
||||
"different_collection": "Cannot reorder requests from different collections",
|
||||
"duplicated": "Request duplicated",
|
||||
"different_collection": "不能对来自不同集合的请求进行重新排序",
|
||||
"duplicated": "重复的请求",
|
||||
"duration": "持续时间",
|
||||
"enter_curl": "输入 cURL",
|
||||
"generate_code": "生成代码",
|
||||
@@ -405,10 +405,10 @@
|
||||
"header_list": "请求头列表",
|
||||
"invalid_name": "请提供请求名称",
|
||||
"method": "方法",
|
||||
"moved": "Request moved",
|
||||
"moved": "请求移动完成",
|
||||
"name": "请求名称",
|
||||
"new": "新请求",
|
||||
"order_changed": "Request Order Updated",
|
||||
"order_changed": "请求顺序更新完成",
|
||||
"override": "覆盖",
|
||||
"override_help": "设置 <kbd>Content-Type</kbd> 头",
|
||||
"overriden": "覆盖",
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "查看我的链接"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "响应体",
|
||||
"filter_response_body": "筛选JSON响应本体(使用JSONPath语法)",
|
||||
"headers": "响应头",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "状态",
|
||||
"time": "时间",
|
||||
"title": "响应",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "等待连接",
|
||||
"xml": "XML"
|
||||
},
|
||||
@@ -479,10 +481,10 @@
|
||||
"language": "语言",
|
||||
"light_mode": "亮色",
|
||||
"official_proxy_hosting": "官方代理由 Hoppscotch 托管。",
|
||||
"profile": "个人档案",
|
||||
"profile_description": "更新你的档案详情",
|
||||
"profile": "个人资料",
|
||||
"profile_description": "更新你的资料",
|
||||
"profile_email": "电子邮箱地址",
|
||||
"profile_name": "档案名称",
|
||||
"profile_name": "名称",
|
||||
"proxy": "网络代理",
|
||||
"proxy_url": "代理网址",
|
||||
"proxy_use_toggle": "使用代理中间件发送请求",
|
||||
@@ -532,7 +534,7 @@
|
||||
"documentation": "前往文档页面",
|
||||
"forward": "前往下一页面",
|
||||
"graphql": "前往 GraphQL 页面",
|
||||
"profile": "前往个人档案页面",
|
||||
"profile": "前往个人资料页面",
|
||||
"realtime": "前往实时页面",
|
||||
"rest": "前往 REST 页面",
|
||||
"settings": "前往设置页面",
|
||||
@@ -574,7 +576,7 @@
|
||||
},
|
||||
"socketio": {
|
||||
"communication": "通讯",
|
||||
"connection_not_authorized": "此SocketIO连接未使用任何验证。",
|
||||
"connection_not_authorized": "此 SocketIO 连接未使用任何验证。",
|
||||
"event_name": "事件名称",
|
||||
"events": "事件",
|
||||
"log": "日志",
|
||||
@@ -614,12 +616,12 @@
|
||||
"none": "无",
|
||||
"nothing_found": "没有找到",
|
||||
"published_error": "将信息:{topic}发布至主题:{message}时发生错误",
|
||||
"published_message": "已将此信息:{message}发布至主题:{topic}",
|
||||
"published_message": "已将此信息:{message} 发布至主题:{topic}",
|
||||
"reconnection_error": "重连失败",
|
||||
"subscribed_failed": "无法订阅此主題:{topic}",
|
||||
"subscribed_success": "成功订阅此主題:{topic}",
|
||||
"unsubscribed_failed": "无法取消订阅此主題:{topic}",
|
||||
"unsubscribed_success": "成功取消订阅此主題:{topic}",
|
||||
"subscribed_failed": "无法订阅此主题:{topic}",
|
||||
"subscribed_success": "成功订阅此主题:{topic}",
|
||||
"unsubscribed_failed": "无法取消订阅此主题:{topic}",
|
||||
"unsubscribed_success": "成功取消订阅此主题:{topic}",
|
||||
"waiting_send_request": "等待发送请求"
|
||||
},
|
||||
"support": {
|
||||
@@ -639,7 +641,7 @@
|
||||
"body": "请求体",
|
||||
"collections": "集合",
|
||||
"documentation": "帮助文档",
|
||||
"environments": "Environments",
|
||||
"environments": "环境",
|
||||
"headers": "请求头",
|
||||
"history": "历史记录",
|
||||
"mqtt": "MQTT",
|
||||
@@ -664,7 +666,7 @@
|
||||
"email_do_not_match": "邮箱无法与你的帐户信息匹配。请联系你的团队者。",
|
||||
"exit": "退出团队",
|
||||
"exit_disabled": "团队所有者无法退出团队",
|
||||
"invalid_coll_id": "Invalid collection ID",
|
||||
"invalid_coll_id": "无效的集合 ID",
|
||||
"invalid_email_format": "电子邮箱格式无效",
|
||||
"invalid_id": "无效的团队 ID,请联系你的团队者。",
|
||||
"invalid_invite_link": "无效的邀请链接",
|
||||
@@ -688,7 +690,7 @@
|
||||
"member_removed": "用户已移除",
|
||||
"member_role_updated": "用户角色已更新",
|
||||
"members": "成员",
|
||||
"more_members": "+{count} more",
|
||||
"more_members": "+{count} 更多",
|
||||
"name_length_insufficient": "团队名称至少为 6 个字符",
|
||||
"name_updated": "团队名称已更新",
|
||||
"new": "新团队",
|
||||
@@ -696,13 +698,13 @@
|
||||
"new_name": "我的新团队",
|
||||
"no_access": "你没有编辑集合的权限",
|
||||
"no_invite_found": "未找到邀请。请联系你的团队者。",
|
||||
"no_request_found": "Request not found.",
|
||||
"no_request_found": "请求不存在",
|
||||
"not_found": "没有找到团队,请联系您的团队所有者。",
|
||||
"not_valid_viewer": "你不是有效的查看者。请联系你的团队者。",
|
||||
"parent_coll_move": "Cannot move collection to a child collection",
|
||||
"parent_coll_move": "不能将集合移动到一个子集合",
|
||||
"pending_invites": "待办邀请",
|
||||
"permissions": "权限",
|
||||
"same_target_destination": "Same target and destination",
|
||||
"same_target_destination": "目标相同",
|
||||
"saved": "团队已保存",
|
||||
"select_a_team": "选择团队",
|
||||
"title": "团队",
|
||||
@@ -732,9 +734,9 @@
|
||||
"url": "URL"
|
||||
},
|
||||
"workspace": {
|
||||
"change": "Change workspace",
|
||||
"personal": "My Workspace",
|
||||
"team": "Team Workspace",
|
||||
"title": "Workspaces"
|
||||
"change": "切换工作空间",
|
||||
"personal": "我的工作空间",
|
||||
"team": "团队工作空间",
|
||||
"title": "工作空间"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Odpovědní orgán",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Záhlaví",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Postavení",
|
||||
"time": "Čas",
|
||||
"title": "Odezva",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "čekání na připojení",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Svarorgan",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Overskrifter",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Status",
|
||||
"time": "Tid",
|
||||
"title": "Respons",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "venter på forbindelse",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Antworttext",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Header",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Status",
|
||||
"time": "Zeit",
|
||||
"title": "Antwort",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "auf Verbindung warten",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "Προβολή των links μου"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Σώμα απόκρισης",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Κεφαλίδες",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Κατάσταση",
|
||||
"time": "χρόνος",
|
||||
"title": "Απάντηση",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "περιμένοντας τη σύνδεση",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"cancel": "Cancel",
|
||||
"choose_file": "Choose a file",
|
||||
"clear": "Clear",
|
||||
"clear_history": "Clear All History",
|
||||
"clear_all": "Clear all",
|
||||
"close": "Close",
|
||||
"connect": "Connect",
|
||||
@@ -173,6 +174,7 @@
|
||||
"folder": "Folder is empty",
|
||||
"headers": "This request does not have any headers",
|
||||
"history": "History is empty",
|
||||
"history_suggestions": "History does not have any matching entries",
|
||||
"invites": "Invite list is empty",
|
||||
"members": "Team is empty",
|
||||
"parameters": "This request does not have any parameters",
|
||||
@@ -432,6 +434,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Response Body",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Headers",
|
||||
@@ -445,6 +448,7 @@
|
||||
"status": "Status",
|
||||
"time": "Time",
|
||||
"title": "Response",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "waiting for connection",
|
||||
"xml": "XML"
|
||||
},
|
||||
@@ -580,6 +584,11 @@
|
||||
"log": "Log",
|
||||
"url": "URL"
|
||||
},
|
||||
"spotlight": {
|
||||
"section": {
|
||||
"user": "User"
|
||||
}
|
||||
},
|
||||
"sse": {
|
||||
"event_type": "Event type",
|
||||
"log": "Log",
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
{
|
||||
"action": {
|
||||
"autoscroll": "Autoscroll",
|
||||
"autoscroll": "Desplazamiento automático",
|
||||
"cancel": "Cancelar",
|
||||
"choose_file": "Seleccionar archivo",
|
||||
"clear": "Limpiar",
|
||||
"clear_all": "Limpiar todo",
|
||||
"close": "Cerrar",
|
||||
"connect": "Conectar",
|
||||
"connecting": "Connecting",
|
||||
"connecting": "Conectando",
|
||||
"copy": "Copiar",
|
||||
"delete": "Borrar",
|
||||
"disconnect": "Desconectar",
|
||||
"dismiss": "Descartar",
|
||||
"dont_save": "Don't save",
|
||||
"dont_save": "No guardar",
|
||||
"download_file": "Descargar archivo",
|
||||
"drag_to_reorder": "Arrastrar para reordenar",
|
||||
"duplicate": "Duplicar",
|
||||
"edit": "Editar",
|
||||
"filter": "Filter",
|
||||
"filter": "Filtrar",
|
||||
"go_back": "Volver",
|
||||
"go_forward": "Go forward",
|
||||
"group_by": "Group by",
|
||||
"go_forward": "Adelante",
|
||||
"group_by": "Agrupar por",
|
||||
"label": "Etiqueta",
|
||||
"learn_more": "Aprender más",
|
||||
"less": "Menos",
|
||||
"more": "Más",
|
||||
"new": "Nuevo",
|
||||
"no": "No",
|
||||
"open_workspace": "Open workspace",
|
||||
"open_workspace": "Abrir espacio de trabajo",
|
||||
"paste": "Pegar",
|
||||
"prettify": "Embellecer",
|
||||
"remove": "Eliminar",
|
||||
"restore": "Restaurar",
|
||||
"save": "Guardar",
|
||||
"scroll_to_bottom": "Scroll to bottom",
|
||||
"scroll_to_top": "Scroll to top",
|
||||
"scroll_to_bottom": "Desplazar hacia abajo",
|
||||
"scroll_to_top": "Desplazar hacia arriba",
|
||||
"search": "Buscar",
|
||||
"send": "Enviar",
|
||||
"start": "Comenzar",
|
||||
"starting": "Starting",
|
||||
"starting": "Iniciando",
|
||||
"stop": "Detener",
|
||||
"to_close": "para cerrar",
|
||||
"to_navigate": "para navegar",
|
||||
@@ -56,16 +56,16 @@
|
||||
"chat_with_us": "Habla con nosotros",
|
||||
"contact_us": "Contáctanos",
|
||||
"copy": "Copiar",
|
||||
"copy_user_id": "Copy User Auth Token",
|
||||
"developer_option": "Developer options",
|
||||
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
|
||||
"copy_user_id": "Copiar token de autenticación de usuario",
|
||||
"developer_option": "Opciones para desarrolladores",
|
||||
"developer_option_description": "Herramientas para desarrolladores que ayudan en el desarrollo y mantenimiento de Hoppscotch.",
|
||||
"discord": "Discord",
|
||||
"documentation": "Documentación",
|
||||
"github": "GitHub",
|
||||
"help": "Ayuda y comentarios",
|
||||
"home": "Inicio",
|
||||
"invite": "Invitar",
|
||||
"invite_description": "En Hoppscotch, diseñamos una interfaz simple e intuitiva para crear y administrar sus APIs. Hoppscotch es una herramienta que le ayuda a crear, probar, documentar y compartir sus APIs.",
|
||||
"invite_description": "En Hoppscotch, diseñamos una interfaz simple e intuitiva para crear y administrar tus APIs. Hoppscotch es una herramienta que le ayuda a crear, probar, documentar y compartir tus APIs.",
|
||||
"invite_your_friends": "Invita a tus amigos",
|
||||
"join_discord_community": "Únete a nuestra comunidad Discord",
|
||||
"keyboard_shortcuts": "Atajos de teclado",
|
||||
@@ -79,7 +79,7 @@
|
||||
"shortcuts": "Atajos",
|
||||
"spotlight": "Destacar",
|
||||
"status": "Estado",
|
||||
"status_description": "Check the status of the website",
|
||||
"status_description": "Comprobar el estado del sitio web",
|
||||
"terms_and_privacy": "Términos y privacidad",
|
||||
"twitter": "Twitter",
|
||||
"type_a_command_search": "Escribe un comando o buscar algo…",
|
||||
@@ -118,18 +118,18 @@
|
||||
},
|
||||
"collection": {
|
||||
"created": "Colección creada",
|
||||
"different_parent": "Cannot reorder collection with different parent",
|
||||
"different_parent": "No se puede reordenar la colección con un padre diferente",
|
||||
"edit": "Editar colección",
|
||||
"invalid_name": "Proporciona un nombre válido para la colección.",
|
||||
"invalid_root_move": "Collection already in the root",
|
||||
"moved": "Moved Successfully",
|
||||
"invalid_root_move": "La colección ya está en la raíz",
|
||||
"moved": "Movido con éxito",
|
||||
"my_collections": "Mis colecciones",
|
||||
"name": "Mi nueva colección",
|
||||
"name_length_insufficient": "El nombre de la colección debe tener al menos 3 caracteres",
|
||||
"new": "Nueva colección",
|
||||
"order_changed": "Collection Order Updated",
|
||||
"order_changed": "Orden de colección actualizada",
|
||||
"renamed": "Colección renombrada",
|
||||
"request_in_use": "Petición en uso",
|
||||
"request_in_use": "Solicitud en uso",
|
||||
"save_as": "Guardar como",
|
||||
"select": "Seleccionar colección",
|
||||
"select_location": "Seleccionar ubicación",
|
||||
@@ -138,17 +138,17 @@
|
||||
},
|
||||
"confirm": {
|
||||
"exit_team": "¿Estás seguro de que quieres dejar este equipo?",
|
||||
"logout": "¿Está seguro de que desea cerrar la sesión?",
|
||||
"remove_collection": "¿Está seguro de que desea eliminar esta colección de forma permanente?",
|
||||
"remove_environment": "¿Está seguro de que desea eliminar este entorno de forma permanente?",
|
||||
"remove_folder": "¿Está seguro de que desea eliminar esta carpeta de forma permanente?",
|
||||
"remove_history": "¿Está seguro de que desea eliminar todo el historial de forma permanente?",
|
||||
"remove_request": "¿Está seguro de que desea eliminar esta petición de forma permanente?",
|
||||
"remove_team": "¿Está seguro de que desea eliminar este equipo?",
|
||||
"remove_telemetry": "¿Está seguro de que desea darse de baja de la telemetría?",
|
||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
||||
"sync": "¿Está seguro de que desea sincronizar este espacio de trabajo?"
|
||||
"logout": "¿Estás seguro de que deseas cerrar la sesión?",
|
||||
"remove_collection": "¿Estás seguro de que deseas eliminar esta colección de forma permanente?",
|
||||
"remove_environment": "¿Estás seguro de que deseas eliminar este entorno de forma permanente?",
|
||||
"remove_folder": "¿Estás seguro de que deseas eliminar esta carpeta de forma permanente?",
|
||||
"remove_history": "¿Estás seguro de que deseas eliminar todo el historial de forma permanente?",
|
||||
"remove_request": "¿Estás seguro de que deseas eliminar esta solicitud de forma permanente?",
|
||||
"remove_team": "¿Estás seguro de que deseas eliminar este equipo?",
|
||||
"remove_telemetry": "¿Estás seguro de que deseas darse de baja de la telemetría?",
|
||||
"request_change": "¿Estás seguro de que deseas descartar la solicitud actual, los cambios no guardados se perderán.",
|
||||
"save_unsaved_tab": "¿Deseas guardar los cambios realizados en esta pestaña?",
|
||||
"sync": "¿Estás seguro de que deseas sincronizar este espacio de trabajo?"
|
||||
},
|
||||
"count": {
|
||||
"header": "Encabezado {count}",
|
||||
@@ -163,28 +163,28 @@
|
||||
"generate_message": "Importar cualquier colección de Hoppscotch para generar documentación de la API sobre la marcha."
|
||||
},
|
||||
"empty": {
|
||||
"authorization": "Esta petición no utiliza ninguna autorización.",
|
||||
"body": "Esta petición no tiene cuerpo",
|
||||
"authorization": "Esta solicitud no utiliza ninguna autorización.",
|
||||
"body": "Esta solicitud no tiene cuerpo",
|
||||
"collection": "La colección está vacía",
|
||||
"collections": "Las colecciones están vacías",
|
||||
"documentation": "Conectarse a un punto final de GraphQL para ver la documentación",
|
||||
"endpoint": "El punto final no puede estar vacío",
|
||||
"environments": "Los entornos están vacíos",
|
||||
"folder": "La carpeta está vacía",
|
||||
"headers": "Esta petición no tiene encabezados",
|
||||
"headers": "Esta solicitud no tiene encabezados",
|
||||
"history": "El historial está vacío",
|
||||
"invites": "La lista de invitados está vacía",
|
||||
"members": "El equipo está vacío",
|
||||
"parameters": "Esta petición no tiene ningún parámetro",
|
||||
"parameters": "Esta solicitud no tiene ningún parámetro",
|
||||
"pending_invites": "No hay invitaciones pendientes para este equipo",
|
||||
"profile": "Iniciar sesión para ver tu perfil",
|
||||
"protocols": "Los protocolos están vacíos",
|
||||
"schema": "Conectarse a un punto final de GraphQL",
|
||||
"shortcodes": "Los shortcodes están vacíos",
|
||||
"shortcodes": "Aún no se han creado Shortcodes",
|
||||
"subscription": "Subscriptions are empty",
|
||||
"team_name": "Nombre del equipo vacío",
|
||||
"teams": "Los equipos están vacíos",
|
||||
"tests": "No hay pruebas para esta petición"
|
||||
"tests": "No hay pruebas para esta solicitud"
|
||||
},
|
||||
"environment": {
|
||||
"add_to_global": "Añadir a Global",
|
||||
@@ -194,38 +194,38 @@
|
||||
"deleted": "Eliminar el entorno",
|
||||
"edit": "Editar entorno",
|
||||
"invalid_name": "Proporciona un nombre válido para el entorno.",
|
||||
"my_environments": "My Environments",
|
||||
"my_environments": "Mis entornos",
|
||||
"nested_overflow": "las variables de entorno anidadas están limitadas a 10 niveles",
|
||||
"new": "Nuevo entorno",
|
||||
"no_environment": "Sin entorno",
|
||||
"no_environment_description": "No se ha seleccionado ningún entorno. Elije qué hacer con las siguientes variables.",
|
||||
"select": "Seleccionar entorno",
|
||||
"team_environments": "Team Environments",
|
||||
"team_environments": "Entornos de trabajo en equipo",
|
||||
"title": "Entornos",
|
||||
"updated": "Actualización del entorno",
|
||||
"updated": "Entorno actualizado",
|
||||
"variable_list": "Lista de variables"
|
||||
},
|
||||
"error": {
|
||||
"browser_support_sse": "Este navegador no parece ser compatible con los eventos enviados por el servidor.",
|
||||
"check_console_details": "Consulta el registro de la consola para obtener más detalles.",
|
||||
"curl_invalid_format": "cURL no está formateado correctamente",
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "Your account is currently an owner in these teams:",
|
||||
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
|
||||
"empty_req_name": "Nombre de petición vacío",
|
||||
"danger_zone": "Zona de peligro",
|
||||
"delete_account": "Tu cuenta es actualmente propietaria en estos equipos:",
|
||||
"delete_account_description": "Para poder eliminar tu cuenta, debes darte de baja, transferir la propiedad o eliminar estos equipos.",
|
||||
"empty_req_name": "Nombre de solicitud vacío",
|
||||
"f12_details": "(F12 para más detalles)",
|
||||
"gql_prettify_invalid_query": "No se puede aplicar embellecedor a una consulta no válida, resuelve los errores de sintaxis de la consulta y vuelve a intentarlo",
|
||||
"incomplete_config_urls": "URLs de configuración incompletas",
|
||||
"incorrect_email": "Correo electrónico incorrecto",
|
||||
"invalid_link": "Enlace no válido",
|
||||
"invalid_link_description": "El enlace que has pulsado no es válido o ha caducado.",
|
||||
"json_parsing_failed": "Invalid JSON",
|
||||
"json_parsing_failed": "JSON no válido",
|
||||
"json_prettify_invalid_body": "No se puede aplicar embellecedor a un cuerpo inválido, resuelve errores de sintaxis json y vuelve a intentarlo",
|
||||
"network_error": "Parece que hay un error de red. Por favor, inténtalo de nuevo.",
|
||||
"network_fail": "No se pudo enviar la petición",
|
||||
"network_fail": "No se pudo enviar la solicitud",
|
||||
"no_duration": "Sin duración",
|
||||
"no_results_found": "No matches found",
|
||||
"page_not_found": "This page could not be found",
|
||||
"no_results_found": "No se han encontrado coincidencias",
|
||||
"page_not_found": "No se ha podido encontrar esta página",
|
||||
"script_fail": "No se pudo ejecutar el script de solicitud previa",
|
||||
"something_went_wrong": "Algo salió mal",
|
||||
"test_script_fail": "No se ha podido ejecutar la secuencia de comandos posterior a la solicitud"
|
||||
@@ -256,7 +256,7 @@
|
||||
"subscriptions": "Suscripciones"
|
||||
},
|
||||
"group": {
|
||||
"time": "Time",
|
||||
"time": "Tiempo",
|
||||
"url": "URL"
|
||||
},
|
||||
"header": {
|
||||
@@ -265,19 +265,19 @@
|
||||
"save_workspace": "Guardar mi espacio de trabajo"
|
||||
},
|
||||
"helpers": {
|
||||
"authorization": "El encabezado de autorización se generará automáticamente cuando se envía la petición.",
|
||||
"authorization": "El encabezado de autorización se generará automáticamente cuando se envía la solicitud.",
|
||||
"generate_documentation_first": "Generar la documentación primero",
|
||||
"network_fail": "No se puede acceder a la API. Comprueba tu conexión de red y vuelve a intentarlo.",
|
||||
"offline": "Parece estar desconectado. Es posible que los datos de este espacio de trabajo no estén actualizados.",
|
||||
"offline_short": "Pareces estar desconectado.",
|
||||
"post_request_tests": "Los scripts de prueba están escritos en JavaScript y se ejecutan después de recibir la respuesta.",
|
||||
"pre_request_script": "Los scripts previos a la petición están escritos en JavaScript y se ejecutan antes de que se envíe la petición.",
|
||||
"pre_request_script": "Los scripts previos a la solicitud están escritos en JavaScript y se ejecutan antes de que se envíe la solicitud.",
|
||||
"script_fail": "Parece que hay un problema técnico en el script de solicitud previa. Comprueba el error a continuación y corrige el script en consecuencia.",
|
||||
"test_script_fail": "Parece que hay un error con el script de prueba. Por favor, corrige los errores y ejecute las pruebas de nuevo",
|
||||
"tests": "Escribir un script de prueba para automatizar la depuración."
|
||||
},
|
||||
"hide": {
|
||||
"collection": "Collapse Collection Panel",
|
||||
"collection": "Colapsar el panel de colecciones",
|
||||
"more": "Ocultar más",
|
||||
"preview": "Ocultar vista previa",
|
||||
"sidebar": "Ocultar barra lateral"
|
||||
@@ -308,40 +308,40 @@
|
||||
"title": "Importar"
|
||||
},
|
||||
"layout": {
|
||||
"collapse_collection": "Collapse or Expand Collections",
|
||||
"collapse_sidebar": "Collapse or Expand the sidebar",
|
||||
"collapse_collection": "Contraer o expandir colecciones",
|
||||
"collapse_sidebar": "Contraer o expandir la barra lateral",
|
||||
"column": "Disposición vertical",
|
||||
"name": "Layout",
|
||||
"name": "Diseño",
|
||||
"row": "Disposición horizontal",
|
||||
"zen_mode": "Modo zen"
|
||||
},
|
||||
"modal": {
|
||||
"close_unsaved_tab": "You have unsaved changes",
|
||||
"close_unsaved_tab": "Tienes cambios sin guardar",
|
||||
"collections": "Colecciones",
|
||||
"confirm": "Confirmar",
|
||||
"edit_request": "Editar petición",
|
||||
"edit_request": "Editar solicitud",
|
||||
"import_export": "Importación y exportación"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "You are already subscribed to this topic.",
|
||||
"clean_session": "Clean Session",
|
||||
"clear_input": "Clear input",
|
||||
"clear_input_on_send": "Clear input on send",
|
||||
"client_id": "Client ID",
|
||||
"color": "Pick a color",
|
||||
"already_subscribed": "Ya estás suscrito a este tema.",
|
||||
"clean_session": "Borrar sesión",
|
||||
"clear_input": "Borrar entrada",
|
||||
"clear_input_on_send": "Borrar entrada al enviar",
|
||||
"client_id": "Identificación del cliente",
|
||||
"color": "Elige un color",
|
||||
"communication": "Comunicación",
|
||||
"connection_config": "Connection Config",
|
||||
"connection_not_authorized": "This MQTT connection does not use any authentication.",
|
||||
"invalid_topic": "Please provide a topic for the subscription",
|
||||
"keep_alive": "Keep Alive",
|
||||
"connection_config": "Configuración de conexión",
|
||||
"connection_not_authorized": "Esta conexión MQTT no utiliza ninguna autenticación.",
|
||||
"invalid_topic": "Indica un tema para la suscripción",
|
||||
"keep_alive": "Mantenerse vivo",
|
||||
"log": "Registro",
|
||||
"lw_message": "Last-Will Message",
|
||||
"lw_qos": "Last-Will QoS",
|
||||
"lw_retain": "Last-Will Retain",
|
||||
"lw_topic": "Last-Will Topic",
|
||||
"lw_message": "Mensaje de última voluntad",
|
||||
"lw_qos": "QoS de última voluntad",
|
||||
"lw_retain": "Última voluntad",
|
||||
"lw_topic": "Tema de última voluntad",
|
||||
"message": "Mensaje",
|
||||
"new": "New Subscription",
|
||||
"not_connected": "Please start a MQTT connection first.",
|
||||
"new": "Nueva suscripción",
|
||||
"not_connected": "Por favor, inicia primero una conexión MQTT.",
|
||||
"publish": "Publicar",
|
||||
"qos": "QoS",
|
||||
"ssl": "SSL",
|
||||
@@ -353,7 +353,7 @@
|
||||
"url": "URL"
|
||||
},
|
||||
"navigation": {
|
||||
"doc": "Docs",
|
||||
"doc": "Documentación",
|
||||
"graphql": "GraphQL",
|
||||
"profile": "Perfil",
|
||||
"realtime": "Tiempo real",
|
||||
@@ -363,7 +363,7 @@
|
||||
"preRequest": {
|
||||
"javascript_code": "Código JavaScript",
|
||||
"learn": "Leer documentación",
|
||||
"script": "Script previo a la petición",
|
||||
"script": "Script previo a la solicitud",
|
||||
"snippets": "Fragmentos"
|
||||
},
|
||||
"profile": {
|
||||
@@ -385,55 +385,56 @@
|
||||
"star": "Eliminar estrella"
|
||||
},
|
||||
"request": {
|
||||
"added": "Petición agregada",
|
||||
"added": "Solicitud agregada",
|
||||
"authorization": "Autorización",
|
||||
"body": "Cuerpo de la petición",
|
||||
"body": "Cuerpo de la solicitud",
|
||||
"choose_language": "Seleccionar lenguaje",
|
||||
"content_type": "Tipo de contenido",
|
||||
"content_type_titles": {
|
||||
"others": "Others",
|
||||
"structured": "Structured",
|
||||
"text": "Text"
|
||||
"others": "Otros",
|
||||
"structured": "Estructurado",
|
||||
"text": "Texto"
|
||||
},
|
||||
"copy_link": "Copiar enlace",
|
||||
"different_collection": "Cannot reorder requests from different collections",
|
||||
"duplicated": "Request duplicated",
|
||||
"different_collection": "No se pueden reordenar solicitudes de diferentes colecciones",
|
||||
"duplicated": "Solicitud duplicada",
|
||||
"duration": "Duración",
|
||||
"enter_curl": "Ingrese cURL",
|
||||
"generate_code": "Generar código",
|
||||
"generated_code": "Código generado",
|
||||
"header_list": "Lista de encabezados",
|
||||
"invalid_name": "Proporciona un nombre para la petición.",
|
||||
"invalid_name": "Proporciona un nombre para la solicitud.",
|
||||
"method": "Método",
|
||||
"moved": "Request moved",
|
||||
"name": "Nombre de petición",
|
||||
"new": "New Request",
|
||||
"order_changed": "Request Order Updated",
|
||||
"override": "Override",
|
||||
"override_help": "Set <kbd>Content-Type</kbd> in Headers",
|
||||
"overriden": "Overridden",
|
||||
"name": "Nombre de solicitud",
|
||||
"new": "Nueva solicitud",
|
||||
"order_changed": "Orden de solicitudes actualizadas",
|
||||
"override": "Anular",
|
||||
"override_help": "Establecer <kbd>Content-Type</kbd> en las cabeceras",
|
||||
"overriden": "Anulado",
|
||||
"parameter_list": "Parámetros de consulta",
|
||||
"parameters": "Parámetros",
|
||||
"path": "Ruta",
|
||||
"payload": "Carga útil",
|
||||
"query": "Consulta",
|
||||
"raw_body": "Cuerpo de petición sin procesar",
|
||||
"renamed": "Petición renombrada",
|
||||
"raw_body": "Cuerpo de solicitud sin procesar",
|
||||
"renamed": "Solicitud renombrada",
|
||||
"run": "Ejecutar",
|
||||
"save": "Guardar",
|
||||
"save_as": "Guardar como",
|
||||
"saved": "Petición guardada",
|
||||
"saved": "Solicitud guardada",
|
||||
"share": "Compartir",
|
||||
"share_description": "Share Hoppscotch with your friends",
|
||||
"title": "Petición",
|
||||
"type": "Tipo de petición",
|
||||
"share_description": "Comparte Hoppscotch con tus amigos",
|
||||
"title": "Solicitud",
|
||||
"type": "Tipo de solicitud",
|
||||
"url": "URL",
|
||||
"variables": "Variables",
|
||||
"view_my_links": "Ver mis enlaces"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Cuerpo de respuesta",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"filter_response_body": "Filtrar el cuerpo de la respuesta JSON (utiliza la sintaxis JSONPath)",
|
||||
"headers": "Encabezados",
|
||||
"html": "HTML",
|
||||
"image": "Imagen",
|
||||
@@ -445,13 +446,14 @@
|
||||
"status": "Estado",
|
||||
"time": "Tiempo",
|
||||
"title": "Respuesta",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "esperando la conexión",
|
||||
"xml": "XML"
|
||||
},
|
||||
"settings": {
|
||||
"accent_color": "Color de acentuación",
|
||||
"account": "Cuenta",
|
||||
"account_deleted": "Your account has been deleted",
|
||||
"account_deleted": "Tu cuenta ha sido eliminada",
|
||||
"account_description": "Personaliza la configuración de tu cuenta.",
|
||||
"account_email_description": "Tu dirección de correo electrónico principal.",
|
||||
"account_name_description": "Este es tu nombre para mostrar.",
|
||||
@@ -460,8 +462,8 @@
|
||||
"change_font_size": "Cambiar tamaño de fuente",
|
||||
"choose_language": "Elegir idioma",
|
||||
"dark_mode": "Oscuro",
|
||||
"delete_account": "Delete account",
|
||||
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
|
||||
"delete_account": "Eliminar cuenta",
|
||||
"delete_account_description": "Una vez que elimines tu cuenta, todos tus datos se borrarán permanentemente. Esta acción no se puede deshacer.",
|
||||
"expand_navigation": "Expandir la navegación",
|
||||
"experiments": "Experimentos",
|
||||
"experiments_notice": "Esta es una colección de experimentos en los que estamos trabajando que podrían resultar útiles, divertidos, ambos o ninguno. No son definitivos y es posible que no sean estables, por lo que si sucede algo demasiado extraño, no se asuste. Solo apaga la maldita cosa. Fuera de bromas,",
|
||||
@@ -480,7 +482,7 @@
|
||||
"light_mode": "Luz",
|
||||
"official_proxy_hosting": "El proxy oficial está alojado en Hoppscotch.",
|
||||
"profile": "Perfil",
|
||||
"profile_description": "Update your profile details",
|
||||
"profile_description": "Actualiza los datos de tu perfil",
|
||||
"profile_email": "Correo electrónico",
|
||||
"profile_name": "Nombre de perfil",
|
||||
"proxy": "Proxy",
|
||||
@@ -488,8 +490,8 @@
|
||||
"proxy_use_toggle": "Utilizar el middleware de proxy para enviar peticiones",
|
||||
"read_the": "Leer el",
|
||||
"reset_default": "Restablecer a los predeterminados",
|
||||
"short_codes": "Short codes",
|
||||
"short_codes_description": "Short codes which were created by you.",
|
||||
"short_codes": "Shortcodes",
|
||||
"short_codes_description": "Shortcodes creados por ti.",
|
||||
"sidebar_on_left": "Barra lateral a la izquierda",
|
||||
"sync": "Sincronizar",
|
||||
"sync_collections": "Colecciones",
|
||||
@@ -503,15 +505,15 @@
|
||||
"theme_description": "Personaliza el tema de tu aplicación.",
|
||||
"use_experimental_url_bar": "Utilizar la barra de URL experimental con resaltado de entorno",
|
||||
"user": "Usuario",
|
||||
"verified_email": "Verified email",
|
||||
"verified_email": "Correo electrónico verificado",
|
||||
"verify_email": "Verificar correo electrónico"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "Actions",
|
||||
"created_on": "Created on",
|
||||
"deleted": "Shortcode deleted",
|
||||
"method": "Method",
|
||||
"not_found": "Shortcode not found",
|
||||
"actions": "Acciones",
|
||||
"created_on": "Creado el",
|
||||
"deleted": "Código corto eliminado",
|
||||
"method": "Método",
|
||||
"not_found": "Shortcode no encontrado",
|
||||
"short_code": "Short code",
|
||||
"url": "URL"
|
||||
},
|
||||
@@ -539,7 +541,7 @@
|
||||
"title": "Navegación"
|
||||
},
|
||||
"request": {
|
||||
"copy_request_link": "Copiar enlace de petición",
|
||||
"copy_request_link": "Copiar enlace de solicitud",
|
||||
"delete_method": "Seleccionar método DELETE",
|
||||
"get_method": "Seleccionar método GET",
|
||||
"head_method": "Seleccionar método HEAD",
|
||||
@@ -548,10 +550,10 @@
|
||||
"post_method": "Seleccionar método POST",
|
||||
"previous_method": "Seleccionar método anterior",
|
||||
"put_method": "Seleccionar método PUT",
|
||||
"reset_request": "Petición de reinicio",
|
||||
"reset_request": "Solicitud de reinicio",
|
||||
"save_to_collections": "Guardar en colecciones",
|
||||
"send_request": "Enviar petición",
|
||||
"title": "Petición"
|
||||
"send_request": "Enviar solicitud",
|
||||
"title": "Solicitud"
|
||||
},
|
||||
"response": {
|
||||
"copy": "Copiar la respuesta al portapapeles",
|
||||
@@ -593,8 +595,8 @@
|
||||
"connected_to": "Conectado a {name}",
|
||||
"connecting_to": "Conectando con {name}...",
|
||||
"connection_error": "Failed to connect",
|
||||
"connection_failed": "Connection failed",
|
||||
"connection_lost": "Connection lost",
|
||||
"connection_failed": "Error de conexión",
|
||||
"connection_lost": "Conexión perdida",
|
||||
"copied_to_clipboard": "Copiado al portapapeles",
|
||||
"deleted": "Eliminado",
|
||||
"deprecated": "OBSOLETO",
|
||||
@@ -609,18 +611,18 @@
|
||||
"history_deleted": "Historial eliminado",
|
||||
"linewrap": "Envolver líneas",
|
||||
"loading": "Cargando...",
|
||||
"message_received": "Message: {message} arrived on topic: {topic}",
|
||||
"mqtt_subscription_failed": "Something went wrong while subscribing to topic: {topic}",
|
||||
"message_received": "Mensaje: {mensaje} llegó sobre el tema: {topic}",
|
||||
"mqtt_subscription_failed": "Algo ha ido mal al suscribirse al tema: {topic}",
|
||||
"none": "Ninguno",
|
||||
"nothing_found": "Nada encontrado para",
|
||||
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
|
||||
"published_message": "Published message: {message} to topic: {topic}",
|
||||
"reconnection_error": "Failed to reconnect",
|
||||
"subscribed_failed": "Failed to subscribe to topic: {topic}",
|
||||
"subscribed_success": "Successfully subscribed to topic: {topic}",
|
||||
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
|
||||
"unsubscribed_success": "Successfully unsubscribed from topic: {topic}",
|
||||
"waiting_send_request": "Esperando para enviar petición"
|
||||
"published_error": "Algo ha ido mal al publicar el mensaje: {topic} al tema: {message}",
|
||||
"published_message": "Mensaje publicado: {mensaje} al tema: {topic}",
|
||||
"reconnection_error": "Fallo en la reconexión",
|
||||
"subscribed_failed": "Error al suscribirse al tema: {topic}",
|
||||
"subscribed_success": "Suscrito con éxito al tema: {topic}",
|
||||
"unsubscribed_failed": "Error al darse de baja del tema: {topic}",
|
||||
"unsubscribed_success": "Se ha cancelado la suscripción al tema: {topic}",
|
||||
"waiting_send_request": "Esperando para enviar solicitud"
|
||||
},
|
||||
"support": {
|
||||
"changelog": "Leer más sobre los últimos lanzamientos",
|
||||
@@ -644,7 +646,7 @@
|
||||
"history": "Historial",
|
||||
"mqtt": "MQTT",
|
||||
"parameters": "Parámetros",
|
||||
"pre_request_script": "Script previo a la petición",
|
||||
"pre_request_script": "Script previo a la solicitud",
|
||||
"queries": "Consultas",
|
||||
"query": "Consulta",
|
||||
"schema": "Esquema",
|
||||
@@ -664,9 +666,9 @@
|
||||
"email_do_not_match": "El correo electrónico no coincide con los datos de tu cuenta. Ponte en contacto con el propietario de tu equipo.",
|
||||
"exit": "Salir del equipo",
|
||||
"exit_disabled": "Solo el propietario puede salir del equipo",
|
||||
"invalid_coll_id": "Invalid collection ID",
|
||||
"invalid_coll_id": "Identificador de colección no válido",
|
||||
"invalid_email_format": "El formato de correo electrónico no es válido",
|
||||
"invalid_id": "ID de equipo inválido. Ponte en contacto con el propietario de tu equipo.",
|
||||
"invalid_id": "Identificador de equipo inválido. Ponte en contacto con el propietario de tu equipo.",
|
||||
"invalid_invite_link": "Enlace de invitación inválido",
|
||||
"invalid_invite_link_description": "El enlace que has seguido no es válido. Ponte en contacto con el propietario de tu equipo.",
|
||||
"invalid_member_permission": "Proporcionar un permiso válido al miembro del equipo",
|
||||
@@ -683,7 +685,7 @@
|
||||
"login_to_continue": "Iniciar sesión para continuar",
|
||||
"login_to_continue_description": "Tienes que estar conectado para unirte a un equipo.",
|
||||
"logout_and_try_again": "Cerrar la sesión e iniciar sesión con otra cuenta",
|
||||
"member_has_invite": "Este ID de correo electrónico ya tiene una invitación. Ponte en contacto con el propietario de tu equipo.",
|
||||
"member_has_invite": "Este Identificador de correo electrónico ya tiene una invitación. Ponte en contacto con el propietario de tu equipo.",
|
||||
"member_not_found": "Miembro no encontrado. Ponte en contacto con el propietario de tu equipo.",
|
||||
"member_removed": "Usuario eliminado",
|
||||
"member_role_updated": "Funciones de usuario actualizadas",
|
||||
@@ -696,10 +698,10 @@
|
||||
"new_name": "Mi nuevo equipo",
|
||||
"no_access": "No tienes acceso de edición a estas colecciones.",
|
||||
"no_invite_found": "No se ha encontrado la invitación. Ponte en contacto con el propietario de tu equipo.",
|
||||
"no_request_found": "Request not found.",
|
||||
"no_request_found": "Solicitud no encontrada.",
|
||||
"not_found": "Equipo no encontrado. Ponte en contacto con el propietario de tu equipo.",
|
||||
"not_valid_viewer": "No eres un espectador válido. Ponte en contacto con el propietario de tu equipo.",
|
||||
"parent_coll_move": "Cannot move collection to a child collection",
|
||||
"parent_coll_move": "No se puede mover la colección a una colección hija",
|
||||
"pending_invites": "Invitaciones pendientes",
|
||||
"permissions": "Permisos",
|
||||
"same_target_destination": "Same target and destination",
|
||||
@@ -707,12 +709,12 @@
|
||||
"select_a_team": "Seleccionar un equipo",
|
||||
"title": "Equipos",
|
||||
"we_sent_invite_link": "¡Hemos enviado un enlace de invitación a todos los invitados!",
|
||||
"we_sent_invite_link_description": "Pide a todos los invitados que revisen su bandeja de entrada. Haz clic en el enlace para unirse al equipo."
|
||||
"we_sent_invite_link_description": "Pide a todos los invitados que revisen tu bandeja de entrada. Haz clic en el enlace para unirse al equipo."
|
||||
},
|
||||
"team_environment": {
|
||||
"deleted": "Environment Deleted",
|
||||
"duplicate": "Environment Duplicated",
|
||||
"not_found": "Environment not found."
|
||||
"deleted": "Entorno eliminado",
|
||||
"duplicate": "Entorno duplicado",
|
||||
"not_found": "Entorno no encontrado."
|
||||
},
|
||||
"test": {
|
||||
"failed": "prueba fallida",
|
||||
@@ -732,9 +734,9 @@
|
||||
"url": "URL"
|
||||
},
|
||||
"workspace": {
|
||||
"change": "Change workspace",
|
||||
"personal": "My Workspace",
|
||||
"team": "Team Workspace",
|
||||
"title": "Workspaces"
|
||||
"change": "Cambiar el espacio de trabajo",
|
||||
"personal": "Mi espacio de trabajo",
|
||||
"team": "Espacio de trabajo en equipo",
|
||||
"title": "Espacios de trabajo"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Vastauselin",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Otsikot",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Tila",
|
||||
"time": "Aika",
|
||||
"title": "Vastaus",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "yhteyttä odotellessa",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "Voir mes liens"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Corps de réponse",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "En-têtes",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Statut",
|
||||
"time": "Temps",
|
||||
"title": "Réponse",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "En attente de connexion",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "גוף תגובה",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "כותרות",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "סטָטוּס",
|
||||
"time": "זְמַן",
|
||||
"title": "תְגוּבָה",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "מחכה לחיבור",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -433,6 +433,7 @@
|
||||
"view_my_links": "मेरे लिंक देखें"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "प्रतिक्रिया निकाय",
|
||||
"filter_response_body": "फ़िल्टर JSON रिस्पांस बॉडी (JSONPATH सिंटैक्स का उपयोग करता है)",
|
||||
"headers": "हेडर",
|
||||
@@ -446,6 +447,7 @@
|
||||
"status": "दर्जा",
|
||||
"time": "समय",
|
||||
"title": "जवाब",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "जुडने के लिए इंतजार",
|
||||
"xml": "एक्सएमएल"
|
||||
},
|
||||
|
||||
@@ -5,29 +5,29 @@
|
||||
"choose_file": "Válasszon egy fájlt",
|
||||
"clear": "Törlés",
|
||||
"clear_all": "Összes törlése",
|
||||
"close": "Close",
|
||||
"close": "Bezárás",
|
||||
"connect": "Kapcsolódás",
|
||||
"connecting": "Connecting",
|
||||
"connecting": "Kapcsolódás",
|
||||
"copy": "Másolás",
|
||||
"delete": "Törlés",
|
||||
"disconnect": "Leválasztás",
|
||||
"dismiss": "Eltüntetés",
|
||||
"dont_save": "Ne mentse",
|
||||
"download_file": "Fájl letöltése",
|
||||
"drag_to_reorder": "Drag to reorder",
|
||||
"drag_to_reorder": "Húzza az átrendezéshez",
|
||||
"duplicate": "Kettőzés",
|
||||
"edit": "Szerkesztés",
|
||||
"filter": "Filter",
|
||||
"filter": "Szűrő",
|
||||
"go_back": "Vissza",
|
||||
"go_forward": "Go forward",
|
||||
"group_by": "Group by",
|
||||
"go_forward": "Előre",
|
||||
"group_by": "Csoportosítás",
|
||||
"label": "Címke",
|
||||
"learn_more": "Tudjon meg többet",
|
||||
"less": "Kevesebb",
|
||||
"more": "Több",
|
||||
"new": "Új",
|
||||
"no": "Nem",
|
||||
"open_workspace": "Open workspace",
|
||||
"open_workspace": "Munkaterület megnyitása",
|
||||
"paste": "Beillesztés",
|
||||
"prettify": "Csinosítás",
|
||||
"remove": "Eltávolítás",
|
||||
@@ -38,7 +38,7 @@
|
||||
"search": "Keresés",
|
||||
"send": "Küldés",
|
||||
"start": "Indítás",
|
||||
"starting": "Starting",
|
||||
"starting": "Indítás",
|
||||
"stop": "Leállítás",
|
||||
"to_close": "a bezáráshoz",
|
||||
"to_navigate": "a navigáláshoz",
|
||||
@@ -118,16 +118,16 @@
|
||||
},
|
||||
"collection": {
|
||||
"created": "Gyűjtemény létrehozva",
|
||||
"different_parent": "Cannot reorder collection with different parent",
|
||||
"different_parent": "Nem lehet átrendezni a különböző szülővel rendelkező gyűjteményt",
|
||||
"edit": "Gyűjtemény szerkesztése",
|
||||
"invalid_name": "Adjon nevet a gyűjteménynek",
|
||||
"invalid_root_move": "Collection already in the root",
|
||||
"moved": "Moved Successfully",
|
||||
"invalid_root_move": "A gyűjtemény már a gyökérben van",
|
||||
"moved": "Sikeresen áthelyezve",
|
||||
"my_collections": "Saját gyűjtemények",
|
||||
"name": "Saját új gyűjtemény",
|
||||
"name_length_insufficient": "A gyűjtemény nevének legalább 3 karakter hosszúságúnak kell lennie",
|
||||
"new": "Új gyűjtemény",
|
||||
"order_changed": "Collection Order Updated",
|
||||
"order_changed": "Gyűjtemény sorrendje frissítve",
|
||||
"renamed": "Gyűjtemény átnevezve",
|
||||
"request_in_use": "A kérés használatban",
|
||||
"save_as": "Mentés másként",
|
||||
@@ -147,7 +147,7 @@
|
||||
"remove_team": "Biztosan törölni szeretné ezt a csapatot?",
|
||||
"remove_telemetry": "Biztosan ki szeretné kapcsolni a telemetriát?",
|
||||
"request_change": "Biztosan el szeretné vetni a jelenlegi kérést? Minden mentetlen változtatás el fog veszni.",
|
||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
||||
"save_unsaved_tab": "Szeretné menteni az ezen a lapon elvégzett változtatásokat?",
|
||||
"sync": "Szeretné visszaállítani a munkaterületét a felhőből? Ez el fogja vetni a helyi folyamatát."
|
||||
},
|
||||
"count": {
|
||||
@@ -180,8 +180,8 @@
|
||||
"profile": "Jelentkezzen be a profilja megtekintéséhez",
|
||||
"protocols": "A protokollok üresek",
|
||||
"schema": "Kapcsolódjon egy GraphQL-végponthoz a séma megtekintéséhez",
|
||||
"shortcodes": "Shortcodes are empty",
|
||||
"subscription": "Subscriptions are empty",
|
||||
"shortcodes": "A rövid kódok üresek",
|
||||
"subscription": "A feliratkozások üresek",
|
||||
"team_name": "A csapat neve üres",
|
||||
"teams": "Ön nem tartozik semmilyen csapathoz",
|
||||
"tests": "Nincsenek tesztek ehhez a kéréshez"
|
||||
@@ -194,13 +194,13 @@
|
||||
"deleted": "Környezet törlése",
|
||||
"edit": "Környezet szerkesztése",
|
||||
"invalid_name": "Adjon nevet a környezetnek",
|
||||
"my_environments": "My Environments",
|
||||
"my_environments": "Saját környezetek",
|
||||
"nested_overflow": "az egymásba ágyazott környezeti változók 10 szintre vannak korlátozva",
|
||||
"new": "Új környezet",
|
||||
"no_environment": "Nincs környezet",
|
||||
"no_environment_description": "Nem lettek környezetek kiválasztva. Válassza ki, hogy mit kell tenni a következő változókkal.",
|
||||
"select": "Környezet kiválasztása",
|
||||
"team_environments": "Team Environments",
|
||||
"team_environments": "Csapatkörnyezetek",
|
||||
"title": "Környezetek",
|
||||
"updated": "Környezet frissítve",
|
||||
"variable_list": "Változólista"
|
||||
@@ -209,9 +209,9 @@
|
||||
"browser_support_sse": "Úgy tűnik, hogy ez a böngésző nem támogatja a kiszolgáló által küldött eseményeket.",
|
||||
"check_console_details": "Nézze meg a konzolnaplót a részletekért.",
|
||||
"curl_invalid_format": "A cURL nincs megfelelően formázva",
|
||||
"danger_zone": "Danger zone",
|
||||
"delete_account": "Your account is currently an owner in these teams:",
|
||||
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
|
||||
"danger_zone": "Veszélyes zóna",
|
||||
"delete_account": "Az Ön fiókja jelenleg tulajdonos ezekben a csapatokban:",
|
||||
"delete_account_description": "El kell távolítani magát, át kell adnia a tulajdonjogot vagy törölnie kell ezeket a csapatokat, mielőtt törölhetné a fiókját.",
|
||||
"empty_req_name": "Üres kérésnév",
|
||||
"f12_details": "(F12 a részletekért)",
|
||||
"gql_prettify_invalid_query": "Nem sikerült csinosítani egy érvénytelen lekérdezést, oldja meg a lekérdezés szintaktikai hibáit, és próbálja újra",
|
||||
@@ -219,13 +219,13 @@
|
||||
"incorrect_email": "Hibás e-mail",
|
||||
"invalid_link": "Érvénytelen hivatkozás",
|
||||
"invalid_link_description": "A kattintott hivatkozás érvénytelen vagy lejárt.",
|
||||
"json_parsing_failed": "Invalid JSON",
|
||||
"json_parsing_failed": "Érvénytelen JSON",
|
||||
"json_prettify_invalid_body": "Nem sikerült csinosítani egy érvénytelen törzset, oldja meg a JSON szintaktikai hibáit, és próbálja újra",
|
||||
"network_error": "Úgy tűnik, hogy hálózati hiba van. Próbálja újra.",
|
||||
"network_fail": "Nem sikerült elküldeni a kérést",
|
||||
"no_duration": "Nincs időtartam",
|
||||
"no_results_found": "No matches found",
|
||||
"page_not_found": "This page could not be found",
|
||||
"no_results_found": "Nincs találat",
|
||||
"page_not_found": "Ez az oldal nem található",
|
||||
"script_fail": "Nem sikerült végrehajtani a kérés előtti parancsfájlt",
|
||||
"something_went_wrong": "Valami elromlott",
|
||||
"test_script_fail": "Nem sikerült végrehajtani a kérés utáni parancsfájlt"
|
||||
@@ -238,9 +238,9 @@
|
||||
"title": "Exportálás"
|
||||
},
|
||||
"filter": {
|
||||
"all": "All",
|
||||
"none": "None",
|
||||
"starred": "Starred"
|
||||
"all": "Összes",
|
||||
"none": "Nincs",
|
||||
"starred": "Csillagozott"
|
||||
},
|
||||
"folder": {
|
||||
"created": "Mappa létrehozva",
|
||||
@@ -256,7 +256,7 @@
|
||||
"subscriptions": "Feliratkozások"
|
||||
},
|
||||
"group": {
|
||||
"time": "Time",
|
||||
"time": "Idő",
|
||||
"url": "URL"
|
||||
},
|
||||
"header": {
|
||||
@@ -316,32 +316,32 @@
|
||||
"zen_mode": "Zen mód"
|
||||
},
|
||||
"modal": {
|
||||
"close_unsaved_tab": "You have unsaved changes",
|
||||
"close_unsaved_tab": "Elmentetlen változtatásai vannak",
|
||||
"collections": "Gyűjtemények",
|
||||
"confirm": "Megerősítés",
|
||||
"edit_request": "Kérés szerkesztése",
|
||||
"import_export": "Importálás és exportálás"
|
||||
},
|
||||
"mqtt": {
|
||||
"already_subscribed": "You are already subscribed to this topic.",
|
||||
"clean_session": "Clean Session",
|
||||
"clear_input": "Clear input",
|
||||
"clear_input_on_send": "Clear input on send",
|
||||
"client_id": "Client ID",
|
||||
"color": "Pick a color",
|
||||
"already_subscribed": "Ön már feliratkozott erre a témára.",
|
||||
"clean_session": "Munkamenet törlése",
|
||||
"clear_input": "Bevitel törlése",
|
||||
"clear_input_on_send": "Bevitel törlése küldéskor",
|
||||
"client_id": "Ügyfél-azonosító",
|
||||
"color": "Válasszon színt",
|
||||
"communication": "Kommunikáció",
|
||||
"connection_config": "Connection Config",
|
||||
"connection_not_authorized": "This MQTT connection does not use any authentication.",
|
||||
"invalid_topic": "Please provide a topic for the subscription",
|
||||
"keep_alive": "Keep Alive",
|
||||
"connection_config": "Kapcsolat beállításai",
|
||||
"connection_not_authorized": "Ez az MQTT-kapcsolat nem használ semmilyen hitelesítést.",
|
||||
"invalid_topic": "Adjon témát a feliratkozáshoz",
|
||||
"keep_alive": "Életben tartás",
|
||||
"log": "Napló",
|
||||
"lw_message": "Last-Will Message",
|
||||
"lw_qos": "Last-Will QoS",
|
||||
"lw_retain": "Last-Will Retain",
|
||||
"lw_topic": "Last-Will Topic",
|
||||
"lw_message": "Utolsó kívánság üzenet",
|
||||
"lw_qos": "Utolsó kívánság QoS",
|
||||
"lw_retain": "Utolsó kívánság megtartás",
|
||||
"lw_topic": "Utolsó kívánság téma",
|
||||
"message": "Üzenet",
|
||||
"new": "New Subscription",
|
||||
"not_connected": "Please start a MQTT connection first.",
|
||||
"new": "Új feliratkozás",
|
||||
"not_connected": "Először indítson egy MQTT-kapcsolatot.",
|
||||
"publish": "Közzététel",
|
||||
"qos": "QoS",
|
||||
"ssl": "SSL",
|
||||
@@ -368,7 +368,7 @@
|
||||
},
|
||||
"profile": {
|
||||
"app_settings": "Alkalmazás beállításai",
|
||||
"default_hopp_displayname": "Unnamed User",
|
||||
"default_hopp_displayname": "Névtelen felhasználó",
|
||||
"editor": "Szerkesztő",
|
||||
"editor_description": "A szerkesztők hozzáadhatnak, szerkeszthetnek és törölhetnek kéréseket.",
|
||||
"email_verification_mail": "Egy ellenőrző e-mail el lett küldve az e-mail-címére. Kattintson a hivatkozásra az e-mail-címe ellenőrzéséhez.",
|
||||
@@ -391,26 +391,26 @@
|
||||
"choose_language": "Nyelv kiválasztása",
|
||||
"content_type": "Tartalom típusa",
|
||||
"content_type_titles": {
|
||||
"others": "Others",
|
||||
"structured": "Structured",
|
||||
"text": "Text"
|
||||
"others": "Egyebek",
|
||||
"structured": "Szerkesztett",
|
||||
"text": "Szöveg"
|
||||
},
|
||||
"copy_link": "Hivatkozás másolása",
|
||||
"different_collection": "Cannot reorder requests from different collections",
|
||||
"duplicated": "Request duplicated",
|
||||
"different_collection": "Nem lehet átrendezni a különböző gyűjteményekből érkező kéréseket",
|
||||
"duplicated": "Kérés megkettőzve",
|
||||
"duration": "Időtartam",
|
||||
"enter_curl": "cURL megadása",
|
||||
"enter_curl": "cURL-parancs megadása",
|
||||
"generate_code": "Kód előállítása",
|
||||
"generated_code": "Előállított kód",
|
||||
"header_list": "Fejléclista",
|
||||
"invalid_name": "Adjon nevet a kérésnek",
|
||||
"method": "Módszer",
|
||||
"moved": "Request moved",
|
||||
"moved": "Kérés áthelyezve",
|
||||
"name": "Kérés neve",
|
||||
"new": "Új kérés",
|
||||
"order_changed": "Request Order Updated",
|
||||
"order_changed": "Kérés sorrendje frissítve",
|
||||
"override": "Felülbírálás",
|
||||
"override_help": "A <kbd>Content-Type</kbd> beállítása a fejlécekben",
|
||||
"override_help": "<kbd>Content-Type</kbd> beállítása a fejlécekben",
|
||||
"overriden": "Felülbírálva",
|
||||
"parameter_list": "Lekérdezési paraméterek",
|
||||
"parameters": "Paraméterek",
|
||||
@@ -429,11 +429,12 @@
|
||||
"type": "Kérés típusa",
|
||||
"url": "URL",
|
||||
"variables": "Változók",
|
||||
"view_my_links": "View my links"
|
||||
"view_my_links": "Saját hivatkozások megtekintése"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Hang",
|
||||
"body": "Válasz törzse",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"filter_response_body": "JSON-válasz törzsének szűrése (JSONPath szintaxist használ)",
|
||||
"headers": "Fejlécek",
|
||||
"html": "HTML",
|
||||
"image": "Kép",
|
||||
@@ -445,13 +446,14 @@
|
||||
"status": "Állapot",
|
||||
"time": "Idő",
|
||||
"title": "Válasz",
|
||||
"video": "Videó",
|
||||
"waiting_for_connection": "várakozás kapcsolódásra",
|
||||
"xml": "XML"
|
||||
},
|
||||
"settings": {
|
||||
"accent_color": "Kiemelőszín",
|
||||
"account": "Fiók",
|
||||
"account_deleted": "Your account has been deleted",
|
||||
"account_deleted": "A fiókja törölve lett",
|
||||
"account_description": "A fiókbeállítások személyre szabása.",
|
||||
"account_email_description": "Az Ön elsődleges e-mail-címe.",
|
||||
"account_name_description": "Ez a megjelenített neve.",
|
||||
@@ -460,8 +462,8 @@
|
||||
"change_font_size": "Betűméret megváltoztatása",
|
||||
"choose_language": "Nyelv kiválasztása",
|
||||
"dark_mode": "Sötét",
|
||||
"delete_account": "Delete account",
|
||||
"delete_account_description": "Once you delete your account, all your data will be permanently deleted. This action cannot be undone.",
|
||||
"delete_account": "Fiók törlése",
|
||||
"delete_account_description": "Ha törli a fiókját, akkor az összes adata véglegesen törlésre kerül. Ezt a műveletet nem lehet visszavonni.",
|
||||
"expand_navigation": "Navigáció kinyitása",
|
||||
"experiments": "Kísérletek",
|
||||
"experiments_notice": "Ez olyan kísérletek gyűjteménye, amelyeken dolgozunk, és amelyek hasznosak, szórakoztatóak lehetnek, mindkettő, vagy egyik sem. Ezek nem véglegesek és nem stabilak, ezért ha valami túl furcsa dolog történik, ne essen pánikba. Egyszerűen kapcsolja ki a hibás dolgot. Viccet félretéve, ",
|
||||
@@ -488,8 +490,8 @@
|
||||
"proxy_use_toggle": "A proxy középprogram használata a kérések küldéséhez",
|
||||
"read_the": "Olvassa el:",
|
||||
"reset_default": "Visszaállítás az alapértelmezettre",
|
||||
"short_codes": "Short codes",
|
||||
"short_codes_description": "Short codes which were created by you.",
|
||||
"short_codes": "Rövid kódok",
|
||||
"short_codes_description": "Az Ön által létrehozott rövid kódok.",
|
||||
"sidebar_on_left": "Oldalsáv a bal oldalon",
|
||||
"sync": "Szinkronizálás",
|
||||
"sync_collections": "Gyűjtemények",
|
||||
@@ -503,16 +505,16 @@
|
||||
"theme_description": "Az alkalmazás témájának személyre szabása.",
|
||||
"use_experimental_url_bar": "Kísérleti URL-sáv használata a környezet kiemelésével",
|
||||
"user": "Felhasználó",
|
||||
"verified_email": "Verified email",
|
||||
"verified_email": "Ellenőrzött e-mail-cím",
|
||||
"verify_email": "E-mail-cím ellenőrzése"
|
||||
},
|
||||
"shortcodes": {
|
||||
"actions": "Actions",
|
||||
"created_on": "Created on",
|
||||
"deleted": "Shortcode deleted",
|
||||
"method": "Method",
|
||||
"not_found": "Shortcode not found",
|
||||
"short_code": "Short code",
|
||||
"actions": "Műveletek",
|
||||
"created_on": "Létrehozva",
|
||||
"deleted": "Rövid kód törölve",
|
||||
"method": "Módszer",
|
||||
"not_found": "A rövid kód nem található",
|
||||
"short_code": "Rövid kód",
|
||||
"url": "URL"
|
||||
},
|
||||
"shortcut": {
|
||||
@@ -554,9 +556,9 @@
|
||||
"title": "Kérés"
|
||||
},
|
||||
"response": {
|
||||
"copy": "Copy response to clipboard",
|
||||
"download": "Download response as file",
|
||||
"title": "Response"
|
||||
"copy": "Válasz másolása a vágólapra",
|
||||
"download": "Válasz letöltés fájlként",
|
||||
"title": "Válasz"
|
||||
},
|
||||
"theme": {
|
||||
"black": "Téma átváltása fekete módra",
|
||||
@@ -574,8 +576,8 @@
|
||||
},
|
||||
"socketio": {
|
||||
"communication": "Kommunikáció",
|
||||
"connection_not_authorized": "This SocketIO connection does not use any authentication.",
|
||||
"event_name": "Esemény neve",
|
||||
"connection_not_authorized": "Ez a SocketIO-kapcsolat nem használ semmilyen hitelesítést.",
|
||||
"event_name": "Esemény vagy téma neve",
|
||||
"events": "Események",
|
||||
"log": "Napló",
|
||||
"url": "URL"
|
||||
@@ -592,9 +594,9 @@
|
||||
"connected": "Kapcsolódva",
|
||||
"connected_to": "Kapcsolódva ehhez: {name}",
|
||||
"connecting_to": "Kapcsolódás ehhez: {name}…",
|
||||
"connection_error": "Failed to connect",
|
||||
"connection_failed": "Connection failed",
|
||||
"connection_lost": "Connection lost",
|
||||
"connection_error": "Nem sikerült kapcsolódni",
|
||||
"connection_failed": "A kapcsolódás sikertelen",
|
||||
"connection_lost": "A kapcsolat elveszett",
|
||||
"copied_to_clipboard": "Vágólapra másolva",
|
||||
"deleted": "Törölve",
|
||||
"deprecated": "ELAVULT",
|
||||
@@ -609,17 +611,17 @@
|
||||
"history_deleted": "Előzmények törölve",
|
||||
"linewrap": "Sorok tördelése",
|
||||
"loading": "Betöltés…",
|
||||
"message_received": "Message: {message} arrived on topic: {topic}",
|
||||
"mqtt_subscription_failed": "Something went wrong while subscribing to topic: {topic}",
|
||||
"message_received": "Üzenet: {message} érkezett ehhez a témához: {topic}",
|
||||
"mqtt_subscription_failed": "Valami elromlott a következő témára való feliratkozás során: {topic}",
|
||||
"none": "Nincs",
|
||||
"nothing_found": "Semmi sem található ehhez:",
|
||||
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
|
||||
"published_message": "Published message: {message} to topic: {topic}",
|
||||
"reconnection_error": "Failed to reconnect",
|
||||
"subscribed_failed": "Failed to subscribe to topic: {topic}",
|
||||
"subscribed_success": "Successfully subscribed to topic: {topic}",
|
||||
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
|
||||
"unsubscribed_success": "Successfully unsubscribed from topic: {topic}",
|
||||
"published_error": "Valami elromlott a következő üzenet közzététele során: {topic}, ehhez a témához: {message}",
|
||||
"published_message": "Közzétett üzenet: {message}, ehhez a témához: {topic}",
|
||||
"reconnection_error": "Nem sikerült újrakapcsolódni",
|
||||
"subscribed_failed": "Nem sikerült feliratkozni erre a témára: {topic}",
|
||||
"subscribed_success": "Sikeresen feliratkozott erre a témára: {topic}",
|
||||
"unsubscribed_failed": "Nem sikerült leiratkozni erről a témáról: {topic}",
|
||||
"unsubscribed_success": "Sikeresen leiratkozott erről a témáról: {topic}",
|
||||
"waiting_send_request": "Várakozás a kérés elküldésére"
|
||||
},
|
||||
"support": {
|
||||
@@ -639,7 +641,7 @@
|
||||
"body": "Törzs",
|
||||
"collections": "Gyűjtemények",
|
||||
"documentation": "Dokumentáció",
|
||||
"environments": "Environments",
|
||||
"environments": "Környezetek",
|
||||
"headers": "Fejlécek",
|
||||
"history": "Előzmények",
|
||||
"mqtt": "MQTT",
|
||||
@@ -664,7 +666,7 @@
|
||||
"email_do_not_match": "Az e-mail-cím nem egyezik a fiókja részleteivel. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
||||
"exit": "Kilépés a csapatból",
|
||||
"exit_disabled": "Csak a tulajdonos nem léphet ki a csapatból",
|
||||
"invalid_coll_id": "Invalid collection ID",
|
||||
"invalid_coll_id": "Érvénytelen gyűjteményazonosító",
|
||||
"invalid_email_format": "Az e-mail formátuma érvénytelen",
|
||||
"invalid_id": "Érvénytelen csapatazonosító. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
||||
"invalid_invite_link": "Érvénytelen meghívási hivatkozás",
|
||||
@@ -688,7 +690,7 @@
|
||||
"member_removed": "Felhasználó eltávolítva",
|
||||
"member_role_updated": "Felhasználói szerepek frissítve",
|
||||
"members": "Tagok",
|
||||
"more_members": "+{count} more",
|
||||
"more_members": "+{count} további",
|
||||
"name_length_insufficient": "A csapat nevének legalább 6 karakter hosszúságúnak kell lennie",
|
||||
"name_updated": "Csapatnév frissítve",
|
||||
"new": "Új csapat",
|
||||
@@ -696,13 +698,13 @@
|
||||
"new_name": "Saját új csapat",
|
||||
"no_access": "Nincs szerkesztési jogosultsága ezekhez a gyűjteményekhez",
|
||||
"no_invite_found": "A meghívás nem található. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
||||
"no_request_found": "Request not found.",
|
||||
"no_request_found": "A kérés nem található.",
|
||||
"not_found": "A csapat nem található. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
||||
"not_valid_viewer": "Ön nem érvényes megtekintő. Vegye fel a kapcsolatot a csapat tulajdonosával.",
|
||||
"parent_coll_move": "Cannot move collection to a child collection",
|
||||
"parent_coll_move": "Nem lehet áthelyezni a gyűjteményt egy gyermekgyűjteménybe",
|
||||
"pending_invites": "Függőben lévő meghívások",
|
||||
"permissions": "Jogosultságok",
|
||||
"same_target_destination": "Same target and destination",
|
||||
"same_target_destination": "Ugyanaz a cél és célhely",
|
||||
"saved": "Csapat elmentve",
|
||||
"select_a_team": "Csapat kiválasztása",
|
||||
"title": "Csapatok",
|
||||
@@ -710,9 +712,9 @@
|
||||
"we_sent_invite_link_description": "Kérje meg az összes meghívottat, hogy nézzék meg a beérkező leveleiket. Kattintsanak a hivatkozásra a csapathoz való csatlakozáshoz."
|
||||
},
|
||||
"team_environment": {
|
||||
"deleted": "Environment Deleted",
|
||||
"duplicate": "Environment Duplicated",
|
||||
"not_found": "Environment not found."
|
||||
"deleted": "Környezet törölve",
|
||||
"duplicate": "Környezet megkettőzve",
|
||||
"not_found": "A környezet nem található."
|
||||
},
|
||||
"test": {
|
||||
"failed": "teszt sikertelen",
|
||||
@@ -732,9 +734,9 @@
|
||||
"url": "URL"
|
||||
},
|
||||
"workspace": {
|
||||
"change": "Change workspace",
|
||||
"personal": "My Workspace",
|
||||
"team": "Team Workspace",
|
||||
"title": "Workspaces"
|
||||
"change": "Munkaterület váltása",
|
||||
"personal": "Saját munkaterület",
|
||||
"team": "Csapat-munkaterület",
|
||||
"title": "Munkaterületek"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "Lihat tautan saya"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Response Body",
|
||||
"filter_response_body": "Filter body respons JSON (menggunakan sintaks JSONPath)",
|
||||
"headers": "Headers",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Status",
|
||||
"time": "Waktu",
|
||||
"title": "Response",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "Menunggu koneksi",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Corpo della risposta",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Intestazioni",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Stato",
|
||||
"time": "Tempo impiegato",
|
||||
"title": "Risposta",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "In attesa di connessione",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "自分のリンクを見る"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "レスポンスボディ",
|
||||
"filter_response_body": "JSONレスポンスボディをフィルタ (JSONPathシンタックスを使用)",
|
||||
"headers": "ヘッダー",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "ステータス",
|
||||
"time": "時間",
|
||||
"title": "レスポンス",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "接続を待っています",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "내 링크 보기"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "응답 본문",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "헤더",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "상태",
|
||||
"time": "시간",
|
||||
"title": "제목",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "연결 대기 중",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Reactie inhoud",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Headers",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Status",
|
||||
"time": "Tijd",
|
||||
"title": "Antwoord",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "wachten op verbinding",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Svarkropp",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Overskrifter",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Status",
|
||||
"time": "Tid",
|
||||
"title": "Respons",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "venter på tilkobling",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Ciało odpowiedzi",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Nagłówki",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Status",
|
||||
"time": "Czas",
|
||||
"title": "Odpowiedź",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "oczekiwanie na połączenie",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Corpo de Resposta",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Cabeçalhos",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Status",
|
||||
"time": "Tempo",
|
||||
"title": "Resposta",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "aguardando conexão",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Corpo de Resposta",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Cabeçalhos",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Status",
|
||||
"time": "Tempo",
|
||||
"title": "Resposta",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "aguardando conexão",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "Vizualizare link-uri"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Corpul de răspuns",
|
||||
"filter_response_body": "Filtrează corpul răspunsului JSON (folosește sintaxa JSONPath)",
|
||||
"headers": "Anteturi",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Stare",
|
||||
"time": "Timp",
|
||||
"title": "Raspuns",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "Așteptând conexiunea",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Тело ответа",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Заголовки",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Статус",
|
||||
"time": "Время",
|
||||
"title": "Ответ",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "Ожидание соединения",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Тело за одговор",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Заглавља",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Статус",
|
||||
"time": "време",
|
||||
"title": "Одговор",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "чека везу",
|
||||
"xml": "КСМЛ"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Svarskommitté",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Rubriker",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Status",
|
||||
"time": "Tid",
|
||||
"title": "Svar",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "väntar på anslutning",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
"edit": "Koleksiyonu düzenle",
|
||||
"invalid_name": "Lütfen koleksiyon için geçerli bir ad girin",
|
||||
"invalid_root_move": "Collection already in the root",
|
||||
"moved": "Moved Successfully",
|
||||
"moved": "Başarıyla taşındı",
|
||||
"my_collections": "Koleksiyonlarım",
|
||||
"name": "Yeni Koleksiyonum",
|
||||
"name_length_insufficient": "Koleksiyon adı en az 3 karakter uzunluğunda olmalıdır",
|
||||
@@ -147,7 +147,7 @@
|
||||
"remove_team": "Bu takımı silmek istediğinizden emin misiniz?",
|
||||
"remove_telemetry": "Telemetriden çıkmak istediğinizden emin misiniz?",
|
||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
||||
"save_unsaved_tab": "Do you want to save changes made in this tab?",
|
||||
"save_unsaved_tab": "Bu sekmede yapılan değişiklikleri kaydetmek istiyor musunuz?",
|
||||
"sync": "Bu çalışma alanını senkronize etmek istediğinizden emin misiniz?"
|
||||
},
|
||||
"count": {
|
||||
@@ -368,9 +368,9 @@
|
||||
},
|
||||
"profile": {
|
||||
"app_settings": "Uygulama ayarları",
|
||||
"default_hopp_displayname": "Unnamed User",
|
||||
"editor": "Düzenleyici",
|
||||
"editor_description": "Editors can add, edit, and delete requests.",
|
||||
"default_hopp_displayname": "Adsız Kullanıcı",
|
||||
"editor": "Editör",
|
||||
"editor_description": "Editörler istekleri ekleyebilir, düzenleyebilir ve silebilir.",
|
||||
"email_verification_mail": "Doğrulama bağlantısı e-postanıza gönderildi. E-postanızı doğrulamak için gelen bağlantıya tıklayınız.",
|
||||
"no_permission": "Bu eylemi gerçekleştirmek için gerekli yetkiniz yok.",
|
||||
"owner": "Kurucu",
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Yanıt gövdesi",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Başlıklar",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Durum",
|
||||
"time": "Zaman",
|
||||
"title": "Cevap",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "Bağlantı için bekleniyor",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "檢視我的連結"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "回應本體",
|
||||
"filter_response_body": "篩選 JSON 回應本體 (使用 JSONPath 語法)",
|
||||
"headers": "回應標頭",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "狀態",
|
||||
"time": "時間",
|
||||
"title": "回應",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "等待連線",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "Переглянути мої посилання"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Орган реагування",
|
||||
"filter_response_body": "Фільтр тіла відповідей JSON (використовує синтаксис JSONPath)",
|
||||
"headers": "Заголовки",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Статус",
|
||||
"time": "Час",
|
||||
"title": "Відповідь",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "очікування підключення",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"cancel": "Hủy bỏ",
|
||||
"choose_file": "Chọn một tệp",
|
||||
"clear": "Thông thoáng",
|
||||
"clear_all": "Quet sạch tât cả",
|
||||
"clear_all": "Quet sạch tất cả",
|
||||
"close": "Close",
|
||||
"connect": "Liên kết",
|
||||
"connecting": "Connecting",
|
||||
@@ -432,6 +432,7 @@
|
||||
"view_my_links": "View my links"
|
||||
},
|
||||
"response": {
|
||||
"audio": "Audio",
|
||||
"body": "Cơ quan phản hồi",
|
||||
"filter_response_body": "Filter JSON response body (uses JSONPath syntax)",
|
||||
"headers": "Tiêu đề",
|
||||
@@ -445,6 +446,7 @@
|
||||
"status": "Tình trạng",
|
||||
"time": "Thời gian",
|
||||
"title": "Phản ứng",
|
||||
"video": "Video",
|
||||
"waiting_for_connection": "Đang đợi kết nối",
|
||||
"xml": "XML"
|
||||
},
|
||||
|
||||
@@ -8,7 +8,9 @@ export const APP_INFO = {
|
||||
keywords:
|
||||
"hoppscotch, hopp scotch, hoppscotch online, hoppscotch app, postwoman, postwoman chrome, postwoman online, postwoman for mac, postwoman app, postwoman for windows, postwoman google chrome, postwoman chrome app, get postwoman, postwoman web, postwoman android, postwoman app for chrome, postwoman mobile app, postwoman web app, api, request, testing, tool, rest, websocket, sse, graphql, socketio",
|
||||
app: {
|
||||
background: "#202124",
|
||||
background: "#181818",
|
||||
lightThemeColor: "#ffffff",
|
||||
darkThemeColor: "#181818",
|
||||
},
|
||||
social: {
|
||||
twitter: "@hoppscotch_io",
|
||||
@@ -108,7 +110,17 @@ export const META_TAGS = (env: Record<string, string>): IHTMLTag[] => [
|
||||
// PWA
|
||||
{
|
||||
name: "theme-color",
|
||||
content: APP_INFO.app.background,
|
||||
content: APP_INFO.app.darkThemeColor,
|
||||
media: "(prefers-color-scheme: dark)",
|
||||
},
|
||||
{
|
||||
name: "theme-color",
|
||||
content: APP_INFO.app.lightThemeColor,
|
||||
media: "(prefers-color-scheme: light)",
|
||||
},
|
||||
{
|
||||
name: "supported-color-schemes",
|
||||
content: "light dark",
|
||||
},
|
||||
{
|
||||
name: "mask-icon",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"name": "@hoppscotch/common",
|
||||
"private": true,
|
||||
"version": "2023.4.0",
|
||||
"version": "2023.4.7",
|
||||
"scripts": {
|
||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||
"test": "vitest --run",
|
||||
"test:watch": "vitest",
|
||||
"dev:vite": "vite",
|
||||
"dev:gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml --watch dotenv_config_path=\"../../.env\"",
|
||||
"lint": "eslint src --ext .ts,.js,.vue --ignore-path .gitignore .",
|
||||
@@ -13,6 +15,7 @@
|
||||
"preview": "vite preview",
|
||||
"gql-codegen": "graphql-codegen --require dotenv/config --config gql-codegen.yml dotenv_config_path=\"../../.env\"",
|
||||
"postinstall": "pnpm run gql-codegen",
|
||||
"do-test": "pnpm run test",
|
||||
"do-lint": "pnpm run prod-lint",
|
||||
"do-typecheck": "pnpm run lint",
|
||||
"do-lintfix": "pnpm run lintfix"
|
||||
@@ -43,11 +46,12 @@
|
||||
"@urql/exchange-auth": "^0.1.7",
|
||||
"@urql/exchange-graphcache": "^4.4.3",
|
||||
"@vitejs/plugin-legacy": "^2.3.0",
|
||||
"@vueuse/core": "^8.7.5",
|
||||
"@vueuse/core": "^8.9.4",
|
||||
"@vueuse/head": "^0.7.9",
|
||||
"acorn-walk": "^8.2.0",
|
||||
"axios": "^0.21.4",
|
||||
"buffer": "^6.0.3",
|
||||
"dioc": "workspace:^",
|
||||
"esprima": "^4.0.1",
|
||||
"events": "^3.3.0",
|
||||
"fp-ts": "^2.12.1",
|
||||
@@ -62,7 +66,8 @@
|
||||
"js-yaml": "^4.1.0",
|
||||
"jsonpath-plus": "^7.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lossless-json": "^1.0.5",
|
||||
"lossless-json": "^2.0.8",
|
||||
"minisearch": "^6.1.0",
|
||||
"nprogress": "^0.2.0",
|
||||
"paho-mqtt": "^1.1.0",
|
||||
"path": "^0.12.7",
|
||||
@@ -92,6 +97,7 @@
|
||||
"vuedraggable-es": "^4.1.1",
|
||||
"wonka": "^4.0.15",
|
||||
"workbox-window": "^6.5.4",
|
||||
"xml-formatter": "^3.4.1",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -106,7 +112,8 @@
|
||||
"@graphql-codegen/urql-introspection": "^2.2.0",
|
||||
"@graphql-typed-document-node/core": "^3.1.1",
|
||||
"@iconify-json/lucide": "^1.1.40",
|
||||
"@intlify/vite-plugin-vue-i18n": "^6.0.1",
|
||||
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
||||
"@relmify/jest-fp-ts": "^2.1.1",
|
||||
"@rushstack/eslint-patch": "^1.1.4",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
@@ -141,10 +148,11 @@
|
||||
"vite-plugin-html-config": "^1.0.10",
|
||||
"vite-plugin-inspect": "^0.7.4",
|
||||
"vite-plugin-pages": "^0.26.0",
|
||||
"vite-plugin-pages-sitemap": "^1.4.0",
|
||||
"vite-plugin-pages-sitemap": "^1.4.5",
|
||||
"vite-plugin-pwa": "^0.13.1",
|
||||
"vite-plugin-vue-layouts": "^0.7.0",
|
||||
"vite-plugin-windicss": "^1.8.8",
|
||||
"vitest": "^0.32.2",
|
||||
"vue-tsc": "^0.38.2",
|
||||
"windicss": "^3.5.6"
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 595 KiB After Width: | Height: | Size: 666 KiB |
BIN
packages/hoppscotch-common/public/icons/pwa-1024x1024.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
packages/hoppscotch-common/public/icons/pwa-128x128.png
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
packages/hoppscotch-common/public/icons/pwa-16x16.png
Normal file
|
After Width: | Height: | Size: 400 B |
BIN
packages/hoppscotch-common/public/icons/pwa-192x192.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
packages/hoppscotch-common/public/icons/pwa-256x256.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
packages/hoppscotch-common/public/icons/pwa-32x32.png
Normal file
|
After Width: | Height: | Size: 871 B |
BIN
packages/hoppscotch-common/public/icons/pwa-512x512.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 331 KiB After Width: | Height: | Size: 358 KiB |
|
Before Width: | Height: | Size: 352 KiB After Width: | Height: | Size: 382 KiB |
47
packages/hoppscotch-common/src/components.d.ts
vendored
@@ -11,20 +11,21 @@ declare module '@vue/runtime-core' {
|
||||
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
|
||||
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
|
||||
AppFooter: typeof import('./components/app/Footer.vue')['default']
|
||||
AppFuse: typeof import('./components/app/Fuse.vue')['default']
|
||||
AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default']
|
||||
AppHeader: typeof import('./components/app/Header.vue')['default']
|
||||
AppInterceptor: typeof import('./components/app/Interceptor.vue')['default']
|
||||
AppLogo: typeof import('./components/app/Logo.vue')['default']
|
||||
AppOptions: typeof import('./components/app/Options.vue')['default']
|
||||
AppPaneLayout: typeof import('./components/app/PaneLayout.vue')['default']
|
||||
AppPowerSearch: typeof import('./components/app/PowerSearch.vue')['default']
|
||||
AppPowerSearchEntry: typeof import('./components/app/PowerSearchEntry.vue')['default']
|
||||
AppShare: typeof import('./components/app/Share.vue')['default']
|
||||
AppShortcuts: typeof import('./components/app/Shortcuts.vue')['default']
|
||||
AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
|
||||
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
|
||||
AppSidenav: typeof import('./components/app/Sidenav.vue')['default']
|
||||
AppSpotlight: typeof import('./components/app/spotlight/index.vue')['default']
|
||||
AppSpotlightEntry: typeof import('./components/app/spotlight/Entry.vue')['default']
|
||||
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']
|
||||
ButtonPrimary: typeof import('./../../hoppscotch-ui/src/components/button/Primary.vue')['default']
|
||||
ButtonSecondary: typeof import('./../../hoppscotch-ui/src/components/button/Secondary.vue')['default']
|
||||
@@ -57,6 +58,7 @@ declare module '@vue/runtime-core' {
|
||||
EnvironmentsMy: typeof import('./components/environments/my/index.vue')['default']
|
||||
EnvironmentsMyDetails: typeof import('./components/environments/my/Details.vue')['default']
|
||||
EnvironmentsMyEnvironment: typeof import('./components/environments/my/Environment.vue')['default']
|
||||
EnvironmentsSelector: typeof import('./components/environments/Selector.vue')['default']
|
||||
EnvironmentsTeams: typeof import('./components/environments/teams/index.vue')['default']
|
||||
EnvironmentsTeamsDetails: typeof import('./components/environments/teams/Details.vue')['default']
|
||||
EnvironmentsTeamsEnvironment: typeof import('./components/environments/teams/Environment.vue')['default']
|
||||
@@ -73,6 +75,27 @@ declare module '@vue/runtime-core' {
|
||||
History: typeof import('./components/history/index.vue')['default']
|
||||
HistoryGraphqlCard: typeof import('./components/history/graphql/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']
|
||||
HttpAuthorizationApiKey: typeof import('./components/http/authorization/ApiKey.vue')['default']
|
||||
HttpAuthorizationBasic: typeof import('./components/http/authorization/Basic.vue')['default']
|
||||
@@ -98,16 +121,30 @@ declare module '@vue/runtime-core' {
|
||||
HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
|
||||
HttpTests: typeof import('./components/http/Tests.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']
|
||||
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
||||
LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
|
||||
LensesRenderersHTMLLensRenderer: typeof import('./components/lenses/renderers/HTMLLensRenderer.vue')['default']
|
||||
LensesRenderersImageLensRenderer: typeof import('./components/lenses/renderers/ImageLensRenderer.vue')['default']
|
||||
LensesRenderersJSONLensRenderer: typeof import('./components/lenses/renderers/JSONLensRenderer.vue')['default']
|
||||
LensesRenderersPDFLensRenderer: typeof import('./components/lenses/renderers/PDFLensRenderer.vue')['default']
|
||||
LensesRenderersRawLensRenderer: typeof import('./components/lenses/renderers/RawLensRenderer.vue')['default']
|
||||
LensesRenderersVideoLensRenderer: typeof import('./components/lenses/renderers/VideoLensRenderer.vue')['default']
|
||||
LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default']
|
||||
LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default']
|
||||
ProfilePicture: typeof import('./components/profile/Picture.vue')['default']
|
||||
ProfileShortcode: typeof import('./components/profile/Shortcode.vue')['default']
|
||||
ProfileShortcodes: typeof import('./components/profile/Shortcodes.vue')['default']
|
||||
ProfileUserDelete: typeof import('./components/profile/UserDelete.vue')['default']
|
||||
@@ -131,6 +168,8 @@ declare module '@vue/runtime-core' {
|
||||
SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default']
|
||||
SmartLink: typeof import('./../../hoppscotch-ui/src/components/smart/Link.vue')['default']
|
||||
SmartModal: typeof import('./../../hoppscotch-ui/src/components/smart/Modal.vue')['default']
|
||||
SmartPicture: typeof import('./../../hoppscotch-ui/src/components/smart/Picture.vue')['default']
|
||||
SmartPlaceholder: typeof import('./../../hoppscotch-ui/src/components/smart/Placeholder.vue')['default']
|
||||
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
|
||||
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
||||
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="`${t(
|
||||
'app.shortcuts'
|
||||
)} <kbd>${getSpecialKey()}</kbd><kbd>K</kbd>`"
|
||||
)} <kbd>${getSpecialKey()}</kbd><kbd>/</kbd>`"
|
||||
:icon="IconZap"
|
||||
@click="invokeAction('flyouts.keybinds.toggle')"
|
||||
/>
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
<template>
|
||||
<div key="outputHash" class="flex flex-col flex-1 overflow-auto">
|
||||
<div class="flex flex-col">
|
||||
<AppPowerSearchEntry
|
||||
v-for="(shortcut, shortcutIndex) in searchResults"
|
||||
:key="`shortcut-${shortcutIndex}`"
|
||||
:active="shortcutIndex === selectedEntry"
|
||||
:shortcut="shortcut.item"
|
||||
@action="emit('action', shortcut.item.action)"
|
||||
@mouseover="selectedEntry = shortcutIndex"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="searchResults.length === 0"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||
<span class="my-2 text-center">
|
||||
{{ t("state.nothing_found") }} "{{ search }}"
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onUnmounted, onMounted } from "vue"
|
||||
import Fuse from "fuse.js"
|
||||
import { useArrowKeysNavigation } from "~/helpers/powerSearchNavigation"
|
||||
import { HoppAction } from "~/helpers/actions"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
input: Record<string, any>[]
|
||||
search: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "action", action: HoppAction): void
|
||||
}>()
|
||||
|
||||
const options = {
|
||||
keys: ["keys", "label", "action", "tags"],
|
||||
}
|
||||
|
||||
const fuse = new Fuse(props.input, options)
|
||||
|
||||
const searchResults = computed(() => fuse.search(props.search))
|
||||
|
||||
const searchResultsItems = computed(() =>
|
||||
searchResults.value.map((searchResult) => searchResult.item)
|
||||
)
|
||||
|
||||
const emitSearchAction = (action: HoppAction) => emit("action", action)
|
||||
|
||||
const { bindArrowKeysListeners, unbindArrowKeysListeners, selectedEntry } =
|
||||
useArrowKeysNavigation(searchResultsItems, {
|
||||
onEnter: emitSearchAction,
|
||||
stopPropagation: true,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
bindArrowKeysListeners()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unbindArrowKeysListeners()
|
||||
})
|
||||
</script>
|
||||
@@ -20,7 +20,9 @@
|
||||
<div class="inline-flex items-center space-x-2">
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="`${t('app.search')} <kbd>/</kbd>`"
|
||||
:title="`${t(
|
||||
'app.search'
|
||||
)} <kbd>${getPlatformSpecialKey()}</kbd> <kbd>K</kbd>`"
|
||||
:icon="IconSearch"
|
||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||
@click="invokeAction('modals.search.toggle')"
|
||||
@@ -124,7 +126,7 @@
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<ProfilePicture
|
||||
<HoppSmartPicture
|
||||
v-if="currentUser.photoURL"
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
@@ -144,7 +146,7 @@
|
||||
network.isOnline ? 'bg-green-500' : 'bg-red-500'
|
||||
"
|
||||
/>
|
||||
<ProfilePicture
|
||||
<HoppSmartPicture
|
||||
v-else
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
@@ -242,11 +244,12 @@ import { pwaDefferedPrompt, installPWA } from "@modules/pwa"
|
||||
import { platform } from "~/platform"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { invokeAction } from "@helpers/actions"
|
||||
import { defineActionHandler, invokeAction } from "@helpers/actions"
|
||||
import { workspaceStatus$, updateWorkspaceTeamName } from "~/newstore/workspace"
|
||||
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
|
||||
import { onLoggedIn } from "~/composables/auth"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -374,4 +377,12 @@ const profile = ref<any | null>(null)
|
||||
const settings = ref<any | null>(null)
|
||||
const logout = ref<any | null>(null)
|
||||
const accountActions = ref<any | null>(null)
|
||||
|
||||
defineActionHandler(
|
||||
"user.login",
|
||||
() => {
|
||||
invokeAction("modals.login.toggle")
|
||||
},
|
||||
computed(() => !currentUser.value)
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
<template>
|
||||
<HoppSmartModal
|
||||
v-if="show"
|
||||
styles="sm:max-w-lg"
|
||||
full-width
|
||||
@close="emit('hide-modal')"
|
||||
>
|
||||
<template #body>
|
||||
<div class="flex flex-col border-b transition border-dividerLight">
|
||||
<input
|
||||
id="command"
|
||||
v-model="search"
|
||||
v-focus
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
name="command"
|
||||
:placeholder="`${t('app.type_a_command_search')}`"
|
||||
class="flex flex-shrink-0 p-6 text-base bg-transparent text-secondaryDark"
|
||||
/>
|
||||
<div
|
||||
class="flex flex-shrink-0 text-tiny text-secondaryLight px-4 pb-4 justify-between whitespace-nowrap overflow-auto <sm:hidden"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<kbd class="shortcut-key">↑</kbd>
|
||||
<kbd class="shortcut-key">↓</kbd>
|
||||
<span class="ml-2 truncate">
|
||||
{{ t("action.to_navigate") }}
|
||||
</span>
|
||||
<kbd class="shortcut-key">↩</kbd>
|
||||
<span class="ml-2 truncate">
|
||||
{{ t("action.to_select") }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<kbd class="shortcut-key">ESC</kbd>
|
||||
<span class="ml-2 truncate">
|
||||
{{ t("action.to_close") }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AppFuse
|
||||
v-if="search && show"
|
||||
:input="fuse"
|
||||
:search="search"
|
||||
@action="runAction"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col flex-1 overflow-auto space-y-4 divide-y divide-dividerLight"
|
||||
>
|
||||
<div
|
||||
v-for="(map, mapIndex) in mappings"
|
||||
:key="`map-${mapIndex}`"
|
||||
class="flex flex-col"
|
||||
>
|
||||
<h5 class="px-6 py-2 my-2 text-secondaryLight">
|
||||
{{ t(map.section) }}
|
||||
</h5>
|
||||
<AppPowerSearchEntry
|
||||
v-for="(shortcut, shortcutIndex) in map.shortcuts"
|
||||
:key="`map-${mapIndex}-shortcut-${shortcutIndex}`"
|
||||
:shortcut="shortcut"
|
||||
:active="shortcutsItems.indexOf(shortcut) === selectedEntry"
|
||||
@action="runAction"
|
||||
@mouseover="selectedEntry = shortcutsItems.indexOf(shortcut)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</HoppSmartModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue"
|
||||
import { HoppAction, invokeAction } from "~/helpers/actions"
|
||||
import { spotlight as mappings, fuse } from "@helpers/shortcuts"
|
||||
import { useArrowKeysNavigation } from "~/helpers/powerSearchNavigation"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "hide-modal"): void
|
||||
}>()
|
||||
|
||||
const search = ref("")
|
||||
|
||||
const hideModal = () => {
|
||||
search.value = ""
|
||||
emit("hide-modal")
|
||||
}
|
||||
|
||||
const runAction = (command: HoppAction) => {
|
||||
invokeAction(command)
|
||||
hideModal()
|
||||
}
|
||||
|
||||
const shortcutsItems = computed(() =>
|
||||
mappings.reduce(
|
||||
(shortcuts, section) => [...shortcuts, ...section.shortcuts],
|
||||
[]
|
||||
)
|
||||
)
|
||||
|
||||
const { bindArrowKeysListeners, unbindArrowKeysListeners, selectedEntry } =
|
||||
useArrowKeysNavigation(shortcutsItems, {
|
||||
onEnter: runAction,
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show) bindArrowKeysListeners()
|
||||
else unbindArrowKeysListeners()
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -1,68 +0,0 @@
|
||||
<template>
|
||||
<button
|
||||
class="flex items-center flex-1 px-6 py-3 font-medium transition cursor-pointer search-entry focus:outline-none"
|
||||
:class="{ active: active }"
|
||||
tabindex="-1"
|
||||
@click="emit('action', shortcut.action)"
|
||||
@keydown.enter="emit('action', shortcut.action)"
|
||||
>
|
||||
<component
|
||||
:is="shortcut.icon"
|
||||
class="mr-4 transition opacity-50 svg-icons"
|
||||
:class="{ 'opacity-100 text-secondaryDark': active }"
|
||||
/>
|
||||
<span
|
||||
class="flex flex-1 mr-4 transition"
|
||||
:class="{ 'text-secondaryDark': active }"
|
||||
>
|
||||
{{ t(shortcut.label) }}
|
||||
</span>
|
||||
<kbd
|
||||
v-for="(key, keyIndex) in shortcut.keys"
|
||||
:key="`key-${String(keyIndex)}`"
|
||||
class="shortcut-key"
|
||||
>
|
||||
{{ key }}
|
||||
</kbd>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Component } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
defineProps<{
|
||||
shortcut: {
|
||||
label: string
|
||||
keys: string[]
|
||||
action: string
|
||||
icon: object | Component
|
||||
}
|
||||
active: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "action", action: string): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-entry {
|
||||
@apply relative;
|
||||
@apply after:absolute;
|
||||
@apply after:top-0;
|
||||
@apply after:left-0;
|
||||
@apply after:bottom-0;
|
||||
@apply after:bg-transparent;
|
||||
@apply after:z-2;
|
||||
@apply after:w-0.5;
|
||||
@apply after:content-DEFAULT;
|
||||
|
||||
&.active {
|
||||
@apply bg-primaryLight;
|
||||
@apply after:bg-accentLight;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -14,45 +14,14 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="filterText" class="flex flex-col divide-y divide-dividerLight">
|
||||
<details
|
||||
v-for="(map, mapIndex) in searchResults"
|
||||
:key="`map-${mapIndex}`"
|
||||
class="flex flex-col"
|
||||
open
|
||||
>
|
||||
<summary
|
||||
class="flex items-center flex-1 min-w-0 px-6 py-4 font-semibold transition cursor-pointer focus:outline-none text-secondaryLight hover:text-secondaryDark"
|
||||
>
|
||||
<icon-lucide-chevron-right class="mr-2 indicator" />
|
||||
<span
|
||||
class="font-semibold truncate capitalize-first text-secondaryDark"
|
||||
>
|
||||
{{ t(map.item.section) }}
|
||||
</span>
|
||||
</summary>
|
||||
<div class="flex flex-col px-6 pb-4 space-y-2">
|
||||
<AppShortcutsEntry
|
||||
v-for="(shortcut, index) in map.item.shortcuts"
|
||||
:key="`shortcut-${index}`"
|
||||
:shortcut="shortcut"
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
<div
|
||||
v-if="searchResults.length === 0"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<div class="flex flex-col divide-y divide-dividerLight">
|
||||
<HoppSmartPlaceholder v-if="isEmpty(shortcutsResults)">
|
||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||
<span class="my-2 text-center">
|
||||
{{ t("state.nothing_found") }} "{{ filterText }}"
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex flex-col divide-y divide-dividerLight">
|
||||
</HoppSmartPlaceholder>
|
||||
<details
|
||||
v-for="(map, mapIndex) in mappings"
|
||||
:key="`map-${mapIndex}`"
|
||||
v-for="(sectionResults, sectionTitle) in shortcutsResults"
|
||||
v-else
|
||||
:key="`section-${sectionTitle}`"
|
||||
class="flex flex-col"
|
||||
open
|
||||
>
|
||||
@@ -63,13 +32,13 @@
|
||||
<span
|
||||
class="font-semibold truncate capitalize-first text-secondaryDark"
|
||||
>
|
||||
{{ t(map.section) }}
|
||||
{{ sectionTitle }}
|
||||
</span>
|
||||
</summary>
|
||||
<div class="flex flex-col px-6 pb-4 space-y-2">
|
||||
<AppShortcutsEntry
|
||||
v-for="(shortcut, shortcutIndex) in map.shortcuts"
|
||||
:key="`map-${mapIndex}-shortcut-${shortcutIndex}`"
|
||||
v-for="(shortcut, index) in sectionResults"
|
||||
:key="`shortcut-${index}`"
|
||||
:shortcut="shortcut"
|
||||
/>
|
||||
</div>
|
||||
@@ -80,10 +49,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue"
|
||||
import Fuse from "fuse.js"
|
||||
import mappings from "~/helpers/shortcuts"
|
||||
import { computed, onBeforeMount, ref } from "vue"
|
||||
import { ShortcutDef, getShortcuts } from "~/helpers/shortcuts"
|
||||
import MiniSearch from "minisearch"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { groupBy, isEmpty } from "lodash-es"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -91,15 +61,33 @@ defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
const options = {
|
||||
keys: ["shortcuts.label"],
|
||||
}
|
||||
const minisearch = new MiniSearch({
|
||||
fields: ["label", "keys", "section"],
|
||||
idField: "label",
|
||||
storeFields: ["label", "keys", "section"],
|
||||
searchOptions: {
|
||||
fuzzy: true,
|
||||
prefix: true,
|
||||
},
|
||||
})
|
||||
|
||||
const fuse = new Fuse(mappings, options)
|
||||
const shortcuts = getShortcuts(t)
|
||||
|
||||
onBeforeMount(() => {
|
||||
minisearch.addAllAsync(shortcuts)
|
||||
})
|
||||
|
||||
const filterText = ref("")
|
||||
|
||||
const searchResults = computed(() => fuse.search(filterText.value))
|
||||
const shortcutsResults = computed(() => {
|
||||
// If there are no search text, return all the shortcuts
|
||||
const results =
|
||||
filterText.value.length > 0
|
||||
? minisearch.search(filterText.value)
|
||||
: shortcuts
|
||||
|
||||
return groupBy(results, "section") as Record<string, ShortcutDef[]>
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "close"): void
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="flex items-center py-1">
|
||||
<span class="flex flex-1 mr-4">
|
||||
{{ t(shortcut.label) }}
|
||||
{{ shortcut.label }}
|
||||
</span>
|
||||
<kbd
|
||||
v-for="(key, index) in shortcut.keys"
|
||||
@@ -14,14 +14,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from "@composables/i18n"
|
||||
|
||||
const t = useI18n()
|
||||
import { ShortcutDef } from "~/helpers/shortcuts"
|
||||
|
||||
defineProps<{
|
||||
shortcut: {
|
||||
label: string
|
||||
keys: string[]
|
||||
}
|
||||
shortcut: ShortcutDef
|
||||
}>()
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
<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>
|
||||